From f125329ed70a5aeaa28d5ecfaf433aed8e318a59 Mon Sep 17 00:00:00 2001 From: John Luo Date: Mon, 10 Jul 2017 03:18:17 -0700 Subject: [PATCH] Use private instance of MemoryCache and impose size limit --- .../Internal/CacheEntry/CacheEntryHelpers .cs | 88 +++++++++++++++++++ .../Internal/MemoryResponseCache.cs | 6 +- .../ResponseCachingMiddleware.cs | 19 ++++ .../ResponseCachingOptions.cs | 5 ++ .../ResponseCachingServicesExtensions.cs | 1 - .../ResponseCachingMiddlewareTests.cs | 33 +++++++ 6 files changed, 149 insertions(+), 3 deletions(-) create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CacheEntryHelpers .cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CacheEntryHelpers .cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CacheEntryHelpers .cs new file mode 100644 index 0000000..f23286a --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CacheEntryHelpers .cs @@ -0,0 +1,88 @@ +// 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 Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.ResponseCaching.Internal +{ + internal static class CacheEntryHelpers + { + + internal static long EstimateCachedResponseSize(CachedResponse cachedResponse) + { + if (cachedResponse == null) + { + return 0L; + } + + checked + { + // StatusCode + long size = sizeof(int); + + // Headers + if (cachedResponse.Headers != null) + { + foreach (var item in cachedResponse.Headers) + { + size += item.Key.Length * sizeof(char) + EstimateStringValuesSize(item.Value); + } + } + + // Body + if (cachedResponse.Body != null) + { + size += cachedResponse.Body.Length; + } + + return size; + } + } + + internal static long EstimateCachedVaryByRulesySize(CachedVaryByRules cachedVaryByRules) + { + if (cachedVaryByRules == null) + { + return 0L; + } + + checked + { + var size = 0L; + + // VaryByKeyPrefix + if (!string.IsNullOrEmpty(cachedVaryByRules.VaryByKeyPrefix)) + { + size = cachedVaryByRules.VaryByKeyPrefix.Length * sizeof(char); + } + + // Headers + size += EstimateStringValuesSize(cachedVaryByRules.Headers); + + // QueryKeys + size += EstimateStringValuesSize(cachedVaryByRules.QueryKeys); + + return size; + } + } + + internal static long EstimateStringValuesSize(StringValues stringValues) + { + checked + { + var size = 0L; + + for (var i = 0; i < stringValues.Count; i++) + { + var stringValue = stringValues[i]; + if (!string.IsNullOrEmpty(stringValue)) + { + size += stringValues[i].Length * sizeof(char); + } + } + + return size; + } + } + } +} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs index c96ba5e..d69d48e 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs @@ -67,7 +67,8 @@ public void Set(string key, IResponseCacheEntry entry, TimeSpan validFor) }, new MemoryCacheEntryOptions { - AbsoluteExpirationRelativeToNow = validFor + AbsoluteExpirationRelativeToNow = validFor, + Size = CacheEntryHelpers.EstimateCachedResponseSize(cachedResponse) }); } else @@ -77,7 +78,8 @@ public void Set(string key, IResponseCacheEntry entry, TimeSpan validFor) entry, new MemoryCacheEntryOptions { - AbsoluteExpirationRelativeToNow = validFor + AbsoluteExpirationRelativeToNow = validFor, + Size = CacheEntryHelpers.EstimateCachedVaryByRulesySize(entry as CachedVaryByRules) }); } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs index 1073181..d2eee86 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.ResponseCaching.Internal; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; @@ -26,6 +27,24 @@ public class ResponseCachingMiddleware private readonly IResponseCachingKeyProvider _keyProvider; public ResponseCachingMiddleware( + RequestDelegate next, + IOptions options, + ILoggerFactory loggerFactory, + IResponseCachingPolicyProvider policyProvider, + IResponseCachingKeyProvider keyProvider) + : this( + next, + options, + loggerFactory, + policyProvider, + new MemoryResponseCache(new MemoryCache(new MemoryCacheOptions + { + SizeLimit = options.Value.SizeLimit + })), keyProvider) + { } + + // for testing + internal ResponseCachingMiddleware( RequestDelegate next, IOptions options, ILoggerFactory loggerFactory, diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingOptions.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingOptions.cs index 89f9d32..4fa75e2 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingOptions.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingOptions.cs @@ -8,6 +8,11 @@ namespace Microsoft.AspNetCore.ResponseCaching { public class ResponseCachingOptions { + /// + /// The size limit for the response cache middleware in bytes. The default is set to 100 MB. + /// + public long SizeLimit { get; set; } = 100 * 1024 * 1024; + /// /// The largest cacheable size for the response body in bytes. The default is set to 64 MB. /// diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingServicesExtensions.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingServicesExtensions.cs index 99187df..ef6f815 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingServicesExtensions.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingServicesExtensions.cs @@ -28,7 +28,6 @@ public static IServiceCollection AddResponseCaching(this IServiceCollection serv services.AddMemoryCache(); services.TryAdd(ServiceDescriptor.Singleton()); services.TryAdd(ServiceDescriptor.Singleton()); - services.TryAdd(ServiceDescriptor.Singleton()); return services; } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs index 64aa0e3..8f7076a 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs @@ -7,6 +7,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.ResponseCaching.Internal; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; @@ -823,6 +824,38 @@ public async Task FinalizeCacheBody_DoNotCache_IfBufferingDisabled() LoggedMessage.ResponseNotCached); } + [Fact] + public async Task FinalizeCacheBody_DoNotCache_IfSizeTooBig() + { + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware( + testSink: sink, + keyProvider: new TestResponseCachingKeyProvider("BaseKey"), + cache: new MemoryResponseCache(new MemoryCache(new MemoryCacheOptions + { + SizeLimit = 100 + }))); + var context = TestUtils.CreateTestContext(); + + context.ShouldCacheResponse = true; + middleware.ShimResponseStream(context); + + await context.HttpContext.Response.WriteAsync(new string('0', 101)); + + context.CachedResponse = new CachedResponse() { Headers = new HeaderDictionary() }; + context.CachedResponseValidFor = TimeSpan.FromSeconds(10); + + await middleware.FinalizeCacheBodyAsync(context); + + // The response cached message will be logged but the adding of the entry will no-op + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.ResponseCached); + + // The entry cannot be retrieved + Assert.False(await middleware.TryServeFromCacheAsync(context)); + } + [Fact] public void AddResponseCachingFeature_SecondInvocation_Throws() {