| File: ResponseCachingKeyProvider.cs | Web Access |
| Project: src\aspnetcore\src\Middleware\ResponseCaching\src\Microsoft.AspNetCore.ResponseCaching.csproj (Microsoft.AspNetCore.ResponseCaching) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Linq; using System.Text; using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.ResponseCaching; internal sealed class ResponseCachingKeyProvider : IResponseCachingKeyProvider { // Use the record separator for delimiting components of the cache key to avoid possible collisions private const char KeyDelimiter = '\x1e'; // Use the unit separator for delimiting subcomponents of the cache key to avoid possible collisions private const char KeySubDelimiter = '\x1f'; private readonly ObjectPool<StringBuilder> _builderPool; private readonly ResponseCachingOptions _options; internal ResponseCachingKeyProvider(ObjectPoolProvider poolProvider, IOptions<ResponseCachingOptions> options) { ArgumentNullException.ThrowIfNull(poolProvider); ArgumentNullException.ThrowIfNull(options); _builderPool = poolProvider.CreateStringBuilderPool(); _options = options.Value; } public IEnumerable<string> CreateLookupVaryByKeys(ResponseCachingContext context) { return new string[] { CreateStorageVaryByKey(context) }; } // GET<delimiter>SCHEME<delimiter>HOST:PORT/PATHBASE/PATH public string CreateBaseKey(ResponseCachingContext context) { ArgumentNullException.ThrowIfNull(context); var request = context.HttpContext.Request; ThrowIfContainsDelimiters(request.PathBase.Value); ThrowIfContainsDelimiters(request.Path.Value); var builder = _builderPool.Get(); try { 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); } return builder.ToString(); } finally { _builderPool.Return(builder); } } // BaseKey<delimiter>H<delimiter>HeaderName=HeaderValue1<subdelimiter>HeaderValue2<delimiter>Q<delimiter>QueryName=QueryValue1<subdelimiter>QueryValue2 public string CreateStorageVaryByKey(ResponseCachingContext context) { ArgumentNullException.ThrowIfNull(context); var varyByRules = context.CachedVaryByRules; if (varyByRules == null) { throw new InvalidOperationException($"{nameof(CachedVaryByRules)} must not be null on the {nameof(ResponseCachingContext)}"); } if (StringValues.IsNullOrEmpty(varyByRules.Headers) && StringValues.IsNullOrEmpty(varyByRules.QueryKeys)) { return varyByRules.VaryByKeyPrefix; } var request = context.HttpContext.Request; var builder = _builderPool.Get(); try { // Prepend with the Guid of the CachedVaryByRules builder.Append(varyByRules.VaryByKeyPrefix); // Vary by headers var headersCount = varyByRules?.Headers.Count ?? 0; if (headersCount > 0) { // Append a group separator for the header segment of the cache key builder.Append(KeyDelimiter) .Append('H'); var requestHeaders = context.HttpContext.Request.Headers; for (var i = 0; i < headersCount; i++) { var header = varyByRules!.Headers[i] ?? string.Empty; var headerValues = requestHeaders[header]; builder.Append(KeyDelimiter) .Append(header) .Append('='); var headerValuesArray = headerValues.ToArray(); Array.Sort(headerValuesArray, StringComparer.Ordinal); for (var j = 0; j < headerValuesArray.Length; j++) { if (j > 0) { builder.Append(KeySubDelimiter); } ThrowIfContainsDelimiters(headerValuesArray[j]); builder.Append(headerValuesArray[j]); } } } // Vary by query keys if (varyByRules?.QueryKeys.Count > 0) { // 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)) { // 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++) { ThrowIfContainsDelimiters(queryArray[i].Key); builder.Append(KeyDelimiter) .AppendUpperInvariant(queryArray[i].Key) .Append('='); var queryValueArray = queryArray[i].Value.ToArray(); Array.Sort(queryValueArray, StringComparer.Ordinal); for (var j = 0; j < queryValueArray.Length; j++) { if (j > 0) { builder.Append(KeySubDelimiter); } ThrowIfContainsDelimiters(queryValueArray[j]); builder.Append(queryValueArray[j]); } } } else { for (var i = 0; i < varyByRules.QueryKeys.Count; i++) { 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) { builder.Append(KeySubDelimiter); } ThrowIfContainsDelimiters(queryValueArray[j]); builder.Append(queryValueArray[j]); } } } } return builder.ToString(); } finally { _builderPool.Return(builder); } } internal static void ThrowIfContainsDelimiters(string? value) { if (!string.IsNullOrEmpty(value) && value.ContainsAny(KeyDelimiter, KeySubDelimiter)) { throw new CacheKeyDelimiterException(); } } private sealed class QueryKeyComparer : IComparer<KeyValuePair<string, StringValues>> { private readonly StringComparer _stringComparer; public static QueryKeyComparer OrdinalIgnoreCase { get; } = new QueryKeyComparer(StringComparer.OrdinalIgnoreCase); public QueryKeyComparer(StringComparer stringComparer) { _stringComparer = stringComparer; } public int Compare(KeyValuePair<string, StringValues> x, KeyValuePair<string, StringValues> y) => _stringComparer.Compare(x.Key, y.Key); } }