Make DistributedResponseCacheStore internal (#61)

Remove DistributedResponseCacheStore and move all interfaces to the Internal namespace
This commit is contained in:
John Luo 2016-09-30 11:24:44 -07:00 committed by GitHub
parent 24385e74c4
commit aa52e66585
18 changed files with 15 additions and 550 deletions

View File

@ -15,7 +15,7 @@ namespace ResponseCachingSample
{
public void ConfigureServices(IServiceCollection services)
{
services.AddDistributedResponseCacheStore();
services.AddMemoryResponseCacheStore();
}
public void Configure(IApplicationBuilder app)
@ -23,7 +23,6 @@ namespace ResponseCachingSample
app.UseResponseCache();
app.Run(async (context) =>
{
// These settings should be configured by context.Response.Cache.*
context.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
{
Public = true,

View File

@ -18,29 +18,9 @@ namespace Microsoft.Extensions.DependencyInjection
}
services.AddMemoryCache();
services.AddResponseCacheServices();
services.TryAdd(ServiceDescriptor.Singleton<IResponseCacheStore, MemoryResponseCacheStore>());
return services;
}
public static IServiceCollection AddDistributedResponseCacheStore(this IServiceCollection services)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
services.AddDistributedMemoryCache();
services.AddResponseCacheServices();
services.TryAdd(ServiceDescriptor.Singleton<IResponseCacheStore, DistributedResponseCacheStore>());
return services;
}
private static IServiceCollection AddResponseCacheServices(this IServiceCollection services)
{
services.TryAdd(ServiceDescriptor.Singleton<IResponseCachePolicyProvider, ResponseCachePolicyProvider>());
services.TryAdd(ServiceDescriptor.Singleton<IResponseCacheKeyProvider, ResponseCacheKeyProvider>());
services.TryAdd(ServiceDescriptor.Singleton<IResponseCacheStore, MemoryResponseCacheStore>());
return services;
}

View File

@ -5,7 +5,7 @@ using System;
using System.IO;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.ResponseCaching
namespace Microsoft.AspNetCore.ResponseCaching.Internal
{
public class CachedResponse : IResponseCacheEntry
{

View File

@ -3,7 +3,7 @@
using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.ResponseCaching
namespace Microsoft.AspNetCore.ResponseCaching.Internal
{
public class CachedVaryByRules : IResponseCacheEntry
{

View File

@ -1,51 +0,0 @@
// 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.Threading.Tasks;
using Microsoft.Extensions.Caching.Distributed;
namespace Microsoft.AspNetCore.ResponseCaching.Internal
{
public class DistributedResponseCacheStore : IResponseCacheStore
{
private readonly IDistributedCache _cache;
public DistributedResponseCacheStore(IDistributedCache cache)
{
if (cache == null)
{
throw new ArgumentNullException(nameof(cache));
}
_cache = cache;
}
public async Task<IResponseCacheEntry> GetAsync(string key)
{
try
{
return ResponseCacheEntrySerializer.Deserialize(await _cache.GetAsync(key));
}
catch
{
return null;
}
}
public async Task SetAsync(string key, IResponseCacheEntry entry, TimeSpan validFor)
{
try
{
await _cache.SetAsync(
key,
ResponseCacheEntrySerializer.Serialize(entry),
new DistributedCacheEntryOptions()
{
AbsoluteExpirationRelativeToNow = validFor
});
}
catch { }
}
}
}

View File

@ -54,7 +54,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
private static unsafe string GenerateGuidString(FastGuid guid)
{
// stackalloc to allocate array on stack rather than heap
char* charBuffer = stackalloc char[13];

View File

@ -1,7 +1,7 @@
// 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.
namespace Microsoft.AspNetCore.ResponseCaching
namespace Microsoft.AspNetCore.ResponseCaching.Internal
{
public interface IResponseCacheEntry
{

View File

@ -5,7 +5,7 @@ using System.Collections.Generic;
namespace Microsoft.AspNetCore.ResponseCaching.Internal
{
internal interface IResponseCacheKeyProvider
public interface IResponseCacheKeyProvider
{
/// <summary>
/// Create a base key for a response cache entry.

View File

@ -1,7 +1,7 @@
// 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.
namespace Microsoft.AspNetCore.ResponseCaching
namespace Microsoft.AspNetCore.ResponseCaching.Internal
{
public interface IResponseCachePolicyProvider
{

View File

@ -4,7 +4,7 @@
using System;
using System.Threading.Tasks;
namespace Microsoft.AspNetCore.ResponseCaching
namespace Microsoft.AspNetCore.ResponseCaching.Internal
{
public interface IResponseCacheStore
{

View File

@ -6,10 +6,9 @@ using System.IO;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Http.Headers;
using Microsoft.AspNetCore.ResponseCaching.Internal;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.ResponseCaching
namespace Microsoft.AspNetCore.ResponseCaching.Internal
{
public class ResponseCacheContext
{

View File

@ -1,249 +0,0 @@
// 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.IO;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.ResponseCaching.Internal
{
internal static class ResponseCacheEntrySerializer
{
private const int FormatVersion = 1;
internal static IResponseCacheEntry Deserialize(byte[] serializedEntry)
{
if (serializedEntry == null)
{
return null;
}
using (var memory = new MemoryStream(serializedEntry))
{
using (var reader = new BinaryReader(memory))
{
return Read(reader);
}
}
}
internal static byte[] Serialize(IResponseCacheEntry entry)
{
using (var memory = new MemoryStream())
{
using (var writer = new BinaryWriter(memory))
{
Write(writer, entry);
writer.Flush();
return memory.ToArray();
}
}
}
// Serialization Format
// Format version (int)
// Type (char: 'R' for CachedResponse, 'V' for CachedVaryByRules)
// Type-dependent data (see CachedResponse and CachedVaryByRules)
private static IResponseCacheEntry Read(BinaryReader reader)
{
if (reader == null)
{
throw new ArgumentNullException(nameof(reader));
}
if (reader.ReadInt32() != FormatVersion)
{
return null;
}
var type = reader.ReadChar();
if (type == 'R')
{
return ReadCachedResponse(reader);
}
else if (type == 'V')
{
return ReadCachedVaryByRules(reader);
}
// Unable to read as CachedResponse or CachedVaryByRules
return null;
}
// Serialization Format
// Creation time - UtcTicks (long)
// Status code (int)
// Header count (int)
// Header(s)
// Key (string)
// ValueCount (int)
// Value(s)
// Value (string)
// BodyLength (int)
// Body (byte[])
private static CachedResponse ReadCachedResponse(BinaryReader reader)
{
var created = new DateTimeOffset(reader.ReadInt64(), TimeSpan.Zero);
var statusCode = reader.ReadInt32();
var headerCount = reader.ReadInt32();
var headers = new HeaderDictionary();
for (var index = 0; index < headerCount; index++)
{
var key = reader.ReadString();
var headerValueCount = reader.ReadInt32();
if (headerValueCount > 1)
{
var headerValues = new string[headerValueCount];
for (var valueIndex = 0; valueIndex < headerValueCount; valueIndex++)
{
headerValues[valueIndex] = reader.ReadString();
}
headers[key] = headerValues;
}
else if (headerValueCount == 1)
{
headers[key] = reader.ReadString();
}
}
var bodyLength = reader.ReadInt32();
var bodyBytes = reader.ReadBytes(bodyLength);
return new CachedResponse
{
Created = created,
StatusCode = statusCode,
Headers = headers,
Body = new MemoryStream(bodyBytes, writable: false)
};
}
// Serialization Format
// VaryKeyPrefix (string)
// Headers count
// Header(s) (comma separated string)
// QueryKey count
// QueryKey(s) (comma separated string)
private static CachedVaryByRules ReadCachedVaryByRules(BinaryReader reader)
{
var varyKeyPrefix = reader.ReadString();
var headerCount = reader.ReadInt32();
var headers = new string[headerCount];
for (var index = 0; index < headerCount; index++)
{
headers[index] = reader.ReadString();
}
var queryKeysCount = reader.ReadInt32();
var queryKeys = new string[queryKeysCount];
for (var index = 0; index < queryKeysCount; index++)
{
queryKeys[index] = reader.ReadString();
}
return new CachedVaryByRules { VaryByKeyPrefix = varyKeyPrefix, Headers = headers, QueryKeys = queryKeys };
}
// See serialization format above
private static void Write(BinaryWriter writer, IResponseCacheEntry entry)
{
if (writer == null)
{
throw new ArgumentNullException(nameof(writer));
}
if (entry == null)
{
throw new ArgumentNullException(nameof(entry));
}
writer.Write(FormatVersion);
if (entry is CachedResponse)
{
writer.Write('R');
WriteCachedResponse(writer, (CachedResponse)entry);
}
else if (entry is CachedVaryByRules)
{
writer.Write('V');
WriteCachedVaryByRules(writer, (CachedVaryByRules)entry);
}
else
{
throw new NotSupportedException($"Unrecognized entry type for {nameof(entry)}.");
}
}
// See serialization format above
private static void WriteCachedResponse(BinaryWriter writer, CachedResponse entry)
{
writer.Write(entry.Created.UtcTicks);
writer.Write(entry.StatusCode);
writer.Write(entry.Headers.Count);
foreach (var header in entry.Headers)
{
writer.Write(header.Key);
writer.Write(header.Value.Count);
foreach (var headerValue in header.Value)
{
writer.Write(headerValue);
}
}
if (entry.Body.CanSeek)
{
if (entry.Body.Length > int.MaxValue)
{
throw new NotSupportedException($"{nameof(entry.Body)} is too large to serialized.");
}
var bodyLength = (int)entry.Body.Length;
var bodyBytes = new byte[bodyLength];
var bytesRead = entry.Body.Read(bodyBytes, 0, bodyLength);
if (bytesRead != bodyLength)
{
throw new InvalidOperationException($"Failed to fully read {nameof(entry.Body)}.");
}
writer.Write(bodyLength);
writer.Write(bodyBytes);
}
else
{
var stream = new MemoryStream();
entry.Body.CopyTo(stream);
if (stream.Length > int.MaxValue)
{
throw new NotSupportedException($"{nameof(entry.Body)} is too large to serialized.");
}
var bodyLength = (int)stream.Length;
writer.Write(bodyLength);
writer.Write(stream.ToArray());
}
}
// See serialization format above
private static void WriteCachedVaryByRules(BinaryWriter writer, CachedVaryByRules varyByRules)
{
writer.Write(varyByRules.VaryByKeyPrefix);
writer.Write(varyByRules.Headers.Count);
foreach (var header in varyByRules.Headers)
{
writer.Write(header);
}
writer.Write(varyByRules.QueryKeys.Count);
foreach (var queryKey in varyByRules.QueryKeys)
{
writer.Write(queryKey);
}
}
}
}

View File

@ -12,7 +12,7 @@ using Microsoft.Extensions.Primitives;
namespace Microsoft.AspNetCore.ResponseCaching.Internal
{
internal class ResponseCacheKeyProvider : IResponseCacheKeyProvider
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';
@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal
private readonly ObjectPool<StringBuilder> _builderPool;
private readonly ResponseCacheOptions _options;
internal ResponseCacheKeyProvider(ObjectPoolProvider poolProvider, IOptions<ResponseCacheOptions> options)
public ResponseCacheKeyProvider(ObjectPoolProvider poolProvider, IOptions<ResponseCacheOptions> options)
{
if (poolProvider == null)
{

View File

@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.ResponseCaching
namespace Microsoft.AspNetCore.ResponseCaching.Internal
{
public class ResponseCachePolicyProvider : IResponseCachePolicyProvider
{

View File

@ -30,20 +30,9 @@ namespace Microsoft.AspNetCore.ResponseCaching
public ResponseCacheMiddleware(
RequestDelegate next,
IResponseCacheStore store,
IOptions<ResponseCacheOptions> options,
IResponseCachePolicyProvider policyProvider,
ObjectPoolProvider poolProvider)
:this (next, store, options, policyProvider, new ResponseCacheKeyProvider(poolProvider, options))
{
}
// Internal for testing
internal ResponseCacheMiddleware(
RequestDelegate next,
IResponseCacheStore store,
IOptions<ResponseCacheOptions> options,
IResponseCachePolicyProvider policyProvider,
IResponseCacheKeyProvider keyProvider)
{
if (next == null)

View File

@ -1,189 +0,0 @@
// 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.Http;
using Microsoft.AspNetCore.ResponseCaching.Internal;
using Microsoft.Extensions.Primitives;
using Xunit;
namespace Microsoft.AspNetCore.ResponseCaching.Tests
{
public class CacheEntrySerializerTests
{
[Fact]
public void Serialize_NullObject_Throws()
{
Assert.Throws<ArgumentNullException>(() => ResponseCacheEntrySerializer.Serialize(null));
}
private class UnknownResponseCacheEntry : IResponseCacheEntry
{
}
[Fact]
public void Serialize_UnknownObject_Throws()
{
Assert.Throws<NotSupportedException>(() => ResponseCacheEntrySerializer.Serialize(new UnknownResponseCacheEntry()));
}
[Fact]
public void Deserialize_NullObject_ReturnsNull()
{
Assert.Null(ResponseCacheEntrySerializer.Deserialize(null));
}
[Fact]
public void RoundTrip_CachedResponseWithBody_Succeeds()
{
var headers = new HeaderDictionary();
headers["keyA"] = "valueA";
headers["keyB"] = "valueB";
var body = Encoding.ASCII.GetBytes("Hello world");
var cachedResponse = new CachedResponse()
{
Created = DateTimeOffset.UtcNow,
StatusCode = StatusCodes.Status200OK,
Body = new SegmentReadStream(new List<byte[]>(new[] { body }), body.Length),
Headers = headers
};
AssertCachedResponseEqual(cachedResponse, (CachedResponse)ResponseCacheEntrySerializer.Deserialize(ResponseCacheEntrySerializer.Serialize(cachedResponse)));
}
[Fact]
public void RoundTrip_CachedResponseWithMultivalueHeaders_Succeeds()
{
var headers = new HeaderDictionary();
headers["keyA"] = new StringValues(new[] { "ValueA", "ValueB" });
var body = Encoding.ASCII.GetBytes("Hello world");
var cachedResponse = new CachedResponse()
{
Created = DateTimeOffset.UtcNow,
StatusCode = StatusCodes.Status200OK,
Body = new SegmentReadStream(new List<byte[]>(new[] { body }), body.Length),
Headers = headers
};
AssertCachedResponseEqual(cachedResponse, (CachedResponse)ResponseCacheEntrySerializer.Deserialize(ResponseCacheEntrySerializer.Serialize(cachedResponse)));
}
[Fact]
public void RoundTrip_CachedResponseWithEmptyHeaders_Succeeds()
{
var headers = new HeaderDictionary();
headers["keyA"] = StringValues.Empty;
var body = Encoding.ASCII.GetBytes("Hello world");
var cachedResponse = new CachedResponse()
{
Created = DateTimeOffset.UtcNow,
StatusCode = StatusCodes.Status200OK,
Body = new SegmentReadStream(new List<byte[]>(new[] { body }), body.Length),
Headers = headers
};
AssertCachedResponseEqual(cachedResponse, (CachedResponse)ResponseCacheEntrySerializer.Deserialize(ResponseCacheEntrySerializer.Serialize(cachedResponse)));
}
[Fact]
public void RoundTrip_CachedVaryByRule_EmptyRules_Succeeds()
{
var cachedVaryByRule = new CachedVaryByRules()
{
VaryByKeyPrefix = FastGuid.NewGuid().IdString
};
AssertCachedVaryByRuleEqual(cachedVaryByRule, (CachedVaryByRules)ResponseCacheEntrySerializer.Deserialize(ResponseCacheEntrySerializer.Serialize(cachedVaryByRule)));
}
[Fact]
public void RoundTrip_CachedVaryByRule_HeadersOnly_Succeeds()
{
var headers = new[] { "headerA", "headerB" };
var cachedVaryByRule = new CachedVaryByRules()
{
VaryByKeyPrefix = FastGuid.NewGuid().IdString,
Headers = headers
};
AssertCachedVaryByRuleEqual(cachedVaryByRule, (CachedVaryByRules)ResponseCacheEntrySerializer.Deserialize(ResponseCacheEntrySerializer.Serialize(cachedVaryByRule)));
}
[Fact]
public void RoundTrip_CachedVaryByRule_QueryKeysOnly_Succeeds()
{
var queryKeys = new[] { "queryA", "queryB" };
var cachedVaryByRule = new CachedVaryByRules()
{
VaryByKeyPrefix = FastGuid.NewGuid().IdString,
QueryKeys = queryKeys
};
AssertCachedVaryByRuleEqual(cachedVaryByRule, (CachedVaryByRules)ResponseCacheEntrySerializer.Deserialize(ResponseCacheEntrySerializer.Serialize(cachedVaryByRule)));
}
[Fact]
public void RoundTrip_CachedVaryByRule_HeadersAndQueryKeys_Succeeds()
{
var headers = new[] { "headerA", "headerB" };
var queryKeys = new[] { "queryA", "queryB" };
var cachedVaryByRule = new CachedVaryByRules()
{
VaryByKeyPrefix = FastGuid.NewGuid().IdString,
Headers = headers,
QueryKeys = queryKeys
};
AssertCachedVaryByRuleEqual(cachedVaryByRule, (CachedVaryByRules)ResponseCacheEntrySerializer.Deserialize(ResponseCacheEntrySerializer.Serialize(cachedVaryByRule)));
}
[Fact]
public void Deserialize_InvalidEntries_ReturnsNull()
{
var headers = new[] { "headerA", "headerB" };
var cachedVaryByRule = new CachedVaryByRules()
{
VaryByKeyPrefix = FastGuid.NewGuid().IdString,
Headers = headers
};
var serializedEntry = ResponseCacheEntrySerializer.Serialize(cachedVaryByRule);
Array.Reverse(serializedEntry);
Assert.Null(ResponseCacheEntrySerializer.Deserialize(serializedEntry));
}
private static void AssertCachedResponseEqual(CachedResponse expected, CachedResponse actual)
{
Assert.NotNull(actual);
Assert.NotNull(expected);
Assert.Equal(expected.Created, actual.Created);
Assert.Equal(expected.StatusCode, actual.StatusCode);
Assert.Equal(expected.Headers.Count, actual.Headers.Count);
foreach (var expectedHeader in expected.Headers)
{
Assert.Equal(expectedHeader.Value, actual.Headers[expectedHeader.Key]);
}
Assert.Equal(expected.Body.Length, actual.Body.Length);
var bodyLength = (int)expected.Body.Length;
var expectedBytes = new byte[bodyLength];
var actualBytes = new byte[bodyLength];
expected.Body.Position = 0; // Rewind
Assert.Equal(bodyLength, expected.Body.Read(expectedBytes, 0, bodyLength));
Assert.Equal(bodyLength, actual.Body.Read(actualBytes, 0, bodyLength));
Assert.True(expectedBytes.SequenceEqual(actualBytes));
}
private static void AssertCachedVaryByRuleEqual(CachedVaryByRules expected, CachedVaryByRules actual)
{
Assert.NotNull(actual);
Assert.NotNull(expected);
Assert.Equal(expected.VaryByKeyPrefix, actual.VaryByKeyPrefix);
Assert.Equal(expected.Headers, actual.Headers);
Assert.Equal(expected.QueryKeys, actual.QueryKeys);
}
}
}

View File

@ -4,6 +4,7 @@
using System;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Headers;
using Microsoft.AspNetCore.ResponseCaching.Internal;
using Microsoft.Net.Http.Headers;
using Xunit;

View File

@ -81,19 +81,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
app.UseResponseCache(options);
app.Run(requestDelegate);
});
// Test with DistributedResponseCacheStore
yield return new WebHostBuilder()
.ConfigureServices(services =>
{
services.AddDistributedResponseCacheStore();
})
.Configure(app =>
{
configureDelegate(app);
app.UseResponseCache(options);
app.Run(requestDelegate);
});
}
internal static ResponseCacheMiddleware CreateTestMiddleware(
@ -121,9 +108,9 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests
return new ResponseCacheMiddleware(
httpContext => TaskCache.CompletedTask,
store,
Options.Create(options),
policyProvider,
store,
keyProvider);
}