diff --git a/samples/ResponseCachingSample/Startup.cs b/samples/ResponseCachingSample/Startup.cs index eca8af2ae0..6dc0ffc99e 100644 --- a/samples/ResponseCachingSample/Startup.cs +++ b/samples/ResponseCachingSample/Startup.cs @@ -15,7 +15,7 @@ namespace ResponseCachingSample { public void ConfigureServices(IServiceCollection services) { - services.AddMemoryCache(); + services.AddDistributedResponseCache(); } public void Configure(IApplicationBuilder app) @@ -23,11 +23,14 @@ namespace ResponseCachingSample app.UseResponseCaching(); app.Run(async (context) => { + // These settings should be configured by context.Response.Cache.* context.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() { 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); }); } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/IResponseCache.cs b/src/Microsoft.AspNetCore.ResponseCaching/IResponseCache.cs new file mode 100644 index 0000000000..a3ba37eeac --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/IResponseCache.cs @@ -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); + } +} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingEntry.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/CachedResponse.cs similarity index 71% rename from src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingEntry.cs rename to src/Microsoft.AspNetCore.ResponseCaching/Internal/CachedResponse.cs index c372e9a1ac..2c43a562a0 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingEntry.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/CachedResponse.cs @@ -3,12 +3,14 @@ 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 byte[] Body { get; set; } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/CachedVaryBy.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/CachedVaryBy.cs new file mode 100644 index 0000000000..8ff382cd09 --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/CachedVaryBy.cs @@ -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; } + } +} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/DefaultResponseCacheEntrySerializer.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/DefaultResponseCacheEntrySerializer.cs new file mode 100644 index 0000000000..4885a6bfcf --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/DefaultResponseCacheEntrySerializer.cs @@ -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); + } + } +} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/DistributedResponseCache.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/DistributedResponseCache.cs new file mode 100644 index 0000000000..d87a4be2b1 --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/DistributedResponseCache.cs @@ -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 + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs new file mode 100644 index 0000000000..2937ad439d --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs @@ -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); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs index 89927a1783..a493e47e4d 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs @@ -3,9 +3,11 @@ using System; using System.IO; +using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Caching.Memory; +using Microsoft.AspNetCore.ResponseCaching.Internal; +using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.ResponseCaching { @@ -13,15 +15,25 @@ namespace Microsoft.AspNetCore.ResponseCaching { 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; Cache = cache; } private HttpContext HttpContext { get; } - private IMemoryCache Cache { get; } + private IResponseCache Cache { get; } private Stream OriginalResponseStream { get; set; } @@ -46,38 +58,81 @@ namespace Microsoft.AspNetCore.ResponseCaching } // Only QueryString is treated as case sensitive - // GET;HTTP://MYDOMAIN.COM:80/PATHBASE/PATH?QueryString + // GET;/PATH;VaryBy private string CreateCacheKey() + { + return CreateCacheKey(varyBy: null); + } + + private string CreateCacheKey(CachedVaryBy varyBy) { var request = HttpContext.Request; - return request.Method.ToUpperInvariant() - + ";" - + request.Scheme.ToUpperInvariant() - + "://" - + request.Host.Value.ToUpperInvariant() - + request.PathBase.Value.ToUpperInvariant() - + request.Path.Value.ToUpperInvariant() - + request.QueryString; + var builder = new StringBuilder() + .Append(request.Method.ToUpperInvariant()) + .Append(";") + .Append(request.Path.Value.ToUpperInvariant()) + .Append(CreateVaryByCacheKey(varyBy)); + + return builder.ToString(); + } + + 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 TryServeFromCacheAsync() { _cacheKey = CreateCacheKey(); - ResponseCachingEntry cacheEntry; - if (Cache.TryGetValue(_cacheKey, out cacheEntry)) + var cacheEntry = Cache.Get(_cacheKey); + + 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: Evaluate Vary-By and select the most appropriate response - // TODO: Content negotiation if there are multiple cached response formats? // TODO: Verify content freshness, or else re-validate the data? + var cachedResponse = cacheEntry as CachedResponse; + var response = HttpContext.Response; // Copy the cached status code and response headers - response.StatusCode = cacheEntry.StatusCode; - foreach (var pair in cacheEntry.Headers) + response.StatusCode = cachedResponse.StatusCode; + foreach (var pair in cachedResponse.Headers) { response.Headers[pair.Key] = pair.Value; } @@ -86,7 +141,7 @@ namespace Microsoft.AspNetCore.ResponseCaching response.Headers["Served_From_Cache"] = DateTime.Now.ToString(); // Copy the cached response body - var body = cacheEntry.Body; + var body = cachedResponse.Body; if (body.Length > 0) { await response.Body.WriteAsync(body, 0, body.Length); @@ -120,15 +175,34 @@ namespace Microsoft.AspNetCore.ResponseCaching { if (CacheResponse) { - // Store the buffer to cache - var cacheEntry = new ResponseCachingEntry(); - cacheEntry.StatusCode = HttpContext.Response.StatusCode; + var response = HttpContext.Response; + var varyHeaderValue = response.Headers["Vary"]; + + // 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) { - 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 diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs index 6e642057da..c18f66ecd9 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs @@ -1,9 +1,9 @@ // 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.AspNetCore.Http; -using Microsoft.Extensions.Caching.Memory; namespace Microsoft.AspNetCore.ResponseCaching { @@ -11,10 +11,20 @@ namespace Microsoft.AspNetCore.ResponseCaching public class ResponseCachingMiddleware { 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; _cache = cache; } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingServiceCollectionExtensions.cs new file mode 100644 index 0000000000..b1e72631c7 --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingServiceCollectionExtensions.cs @@ -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()); + + return services; + } + + public static IServiceCollection AddDistributedResponseCache(this IServiceCollection services) + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + services.AddDistributedMemoryCache(); + services.TryAdd(ServiceDescriptor.Singleton()); + + return services; + } + } +} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/project.json b/src/Microsoft.AspNetCore.ResponseCaching/project.json index 8023c1b533..1fd13308ab 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/project.json +++ b/src/Microsoft.AspNetCore.ResponseCaching/project.json @@ -22,7 +22,7 @@ }, "dependencies": { "Microsoft.AspNetCore.Http": "1.1.0-*", - "Microsoft.Extensions.Caching.Abstractions": "1.1.0-*" + "Microsoft.Extensions.Caching.Memory": "1.1.0-*" }, "frameworks": { "net451": {}, diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.xproj b/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.xproj index fce053679a..81a4f88eab 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.xproj +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.xproj @@ -14,5 +14,8 @@ 2.0 + + + \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs index dc9462d295..5c83622de7 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs @@ -1,11 +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 System; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Caching.Memory; using Xunit; -namespace Microsoft.AspNetCore.ResponseCaching +namespace Microsoft.AspNetCore.ResponseCaching.Tests { public class ResponseCachingContextTests { @@ -14,7 +15,7 @@ namespace Microsoft.AspNetCore.ResponseCaching { var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "GET"; - var context = new ResponseCachingContext(httpContext, new MemoryCache(new MemoryCacheOptions())); + var context = new ResponseCachingContext(httpContext, new TestResponseCache()); Assert.True(context.CheckRequestAllowsCaching()); } @@ -25,9 +26,25 @@ namespace Microsoft.AspNetCore.ResponseCaching { var httpContext = new DefaultHttpContext(); httpContext.Request.Method = method; - var context = new ResponseCachingContext(httpContext, new MemoryCache(new MemoryCacheOptions())); + var context = new ResponseCachingContext(httpContext, new TestResponseCache()); 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) + { + } + } } } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs new file mode 100644 index 0000000000..e66933d97d --- /dev/null +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs @@ -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()); + } + } + } +} diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json b/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json index be74f330a0..48dbeeae41 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json @@ -5,9 +5,8 @@ }, "dependencies": { "dotnet-test-xunit": "2.2.0-*", - "Microsoft.AspNetCore.Http": "1.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-*" }, "frameworks": {