aspnetcore/src/Microsoft.AspNetCore.Respon.../ResponseCacheKeyProvider.cs

153 lines
5.6 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.AspNetCore.Builder;
using Microsoft.Extensions.ObjectPool;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.ResponseCaching
{
public class ResponseCacheKeyProvider : IResponseCacheKeyProvider
{
// 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 ResponseCacheOptions _options;
public ResponseCacheKeyProvider(ObjectPoolProvider poolProvider, IOptions<ResponseCacheOptions> options)
{
if (poolProvider == null)
{
throw new ArgumentNullException(nameof(poolProvider));
}
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
_builderPool = poolProvider.CreateStringBuilderPool();
_options = options.Value;
}
public virtual IEnumerable<string> CreateLookupVaryByKeys(ResponseCacheContext context)
{
return new string[] { CreateStorageVaryByKey(context) };
}
// GET<delimiter>/PATH
public virtual string CreateBaseKey(ResponseCacheContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var request = context.HttpContext.Request;
var builder = _builderPool.Get();
try
{
builder
.Append(request.Method.ToUpperInvariant())
.Append(KeyDelimiter)
.Append(_options.UseCaseSensitivePaths ? request.Path.Value : request.Path.Value.ToUpperInvariant());
return builder.ToString();;
}
finally
{
_builderPool.Return(builder);
}
}
// BaseKey<delimiter>H<delimiter>HeaderName=HeaderValue<delimiter>Q<delimiter>QueryName=QueryValue
public virtual string CreateStorageVaryByKey(ResponseCacheContext 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(ResponseCacheContext)}");
}
if ((StringValues.IsNullOrEmpty(varyByRules.Headers) && StringValues.IsNullOrEmpty(varyByRules.Params)))
{
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 params
if (varyByRules?.Params.Count > 0)
{
// Append a group separator for the query parameter segment of the cache key
builder.Append(KeyDelimiter)
.Append('Q');
if (varyByRules.Params.Count == 1 && string.Equals(varyByRules.Params[0], "*", StringComparison.Ordinal))
{
// Vary by all available query params
foreach (var query in context.HttpContext.Request.Query.OrderBy(q => q.Key, StringComparer.OrdinalIgnoreCase))
{
builder.Append(KeyDelimiter)
.Append(query.Key.ToUpperInvariant())
.Append("=")
.Append(query.Value);
}
}
else
{
foreach (var param in varyByRules.Params)
{
builder.Append(KeyDelimiter)
.Append(param)
.Append("=")
// TODO: Perf - iterate the string values instead?
.Append(context.HttpContext.Request.Query[param]);
}
}
}
return builder.ToString();
}
finally
{
_builderPool.Return(builder);
}
}
}
}