diff --git a/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs b/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs
index ca91981bc6b7d1f7246cef4eb5eca556de521b5e..330f302b3f44dde0b29d68f2205cbfaadebab53c 100644
--- a/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs
+++ b/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs
@@ -27,7 +27,7 @@ app.MapGet("/nocache", Gravatar.WriteGravatar).CacheOutput(x => x.NoCache());
 
 app.MapGet("/profile", Gravatar.WriteGravatar).CacheOutput("NoCache");
 
-app.MapGet("/attribute", [OutputCache(PolicyName = "NoCache")] () => Gravatar.WriteGravatar);
+app.MapGet("/attribute", [OutputCache(PolicyName = "NoCache")] (context) => Gravatar.WriteGravatar(context));
 
 var blog = app.MapGroup("blog").CacheOutput(x => x.Tag("blog"));
 blog.MapGet("/", Gravatar.WriteGravatar);
diff --git a/src/Middleware/OutputCaching/src/CacheVaryByRules.cs b/src/Middleware/OutputCaching/src/CacheVaryByRules.cs
index abbd9f8d62aa4393b8a843c942cf059745cb197c..fd2f66cd5ebfd12c0af81ed6b3c14725b4556e92 100644
--- a/src/Middleware/OutputCaching/src/CacheVaryByRules.cs
+++ b/src/Middleware/OutputCaching/src/CacheVaryByRules.cs
@@ -11,14 +11,14 @@ namespace Microsoft.AspNetCore.OutputCaching;
 /// </summary>
 public sealed class CacheVaryByRules
 {
-    private Dictionary<string, string>? _varyByCustom;
+    private Dictionary<string, string>? _varyByValues;
 
-    internal bool HasVaryByCustom => _varyByCustom != null && _varyByCustom.Any();
+    internal bool HasVaryByValues => _varyByValues != null && _varyByValues.Any();
 
     /// <summary>
-    /// Gets a dictionary of key-pair values to vary the cache by.
+    /// Gets a dictionary of key-pair values to vary by.
     /// </summary>
-    public IDictionary<string, string> VaryByCustom => _varyByCustom ??= new();
+    public IDictionary<string, string> VaryByValues => _varyByValues ??= new Dictionary<string, string>();
 
     /// <summary>
     /// Gets or sets the list of route value names to vary by.
@@ -38,5 +38,5 @@ public sealed class CacheVaryByRules
     /// <summary>
     /// Gets or sets a prefix to vary by.
     /// </summary>
-    public StringValues VaryByPrefix { get; set; }
+    public string? CacheKeyPrefix { get; set; }
 }
diff --git a/src/Middleware/OutputCaching/src/LoggerExtensions.cs b/src/Middleware/OutputCaching/src/LoggerExtensions.cs
index 7a05e21f7c4c1af1b7db7103f44df47ea0edeff3..ab200a95b3f06cbaca8b03d4c091115090cfdbcb 100644
--- a/src/Middleware/OutputCaching/src/LoggerExtensions.cs
+++ b/src/Middleware/OutputCaching/src/LoggerExtensions.cs
@@ -35,20 +35,17 @@ internal static partial class LoggerExtensions
     [LoggerMessage(7, LogLevel.Information, "No cached response available for this request.", EventName = "NoResponseServed")]
     internal static partial void NoResponseServed(this ILogger logger);
 
-    [LoggerMessage(8, LogLevel.Debug, "Vary by rules were updated. Header names: {HeaderNames}, Query keys: {QueryKeys}, Route value names: {RouteValueNames}", EventName = "VaryByRulesUpdated")]
-    internal static partial void VaryByRulesUpdated(this ILogger logger, string headerNames, string queryKeys, string routeValueNames);
-
-    [LoggerMessage(9, LogLevel.Information, "The response has been cached.", EventName = "ResponseCached")]
+    [LoggerMessage(8, LogLevel.Information, "The response has been cached.", EventName = "ResponseCached")]
     internal static partial void ResponseCached(this ILogger logger);
 
-    [LoggerMessage(10, LogLevel.Information, "The response could not be cached for this request.", EventName = "ResponseNotCached")]
+    [LoggerMessage(9, LogLevel.Information, "The response could not be cached for this request.", EventName = "ResponseNotCached")]
     internal static partial void ResponseNotCached(this ILogger logger);
 
-    [LoggerMessage(11, LogLevel.Warning, "The response could not be cached for this request because the 'Content-Length' did not match the body length.",
+    [LoggerMessage(10, LogLevel.Warning, "The response could not be cached for this request because the 'Content-Length' did not match the body length.",
         EventName = "ResponseContentLengthMismatchNotCached")]
     internal static partial void ResponseContentLengthMismatchNotCached(this ILogger logger);
 
-    [LoggerMessage(12, LogLevel.Debug, "The response time of the entry is {ResponseTime} and has exceeded its expiry date.",
+    [LoggerMessage(11, LogLevel.Debug, "The response time of the entry is {ResponseTime} and has exceeded its expiry date.",
         EventName = "ExpirationExpiresExceeded")]
     internal static partial void ExpirationExpiresExceeded(this ILogger logger, DateTimeOffset responseTime);
 
diff --git a/src/Middleware/OutputCaching/src/OutputCacheKeyProvider.cs b/src/Middleware/OutputCaching/src/OutputCacheKeyProvider.cs
index 456ff014722003268118f86ad1529f8aa9b814e8..673d96e459085b457fb45ef23ac2fe65e0caa06a 100644
--- a/src/Middleware/OutputCaching/src/OutputCacheKeyProvider.cs
+++ b/src/Middleware/OutputCaching/src/OutputCacheKeyProvider.cs
@@ -29,173 +29,236 @@ internal sealed class OutputCacheKeyProvider : IOutputCacheKeyProvider
         _options = options.Value;
     }
 
-    // GET<delimiter>SCHEME<delimiter>HOST:PORT/PATHBASE/PATH<delimiter>H<delimiter>HeaderName=HeaderValue<delimiter>Q<delimiter>QueryName=QueryValue1<subdelimiter>QueryValue2<delimiter>R<delimiter>RouteName1=RouteValue1<subdelimiter>RouteName2=RouteValue2
+    // <VaryByKeyPrefix><delimiter>
+    // GET<delimiter>SCHEME<delimiter>HOST:PORT/PATHBASE/PATH<delimiter>
+    // H<delimiter>HeaderName=HeaderValue<delimiter>
+    // Q<delimiter>QueryName=QueryValue1<subdelimiter>QueryValue2<delimiter>
+    // R<delimiter>RouteName1=RouteValue1<delimiter>RouteName2=RouteValue2
+    // V<delimiter>ValueName1=Value1<delimiter>ValueName2=Value2
     public string CreateStorageKey(OutputCacheContext context)
     {
         ArgumentNullException.ThrowIfNull(_builderPool);
 
+        var builder = _builderPool.Get();
+
+        try
+        {
+            if (!string.IsNullOrEmpty(context.CacheVaryByRules.CacheKeyPrefix))
+            {
+                builder
+                    .Append(context.CacheVaryByRules.CacheKeyPrefix)
+                    .Append(KeyDelimiter);
+            }
+
+            AppendBaseKey(context, builder);
+
+            AppendVaryByKey(context, builder);
+
+            return builder.ToString();
+        }
+        finally
+        {
+            _builderPool.Return(builder);
+        }
+    }
+
+    // GET<delimiter>SCHEME<delimiter>HOST:PORT/PATHBASE/PATH
+    public void AppendBaseKey(OutputCacheContext context, StringBuilder builder)
+    {
+        var request = context.HttpContext.Request;
+
+        builder
+            .AppendUpperInvariant(request.Method)
+            .Append(KeyDelimiter)
+            .AppendUpperInvariant(request.Scheme)
+            .Append(KeyDelimiter)
+            .AppendUpperInvariant(request.Host.Value);
+
+        if (_options.UseCaseSensitivePaths)
+        {
+            builder
+                .Append(request.PathBase.Value)
+                .Append(request.Path.Value);
+        }
+        else
+        {
+            builder
+                .AppendUpperInvariant(request.PathBase.Value)
+                .AppendUpperInvariant(request.Path.Value);
+        }
+    }
+
+    public void AppendVaryByKey(OutputCacheContext context, StringBuilder builder)
+    {
         var varyByRules = context.CacheVaryByRules;
+
         if (varyByRules == null)
         {
-            throw new InvalidOperationException($"{nameof(CacheVaryByRules)} must not be null on the {nameof(OutputCacheContext)}");
+            throw new InvalidOperationException($"{nameof(OutputCacheContext.CacheVaryByRules)} must not be null on the {nameof(OutputCacheContext)}");
         }
 
-        var request = context.HttpContext.Request;
-        var builder = _builderPool.Get();
+        var varyHeaderNames = context.CacheVaryByRules.HeaderNames;
+        var varyRouteValueNames = context.CacheVaryByRules.RouteValueNames;
+        var varyQueryKeys = context.CacheVaryByRules.QueryKeys;
+        var varyByValues = context.CacheVaryByRules.HasVaryByValues ? context.CacheVaryByRules.VaryByValues : null;
 
-        try
+        // Vary by header names
+        var headersCount = varyByRules.HeaderNames.Count;
+
+        if (headersCount > 0)
         {
+            // Append a group separator for the header segment of the cache key
             builder
-                .AppendUpperInvariant(request.Method)
-                .Append(KeyDelimiter)
-                .AppendUpperInvariant(request.Scheme)
                 .Append(KeyDelimiter)
-                .AppendUpperInvariant(request.Host.Value);
+                .Append('H');
 
-            if (_options.UseCaseSensitivePaths)
+            var requestHeaders = context.HttpContext.Request.Headers;
+            for (var i = 0; i < headersCount; i++)
             {
+                var header = varyByRules.HeaderNames[i] ?? string.Empty;
+                var headerValues = requestHeaders[header];
                 builder
-                    .Append(request.PathBase.Value)
-                    .Append(request.Path.Value);
-            }
-            else
-            {
-                builder
-                    .AppendUpperInvariant(request.PathBase.Value)
-                    .AppendUpperInvariant(request.Path.Value);
-            }
+                    .Append(KeyDelimiter)
+                    .Append(header)
+                    .Append('=');
 
-            // Vary by prefix and custom
-            var prefixCount = varyByRules?.VaryByPrefix.Count ?? 0;
-            if (prefixCount > 0)
-            {
-                // Append a group separator for the header segment of the cache key
-                builder.Append(KeyDelimiter)
-                    .Append('C');
+                var headerValuesArray = headerValues.ToArray();
+                Array.Sort(headerValuesArray, StringComparer.Ordinal);
 
-                for (var i = 0; i < prefixCount; i++)
+                for (var j = 0; j < headerValuesArray.Length; j++)
                 {
-                    var value = varyByRules?.VaryByPrefix[i] ?? string.Empty;
-                    builder.Append(KeyDelimiter).Append(value);
+                    builder.Append(headerValuesArray[j]);
                 }
             }
+        }
+
+        // Vary by query keys
+        if (varyQueryKeys.Count > 0)
+        {
+            // Append a group separator for the query key segment of the cache key
+            builder
+                .Append(KeyDelimiter)
+                .Append('Q');
 
-            // Vary by header names
-            var headersCount = varyByRules?.HeaderNames.Count ?? 0;
-            if (headersCount > 0)
+            if (varyQueryKeys.Count == 1 && string.Equals(varyQueryKeys[0], "*", StringComparison.Ordinal) && context.HttpContext.Request.Query.Count > 0)
             {
-                // Append a group separator for the header segment of the cache key
-                builder.Append(KeyDelimiter)
-                    .Append('H');
+                // Vary by all available query keys
+                var queryArray = context.HttpContext.Request.Query.ToArray();
+                // Query keys are aggregated case-insensitively whereas the query values are compared ordinally.
+                Array.Sort(queryArray, QueryKeyComparer.OrdinalIgnoreCase);
 
-                var requestHeaders = context.HttpContext.Request.Headers;
-                for (var i = 0; i < headersCount; i++)
+                for (var i = 0; i < queryArray.Length; i++)
                 {
-                    var header = varyByRules!.HeaderNames[i] ?? string.Empty;
-                    var headerValues = requestHeaders[header];
-                    builder.Append(KeyDelimiter)
-                        .Append(header)
+                    builder
+                        .Append(KeyDelimiter)
+                        .AppendUpperInvariant(queryArray[i].Key)
                         .Append('=');
 
-                    var headerValuesArray = headerValues.ToArray();
-                    Array.Sort(headerValuesArray, StringComparer.Ordinal);
+                    var queryValueArray = queryArray[i].Value.ToArray();
+                    Array.Sort(queryValueArray, StringComparer.Ordinal);
 
-                    for (var j = 0; j < headerValuesArray.Length; j++)
+                    for (var j = 0; j < queryValueArray.Length; j++)
                     {
-                        builder.Append(headerValuesArray[j]);
+                        if (j > 0)
+                        {
+                            builder.Append(KeySubDelimiter);
+                        }
+
+                        builder.Append(queryValueArray[j]);
                     }
                 }
             }
-
-            // Vary by query keys
-            if (varyByRules?.QueryKeys.Count > 0)
+            else
             {
-                // Append a group separator for the query key segment of the cache key
-                builder.Append(KeyDelimiter)
-                    .Append('Q');
-
-                if (varyByRules.QueryKeys.Count == 1 && string.Equals(varyByRules.QueryKeys[0], "*", StringComparison.Ordinal) && context.HttpContext.Request.Query.Count > 0)
+                for (var i = 0; i < varyByRules.QueryKeys.Count; i++)
                 {
-                    // Vary by all available query keys
-                    var queryArray = context.HttpContext.Request.Query.ToArray();
-                    // Query keys are aggregated case-insensitively whereas the query values are compared ordinally.
-                    Array.Sort(queryArray, QueryKeyComparer.OrdinalIgnoreCase);
-
-                    for (var i = 0; i < queryArray.Length; i++)
-                    {
-                        builder.Append(KeyDelimiter)
-                            .AppendUpperInvariant(queryArray[i].Key)
-                            .Append('=');
-
-                        var queryValueArray = queryArray[i].Value.ToArray();
-                        Array.Sort(queryValueArray, StringComparer.Ordinal);
+                    var queryKey = varyByRules.QueryKeys[i] ?? string.Empty;
+                    var queryKeyValues = context.HttpContext.Request.Query[queryKey];
+                    builder
+                        .Append(KeyDelimiter)
+                        .Append(queryKey)
+                        .Append('=');
 
-                        for (var j = 0; j < queryValueArray.Length; j++)
-                        {
-                            if (j > 0)
-                            {
-                                builder.Append(KeySubDelimiter);
-                            }
+                    var queryValueArray = queryKeyValues.ToArray();
+                    Array.Sort(queryValueArray, StringComparer.Ordinal);
 
-                            builder.Append(queryValueArray[j]);
-                        }
-                    }
-                }
-                else
-                {
-                    for (var i = 0; i < varyByRules.QueryKeys.Count; i++)
+                    for (var j = 0; j < queryValueArray.Length; j++)
                     {
-                        var queryKey = varyByRules.QueryKeys[i] ?? string.Empty;
-                        var queryKeyValues = context.HttpContext.Request.Query[queryKey];
-                        builder.Append(KeyDelimiter)
-                            .Append(queryKey)
-                            .Append('=');
-
-                        var queryValueArray = queryKeyValues.ToArray();
-                        Array.Sort(queryValueArray, StringComparer.Ordinal);
-
-                        for (var j = 0; j < queryValueArray.Length; j++)
+                        if (j > 0)
                         {
-                            if (j > 0)
-                            {
-                                builder.Append(KeySubDelimiter);
-                            }
-
-                            builder.Append(queryValueArray[j]);
+                            builder.Append(KeySubDelimiter);
                         }
+
+                        builder.Append(queryValueArray[j]);
                     }
                 }
             }
+        }
+
+        // Vary by route value names
+        var routeValueNamesCount = varyByRules.RouteValueNames.Count;
+        if (routeValueNamesCount > 0)
+        {
+            // Append a group separator for the route values segment of the cache key
+            builder
+                .Append(KeyDelimiter)
+                .Append('R');
 
-            // Vary by route value names
-            var routeValueNamesCount = varyByRules?.RouteValueNames.Count ?? 0;
-            if (routeValueNamesCount > 0)
+            for (var i = 0; i < routeValueNamesCount; i++)
             {
-                // Append a group separator for the route values segment of the cache key
+                // The lookup key can't be null
+                var routeValueName = varyByRules.RouteValueNames[i] ?? string.Empty;
+
+                // RouteValueNames returns null if the key doesn't exist
+                var routeValueValue = context.HttpContext.Request.RouteValues[routeValueName];
+
                 builder.Append(KeyDelimiter)
-                    .Append('R');
+                    .Append(routeValueName)
+                    .Append('=')
+                    .Append(Convert.ToString(routeValueValue, CultureInfo.InvariantCulture));
+            }
+        }
 
-                for (var i = 0; i < routeValueNamesCount; i++)
-                {
-                    // The lookup key can't be null
-                    var routeValueName = varyByRules!.RouteValueNames[i] ?? string.Empty;
+        // Vary by values
 
-                    // RouteValueNames returns null if the key doesn't exist
-                    var routeValueValue = context.HttpContext.Request.RouteValues[routeValueName];
+        // Order keys to have a deterministic key
+        var orderedKeys = GetOrderDictionaryKeys(varyByValues);
 
-                    builder.Append(KeyDelimiter)
-                        .Append(routeValueName)
-                        .Append('=')
-                        .Append(Convert.ToString(routeValueValue, CultureInfo.InvariantCulture));
-                }
-            }
+        var valueNamesCount = orderedKeys.Length;
+        if (valueNamesCount > 0)
+        {
+            // Append a group separator for the values segment of the cache key
+            builder
+                .Append(KeyDelimiter)
+                .Append('V');
 
-            return builder.ToString();
+            for (var i = 0; i < valueNamesCount; i++)
+            {
+                // The lookup key can't be null
+                var key = orderedKeys[i] ?? string.Empty;
+
+                var value = varyByRules.VaryByValues[key];
+
+                builder.Append(KeyDelimiter)
+                    .Append(key)
+                    .Append('=')
+                    .Append(value);
+            }
         }
-        finally
+    }
+
+    internal static string[] GetOrderDictionaryKeys(IDictionary<string, string>? dictionary)
+    {
+        if (dictionary == null || dictionary.Count == 0)
         {
-            _builderPool.Return(builder);
+            return Array.Empty<string>();
         }
+
+        var newArray = dictionary.Keys.ToArray();
+
+        Array.Sort(newArray, StringComparer.OrdinalIgnoreCase);
+
+        return newArray;
     }
 
     private sealed class QueryKeyComparer : IComparer<KeyValuePair<string, StringValues>>
diff --git a/src/Middleware/OutputCaching/src/OutputCacheMiddleware.cs b/src/Middleware/OutputCaching/src/OutputCacheMiddleware.cs
index 78ea55b1566cff35d570fbc0693c1dbf58d56032..fcd584d09e782d6e53ecbf86bc8a7c63da1b4826 100644
--- a/src/Middleware/OutputCaching/src/OutputCacheMiddleware.cs
+++ b/src/Middleware/OutputCaching/src/OutputCacheMiddleware.cs
@@ -341,36 +341,6 @@ internal sealed class OutputCacheMiddleware
             return;
         }
 
-        var varyHeaderNames = context.CacheVaryByRules.HeaderNames;
-        var varyRouteValueNames = context.CacheVaryByRules.RouteValueNames;
-        var varyQueryKeys = context.CacheVaryByRules.QueryKeys;
-        var varyByCustomKeys = context.CacheVaryByRules.HasVaryByCustom ? context.CacheVaryByRules.VaryByCustom : null;
-        var varyByPrefix = context.CacheVaryByRules.VaryByPrefix;
-
-        // Check if any vary rules exist
-        if (!StringValues.IsNullOrEmpty(varyHeaderNames) || !StringValues.IsNullOrEmpty(varyRouteValueNames) || !StringValues.IsNullOrEmpty(varyQueryKeys) || !StringValues.IsNullOrEmpty(varyByPrefix) || varyByCustomKeys?.Count > 0)
-        {
-            // Normalize order and casing of vary by rules
-            var normalizedVaryHeaderNames = GetOrderCasingNormalizedStringValues(varyHeaderNames);
-            var normalizedVaryRouteValueNames = GetOrderCasingNormalizedStringValues(varyRouteValueNames);
-            var normalizedVaryQueryKeys = GetOrderCasingNormalizedStringValues(varyQueryKeys);
-            var normalizedVaryByCustom = GetOrderCasingNormalizedDictionary(varyByCustomKeys);
-
-            // Update vary rules with normalized values
-            context.CacheVaryByRules.VaryByCustom.Clear();
-            context.CacheVaryByRules.VaryByPrefix = varyByPrefix + normalizedVaryByCustom;
-            context.CacheVaryByRules.HeaderNames = normalizedVaryHeaderNames;
-            context.CacheVaryByRules.RouteValueNames = normalizedVaryRouteValueNames;
-            context.CacheVaryByRules.QueryKeys = normalizedVaryQueryKeys;
-
-            // TODO: Add same condition on LogLevel in Response Caching
-            // Always overwrite the CachedVaryByRules to update the expiry information
-            if (_logger.IsEnabled(LogLevel.Debug))
-            {
-                _logger.VaryByRulesUpdated(normalizedVaryHeaderNames.ToString(), normalizedVaryQueryKeys.ToString(), normalizedVaryRouteValueNames.ToString());
-            }
-        }
-
         context.CacheKey = _keyProvider.CreateStorageKey(context);
     }
 
@@ -567,55 +537,4 @@ internal sealed class OutputCacheMiddleware
 
         return false;
     }
-
-    // Normalize order and casing
-    internal static StringValues GetOrderCasingNormalizedStringValues(StringValues stringValues)
-    {
-        if (stringValues.Count == 0)
-        {
-            return StringValues.Empty;
-        }
-        else if (stringValues.Count == 1)
-        {
-            return new StringValues(stringValues.ToString().ToUpperInvariant());
-        }
-        else
-        {
-            var originalArray = stringValues.ToArray();
-            var newArray = new string[originalArray.Length];
-
-            for (var i = 0; i < originalArray.Length; i++)
-            {
-                newArray[i] = originalArray[i]!.ToUpperInvariant();
-            }
-
-            // Since the casing has already been normalized, use Ordinal comparison
-            Array.Sort(newArray, StringComparer.Ordinal);
-
-            return new StringValues(newArray);
-        }
-    }
-
-    internal static StringValues GetOrderCasingNormalizedDictionary(IDictionary<string, string>? dictionary)
-    {
-        const char KeySubDelimiter = '\x1f';
-
-        if (dictionary == null || dictionary.Count == 0)
-        {
-            return StringValues.Empty;
-        }
-
-        var newArray = new string[dictionary.Count];
-
-        var i = 0;
-        foreach (var (key, value) in dictionary)
-        {
-            newArray[i++] = $"{key.ToUpperInvariant()}{KeySubDelimiter}{value}";
-        }
-
-        // Since the casing has already been normalized, use Ordinal comparison
-        Array.Sort(newArray, StringComparer.Ordinal);
-
-        return new StringValues(newArray);
-    }
 }
diff --git a/src/Middleware/OutputCaching/src/OutputCachePolicyBuilder.cs b/src/Middleware/OutputCaching/src/OutputCachePolicyBuilder.cs
index b386083e912bd80278d42f40b1ca6d18ab86c004..1eb8eaeae9e1b0f8dec6662eeff171c424b55b26 100644
--- a/src/Middleware/OutputCaching/src/OutputCachePolicyBuilder.cs
+++ b/src/Middleware/OutputCaching/src/OutputCachePolicyBuilder.cs
@@ -118,36 +118,64 @@ public sealed class OutputCachePolicyBuilder
     }
 
     /// <summary>
-    /// Adds a policy to vary the cached responses by custom values.
+    /// Adds a policy that varies the cache key using the specified value.
     /// </summary>
-    /// <param name="varyBy">The value to vary the cached responses by.</param>
-    public OutputCachePolicyBuilder VaryByValue(Func<HttpContext, CancellationToken, ValueTask<string>> varyBy)
+    /// <param name="keyPrefix">The value to vary the cache key by.</param>
+    public OutputCachePolicyBuilder SetCacheKeyPrefix(string keyPrefix)
     {
-        ArgumentNullException.ThrowIfNull(varyBy);
+        ArgumentNullException.ThrowIfNull(keyPrefix);
 
-        return AddPolicy(new VaryByValuePolicy(varyBy));
+        ValueTask<string> varyByKeyFunc(HttpContext context, CancellationToken cancellationToken)
+        {
+            return ValueTask.FromResult(keyPrefix);
+        }
+
+        return AddPolicy(new SetCacheKeyPrefixPolicy(varyByKeyFunc));
     }
 
     /// <summary>
-    /// Adds a policy to vary the cached responses by custom key/value.
+    /// Adds a policy that varies the cache key using the specified value.
     /// </summary>
-    /// <param name="varyBy">The key/value to vary the cached responses by.</param>
-    public OutputCachePolicyBuilder VaryByValue(Func<HttpContext, CancellationToken, ValueTask<KeyValuePair<string, string>>> varyBy)
+    /// <param name="keyPrefix">The value to vary the cache key by.</param>
+    public OutputCachePolicyBuilder SetCacheKeyPrefix(Func<HttpContext, string> keyPrefix)
     {
-        ArgumentNullException.ThrowIfNull(varyBy);
+        ArgumentNullException.ThrowIfNull(keyPrefix);
 
-        return AddPolicy(new VaryByValuePolicy(varyBy));
+        ValueTask<string> varyByKeyFunc(HttpContext context, CancellationToken cancellationToken)
+        {
+            return ValueTask.FromResult(keyPrefix(context));
+        }
+
+        return AddPolicy(new SetCacheKeyPrefixPolicy(varyByKeyFunc));
     }
 
     /// <summary>
-    /// Adds a policy to vary the cached responses by custom values.
+    /// Adds a policy that varies the cache key using the specified value.
     /// </summary>
-    /// <param name="varyBy">The value to vary the cached responses by.</param>
-    public OutputCachePolicyBuilder VaryByValue(Func<HttpContext, string> varyBy)
+    /// <param name="keyPrefix">The value to vary the cache key by.</param>
+    public OutputCachePolicyBuilder SetCacheKeyPrefix(Func<HttpContext, CancellationToken, ValueTask<string>> keyPrefix)
     {
-        ArgumentNullException.ThrowIfNull(varyBy);
+        ArgumentNullException.ThrowIfNull(keyPrefix);
 
-        return AddPolicy(new VaryByValuePolicy(varyBy));
+        return AddPolicy(new SetCacheKeyPrefixPolicy(keyPrefix));
+    }
+
+    /// <summary>
+    /// Adds a policy to vary the cached responses by custom key/value.
+    /// </summary>
+    /// <param name="key">The key to vary the cached responses by.</param>
+    /// <param name="value">The value to vary the cached responses by.</param>
+    public OutputCachePolicyBuilder VaryByValue(string key, string value)
+    {
+        ArgumentNullException.ThrowIfNull(key);
+        ArgumentNullException.ThrowIfNull(value);
+
+        ValueTask<KeyValuePair<string, string>> varyByFunc(HttpContext context, CancellationToken cancellationToken)
+        {
+            return ValueTask.FromResult(new KeyValuePair<string, string>(key, value));
+        }
+
+        return AddPolicy(new VaryByValuePolicy(varyByFunc));
     }
 
     /// <summary>
@@ -158,6 +186,22 @@ public sealed class OutputCachePolicyBuilder
     {
         ArgumentNullException.ThrowIfNull(varyBy);
 
+        ValueTask<KeyValuePair<string, string>> varyByFunc(HttpContext context, CancellationToken cancellationToken)
+        {
+            return ValueTask.FromResult(varyBy(context));
+        }
+
+        return AddPolicy(new VaryByValuePolicy(varyByFunc));
+    }
+
+    /// <summary>
+    /// Adds a policy that vary the cached content based on the specified value.
+    /// </summary>
+    /// <param name="varyBy">The key/value to vary the cached responses by.</param>
+    public OutputCachePolicyBuilder VaryByValue(Func<HttpContext, CancellationToken, ValueTask<KeyValuePair<string, string>>> varyBy)
+    {
+        ArgumentNullException.ThrowIfNull(varyBy);
+
         return AddPolicy(new VaryByValuePolicy(varyBy));
     }
 
diff --git a/src/Middleware/OutputCaching/src/Policies/SetCacheKeyPrefixPolicy.cs b/src/Middleware/OutputCaching/src/Policies/SetCacheKeyPrefixPolicy.cs
new file mode 100644
index 0000000000000000000000000000000000000000..8ffdbdd225d3da8087edcea3d0075b10d7bbbf85
--- /dev/null
+++ b/src/Middleware/OutputCaching/src/Policies/SetCacheKeyPrefixPolicy.cs
@@ -0,0 +1,40 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.AspNetCore.Http;
+
+namespace Microsoft.AspNetCore.OutputCaching;
+
+/// <summary>
+/// A policy that sets the cache key prefix using the specified value.
+/// </summary>
+internal sealed class SetCacheKeyPrefixPolicy : IOutputCachePolicy
+{
+    private readonly Func<HttpContext, CacheVaryByRules, CancellationToken, ValueTask> _varyByAsync;
+
+    /// <summary>
+    /// Creates a policy that varies the cache key using the specified value.
+    /// </summary>
+    public SetCacheKeyPrefixPolicy(Func<HttpContext, CancellationToken, ValueTask<string>> varyBy)
+    {
+        _varyByAsync = async (context, rules, cancellationToken) => rules.CacheKeyPrefix = await varyBy(context, cancellationToken);
+    }
+
+    /// <inheritdoc/>
+    ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken)
+    {
+        return _varyByAsync.Invoke(context.HttpContext, context.CacheVaryByRules, cancellationToken);
+    }
+
+    /// <inheritdoc/>
+    ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellationToken)
+    {
+        return ValueTask.CompletedTask;
+    }
+
+    /// <inheritdoc/>
+    ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context, CancellationToken cancellationToken)
+    {
+        return ValueTask.CompletedTask;
+    }
+}
diff --git a/src/Middleware/OutputCaching/src/Policies/VaryByValuePolicy.cs b/src/Middleware/OutputCaching/src/Policies/VaryByValuePolicy.cs
index 5de5366307a658c4d0e2173e38a5f38a56766f46..8f14866c9eb1faebbbeaf2cd01e74ad80ad25e82 100644
--- a/src/Middleware/OutputCaching/src/Policies/VaryByValuePolicy.cs
+++ b/src/Middleware/OutputCaching/src/Policies/VaryByValuePolicy.cs
@@ -10,62 +10,24 @@ namespace Microsoft.AspNetCore.OutputCaching;
 /// </summary>
 internal sealed class VaryByValuePolicy : IOutputCachePolicy
 {
-    private readonly Action<HttpContext, CacheVaryByRules>? _varyBy;
-    private readonly Func<HttpContext, CacheVaryByRules, CancellationToken, ValueTask>? _varyByAsync;
-
-    /// <summary>
-    /// Creates a policy that doesn't vary the cached content based on values.
-    /// </summary>
-    public VaryByValuePolicy()
-    {
-    }
-
-    /// <summary>
-    /// Creates a policy that vary the cached content based on the specified value.
-    /// </summary>
-    public VaryByValuePolicy(Func<HttpContext, string> varyBy)
-    {
-        _varyBy = (context, rules) => rules.VaryByPrefix += varyBy(context);
-    }
-
-    /// <summary>
-    /// Creates a policy that vary the cached content based on the specified value.
-    /// </summary>
-    public VaryByValuePolicy(Func<HttpContext, CancellationToken, ValueTask<string>> varyBy)
-    {
-        _varyByAsync = async (context, rules, token) => rules.VaryByPrefix += await varyBy(context, token);
-    }
-
-    /// <summary>
-    /// Creates a policy that vary the cached content based on the specified value.
-    /// </summary>
-    public VaryByValuePolicy(Func<HttpContext, KeyValuePair<string, string>> varyBy)
-    {
-        _varyBy = (context, rules) =>
-        {
-            var result = varyBy(context);
-            rules.VaryByCustom?.TryAdd(result.Key, result.Value);
-        };
-    }
+    private readonly Func<HttpContext, CacheVaryByRules, CancellationToken, ValueTask> _varyByAsync;
 
     /// <summary>
     /// Creates a policy that vary the cached content based on the specified value.
     /// </summary>
     public VaryByValuePolicy(Func<HttpContext, CancellationToken, ValueTask<KeyValuePair<string, string>>> varyBy)
     {
-        _varyBy = async (context, rules) =>
+        _varyByAsync = async (context, rules, cancellationToken) =>
         {
-            var result = await varyBy(context, context.RequestAborted);
-            rules.VaryByCustom?.TryAdd(result.Key, result.Value);
+            var result = await varyBy(context, cancellationToken);
+            rules.VaryByValues[result.Key] = result.Value;
         };
     }
 
     /// <inheritdoc/>
     ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken)
     {
-        _varyBy?.Invoke(context.HttpContext, context.CacheVaryByRules);
-
-        return _varyByAsync?.Invoke(context.HttpContext, context.CacheVaryByRules, context.HttpContext.RequestAborted) ?? ValueTask.CompletedTask;
+        return _varyByAsync.Invoke(context.HttpContext, context.CacheVaryByRules, cancellationToken);
     }
 
     /// <inheritdoc/>
diff --git a/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt b/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt
index fd1ea274618ddc9ebea9d532726aa445d5bdc24c..59d937671c505e2b5c831ad1514173c3a515a3ee 100644
--- a/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt
+++ b/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt
@@ -1,6 +1,8 @@
 #nullable enable
 Microsoft.AspNetCore.Builder.OutputCacheApplicationBuilderExtensions
 Microsoft.AspNetCore.OutputCaching.CacheVaryByRules
+Microsoft.AspNetCore.OutputCaching.CacheVaryByRules.CacheKeyPrefix.get -> string?
+Microsoft.AspNetCore.OutputCaching.CacheVaryByRules.CacheKeyPrefix.set -> void
 Microsoft.AspNetCore.OutputCaching.CacheVaryByRules.CacheVaryByRules() -> void
 Microsoft.AspNetCore.OutputCaching.CacheVaryByRules.HeaderNames.get -> Microsoft.Extensions.Primitives.StringValues
 Microsoft.AspNetCore.OutputCaching.CacheVaryByRules.HeaderNames.set -> void
@@ -8,9 +10,7 @@ Microsoft.AspNetCore.OutputCaching.CacheVaryByRules.QueryKeys.get -> Microsoft.E
 Microsoft.AspNetCore.OutputCaching.CacheVaryByRules.QueryKeys.set -> void
 Microsoft.AspNetCore.OutputCaching.CacheVaryByRules.RouteValueNames.get -> Microsoft.Extensions.Primitives.StringValues
 Microsoft.AspNetCore.OutputCaching.CacheVaryByRules.RouteValueNames.set -> void
-Microsoft.AspNetCore.OutputCaching.CacheVaryByRules.VaryByCustom.get -> System.Collections.Generic.IDictionary<string!, string!>!
-Microsoft.AspNetCore.OutputCaching.CacheVaryByRules.VaryByPrefix.get -> Microsoft.Extensions.Primitives.StringValues
-Microsoft.AspNetCore.OutputCaching.CacheVaryByRules.VaryByPrefix.set -> void
+Microsoft.AspNetCore.OutputCaching.CacheVaryByRules.VaryByValues.get -> System.Collections.Generic.IDictionary<string!, string!>!
 Microsoft.AspNetCore.OutputCaching.IOutputCacheFeature
 Microsoft.AspNetCore.OutputCaching.IOutputCacheFeature.Context.get -> Microsoft.AspNetCore.OutputCaching.OutputCacheContext!
 Microsoft.AspNetCore.OutputCaching.IOutputCachePolicy.CacheRequestAsync(Microsoft.AspNetCore.OutputCaching.OutputCacheContext! context, System.Threading.CancellationToken cancellation) -> System.Threading.Tasks.ValueTask
@@ -53,13 +53,15 @@ Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Clear() -> Microsoft
 Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Expire(System.TimeSpan expiration) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder!
 Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.NoCache() -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder!
 Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.OutputCachePolicyBuilder() -> void
+Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.SetCacheKeyPrefix(string! keyPrefix) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder!
+Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.SetCacheKeyPrefix(System.Func<Microsoft.AspNetCore.Http.HttpContext!, string!>! keyPrefix) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder!
+Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.SetCacheKeyPrefix(System.Func<Microsoft.AspNetCore.Http.HttpContext!, System.Threading.CancellationToken, System.Threading.Tasks.ValueTask<string!>>! keyPrefix) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder!
 Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Tag(params string![]! tags) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder!
 Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByHeader(params string![]! headerNames) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder!
 Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByQuery(params string![]! queryKeys) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder!
 Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByRouteValue(params string![]! routeValueNames) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder!
+Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(string! key, string! value) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder!
 Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.Func<Microsoft.AspNetCore.Http.HttpContext!, System.Collections.Generic.KeyValuePair<string!, string!>>! varyBy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder!
-Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.Func<Microsoft.AspNetCore.Http.HttpContext!, string!>! varyBy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder!
-Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.Func<Microsoft.AspNetCore.Http.HttpContext!, System.Threading.CancellationToken, System.Threading.Tasks.ValueTask<string!>>! varyBy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder!
 Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.Func<Microsoft.AspNetCore.Http.HttpContext!, System.Threading.CancellationToken, System.Threading.Tasks.ValueTask<System.Collections.Generic.KeyValuePair<string!, string!>>>! varyBy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder!
 Microsoft.AspNetCore.OutputCaching.OutputCacheContext
 Microsoft.AspNetCore.OutputCaching.OutputCacheContext.AllowCacheLookup.get -> bool
diff --git a/src/Middleware/OutputCaching/test/MemoryOutputCacheStoreTests.cs b/src/Middleware/OutputCaching/test/MemoryOutputCacheStoreTests.cs
index c3a8d483884da6b5da95f235d7e7e70cd86d4223..e15f4515ca117a72dd939b33a30f19537b6ee78d 100644
--- a/src/Middleware/OutputCaching/test/MemoryOutputCacheStoreTests.cs
+++ b/src/Middleware/OutputCaching/test/MemoryOutputCacheStoreTests.cs
@@ -1,12 +1,8 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
-using System.Net.Http;
-using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.OutputCaching.Memory;
-using Microsoft.AspNetCore.TestHost;
 using Microsoft.Extensions.Caching.Memory;
-using Microsoft.Net.Http.Headers;
 
 namespace Microsoft.AspNetCore.OutputCaching.Tests;
 
diff --git a/src/Middleware/OutputCaching/test/OutputCacheEntryFormatterTests.cs b/src/Middleware/OutputCaching/test/OutputCacheEntryFormatterTests.cs
index a0de533fc0b511936aed83e1b00df1accf6a4063..c8dc77113782f778e4cf81ee3ef5a661f3099508 100644
--- a/src/Middleware/OutputCaching/test/OutputCacheEntryFormatterTests.cs
+++ b/src/Middleware/OutputCaching/test/OutputCacheEntryFormatterTests.cs
@@ -1,11 +1,7 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
-using System.Net.Http;
 using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.OutputCaching.Memory;
-using Microsoft.AspNetCore.TestHost;
-using Microsoft.Extensions.Caching.Memory;
 using Microsoft.Net.Http.Headers;
 
 namespace Microsoft.AspNetCore.OutputCaching.Tests;
diff --git a/src/Middleware/OutputCaching/test/OutputCacheKeyProviderTests.cs b/src/Middleware/OutputCaching/test/OutputCacheKeyProviderTests.cs
index 92809d018fa4f2fb32343b60134140f77ad9d651..c5bb9558c6353d6b70aad7219e625adc39ee5481 100644
--- a/src/Middleware/OutputCaching/test/OutputCacheKeyProviderTests.cs
+++ b/src/Middleware/OutputCaching/test/OutputCacheKeyProviderTests.cs
@@ -10,6 +10,7 @@ public class OutputCacheKeyProviderTests
 {
     private const char KeyDelimiter = '\x1e';
     private const char KeySubDelimiter = '\x1f';
+    private static readonly string EmptyBaseKey = $"{KeyDelimiter}{KeyDelimiter}";
 
     [Fact]
     public void OutputCachingKeyProvider_CreateStorageKey_IncludesOnlyNormalizedMethodSchemeHostPortAndPath()
@@ -67,13 +68,13 @@ public class OutputCacheKeyProviderTests
     {
         var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
         var context = TestUtils.CreateTestContext();
-        context.CacheVaryByRules.VaryByPrefix = Guid.NewGuid().ToString("n");
+        context.CacheVaryByRules.CacheKeyPrefix = Guid.NewGuid().ToString("n");
 
-        Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}C{KeyDelimiter}{context.CacheVaryByRules.VaryByPrefix}", cacheKeyProvider.CreateStorageKey(context));
+        Assert.Equal($"{context.CacheVaryByRules.CacheKeyPrefix}{KeyDelimiter}{EmptyBaseKey}", cacheKeyProvider.CreateStorageKey(context));
     }
 
     [Fact]
-    public void OutputCachingKeyProvider_CreateStorageVaryKey_IncludesListedRouteValuesOnly()
+    public void OutputCachingKeyProvider_CreateStorageKey_IncludesListedRouteValuesOnly()
     {
         var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
         var context = TestUtils.CreateTestContext();
@@ -81,12 +82,12 @@ public class OutputCacheKeyProviderTests
         context.HttpContext.Request.RouteValues["RouteB"] = "ValueB";
         context.CacheVaryByRules.RouteValueNames = new string[] { "RouteA", "RouteC" };
 
-        Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}R{KeyDelimiter}RouteA=ValueA{KeyDelimiter}RouteC=",
+        Assert.Equal($"{EmptyBaseKey}{KeyDelimiter}R{KeyDelimiter}RouteA=ValueA{KeyDelimiter}RouteC=",
             cacheKeyProvider.CreateStorageKey(context));
     }
 
     [Fact]
-    public void OutputCachingKeyProvider_CreateStorageVaryKey_SerializeRouteValueToStringInvariantCulture()
+    public void OutputCachingKeyProvider_CreateStorageKey_SerializeRouteValueToStringInvariantCulture()
     {
         var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
         var context = TestUtils.CreateTestContext();
@@ -97,7 +98,7 @@ public class OutputCacheKeyProviderTests
         try
         {
             Thread.CurrentThread.CurrentCulture = CultureInfo.GetCultureInfo("fr-FR");
-            Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}R{KeyDelimiter}RouteA=123.456{KeyDelimiter}RouteC=",
+            Assert.Equal($"{EmptyBaseKey}{KeyDelimiter}R{KeyDelimiter}RouteA=123.456{KeyDelimiter}RouteC=",
                 cacheKeyProvider.CreateStorageKey(context));
         }
         finally
@@ -107,7 +108,19 @@ public class OutputCacheKeyProviderTests
     }
 
     [Fact]
-    public void OutputCachingKeyProvider_CreateStorageVaryKey_IncludesListedHeadersOnly()
+    public void OutputCachingKeyProvider_CreateStorageKey_ValuesAreSorted()
+    {
+        var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
+        var context = TestUtils.CreateTestContext();
+        context.CacheVaryByRules.VaryByValues["b"] = "ValueB";
+        context.CacheVaryByRules.VaryByValues["a"] = "ValueA";
+
+        Assert.Equal($"{EmptyBaseKey}{KeyDelimiter}V{KeyDelimiter}a=ValueA{KeyDelimiter}b=ValueB",
+            cacheKeyProvider.CreateStorageKey(context));
+    }
+
+    [Fact]
+    public void OutputCachingKeyProvider_CreateStorageKey_IncludesListedHeadersOnly()
     {
         var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
         var context = TestUtils.CreateTestContext();
@@ -115,12 +128,24 @@ public class OutputCacheKeyProviderTests
         context.HttpContext.Request.Headers["HeaderB"] = "ValueB";
         context.CacheVaryByRules.HeaderNames = new string[] { "HeaderA", "HeaderC" };
 
-        Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=",
+        Assert.Equal($"{EmptyBaseKey}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=",
+            cacheKeyProvider.CreateStorageKey(context));
+    }
+
+    [Fact]
+    public void OutputCachingKeyProvider_CreateStorageKey_UsesListedHeaderKey_AsKey()
+    {
+        var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
+        var context = TestUtils.CreateTestContext();
+        context.HttpContext.Request.Headers["HeaderA"] = "ValueA";
+        context.CacheVaryByRules.HeaderNames = new string[] { "HEADERA" };
+
+        Assert.Equal($"{EmptyBaseKey}{KeyDelimiter}H{KeyDelimiter}HEADERA=ValueA",
             cacheKeyProvider.CreateStorageKey(context));
     }
 
     [Fact]
-    public void OutputCachingKeyProvider_CreateStorageVaryKey_HeaderValuesAreSorted()
+    public void OutputCachingKeyProvider_CreateStorageKey_HeaderValuesAreSorted()
     {
         var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
         var context = TestUtils.CreateTestContext();
@@ -128,83 +153,96 @@ public class OutputCacheKeyProviderTests
         context.HttpContext.Request.Headers.Append("HeaderA", "ValueA");
         context.CacheVaryByRules.HeaderNames = new string[] { "HeaderA", "HeaderC" };
 
-        Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueAValueB{KeyDelimiter}HeaderC=",
+        Assert.Equal($"{EmptyBaseKey}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueAValueB{KeyDelimiter}HeaderC=",
             cacheKeyProvider.CreateStorageKey(context));
     }
 
     [Fact]
-    public void OutputCachingKeyProvider_CreateStorageVaryKey_IncludesListedQueryKeysOnly()
+    public void OutputCachingKeyProvider_CreateStorageKey_IncludesListedQueryKeysOnly()
     {
         var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
         var context = TestUtils.CreateTestContext();
         context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueA&QueryB=ValueB");
-        context.CacheVaryByRules.VaryByPrefix = Guid.NewGuid().ToString("n");
+        context.CacheVaryByRules.CacheKeyPrefix = Guid.NewGuid().ToString("n");
         context.CacheVaryByRules.QueryKeys = new string[] { "QueryA", "QueryC" };
 
-        Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}C{KeyDelimiter}{context.CacheVaryByRules.VaryByPrefix}{KeyDelimiter}Q{KeyDelimiter}QueryA=ValueA{KeyDelimiter}QueryC=",
+        Assert.Equal($"{context.CacheVaryByRules.CacheKeyPrefix}{KeyDelimiter}{EmptyBaseKey}{KeyDelimiter}Q{KeyDelimiter}QueryA=ValueA{KeyDelimiter}QueryC=",
             cacheKeyProvider.CreateStorageKey(context));
     }
 
     [Fact]
-    public void OutputCachingKeyProvider_CreateStorageVaryKey_IncludesQueryKeys_QueryKeyCaseInsensitive_UseQueryKeyCasing()
+    public void OutputCachingKeyProvider_CreateStorageKey_IncludesQueryKeys_QueryKeyCaseInsensitive_UseQueryKeyCasing()
     {
         var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
         var context = TestUtils.CreateTestContext();
         context.HttpContext.Request.QueryString = new QueryString("?queryA=ValueA&queryB=ValueB");
-        context.CacheVaryByRules.VaryByPrefix = Guid.NewGuid().ToString("n");
+        context.CacheVaryByRules.CacheKeyPrefix = Guid.NewGuid().ToString("n");
         context.CacheVaryByRules.QueryKeys = new string[] { "QueryA", "QueryC" };
 
-        Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}C{KeyDelimiter}{context.CacheVaryByRules.VaryByPrefix}{KeyDelimiter}Q{KeyDelimiter}QueryA=ValueA{KeyDelimiter}QueryC=",
+        Assert.Equal($"{context.CacheVaryByRules.CacheKeyPrefix}{KeyDelimiter}{EmptyBaseKey}{KeyDelimiter}Q{KeyDelimiter}QueryA=ValueA{KeyDelimiter}QueryC=",
             cacheKeyProvider.CreateStorageKey(context));
     }
 
     [Fact]
-    public void OutputCachingKeyProvider_CreateStorageVaryKey_IncludesAllQueryKeysGivenAsterisk()
+    public void OutputCachingKeyProvider_CreateStorageKey_UseListedQueryKeys_AsKey()
+    {
+        var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
+        var context = TestUtils.CreateTestContext();
+        context.HttpContext.Request.QueryString = new QueryString("?queryA=ValueA");
+        context.CacheVaryByRules.CacheKeyPrefix = Guid.NewGuid().ToString("n");
+        context.CacheVaryByRules.QueryKeys = new string[] { "QUERYA" };
+
+        Assert.Equal($"{context.CacheVaryByRules.CacheKeyPrefix}{KeyDelimiter}{EmptyBaseKey}{KeyDelimiter}Q{KeyDelimiter}QUERYA=ValueA",
+            cacheKeyProvider.CreateStorageKey(context));
+    }
+
+    [Fact]
+    public void OutputCachingKeyProvider_CreateStorageKey_IncludesAllQueryKeysGivenAsterisk()
     {
         var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
         var context = TestUtils.CreateTestContext();
         context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueA&QueryB=ValueB");
-        context.CacheVaryByRules.VaryByPrefix = Guid.NewGuid().ToString("n");
+        context.CacheVaryByRules.CacheKeyPrefix = Guid.NewGuid().ToString("n");
         context.CacheVaryByRules.QueryKeys = new string[] { "*" };
 
         // To support case insensitivity, all query keys are converted to upper case.
         // Explicit query keys uses the casing specified in the setting.
-        Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}C{KeyDelimiter}{context.CacheVaryByRules.VaryByPrefix}{KeyDelimiter}Q{KeyDelimiter}QUERYA=ValueA{KeyDelimiter}QUERYB=ValueB",
+        Assert.Equal($"{context.CacheVaryByRules.CacheKeyPrefix}{KeyDelimiter}{EmptyBaseKey}{KeyDelimiter}Q{KeyDelimiter}QUERYA=ValueA{KeyDelimiter}QUERYB=ValueB",
             cacheKeyProvider.CreateStorageKey(context));
     }
 
     [Fact]
-    public void OutputCachingKeyProvider_CreateStorageVaryKey_QueryKeysValuesNotConsolidated()
+    public void OutputCachingKeyProvider_CreateStorageKey_QueryKeysValuesNotConsolidated()
     {
         var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
         var context = TestUtils.CreateTestContext();
         context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueA&QueryA=ValueB");
-        context.CacheVaryByRules.VaryByPrefix = Guid.NewGuid().ToString("n");
+        context.CacheVaryByRules.CacheKeyPrefix = Guid.NewGuid().ToString("n");
         context.CacheVaryByRules.QueryKeys = new string[] { "*" };
-    
+
         // To support case insensitivity, all query keys are converted to upper case.
         // Explicit query keys uses the casing specified in the setting.
-        Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}C{KeyDelimiter}{context.CacheVaryByRules.VaryByPrefix}{KeyDelimiter}Q{KeyDelimiter}QUERYA=ValueA{KeySubDelimiter}ValueB",
+        Assert.Equal($"{context.CacheVaryByRules.CacheKeyPrefix}{KeyDelimiter}{EmptyBaseKey}{KeyDelimiter}Q{KeyDelimiter}QUERYA=ValueA{KeySubDelimiter}ValueB",
             cacheKeyProvider.CreateStorageKey(context));
     }
 
     [Fact]
-    public void OutputCachingKeyProvider_CreateStorageVaryKey_QueryKeysValuesAreSorted()
+    public void OutputCachingKeyProvider_CreateStorageKey_QueryKeysValuesAreSorted()
     {
         var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
         var context = TestUtils.CreateTestContext();
         context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueB&QueryA=ValueA");
-        context.CacheVaryByRules.VaryByPrefix = Guid.NewGuid().ToString("n");
+        context.CacheVaryByRules.CacheKeyPrefix = Guid.NewGuid().ToString("n");
         context.CacheVaryByRules.QueryKeys = new string[] { "*" };
 
         // To support case insensitivity, all query keys are converted to upper case.
         // Explicit query keys uses the casing specified in the setting.
-        Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}C{KeyDelimiter}{context.CacheVaryByRules.VaryByPrefix}{KeyDelimiter}Q{KeyDelimiter}QUERYA=ValueA{KeySubDelimiter}ValueB",
+        Assert.Equal($"{context.CacheVaryByRules.CacheKeyPrefix}{KeyDelimiter}{EmptyBaseKey}{KeyDelimiter}Q{KeyDelimiter}QUERYA=ValueA{KeySubDelimiter}ValueB",
             cacheKeyProvider.CreateStorageKey(context));
     }
 
     [Fact]
-    public void OutputCachingKeyProvider_CreateStorageVaryKey_IncludesListedHeadersAndQueryKeysAndRouteValues()
+    public void OutputCachingKeyProvider_CreateStorageKey_IncludesListedHeadersAndQueryKeysAndRouteValues()
     {
         var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
         var context = TestUtils.CreateTestContext();
@@ -213,12 +251,25 @@ public class OutputCacheKeyProviderTests
         context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueA&QueryB=ValueB");
         context.HttpContext.Request.RouteValues["RouteA"] = "ValueA";
         context.HttpContext.Request.RouteValues["RouteB"] = "ValueB";
-        context.CacheVaryByRules.VaryByPrefix = Guid.NewGuid().ToString("n");
+        context.CacheVaryByRules.CacheKeyPrefix = Guid.NewGuid().ToString("n");
         context.CacheVaryByRules.HeaderNames = new string[] { "HeaderA", "HeaderC" };
         context.CacheVaryByRules.QueryKeys = new string[] { "QueryA", "QueryC" };
         context.CacheVaryByRules.RouteValueNames = new string[] { "RouteA", "RouteC" };
 
-        Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}C{KeyDelimiter}{context.CacheVaryByRules.VaryByPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC={KeyDelimiter}Q{KeyDelimiter}QueryA=ValueA{KeyDelimiter}QueryC={KeyDelimiter}R{KeyDelimiter}RouteA=ValueA{KeyDelimiter}RouteC=",
+        Assert.Equal($"{context.CacheVaryByRules.CacheKeyPrefix}{KeyDelimiter}{EmptyBaseKey}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC={KeyDelimiter}Q{KeyDelimiter}QueryA=ValueA{KeyDelimiter}QueryC={KeyDelimiter}R{KeyDelimiter}RouteA=ValueA{KeyDelimiter}RouteC=",
+            cacheKeyProvider.CreateStorageKey(context));
+    }
+
+    [Fact]
+    public void OutputCachingKeyProvider_CreateStorageKey_UseListedRouteValueNames_AsKey()
+    {
+        var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
+        var context = TestUtils.CreateTestContext();
+        context.HttpContext.Request.RouteValues["RouteA"] = "ValueA";
+        context.CacheVaryByRules.CacheKeyPrefix = Guid.NewGuid().ToString("n");
+        context.CacheVaryByRules.RouteValueNames= new string[] { "ROUTEA" };
+
+        Assert.Equal($"{context.CacheVaryByRules.CacheKeyPrefix}{KeyDelimiter}{EmptyBaseKey}{KeyDelimiter}R{KeyDelimiter}ROUTEA=ValueA",
             cacheKeyProvider.CreateStorageKey(context));
     }
 }
diff --git a/src/Middleware/OutputCaching/test/OutputCacheMiddlewareTests.cs b/src/Middleware/OutputCaching/test/OutputCacheMiddlewareTests.cs
index 550e2077cb291a9efb09dfa45f0db736fe624c8b..a4a030634caab15cbefa787c70b4fdd275cc0415 100644
--- a/src/Middleware/OutputCaching/test/OutputCacheMiddlewareTests.cs
+++ b/src/Middleware/OutputCaching/test/OutputCacheMiddlewareTests.cs
@@ -1,9 +1,6 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
-using System.Diagnostics.Metrics;
-using System.Threading.Tasks;
-using System;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Http.Features;
 using Microsoft.AspNetCore.OutputCaching.Memory;
@@ -782,38 +779,6 @@ public class OutputCacheMiddlewareTests
         public override void OnStarting(Func<object, Task> callback, object state) { }
     }
 
-    [Fact]
-    public void GetOrderCasingNormalizedStringValues_NormalizesCasingToUpper()
-    {
-        var uppercaseStrings = new StringValues(new[] { "STRINGA", "STRINGB" });
-        var lowercaseStrings = new StringValues(new[] { "stringA", "stringB" });
-
-        var normalizedStrings = OutputCacheMiddleware.GetOrderCasingNormalizedStringValues(lowercaseStrings);
-
-        Assert.Equal(uppercaseStrings, normalizedStrings);
-    }
-
-    [Fact]
-    public void GetOrderCasingNormalizedStringValues_NormalizesOrder()
-    {
-        var orderedStrings = new StringValues(new[] { "STRINGA", "STRINGB" });
-        var reverseOrderStrings = new StringValues(new[] { "STRINGB", "STRINGA" });
-
-        var normalizedStrings = OutputCacheMiddleware.GetOrderCasingNormalizedStringValues(reverseOrderStrings);
-
-        Assert.Equal(orderedStrings, normalizedStrings);
-    }
-
-    [Fact]
-    public void GetOrderCasingNormalizedStringValues_PreservesCommas()
-    {
-        var originalStrings = new StringValues(new[] { "STRINGA, STRINGB" });
-
-        var normalizedStrings = OutputCacheMiddleware.GetOrderCasingNormalizedStringValues(originalStrings);
-
-        Assert.Equal(originalStrings, normalizedStrings);
-    }
-
     [Fact]
     public async Task Locking_PreventsConcurrentRequests()
     {
@@ -887,7 +852,7 @@ public class OutputCacheMiddlewareTests
                     task2Executing.Set();
                     break;
             }
-            
+
             c.Response.Write("Hello" + responseCounter);
             return Task.CompletedTask;
         });
diff --git a/src/Middleware/OutputCaching/test/OutputCachePoliciesTests.cs b/src/Middleware/OutputCaching/test/OutputCachePoliciesTests.cs
index a33e4f103a520ae1f68124a730539f8722245ebb..3611828f08132c185fac6a69a28d0cda05196d8c 100644
--- a/src/Middleware/OutputCaching/test/OutputCachePoliciesTests.cs
+++ b/src/Middleware/OutputCaching/test/OutputCachePoliciesTests.cs
@@ -234,29 +234,16 @@ public class OutputCachePoliciesTests
     }
 
     [Fact]
-    public async Task VaryByValuePolicy_SingleValue()
+    public async Task VaryByKeyPrefixPolicy_AddsKeyPrefix()
     {
         var context = TestUtils.CreateUninitializedContext();
         var value = "value";
 
-        IOutputCachePolicy policy = new VaryByValuePolicy(context => value);
+        IOutputCachePolicy policy = new SetCacheKeyPrefixPolicy((context, cancellationToken) => ValueTask.FromResult(value));
 
         await policy.CacheRequestAsync(context, default);
 
-        Assert.Equal(value, context.CacheVaryByRules.VaryByPrefix);
-    }
-
-    [Fact]
-    public async Task VaryByValuePolicy_SingleValueAsync()
-    {
-        var context = TestUtils.CreateUninitializedContext();
-        var value = "value";
-
-        IOutputCachePolicy policy = new VaryByValuePolicy((context, token) => ValueTask.FromResult(value));
-
-        await policy.CacheRequestAsync(context, default);
-
-        Assert.Equal(value, context.CacheVaryByRules.VaryByPrefix);
+        Assert.Equal(value, context.CacheVaryByRules.CacheKeyPrefix);
     }
 
     [Fact]
@@ -266,24 +253,10 @@ public class OutputCachePoliciesTests
         var key = "key";
         var value = "value";
 
-        IOutputCachePolicy policy = new VaryByValuePolicy(context => new KeyValuePair<string, string>(key, value));
-
-        await policy.CacheRequestAsync(context, default);
-
-        Assert.Contains(new KeyValuePair<string, string>(key, value), context.CacheVaryByRules.VaryByCustom);
-    }
-
-    [Fact]
-    public async Task VaryByValuePolicy_KeyValuePairAsync()
-    {
-        var context = TestUtils.CreateUninitializedContext();
-        var key = "key";
-        var value = "value";
-
-        IOutputCachePolicy policy = new VaryByValuePolicy((context, token) => ValueTask.FromResult(new KeyValuePair<string, string>(key, value)));
+        IOutputCachePolicy policy = new VaryByValuePolicy((context, CancellationToken) => ValueTask.FromResult(new KeyValuePair<string, string>(key, value)));
 
         await policy.CacheRequestAsync(context, default);
 
-        Assert.Contains(new KeyValuePair<string, string>(key, value), context.CacheVaryByRules.VaryByCustom);
+        Assert.Contains(new KeyValuePair<string, string>(key, value), context.CacheVaryByRules.VaryByValues);
     }
 }
diff --git a/src/Middleware/OutputCaching/test/OutputCachePolicyBuilderTests.cs b/src/Middleware/OutputCaching/test/OutputCachePolicyBuilderTests.cs
index 6fbe8e193fe79ca8a1b90dac615d4282535198c9..79762406c101f016c48f6810794380ae9d028b5f 100644
--- a/src/Middleware/OutputCaching/test/OutputCachePolicyBuilderTests.cs
+++ b/src/Middleware/OutputCaching/test/OutputCachePolicyBuilderTests.cs
@@ -1,7 +1,6 @@
 // Licensed to the .NET Foundation under one or more agreements.
 // The .NET Foundation licenses this file to you under the MIT license.
 
-using System.Net.Http;
 using Microsoft.AspNetCore.Http;
 
 namespace Microsoft.AspNetCore.OutputCaching.Tests;
@@ -114,17 +113,44 @@ public class OutputCachePolicyBuilderTests
         Assert.DoesNotContain("RouteB", (IEnumerable<string>)context.CacheVaryByRules.RouteValueNames);
     }
 
+    [Fact]
+    public async Task BuildPolicy_CreatesVaryByKeyPrefixPolicy()
+    {
+        var context1 = TestUtils.CreateUninitializedContext();
+        var context2 = TestUtils.CreateUninitializedContext();
+        var context3 = TestUtils.CreateUninitializedContext();
+
+        var policy1 = new OutputCachePolicyBuilder().SetCacheKeyPrefix("tenant1").Build();
+        var policy2 = new OutputCachePolicyBuilder().SetCacheKeyPrefix(context => "tenant2").Build();
+        var policy3 = new OutputCachePolicyBuilder().SetCacheKeyPrefix((context, cancellationToken) => ValueTask.FromResult("tenant3")).Build();
+
+        await policy1.CacheRequestAsync(context1, cancellation: default);
+        await policy2.CacheRequestAsync(context2, cancellation: default);
+        await policy3.CacheRequestAsync(context3, cancellation: default);
+
+        Assert.Equal("tenant1", context1.CacheVaryByRules.CacheKeyPrefix);
+        Assert.Equal("tenant2", context2.CacheVaryByRules.CacheKeyPrefix);
+        Assert.Equal("tenant3", context3.CacheVaryByRules.CacheKeyPrefix);
+    }
+
     [Fact]
     public async Task BuildPolicy_CreatesVaryByValuePolicy()
     {
         var context = TestUtils.CreateUninitializedContext();
 
         var builder = new OutputCachePolicyBuilder();
-        var policy = builder.VaryByValue(context => new KeyValuePair<string, string>("color", "blue")).Build();
+        var policy = builder
+            .VaryByValue("shape", "circle")
+            .VaryByValue(context => new KeyValuePair<string, string>("color", "blue"))
+            .VaryByValue((context, cancellationToken) => ValueTask.FromResult(new KeyValuePair<string, string>("size", "1m")))
+            .Build();
+
         await policy.CacheRequestAsync(context, cancellation: default);
 
         Assert.True(context.EnableOutputCaching);
-        Assert.Equal("blue", context.CacheVaryByRules.VaryByCustom["color"]);
+        Assert.Equal("circle", context.CacheVaryByRules.VaryByValues["shape"]);
+        Assert.Equal("blue", context.CacheVaryByRules.VaryByValues["color"]);
+        Assert.Equal("1m", context.CacheVaryByRules.VaryByValues["size"]);
     }
 
     [Fact]
diff --git a/src/Middleware/OutputCaching/test/OutputCachePolicyProviderTests.cs b/src/Middleware/OutputCaching/test/OutputCachePolicyProviderTests.cs
index ce93251c186952af31e84abdb72b4aab23bb3136..a91138df1422574c7adbcc36066a32ffa25caf21 100644
--- a/src/Middleware/OutputCaching/test/OutputCachePolicyProviderTests.cs
+++ b/src/Middleware/OutputCaching/test/OutputCachePolicyProviderTests.cs
@@ -2,9 +2,7 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using Microsoft.AspNetCore.Http;
-using Microsoft.AspNetCore.OutputCaching.Policies;
 using Microsoft.Extensions.Logging.Testing;
-using Microsoft.Extensions.Options;
 using Microsoft.Net.Http.Headers;
 
 namespace Microsoft.AspNetCore.OutputCaching.Tests;
diff --git a/src/Middleware/OutputCaching/test/TestUtils.cs b/src/Middleware/OutputCaching/test/TestUtils.cs
index f8fe9fce581a2688f6017b9dfd2ff88a9c0cba5b..51039bb81cd9968aefe4c41ab6dbff4128337d09 100644
--- a/src/Middleware/OutputCaching/test/TestUtils.cs
+++ b/src/Middleware/OutputCaching/test/TestUtils.cs
@@ -275,11 +275,10 @@ internal class LoggedMessage
     internal static LoggedMessage CachedResponseServed => new LoggedMessage(5, LogLevel.Information);
     internal static LoggedMessage GatewayTimeoutServed => new LoggedMessage(6, LogLevel.Information);
     internal static LoggedMessage NoResponseServed => new LoggedMessage(7, LogLevel.Information);
-    internal static LoggedMessage VaryByRulesUpdated => new LoggedMessage(8, LogLevel.Debug);
-    internal static LoggedMessage ResponseCached => new LoggedMessage(9, LogLevel.Information);
-    internal static LoggedMessage ResponseNotCached => new LoggedMessage(10, LogLevel.Information);
-    internal static LoggedMessage ResponseContentLengthMismatchNotCached => new LoggedMessage(11, LogLevel.Warning);
-    internal static LoggedMessage ExpirationExpiresExceeded => new LoggedMessage(12, LogLevel.Debug);
+    internal static LoggedMessage ResponseCached => new LoggedMessage(8, LogLevel.Information);
+    internal static LoggedMessage ResponseNotCached => new LoggedMessage(9, LogLevel.Information);
+    internal static LoggedMessage ResponseContentLengthMismatchNotCached => new LoggedMessage(10, LogLevel.Warning);
+    internal static LoggedMessage ExpirationExpiresExceeded => new LoggedMessage(11, LogLevel.Debug);
 
     private LoggedMessage(int evenId, LogLevel logLevel)
     {