aspnetcore/src/Microsoft.AspNetCore.Respon.../Internal/ResponseCachingKeyProvider.cs

160 lines
5.7 KiB
C#

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.ResponseCaching.Internal
{
public class ResponseCachingKeyProvider : IResponseCachingKeyProvider
{
// Use the record separator for delimiting components of the cache key to avoid possible collisions
private static readonly char KeyDelimiter = '\x1e';
private readonly ObjectPool<StringBuilder> _builderPool;
private readonly ResponseCachingOptions _options;
public ResponseCachingKeyProvider(ObjectPoolProvider poolProvider, IOptions<ResponseCachingOptions> options)
{
if (poolProvider == null)
{
throw new ArgumentNullException(nameof(poolProvider));
}
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
_builderPool = poolProvider.CreateStringBuilderPool();
_options = options.Value;
}
public IEnumerable<string> CreateLookupVaryByKeys(ResponseCachingContext context)
{
return new string[] { CreateStorageVaryByKey(context) };
}
// GET<delimiter>/PATH
public string CreateBaseKey(ResponseCachingContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var request = context.HttpContext.Request;
var builder = _builderPool.Get();
try
{
builder
.AppendUpperInvariant(request.Method)
.Append(KeyDelimiter);
if (_options.UseCaseSensitivePaths)
{
builder.Append(request.Path.Value);
}
else
{
builder.AppendUpperInvariant(request.Path.Value);
}
return builder.ToString();
}
finally
{
_builderPool.Return(builder);
}
}
// BaseKey<delimiter>H<delimiter>HeaderName=HeaderValue<delimiter>Q<delimiter>QueryName=QueryValue
public string CreateStorageVaryByKey(ResponseCachingContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(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
if (varyByRules?.Headers.Count > 0)
{
// Append a group separator for the header segment of the cache key
builder.Append(KeyDelimiter)
.Append('H');
foreach (var header in varyByRules.Headers)
{
builder.Append(KeyDelimiter)
.Append(header)
.Append("=")
// TODO: Perf - iterate the string values instead?
.Append(context.HttpContext.Request.Headers[header]);
}
}
// 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
foreach (var query in context.HttpContext.Request.Query.OrderBy(q => q.Key, StringComparer.OrdinalIgnoreCase))
{
builder.Append(KeyDelimiter)
.AppendUpperInvariant(query.Key)
.Append("=")
.Append(query.Value);
}
}
else
{
foreach (var queryKey in varyByRules.QueryKeys)
{
builder.Append(KeyDelimiter)
.Append(queryKey)
.Append("=")
// TODO: Perf - iterate the string values instead?
.Append(context.HttpContext.Request.Query[queryKey]);
}
}
}
return builder.ToString();
}
finally
{
_builderPool.Return(builder);
}
}
}
}