diff --git a/.gitignore b/.gitignore
index 216e8d9c58..be311a1f7d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,4 +23,5 @@ nuget.exe
*DS_Store
*.ncrunchsolution
*.*sdf
-*.ipch
\ No newline at end of file
+*.ipch
+project.lock.json
\ No newline at end of file
diff --git a/ResponseCaching.sln b/ResponseCaching.sln
new file mode 100644
index 0000000000..358a798cbb
--- /dev/null
+++ b/ResponseCaching.sln
@@ -0,0 +1,50 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 14
+VisualStudioVersion = 14.0.22823.1
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.ResponseCaching", "src\Microsoft.AspNet.ResponseCaching\Microsoft.AspNet.ResponseCaching.xproj", "{D1031270-DBD3-4F02-A3DC-3E7DADE8EBE6}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{367AABAF-E03C-4491-A9A7-BDDE8903D1B4}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{C51DF5BD-B53D-4795-BC01-A9AB066BF286}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{89A50974-E9D4-4F87-ACF2-6A6005E64931}"
+EndProject
+Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "ResponseCachingSample", "samples\ResponseCachingSample\ResponseCachingSample.xproj", "{1139BDEE-FA15-474D-8855-0AB91F23CF26}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F787A492-C2FF-4569-A663-F8F24B900657}"
+ ProjectSection(SolutionItems) = preProject
+ global.json = global.json
+ EndProjectSection
+EndProject
+Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNet.ResponseCaching.Tests", "test\Microsoft.AspNet.ResponseCaching.Tests\Microsoft.AspNet.ResponseCaching.Tests.xproj", "{151B2027-3936-44B9-A4A0-E1E5902125AB}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {D1031270-DBD3-4F02-A3DC-3E7DADE8EBE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D1031270-DBD3-4F02-A3DC-3E7DADE8EBE6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D1031270-DBD3-4F02-A3DC-3E7DADE8EBE6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D1031270-DBD3-4F02-A3DC-3E7DADE8EBE6}.Release|Any CPU.Build.0 = Release|Any CPU
+ {1139BDEE-FA15-474D-8855-0AB91F23CF26}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {1139BDEE-FA15-474D-8855-0AB91F23CF26}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {1139BDEE-FA15-474D-8855-0AB91F23CF26}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {1139BDEE-FA15-474D-8855-0AB91F23CF26}.Release|Any CPU.Build.0 = Release|Any CPU
+ {151B2027-3936-44B9-A4A0-E1E5902125AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {151B2027-3936-44B9-A4A0-E1E5902125AB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {151B2027-3936-44B9-A4A0-E1E5902125AB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {151B2027-3936-44B9-A4A0-E1E5902125AB}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(NestedProjects) = preSolution
+ {D1031270-DBD3-4F02-A3DC-3E7DADE8EBE6} = {367AABAF-E03C-4491-A9A7-BDDE8903D1B4}
+ {1139BDEE-FA15-474D-8855-0AB91F23CF26} = {C51DF5BD-B53D-4795-BC01-A9AB066BF286}
+ {151B2027-3936-44B9-A4A0-E1E5902125AB} = {89A50974-E9D4-4F87-ACF2-6A6005E64931}
+ EndGlobalSection
+EndGlobal
diff --git a/global.json b/global.json
new file mode 100644
index 0000000000..397ac5fc98
--- /dev/null
+++ b/global.json
@@ -0,0 +1,3 @@
+{
+ "projects": ["src"]
+}
\ No newline at end of file
diff --git a/samples/ResponseCachingSample/ResponseCachingSample.xproj b/samples/ResponseCachingSample/ResponseCachingSample.xproj
new file mode 100644
index 0000000000..2403e53763
--- /dev/null
+++ b/samples/ResponseCachingSample/ResponseCachingSample.xproj
@@ -0,0 +1,19 @@
+
+
+
+ 14.0
+ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
+
+
+
+ 1139bdee-fa15-474d-8855-0ab91f23cf26
+ ResponseCachingSample
+ ..\..\artifacts\obj\$(MSBuildProjectName)
+ ..\..\artifacts\bin\$(MSBuildProjectName)\
+
+
+ 2.0
+ 2931
+
+
+
\ No newline at end of file
diff --git a/samples/ResponseCachingSample/Startup.cs b/samples/ResponseCachingSample/Startup.cs
new file mode 100644
index 0000000000..1ca6d9ac4c
--- /dev/null
+++ b/samples/ResponseCachingSample/Startup.cs
@@ -0,0 +1,33 @@
+// 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.AspNet.Builder;
+using Microsoft.AspNet.Http;
+using Microsoft.Framework.DependencyInjection;
+using Microsoft.Net.Http.Headers;
+
+namespace ResponseCachingSample
+{
+ public class Startup
+ {
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services.AddCaching();
+ }
+
+ public void Configure(IApplicationBuilder app)
+ {
+ app.UseResponseCaching();
+ app.Run(async (context) =>
+ {
+ context.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
+ {
+ Public = true,
+ MaxAge = TimeSpan.FromSeconds(10)
+ };
+ await context.Response.WriteAsync("Hello World! " + DateTime.UtcNow);
+ });
+ }
+ }
+}
diff --git a/samples/ResponseCachingSample/project.json b/samples/ResponseCachingSample/project.json
new file mode 100644
index 0000000000..ca19e78857
--- /dev/null
+++ b/samples/ResponseCachingSample/project.json
@@ -0,0 +1,33 @@
+{
+ "webroot": "wwwroot",
+ "version": "1.0.0-*",
+
+ "dependencies": {
+ "Microsoft.AspNet.ResponseCaching": "1.0.0-*",
+ "Microsoft.AspNet.Server.IIS": "1.0.0-*",
+ "Microsoft.AspNet.Server.WebListener": "1.0.0-*",
+ "Microsoft.Framework.Caching.Memory": "1.0.0-*"
+ },
+
+ "commands": {
+ "web": "Microsoft.AspNet.Hosting --server Microsoft.AspNet.Server.WebListener --server.urls http://localhost:5000"
+ },
+
+ "frameworks": {
+ "dnx451": { },
+ "dnxcore50": { }
+ },
+
+ "publishExclude": [
+ "node_modules",
+ "bower_components",
+ "**.xproj",
+ "**.user",
+ "**.vspscc"
+ ],
+ "exclude": [
+ "wwwroot",
+ "node_modules",
+ "bower_components"
+ ]
+}
diff --git a/src/Microsoft.AspNet.ResponseCaching/CachingContext.cs b/src/Microsoft.AspNet.ResponseCaching/CachingContext.cs
new file mode 100644
index 0000000000..c5586ce04e
--- /dev/null
+++ b/src/Microsoft.AspNet.ResponseCaching/CachingContext.cs
@@ -0,0 +1,143 @@
+// 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 System.Linq;
+using System.Threading.Tasks;
+using Microsoft.AspNet.Http;
+using Microsoft.Framework.Caching.Memory;
+
+namespace Microsoft.AspNet.ResponseCaching
+{
+ internal class CachingContext
+ {
+ private string _cacheKey;
+
+ public CachingContext(HttpContext httpContext, IMemoryCache cache)
+ {
+ HttpContext = httpContext;
+ Cache = cache;
+ }
+
+ private HttpContext HttpContext { get; }
+
+ private IMemoryCache Cache { get; }
+
+ private Stream OriginalResponseStream { get; set; }
+
+ private MemoryStream Buffer { get; set; }
+
+ internal bool ResponseStarted { get; set; }
+
+ private bool CacheResponse { get; set; }
+
+ internal bool CheckRequestAllowsCaching()
+ {
+ // Verify the method
+ // TODO: What other methods should be supported?
+ if (!string.Equals("GET", HttpContext.Request.Method, StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ // Verify the request headers do not opt-out of caching
+ // TODO:
+ return true;
+ }
+
+ // Only QueryString is treated as case sensitive
+ // GET;HTTP://MYDOMAIN.COM:80/PATHBASE/PATH?QueryString
+ private string CreateCacheKey()
+ {
+ 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;
+ }
+
+ internal async Task TryServeFromCacheAsync()
+ {
+ _cacheKey = CreateCacheKey();
+ ResponseCacheEntry cacheEntry;
+ if (Cache.TryGetValue(_cacheKey, out cacheEntry))
+ {
+ // 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 response = HttpContext.Response;
+ // Copy the cached status code and response headers
+ response.StatusCode = cacheEntry.StatusCode;
+ foreach (var pair in cacheEntry.Headers)
+ {
+ response.Headers.SetValues(pair.Key, pair.Value);
+ }
+
+ // TODO: Update cache headers (Age)
+ response.Headers["Served_From_Cache"] = DateTime.Now.ToString();
+
+ // Copy the cached response body
+ var body = cacheEntry.Body;
+ if (body.Length > 0)
+ {
+ await response.Body.WriteAsync(body, 0, body.Length);
+ }
+ return true;
+ }
+
+ return false;
+ }
+
+ internal void HookResponseStream()
+ {
+ // TODO: Use a wrapper stream to listen for writes (e.g. the start of the response),
+ // check the headers, and verify if we should cache the response.
+ // Then we should stream data out to the client at the same time as we buffer for the cache.
+ // For now we'll just buffer everything in memory before checking the response headers.
+ // TODO: Consider caching large responses on disk and serving them from there.
+ OriginalResponseStream = HttpContext.Response.Body;
+ Buffer = new MemoryStream();
+ HttpContext.Response.Body = Buffer;
+ }
+
+ internal bool OnResponseStarting()
+ {
+ // Evaluate the response headers, see if we should buffer and cache
+ CacheResponse = true; // TODO:
+ return CacheResponse;
+ }
+
+ internal void FinalizeCaching()
+ {
+ if (CacheResponse)
+ {
+ // Store the buffer to cache
+ var cacheEntry = new ResponseCacheEntry();
+ cacheEntry.StatusCode = HttpContext.Response.StatusCode;
+ cacheEntry.Headers = HttpContext.Response.Headers.ToList();
+ cacheEntry.Body = Buffer.ToArray();
+ Cache.Set(_cacheKey, cacheEntry); // TODO: Timeouts
+ }
+
+ // TODO: TEMP, flush the buffer to the client
+ Buffer.Seek(0, SeekOrigin.Begin);
+ Buffer.CopyTo(OriginalResponseStream);
+ }
+
+ internal void UnhookResponseStream()
+ {
+ // Unhook the response stream.
+ HttpContext.Response.Body = OriginalResponseStream;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.ResponseCaching/Microsoft.AspNet.ResponseCaching.xproj b/src/Microsoft.AspNet.ResponseCaching/Microsoft.AspNet.ResponseCaching.xproj
new file mode 100644
index 0000000000..bb65aad14e
--- /dev/null
+++ b/src/Microsoft.AspNet.ResponseCaching/Microsoft.AspNet.ResponseCaching.xproj
@@ -0,0 +1,20 @@
+
+
+
+ 14.0
+ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
+
+
+
+
+ d1031270-dbd3-4f02-a3dc-3e7dade8ebe6
+ Microsoft.AspNet.ResponseCaching
+ ..\artifacts\obj\$(MSBuildProjectName)
+ ..\artifacts\bin\$(MSBuildProjectName)\
+
+
+
+ 2.0
+
+
+
diff --git a/src/Microsoft.AspNet.ResponseCaching/Properties/AssemblyInfo.cs b/src/Microsoft.AspNet.ResponseCaching/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..09bbb58941
--- /dev/null
+++ b/src/Microsoft.AspNet.ResponseCaching/Properties/AssemblyInfo.cs
@@ -0,0 +1,8 @@
+// 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.Reflection;
+using System.Runtime.CompilerServices;
+
+[assembly: InternalsVisibleTo("Microsoft.AspNet.ResponseCaching.Tests")]
+[assembly: AssemblyMetadata("Serviceable", "True")]
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.ResponseCaching/ResponseCacheEntry.cs b/src/Microsoft.AspNet.ResponseCaching/ResponseCacheEntry.cs
new file mode 100644
index 0000000000..f23685de33
--- /dev/null
+++ b/src/Microsoft.AspNet.ResponseCaching/ResponseCacheEntry.cs
@@ -0,0 +1,14 @@
+// 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.Collections.Generic;
+
+namespace Microsoft.AspNet.ResponseCaching
+{
+ internal class ResponseCacheEntry
+ {
+ public int StatusCode { get; set; }
+ internal IEnumerable> Headers { get; set; }
+ internal byte[] Body { get; set; }
+ }
+}
diff --git a/src/Microsoft.AspNet.ResponseCaching/ResponseCachingExtensions.cs b/src/Microsoft.AspNet.ResponseCaching/ResponseCachingExtensions.cs
new file mode 100644
index 0000000000..6632ad0d2b
--- /dev/null
+++ b/src/Microsoft.AspNet.ResponseCaching/ResponseCachingExtensions.cs
@@ -0,0 +1,15 @@
+// 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.AspNet.ResponseCaching;
+
+namespace Microsoft.AspNet.Builder
+{
+ public static class ResponseCachingExtensions
+ {
+ public static IApplicationBuilder UseResponseCaching(this IApplicationBuilder app)
+ {
+ return app.UseMiddleware();
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.ResponseCaching/ResponseCachingMiddleware.cs b/src/Microsoft.AspNet.ResponseCaching/ResponseCachingMiddleware.cs
new file mode 100644
index 0000000000..0c0fda42a2
--- /dev/null
+++ b/src/Microsoft.AspNet.ResponseCaching/ResponseCachingMiddleware.cs
@@ -0,0 +1,61 @@
+// 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.Threading.Tasks;
+using Microsoft.AspNet.Builder;
+using Microsoft.AspNet.Http;
+using Microsoft.Framework.Caching.Memory;
+
+namespace Microsoft.AspNet.ResponseCaching
+{
+ // http://tools.ietf.org/html/rfc7234
+ public class ResponseCachingMiddleware
+ {
+ private readonly RequestDelegate _next;
+ private readonly IMemoryCache _cache;
+
+ public ResponseCachingMiddleware(RequestDelegate next, IMemoryCache cache)
+ {
+ _next = next;
+ _cache = cache;
+ }
+
+ public async Task Invoke(HttpContext context)
+ {
+ var cachingContext = new CachingContext(context, _cache);
+ // Should we attempt any caching logic?
+ if (cachingContext.CheckRequestAllowsCaching())
+ {
+ // Can this request be served from cache?
+ if (await cachingContext.TryServeFromCacheAsync())
+ {
+ return;
+ }
+
+ // Hook up to listen to the response stream
+ cachingContext.HookResponseStream();
+
+ try
+ {
+ await _next(context);
+
+ // If there was no response body, check the response headers now. We can cache things like redirects.
+ if (!cachingContext.ResponseStarted)
+ {
+ cachingContext.OnResponseStarting();
+ }
+ // Finalize the cache entry
+ cachingContext.FinalizeCaching();
+ }
+ finally
+ {
+ cachingContext.UnhookResponseStream();
+ }
+ }
+ else
+ {
+ await _next(context);
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.ResponseCaching/project.json b/src/Microsoft.AspNet.ResponseCaching/project.json
new file mode 100644
index 0000000000..b7f364b6cc
--- /dev/null
+++ b/src/Microsoft.AspNet.ResponseCaching/project.json
@@ -0,0 +1,15 @@
+{
+ "version": "1.0.0-*",
+ "description": "Middleare that automatically caches HTTP responses on the server.",
+ "dependencies": {
+ "Microsoft.AspNet.Http.Abstractions": "1.0.0-*",
+ "Microsoft.Framework.Caching.Abstractions": "1.0.0-*"
+ },
+ "frameworks" : {
+ "dnx451": { },
+ "dnxcore50" : {
+ "dependencies": {
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.AspNet.ResponseCaching.Tests/CachingContextTests.cs b/test/Microsoft.AspNet.ResponseCaching.Tests/CachingContextTests.cs
new file mode 100644
index 0000000000..d0315b5a58
--- /dev/null
+++ b/test/Microsoft.AspNet.ResponseCaching.Tests/CachingContextTests.cs
@@ -0,0 +1,33 @@
+// 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.AspNet.Http.Internal;
+using Microsoft.Framework.Caching.Memory;
+using Xunit;
+
+namespace Microsoft.AspNet.ResponseCaching
+{
+ public class CachingContextTests
+ {
+ [Fact]
+ public void CheckRequestAllowsCaching_Method_GET_Allowed()
+ {
+ var httpContext = new DefaultHttpContext();
+ httpContext.Request.Method = "GET";
+ var context = new CachingContext(httpContext, new MemoryCache(new MemoryCacheOptions()));
+
+ Assert.True(context.CheckRequestAllowsCaching());
+ }
+
+ [Theory]
+ [InlineData("POST")]
+ public void CheckRequestAllowsCaching_Method_Unsafe_NotAllowed(string method)
+ {
+ var httpContext = new DefaultHttpContext();
+ httpContext.Request.Method = method;
+ var context = new CachingContext(httpContext, new MemoryCache(new MemoryCacheOptions()));
+
+ Assert.False(context.CheckRequestAllowsCaching());
+ }
+ }
+}
diff --git a/test/Microsoft.AspNet.ResponseCaching.Tests/Microsoft.AspNet.ResponseCaching.Tests.xproj b/test/Microsoft.AspNet.ResponseCaching.Tests/Microsoft.AspNet.ResponseCaching.Tests.xproj
new file mode 100644
index 0000000000..ee98d4b4cd
--- /dev/null
+++ b/test/Microsoft.AspNet.ResponseCaching.Tests/Microsoft.AspNet.ResponseCaching.Tests.xproj
@@ -0,0 +1,20 @@
+
+
+
+ 14.0
+ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
+
+
+
+
+ 151b2027-3936-44b9-a4a0-e1e5902125ab
+ Microsoft.AspNet.ResponseCaching.Tests
+ ..\..\artifacts\obj\$(MSBuildProjectName)
+ ..\..\artifacts\bin\$(MSBuildProjectName)\
+
+
+
+ 2.0
+
+
+
diff --git a/test/Microsoft.AspNet.ResponseCaching.Tests/project.json b/test/Microsoft.AspNet.ResponseCaching.Tests/project.json
new file mode 100644
index 0000000000..5ad3ebb2d2
--- /dev/null
+++ b/test/Microsoft.AspNet.ResponseCaching.Tests/project.json
@@ -0,0 +1,15 @@
+{
+ "dependencies": {
+ "Microsoft.AspNet.ResponseCaching": "1.0.0-*",
+ "xunit.runner.aspnet": "2.0.0-aspnet-*",
+ "Microsoft.AspNet.Http": "1.0.0-beta5-11528",
+ "Microsoft.Framework.Caching.Memory": "1.0.0-beta5-11395"
+ },
+ "commands": {
+ "test": "xunit.runner.aspnet"
+ },
+ "frameworks": {
+ "dnx451": { },
+ "dnxcore50": { }
+ }
+}