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

Use private instance of MemoryCache and impose size limit #132

Merged
merged 1 commit into from
Jul 10, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ public void Set(string key, IResponseCacheEntry entry, TimeSpan validFor)
},
new MemoryCacheEntryOptions
{
AbsoluteExpirationRelativeToNow = validFor
AbsoluteExpirationRelativeToNow = validFor,
Size = CacheEntryHelpers.EstimateCachedResponseSize(cachedResponse)
});
}
else
Expand All @@ -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)
});
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -26,6 +27,24 @@ public class ResponseCachingMiddleware
private readonly IResponseCachingKeyProvider _keyProvider;

public ResponseCachingMiddleware(
RequestDelegate next,
IOptions<ResponseCachingOptions> options,
ILoggerFactory loggerFactory,
IResponseCachingPolicyProvider policyProvider,
IResponseCachingKeyProvider keyProvider)
: this(
next,
options,
loggerFactory,
policyProvider,
new MemoryResponseCache(new MemoryCache(new MemoryCacheOptions
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alternatively, we can add the MemoryResponseCache to DI but have it resolve ResponseCachingOptions to figure out the size.

{
SizeLimit = options.Value.SizeLimit
})), keyProvider)
{ }

// for testing
internal ResponseCachingMiddleware(
RequestDelegate next,
IOptions<ResponseCachingOptions> options,
ILoggerFactory loggerFactory,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ namespace Microsoft.AspNetCore.ResponseCaching
{
public class ResponseCachingOptions
{
/// <summary>
/// The size limit for the response cache middleware in bytes. The default is set to 100 MB.
/// </summary>
public long SizeLimit { get; set; } = 100 * 1024 * 1024;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we have ResponseCachingOptions extend MemoryCacheOptions so other options can be set?


/// <summary>
/// The largest cacheable size for the response body in bytes. The default is set to 64 MB.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ public static IServiceCollection AddResponseCaching(this IServiceCollection serv
services.AddMemoryCache();
services.TryAdd(ServiceDescriptor.Singleton<IResponseCachingPolicyProvider, ResponseCachingPolicyProvider>());
services.TryAdd(ServiceDescriptor.Singleton<IResponseCachingKeyProvider, ResponseCachingKeyProvider>());
services.TryAdd(ServiceDescriptor.Singleton<IResponseCache, MemoryResponseCache>());

return services;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()
{
Expand Down