Skip to content
This repository was archived by the owner on Nov 22, 2018. It is now read-only.

Commit f125329

Browse files
committed
Use private instance of MemoryCache and impose size limit
1 parent 46e0a26 commit f125329

File tree

6 files changed

+149
-3
lines changed

6 files changed

+149
-3
lines changed
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Microsoft.Extensions.Primitives;
5+
6+
namespace Microsoft.AspNetCore.ResponseCaching.Internal
7+
{
8+
internal static class CacheEntryHelpers
9+
{
10+
11+
internal static long EstimateCachedResponseSize(CachedResponse cachedResponse)
12+
{
13+
if (cachedResponse == null)
14+
{
15+
return 0L;
16+
}
17+
18+
checked
19+
{
20+
// StatusCode
21+
long size = sizeof(int);
22+
23+
// Headers
24+
if (cachedResponse.Headers != null)
25+
{
26+
foreach (var item in cachedResponse.Headers)
27+
{
28+
size += item.Key.Length * sizeof(char) + EstimateStringValuesSize(item.Value);
29+
}
30+
}
31+
32+
// Body
33+
if (cachedResponse.Body != null)
34+
{
35+
size += cachedResponse.Body.Length;
36+
}
37+
38+
return size;
39+
}
40+
}
41+
42+
internal static long EstimateCachedVaryByRulesySize(CachedVaryByRules cachedVaryByRules)
43+
{
44+
if (cachedVaryByRules == null)
45+
{
46+
return 0L;
47+
}
48+
49+
checked
50+
{
51+
var size = 0L;
52+
53+
// VaryByKeyPrefix
54+
if (!string.IsNullOrEmpty(cachedVaryByRules.VaryByKeyPrefix))
55+
{
56+
size = cachedVaryByRules.VaryByKeyPrefix.Length * sizeof(char);
57+
}
58+
59+
// Headers
60+
size += EstimateStringValuesSize(cachedVaryByRules.Headers);
61+
62+
// QueryKeys
63+
size += EstimateStringValuesSize(cachedVaryByRules.QueryKeys);
64+
65+
return size;
66+
}
67+
}
68+
69+
internal static long EstimateStringValuesSize(StringValues stringValues)
70+
{
71+
checked
72+
{
73+
var size = 0L;
74+
75+
for (var i = 0; i < stringValues.Count; i++)
76+
{
77+
var stringValue = stringValues[i];
78+
if (!string.IsNullOrEmpty(stringValue))
79+
{
80+
size += stringValues[i].Length * sizeof(char);
81+
}
82+
}
83+
84+
return size;
85+
}
86+
}
87+
}
88+
}

src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ public void Set(string key, IResponseCacheEntry entry, TimeSpan validFor)
6767
},
6868
new MemoryCacheEntryOptions
6969
{
70-
AbsoluteExpirationRelativeToNow = validFor
70+
AbsoluteExpirationRelativeToNow = validFor,
71+
Size = CacheEntryHelpers.EstimateCachedResponseSize(cachedResponse)
7172
});
7273
}
7374
else
@@ -77,7 +78,8 @@ public void Set(string key, IResponseCacheEntry entry, TimeSpan validFor)
7778
entry,
7879
new MemoryCacheEntryOptions
7980
{
80-
AbsoluteExpirationRelativeToNow = validFor
81+
AbsoluteExpirationRelativeToNow = validFor,
82+
Size = CacheEntryHelpers.EstimateCachedVaryByRulesySize(entry as CachedVaryByRules)
8183
});
8284
}
8385
}

src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using Microsoft.AspNetCore.Http;
88
using Microsoft.AspNetCore.Http.Features;
99
using Microsoft.AspNetCore.ResponseCaching.Internal;
10+
using Microsoft.Extensions.Caching.Memory;
1011
using Microsoft.Extensions.Logging;
1112
using Microsoft.Extensions.Options;
1213
using Microsoft.Extensions.Primitives;
@@ -26,6 +27,24 @@ public class ResponseCachingMiddleware
2627
private readonly IResponseCachingKeyProvider _keyProvider;
2728

2829
public ResponseCachingMiddleware(
30+
RequestDelegate next,
31+
IOptions<ResponseCachingOptions> options,
32+
ILoggerFactory loggerFactory,
33+
IResponseCachingPolicyProvider policyProvider,
34+
IResponseCachingKeyProvider keyProvider)
35+
: this(
36+
next,
37+
options,
38+
loggerFactory,
39+
policyProvider,
40+
new MemoryResponseCache(new MemoryCache(new MemoryCacheOptions
41+
{
42+
SizeLimit = options.Value.SizeLimit
43+
})), keyProvider)
44+
{ }
45+
46+
// for testing
47+
internal ResponseCachingMiddleware(
2948
RequestDelegate next,
3049
IOptions<ResponseCachingOptions> options,
3150
ILoggerFactory loggerFactory,

src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingOptions.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ namespace Microsoft.AspNetCore.ResponseCaching
88
{
99
public class ResponseCachingOptions
1010
{
11+
/// <summary>
12+
/// The size limit for the response cache middleware in bytes. The default is set to 100 MB.
13+
/// </summary>
14+
public long SizeLimit { get; set; } = 100 * 1024 * 1024;
15+
1116
/// <summary>
1217
/// The largest cacheable size for the response body in bytes. The default is set to 64 MB.
1318
/// </summary>

src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingServicesExtensions.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ public static IServiceCollection AddResponseCaching(this IServiceCollection serv
2828
services.AddMemoryCache();
2929
services.TryAdd(ServiceDescriptor.Singleton<IResponseCachingPolicyProvider, ResponseCachingPolicyProvider>());
3030
services.TryAdd(ServiceDescriptor.Singleton<IResponseCachingKeyProvider, ResponseCachingKeyProvider>());
31-
services.TryAdd(ServiceDescriptor.Singleton<IResponseCache, MemoryResponseCache>());
3231

3332
return services;
3433
}

test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using Microsoft.AspNetCore.Http;
88
using Microsoft.AspNetCore.Http.Features;
99
using Microsoft.AspNetCore.ResponseCaching.Internal;
10+
using Microsoft.Extensions.Caching.Memory;
1011
using Microsoft.Extensions.Logging.Testing;
1112
using Microsoft.Extensions.Primitives;
1213
using Microsoft.Net.Http.Headers;
@@ -823,6 +824,38 @@ public async Task FinalizeCacheBody_DoNotCache_IfBufferingDisabled()
823824
LoggedMessage.ResponseNotCached);
824825
}
825826

827+
[Fact]
828+
public async Task FinalizeCacheBody_DoNotCache_IfSizeTooBig()
829+
{
830+
var sink = new TestSink();
831+
var middleware = TestUtils.CreateTestMiddleware(
832+
testSink: sink,
833+
keyProvider: new TestResponseCachingKeyProvider("BaseKey"),
834+
cache: new MemoryResponseCache(new MemoryCache(new MemoryCacheOptions
835+
{
836+
SizeLimit = 100
837+
})));
838+
var context = TestUtils.CreateTestContext();
839+
840+
context.ShouldCacheResponse = true;
841+
middleware.ShimResponseStream(context);
842+
843+
await context.HttpContext.Response.WriteAsync(new string('0', 101));
844+
845+
context.CachedResponse = new CachedResponse() { Headers = new HeaderDictionary() };
846+
context.CachedResponseValidFor = TimeSpan.FromSeconds(10);
847+
848+
await middleware.FinalizeCacheBodyAsync(context);
849+
850+
// The response cached message will be logged but the adding of the entry will no-op
851+
TestUtils.AssertLoggedMessages(
852+
sink.Writes,
853+
LoggedMessage.ResponseCached);
854+
855+
// The entry cannot be retrieved
856+
Assert.False(await middleware.TryServeFromCacheAsync(context));
857+
}
858+
826859
[Fact]
827860
public void AddResponseCachingFeature_SecondInvocation_Throws()
828861
{

0 commit comments

Comments
 (0)