IResponseCache adapter and support for vary by header
This commit is contained in:
parent
8e7eb5d99e
commit
a40cc88169
|
|
@ -15,7 +15,7 @@ namespace ResponseCachingSample
|
||||||
{
|
{
|
||||||
public void ConfigureServices(IServiceCollection services)
|
public void ConfigureServices(IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddMemoryCache();
|
services.AddDistributedResponseCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Configure(IApplicationBuilder app)
|
public void Configure(IApplicationBuilder app)
|
||||||
|
|
@ -23,11 +23,14 @@ namespace ResponseCachingSample
|
||||||
app.UseResponseCaching();
|
app.UseResponseCaching();
|
||||||
app.Run(async (context) =>
|
app.Run(async (context) =>
|
||||||
{
|
{
|
||||||
|
// These settings should be configured by context.Response.Cache.*
|
||||||
context.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
|
context.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
|
||||||
{
|
{
|
||||||
Public = true,
|
Public = true,
|
||||||
MaxAge = TimeSpan.FromSeconds(10)
|
MaxAge = TimeSpan.FromSeconds(10)
|
||||||
};
|
};
|
||||||
|
context.Response.Headers["Vary"] = new string[] { "Accept-Encoding", "Non-Existent" };
|
||||||
|
|
||||||
await context.Response.WriteAsync("Hello World! " + DateTime.UtcNow);
|
await context.Response.WriteAsync("Hello World! " + DateTime.UtcNow);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
// 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
|
||||||
|
{
|
||||||
|
public interface IResponseCache
|
||||||
|
{
|
||||||
|
object Get(string key);
|
||||||
|
// TODO: Set expiry policy in the underlying cache?
|
||||||
|
void Set(string key, object entry);
|
||||||
|
void Remove(string key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,12 +3,14 @@
|
||||||
|
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.ResponseCaching
|
namespace Microsoft.AspNetCore.ResponseCaching.Internal
|
||||||
{
|
{
|
||||||
internal class ResponseCachingEntry
|
internal class CachedResponse
|
||||||
{
|
{
|
||||||
public int StatusCode { get; set; }
|
internal int StatusCode { get; set; }
|
||||||
|
|
||||||
internal IHeaderDictionary Headers { get; set; } = new HeaderDictionary();
|
internal IHeaderDictionary Headers { get; set; } = new HeaderDictionary();
|
||||||
|
|
||||||
internal byte[] Body { get; set; }
|
internal byte[] Body { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
// 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 class CachedVaryBy
|
||||||
|
{
|
||||||
|
internal StringValues Headers { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,157 @@
|
||||||
|
// 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 DefaultResponseCacheSerializer
|
||||||
|
{
|
||||||
|
private const int FormatVersion = 1;
|
||||||
|
|
||||||
|
public static object Deserialize(byte[] serializedEntry)
|
||||||
|
{
|
||||||
|
using (var memory = new MemoryStream(serializedEntry))
|
||||||
|
{
|
||||||
|
using (var reader = new BinaryReader(memory))
|
||||||
|
{
|
||||||
|
return Read(reader);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] Serialize(object 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 (string)
|
||||||
|
// Type-dependent data (see CachedResponse and CachedVaryBy)
|
||||||
|
public static object Read(BinaryReader reader)
|
||||||
|
{
|
||||||
|
if (reader == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(reader));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reader.ReadInt32() != FormatVersion)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var type = reader.ReadString();
|
||||||
|
|
||||||
|
if (string.Equals(nameof(CachedResponse), type))
|
||||||
|
{
|
||||||
|
var cachedResponse = ReadCachedResponse(reader);
|
||||||
|
return cachedResponse;
|
||||||
|
}
|
||||||
|
else if (string.Equals(nameof(CachedVaryBy), type))
|
||||||
|
{
|
||||||
|
var cachedResponse = ReadCachedVaryBy(reader);
|
||||||
|
return cachedResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unable to read as CachedResponse or CachedVaryBy
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialization Format
|
||||||
|
// Status code (int)
|
||||||
|
// Header count (int)
|
||||||
|
// Header(s)
|
||||||
|
// Key (string)
|
||||||
|
// Value (string)
|
||||||
|
// Body length (int)
|
||||||
|
// Body (byte[])
|
||||||
|
private static CachedResponse ReadCachedResponse(BinaryReader reader)
|
||||||
|
{
|
||||||
|
var statusCode = reader.ReadInt32();
|
||||||
|
var headerCount = reader.ReadInt32();
|
||||||
|
var headers = new HeaderDictionary();
|
||||||
|
for (var index = 0; index < headerCount; index++)
|
||||||
|
{
|
||||||
|
var key = reader.ReadString();
|
||||||
|
var value = reader.ReadString();
|
||||||
|
headers[key] = value;
|
||||||
|
}
|
||||||
|
var bodyLength = reader.ReadInt32();
|
||||||
|
var body = reader.ReadBytes(bodyLength);
|
||||||
|
|
||||||
|
return new CachedResponse { StatusCode = statusCode, Headers = headers, Body = body };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialization Format
|
||||||
|
// Headers (comma separated string)
|
||||||
|
private static CachedVaryBy ReadCachedVaryBy(BinaryReader reader)
|
||||||
|
{
|
||||||
|
var headers = reader.ReadString().Split(',');
|
||||||
|
|
||||||
|
return new CachedVaryBy { Headers = headers };
|
||||||
|
}
|
||||||
|
|
||||||
|
// See serialization format above
|
||||||
|
public static void Write(BinaryWriter writer, object entry)
|
||||||
|
{
|
||||||
|
if (writer == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(writer));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(entry));
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.Write(FormatVersion);
|
||||||
|
|
||||||
|
if (entry is CachedResponse)
|
||||||
|
{
|
||||||
|
WriteCachedResponse(writer, entry as CachedResponse);
|
||||||
|
}
|
||||||
|
else if (entry is CachedVaryBy)
|
||||||
|
{
|
||||||
|
WriteCachedVaryBy(writer, entry as CachedVaryBy);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new NotSupportedException($"Unrecognized entry format for {nameof(entry)}.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// See serialization format above
|
||||||
|
private static void WriteCachedResponse(BinaryWriter writer, CachedResponse entry)
|
||||||
|
{
|
||||||
|
writer.Write(nameof(CachedResponse));
|
||||||
|
writer.Write(entry.StatusCode);
|
||||||
|
writer.Write(entry.Headers.Count);
|
||||||
|
foreach (var header in entry.Headers)
|
||||||
|
{
|
||||||
|
writer.Write(header.Key);
|
||||||
|
writer.Write(header.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
writer.Write(entry.Body.Length);
|
||||||
|
writer.Write(entry.Body);
|
||||||
|
}
|
||||||
|
|
||||||
|
// See serialization format above
|
||||||
|
private static void WriteCachedVaryBy(BinaryWriter writer, CachedVaryBy entry)
|
||||||
|
{
|
||||||
|
writer.Write(nameof(CachedVaryBy));
|
||||||
|
writer.Write(entry.Headers);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
// 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 Microsoft.Extensions.Caching.Distributed;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.ResponseCaching.Internal
|
||||||
|
{
|
||||||
|
internal class DistributedResponseCache : IResponseCache
|
||||||
|
{
|
||||||
|
private readonly IDistributedCache _cache;
|
||||||
|
|
||||||
|
public DistributedResponseCache(IDistributedCache cache)
|
||||||
|
{
|
||||||
|
if (cache == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(cache));
|
||||||
|
}
|
||||||
|
|
||||||
|
_cache = cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
public object Get(string key)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return DefaultResponseCacheSerializer.Deserialize(_cache.Get(key));
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// TODO: Log error
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Remove(string key)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_cache.Remove(key);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// TODO: Log error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Set(string key, object entry)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_cache.Set(key, DefaultResponseCacheSerializer.Serialize(entry));
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// TODO: Log error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,38 @@
|
||||||
|
// 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 Microsoft.Extensions.Caching.Memory;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.ResponseCaching.Internal
|
||||||
|
{
|
||||||
|
internal class MemoryResponseCache : IResponseCache
|
||||||
|
{
|
||||||
|
private readonly IMemoryCache _cache;
|
||||||
|
|
||||||
|
public MemoryResponseCache(IMemoryCache cache)
|
||||||
|
{
|
||||||
|
if (cache == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(cache));
|
||||||
|
}
|
||||||
|
|
||||||
|
_cache = cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
public object Get(string key)
|
||||||
|
{
|
||||||
|
return _cache.Get(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Remove(string key)
|
||||||
|
{
|
||||||
|
_cache.Remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Set(string key, object entry)
|
||||||
|
{
|
||||||
|
_cache.Set(key, entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,9 +3,11 @@
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.AspNetCore.ResponseCaching.Internal;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.ResponseCaching
|
namespace Microsoft.AspNetCore.ResponseCaching
|
||||||
{
|
{
|
||||||
|
|
@ -13,15 +15,25 @@ namespace Microsoft.AspNetCore.ResponseCaching
|
||||||
{
|
{
|
||||||
private string _cacheKey;
|
private string _cacheKey;
|
||||||
|
|
||||||
public ResponseCachingContext(HttpContext httpContext, IMemoryCache cache)
|
public ResponseCachingContext(HttpContext httpContext, IResponseCache cache)
|
||||||
{
|
{
|
||||||
|
if (cache == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(cache));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (httpContext == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(httpContext));
|
||||||
|
}
|
||||||
|
|
||||||
HttpContext = httpContext;
|
HttpContext = httpContext;
|
||||||
Cache = cache;
|
Cache = cache;
|
||||||
}
|
}
|
||||||
|
|
||||||
private HttpContext HttpContext { get; }
|
private HttpContext HttpContext { get; }
|
||||||
|
|
||||||
private IMemoryCache Cache { get; }
|
private IResponseCache Cache { get; }
|
||||||
|
|
||||||
private Stream OriginalResponseStream { get; set; }
|
private Stream OriginalResponseStream { get; set; }
|
||||||
|
|
||||||
|
|
@ -46,38 +58,81 @@ namespace Microsoft.AspNetCore.ResponseCaching
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only QueryString is treated as case sensitive
|
// Only QueryString is treated as case sensitive
|
||||||
// GET;HTTP://MYDOMAIN.COM:80/PATHBASE/PATH?QueryString
|
// GET;/PATH;VaryBy
|
||||||
private string CreateCacheKey()
|
private string CreateCacheKey()
|
||||||
|
{
|
||||||
|
return CreateCacheKey(varyBy: null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string CreateCacheKey(CachedVaryBy varyBy)
|
||||||
{
|
{
|
||||||
var request = HttpContext.Request;
|
var request = HttpContext.Request;
|
||||||
return request.Method.ToUpperInvariant()
|
var builder = new StringBuilder()
|
||||||
+ ";"
|
.Append(request.Method.ToUpperInvariant())
|
||||||
+ request.Scheme.ToUpperInvariant()
|
.Append(";")
|
||||||
+ "://"
|
.Append(request.Path.Value.ToUpperInvariant())
|
||||||
+ request.Host.Value.ToUpperInvariant()
|
.Append(CreateVaryByCacheKey(varyBy));
|
||||||
+ request.PathBase.Value.ToUpperInvariant()
|
|
||||||
+ request.Path.Value.ToUpperInvariant()
|
return builder.ToString();
|
||||||
+ request.QueryString;
|
}
|
||||||
|
|
||||||
|
private string CreateVaryByCacheKey(CachedVaryBy varyBy)
|
||||||
|
{
|
||||||
|
// TODO: resolve key format and delimiters
|
||||||
|
if (varyBy == null)
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var request = HttpContext.Request;
|
||||||
|
var builder = new StringBuilder(";");
|
||||||
|
|
||||||
|
foreach (var header in varyBy.Headers)
|
||||||
|
{
|
||||||
|
var value = request.Headers[header].ToString();
|
||||||
|
// null vs Empty?
|
||||||
|
if (string.IsNullOrEmpty(value))
|
||||||
|
{
|
||||||
|
value = "null";
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Append(header)
|
||||||
|
.Append("=")
|
||||||
|
.Append(value)
|
||||||
|
.Append(";");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse querystring params
|
||||||
|
|
||||||
|
return builder.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
internal async Task<bool> TryServeFromCacheAsync()
|
internal async Task<bool> TryServeFromCacheAsync()
|
||||||
{
|
{
|
||||||
_cacheKey = CreateCacheKey();
|
_cacheKey = CreateCacheKey();
|
||||||
ResponseCachingEntry cacheEntry;
|
var cacheEntry = Cache.Get(_cacheKey);
|
||||||
if (Cache.TryGetValue(_cacheKey, out cacheEntry))
|
|
||||||
|
if (cacheEntry is CachedVaryBy)
|
||||||
|
{
|
||||||
|
// Request contains VaryBy rules, recompute key and try again
|
||||||
|
_cacheKey = CreateCacheKey(cacheEntry as CachedVaryBy);
|
||||||
|
cacheEntry = Cache.Get(_cacheKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cacheEntry is CachedResponse)
|
||||||
{
|
{
|
||||||
// TODO: Compare cached request headers
|
// TODO: Compare cached request headers
|
||||||
|
|
||||||
// TODO: Evaluate Vary-By and select the most appropriate response
|
|
||||||
|
|
||||||
// TODO: Content negotiation if there are multiple cached response formats?
|
// TODO: Content negotiation if there are multiple cached response formats?
|
||||||
|
|
||||||
// TODO: Verify content freshness, or else re-validate the data?
|
// TODO: Verify content freshness, or else re-validate the data?
|
||||||
|
|
||||||
|
var cachedResponse = cacheEntry as CachedResponse;
|
||||||
|
|
||||||
var response = HttpContext.Response;
|
var response = HttpContext.Response;
|
||||||
// Copy the cached status code and response headers
|
// Copy the cached status code and response headers
|
||||||
response.StatusCode = cacheEntry.StatusCode;
|
response.StatusCode = cachedResponse.StatusCode;
|
||||||
foreach (var pair in cacheEntry.Headers)
|
foreach (var pair in cachedResponse.Headers)
|
||||||
{
|
{
|
||||||
response.Headers[pair.Key] = pair.Value;
|
response.Headers[pair.Key] = pair.Value;
|
||||||
}
|
}
|
||||||
|
|
@ -86,7 +141,7 @@ namespace Microsoft.AspNetCore.ResponseCaching
|
||||||
response.Headers["Served_From_Cache"] = DateTime.Now.ToString();
|
response.Headers["Served_From_Cache"] = DateTime.Now.ToString();
|
||||||
|
|
||||||
// Copy the cached response body
|
// Copy the cached response body
|
||||||
var body = cacheEntry.Body;
|
var body = cachedResponse.Body;
|
||||||
if (body.Length > 0)
|
if (body.Length > 0)
|
||||||
{
|
{
|
||||||
await response.Body.WriteAsync(body, 0, body.Length);
|
await response.Body.WriteAsync(body, 0, body.Length);
|
||||||
|
|
@ -120,15 +175,34 @@ namespace Microsoft.AspNetCore.ResponseCaching
|
||||||
{
|
{
|
||||||
if (CacheResponse)
|
if (CacheResponse)
|
||||||
{
|
{
|
||||||
// Store the buffer to cache
|
var response = HttpContext.Response;
|
||||||
var cacheEntry = new ResponseCachingEntry();
|
var varyHeaderValue = response.Headers["Vary"];
|
||||||
cacheEntry.StatusCode = HttpContext.Response.StatusCode;
|
|
||||||
|
// Check if any VaryBy rules exist
|
||||||
|
if (!StringValues.IsNullOrEmpty(varyHeaderValue))
|
||||||
|
{
|
||||||
|
var cachedVaryBy = new CachedVaryBy
|
||||||
|
{
|
||||||
|
// Only vary by headers for now
|
||||||
|
Headers = varyHeaderValue
|
||||||
|
};
|
||||||
|
|
||||||
|
Cache.Set(_cacheKey, cachedVaryBy);
|
||||||
|
_cacheKey = CreateCacheKey(cachedVaryBy);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the response to cache
|
||||||
|
var cachedResponse = new CachedResponse
|
||||||
|
{
|
||||||
|
StatusCode = HttpContext.Response.StatusCode,
|
||||||
|
Body = Buffer.ToArray()
|
||||||
|
};
|
||||||
foreach (var pair in HttpContext.Response.Headers)
|
foreach (var pair in HttpContext.Response.Headers)
|
||||||
{
|
{
|
||||||
cacheEntry.Headers[pair.Key] = pair.Value;
|
cachedResponse.Headers[pair.Key] = pair.Value;
|
||||||
}
|
}
|
||||||
cacheEntry.Body = Buffer.ToArray();
|
|
||||||
Cache.Set(_cacheKey, cacheEntry); // TODO: Timeouts
|
Cache.Set(_cacheKey, cachedResponse); // TODO: Timeouts
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: TEMP, flush the buffer to the client
|
// TODO: TEMP, flush the buffer to the client
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// 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.
|
// 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 System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.ResponseCaching
|
namespace Microsoft.AspNetCore.ResponseCaching
|
||||||
{
|
{
|
||||||
|
|
@ -11,10 +11,20 @@ namespace Microsoft.AspNetCore.ResponseCaching
|
||||||
public class ResponseCachingMiddleware
|
public class ResponseCachingMiddleware
|
||||||
{
|
{
|
||||||
private readonly RequestDelegate _next;
|
private readonly RequestDelegate _next;
|
||||||
private readonly IMemoryCache _cache;
|
private readonly IResponseCache _cache;
|
||||||
|
|
||||||
public ResponseCachingMiddleware(RequestDelegate next, IMemoryCache cache)
|
public ResponseCachingMiddleware(RequestDelegate next, IResponseCache cache)
|
||||||
{
|
{
|
||||||
|
if (cache == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(cache));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (next == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(next));
|
||||||
|
}
|
||||||
|
|
||||||
_next = next;
|
_next = next;
|
||||||
_cache = cache;
|
_cache = cache;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
// 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 Microsoft.AspNetCore.ResponseCaching;
|
||||||
|
using Microsoft.AspNetCore.ResponseCaching.Internal;
|
||||||
|
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.DependencyInjection
|
||||||
|
{
|
||||||
|
public static class ResponseCachingServiceCollectionExtensions
|
||||||
|
{
|
||||||
|
public static IServiceCollection AddMemoryResponseCache(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
if (services == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(services));
|
||||||
|
}
|
||||||
|
|
||||||
|
services.AddMemoryCache();
|
||||||
|
services.TryAdd(ServiceDescriptor.Singleton<IResponseCache, MemoryResponseCache>());
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IServiceCollection AddDistributedResponseCache(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
if (services == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(services));
|
||||||
|
}
|
||||||
|
|
||||||
|
services.AddDistributedMemoryCache();
|
||||||
|
services.TryAdd(ServiceDescriptor.Singleton<IResponseCache, DistributedResponseCache>());
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"Microsoft.AspNetCore.Http": "1.1.0-*",
|
"Microsoft.AspNetCore.Http": "1.1.0-*",
|
||||||
"Microsoft.Extensions.Caching.Abstractions": "1.1.0-*"
|
"Microsoft.Extensions.Caching.Memory": "1.1.0-*"
|
||||||
},
|
},
|
||||||
"frameworks": {
|
"frameworks": {
|
||||||
"net451": {},
|
"net451": {},
|
||||||
|
|
|
||||||
|
|
@ -14,5 +14,8 @@
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<SchemaVersion>2.0</SchemaVersion>
|
<SchemaVersion>2.0</SchemaVersion>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
|
||||||
|
</ItemGroup>
|
||||||
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" />
|
<Import Project="$(VSToolsPath)\DotNet\Microsoft.DotNet.targets" Condition="'$(VSToolsPath)' != ''" />
|
||||||
</Project>
|
</Project>
|
||||||
|
|
@ -1,11 +1,12 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// 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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
using Microsoft.Extensions.Caching.Memory;
|
using Microsoft.Extensions.Caching.Memory;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.ResponseCaching
|
namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
{
|
{
|
||||||
public class ResponseCachingContextTests
|
public class ResponseCachingContextTests
|
||||||
{
|
{
|
||||||
|
|
@ -14,7 +15,7 @@ namespace Microsoft.AspNetCore.ResponseCaching
|
||||||
{
|
{
|
||||||
var httpContext = new DefaultHttpContext();
|
var httpContext = new DefaultHttpContext();
|
||||||
httpContext.Request.Method = "GET";
|
httpContext.Request.Method = "GET";
|
||||||
var context = new ResponseCachingContext(httpContext, new MemoryCache(new MemoryCacheOptions()));
|
var context = new ResponseCachingContext(httpContext, new TestResponseCache());
|
||||||
|
|
||||||
Assert.True(context.CheckRequestAllowsCaching());
|
Assert.True(context.CheckRequestAllowsCaching());
|
||||||
}
|
}
|
||||||
|
|
@ -25,9 +26,25 @@ namespace Microsoft.AspNetCore.ResponseCaching
|
||||||
{
|
{
|
||||||
var httpContext = new DefaultHttpContext();
|
var httpContext = new DefaultHttpContext();
|
||||||
httpContext.Request.Method = method;
|
httpContext.Request.Method = method;
|
||||||
var context = new ResponseCachingContext(httpContext, new MemoryCache(new MemoryCacheOptions()));
|
var context = new ResponseCachingContext(httpContext, new TestResponseCache());
|
||||||
|
|
||||||
Assert.False(context.CheckRequestAllowsCaching());
|
Assert.False(context.CheckRequestAllowsCaching());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class TestResponseCache : IResponseCache
|
||||||
|
{
|
||||||
|
public object Get(string key)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Remove(string key)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Set(string key, object entry)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
// 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.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.TestHost;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using System;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
|
{
|
||||||
|
public class ResponseCachingTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async void ServesCachedContentIfAvailable()
|
||||||
|
{
|
||||||
|
var builder = new WebHostBuilder()
|
||||||
|
.ConfigureServices(services =>
|
||||||
|
{
|
||||||
|
services.AddDistributedResponseCache();
|
||||||
|
})
|
||||||
|
.Configure(app =>
|
||||||
|
{
|
||||||
|
app.UseResponseCaching();
|
||||||
|
app.Run(async (context) =>
|
||||||
|
{
|
||||||
|
context.Response.Headers["Cache-Control"] = "public";
|
||||||
|
await context.Response.WriteAsync(DateTime.UtcNow.ToString());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
using (var server = new TestServer(builder))
|
||||||
|
{
|
||||||
|
var client = server.CreateClient();
|
||||||
|
var initialResponse = await client.GetAsync("");
|
||||||
|
var subsequentResponse = await client.GetAsync("");
|
||||||
|
|
||||||
|
initialResponse.EnsureSuccessStatusCode();
|
||||||
|
subsequentResponse.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
// TODO: Check for the appropriate headers once we actually set them
|
||||||
|
Assert.False(initialResponse.Headers.Contains("Served_From_Cache"));
|
||||||
|
Assert.True(subsequentResponse.Headers.Contains("Served_From_Cache"));
|
||||||
|
Assert.Equal(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,9 +5,8 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dotnet-test-xunit": "2.2.0-*",
|
"dotnet-test-xunit": "2.2.0-*",
|
||||||
"Microsoft.AspNetCore.Http": "1.1.0-*",
|
|
||||||
"Microsoft.AspNetCore.ResponseCaching": "0.1.0-*",
|
"Microsoft.AspNetCore.ResponseCaching": "0.1.0-*",
|
||||||
"Microsoft.Extensions.Caching.Memory": "1.1.0-*",
|
"Microsoft.AspNetCore.TestHost": "1.1.0-*",
|
||||||
"xunit": "2.2.0-*"
|
"xunit": "2.2.0-*"
|
||||||
},
|
},
|
||||||
"frameworks": {
|
"frameworks": {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue