diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..c2f0f84
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,52 @@
+*.doc diff=astextplain
+*.DOC diff=astextplain
+*.docx diff=astextplain
+*.DOCX diff=astextplain
+*.dot diff=astextplain
+*.DOT diff=astextplain
+*.pdf diff=astextplain
+*.PDF diff=astextplain
+*.rtf diff=astextplain
+*.RTF diff=astextplain
+
+*.jpg binary
+*.png binary
+*.gif binary
+
+*.cs text=auto diff=csharp
+*.vb text=auto
+*.resx text=auto
+*.c text=auto
+*.cpp text=auto
+*.cxx text=auto
+*.h text=auto
+*.hxx text=auto
+*.py text=auto
+*.rb text=auto
+*.java text=auto
+*.html text=auto
+*.htm text=auto
+*.css text=auto
+*.scss text=auto
+*.sass text=auto
+*.less text=auto
+*.js text=auto
+*.lisp text=auto
+*.clj text=auto
+*.sql text=auto
+*.php text=auto
+*.lua text=auto
+*.m text=auto
+*.asm text=auto
+*.erl text=auto
+*.fs text=auto
+*.fsx text=auto
+*.hs text=auto
+
+*.csproj text=auto
+*.vbproj text=auto
+*.fsproj text=auto
+*.dbproj text=auto
+*.sln text=auto eol=crlf
+
+*.sh eol=lf
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6acc284
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,32 @@
+[Oo]bj/
+[Bb]in/
+TestResults/
+.nuget/
+*.sln.ide/
+_ReSharper.*/
+packages/
+artifacts/
+PublishProfiles/
+.vs/
+*.user
+*.suo
+*.cache
+*.docstates
+_ReSharper.*
+nuget.exe
+*net45.csproj
+*net451.csproj
+*k10.csproj
+*.psess
+*.vsp
+*.pidb
+*.userprefs
+*DS_Store
+*.ncrunchsolution
+*.*sdf
+*.ipch
+project.lock.json
+runtimes/
+.build/
+.testPublish/
+launchSettings.json
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..903efa7
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,23 @@
+language: csharp
+sudo: required
+dist: trusty
+addons:
+ apt:
+ packages:
+ - libunwind8
+mono:
+ - 4.0.5
+os:
+ - linux
+ - osx
+osx_image: xcode7.1
+branches:
+ only:
+ - master
+ - release
+ - dev
+ - /^(.*\/)?ci-.*$/
+before_install:
+ - if test "$TRAVIS_OS_NAME" == "osx"; then brew update; brew install openssl; brew link --force openssl; fi
+script:
+ - ./build.sh --quiet verify
\ No newline at end of file
diff --git a/NuGet.Config b/NuGet.Config
new file mode 100644
index 0000000..46c3b3e
--- /dev/null
+++ b/NuGet.Config
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/NuGet.master.config b/NuGet.master.config
new file mode 100644
index 0000000..e2edffc
--- /dev/null
+++ b/NuGet.master.config
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/NuGet.release.config b/NuGet.release.config
new file mode 100644
index 0000000..1978dc0
--- /dev/null
+++ b/NuGet.release.config
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/ResponseCaching.sln b/ResponseCaching.sln
new file mode 100644
index 0000000..6f455f5
--- /dev/null
+++ b/ResponseCaching.sln
@@ -0,0 +1,50 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio 14
+VisualStudioVersion = 14.0.25123.0
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.ResponseCaching", "src\Microsoft.AspNetCore.ResponseCaching\Microsoft.AspNetCore.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.AspNetCore.ResponseCaching.Tests", "test\Microsoft.AspNetCore.ResponseCaching.Tests\Microsoft.AspNetCore.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/appveyor.yml b/appveyor.yml
new file mode 100644
index 0000000..be95b88
--- /dev/null
+++ b/appveyor.yml
@@ -0,0 +1,13 @@
+init:
+ - git config --global core.autocrlf true
+branches:
+ only:
+ - master
+ - release
+ - dev
+ - /^(.*\/)?ci-.*$/
+build_script:
+ - build.cmd --quiet verify
+clone_depth: 1
+test: off
+deploy: off
\ No newline at end of file
diff --git a/build.cmd b/build.cmd
new file mode 100644
index 0000000..7d4894c
--- /dev/null
+++ b/build.cmd
@@ -0,0 +1,2 @@
+@ECHO OFF
+PowerShell -NoProfile -NoLogo -ExecutionPolicy unrestricted -Command "[System.Threading.Thread]::CurrentThread.CurrentCulture = ''; [System.Threading.Thread]::CurrentThread.CurrentUICulture = '';& '%~dp0build.ps1' %*; exit $LASTEXITCODE"
\ No newline at end of file
diff --git a/build.ps1 b/build.ps1
new file mode 100644
index 0000000..8f2f996
--- /dev/null
+++ b/build.ps1
@@ -0,0 +1,67 @@
+$ErrorActionPreference = "Stop"
+
+function DownloadWithRetry([string] $url, [string] $downloadLocation, [int] $retries)
+{
+ while($true)
+ {
+ try
+ {
+ Invoke-WebRequest $url -OutFile $downloadLocation
+ break
+ }
+ catch
+ {
+ $exceptionMessage = $_.Exception.Message
+ Write-Host "Failed to download '$url': $exceptionMessage"
+ if ($retries -gt 0) {
+ $retries--
+ Write-Host "Waiting 10 seconds before retrying. Retries left: $retries"
+ Start-Sleep -Seconds 10
+
+ }
+ else
+ {
+ $exception = $_.Exception
+ throw $exception
+ }
+ }
+ }
+}
+
+cd $PSScriptRoot
+
+$repoFolder = $PSScriptRoot
+$env:REPO_FOLDER = $repoFolder
+
+$koreBuildZip="https://github.com/aspnet/KoreBuild/archive/dev.zip"
+if ($env:KOREBUILD_ZIP)
+{
+ $koreBuildZip=$env:KOREBUILD_ZIP
+}
+
+$buildFolder = ".build"
+$buildFile="$buildFolder\KoreBuild.ps1"
+
+if (!(Test-Path $buildFolder)) {
+ Write-Host "Downloading KoreBuild from $koreBuildZip"
+
+ $tempFolder=$env:TEMP + "\KoreBuild-" + [guid]::NewGuid()
+ New-Item -Path "$tempFolder" -Type directory | Out-Null
+
+ $localZipFile="$tempFolder\korebuild.zip"
+
+ DownloadWithRetry -url $koreBuildZip -downloadLocation $localZipFile -retries 6
+
+ Add-Type -AssemblyName System.IO.Compression.FileSystem
+ [System.IO.Compression.ZipFile]::ExtractToDirectory($localZipFile, $tempFolder)
+
+ New-Item -Path "$buildFolder" -Type directory | Out-Null
+ copy-item "$tempFolder\**\build\*" $buildFolder -Recurse
+
+ # Cleanup
+ if (Test-Path $tempFolder) {
+ Remove-Item -Recurse -Force $tempFolder
+ }
+}
+
+&"$buildFile" $args
\ No newline at end of file
diff --git a/build.sh b/build.sh
new file mode 100644
index 0000000..f420810
--- /dev/null
+++ b/build.sh
@@ -0,0 +1,46 @@
+#!/usr/bin/env bash
+repoFolder="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
+cd $repoFolder
+
+koreBuildZip="https://github.com/aspnet/KoreBuild/archive/dev.zip"
+if [ ! -z $KOREBUILD_ZIP ]; then
+ koreBuildZip=$KOREBUILD_ZIP
+fi
+
+buildFolder=".build"
+buildFile="$buildFolder/KoreBuild.sh"
+
+if test ! -d $buildFolder; then
+ echo "Downloading KoreBuild from $koreBuildZip"
+
+ tempFolder="/tmp/KoreBuild-$(uuidgen)"
+ mkdir $tempFolder
+
+ localZipFile="$tempFolder/korebuild.zip"
+
+ retries=6
+ until (wget -O $localZipFile $koreBuildZip 2>/dev/null || curl -o $localZipFile --location $koreBuildZip 2>/dev/null)
+ do
+ echo "Failed to download '$koreBuildZip'"
+ if [ "$retries" -le 0 ]; then
+ exit 1
+ fi
+ retries=$((retries - 1))
+ echo "Waiting 10 seconds before retrying. Retries left: $retries"
+ sleep 10s
+ done
+
+ unzip -q -d $tempFolder $localZipFile
+
+ mkdir $buildFolder
+ cp -r $tempFolder/**/build/** $buildFolder
+
+ chmod +x $buildFile
+
+ # Cleanup
+ if test ! -d $tempFolder; then
+ rm -rf $tempFolder
+ fi
+fi
+
+$buildFile -r $repoFolder "$@"
\ No newline at end of file
diff --git a/global.json b/global.json
new file mode 100644
index 0000000..0ddf69c
--- /dev/null
+++ b/global.json
@@ -0,0 +1,3 @@
+{
+ "projects": [ "src" ]
+}
\ No newline at end of file
diff --git a/makefile.shade b/makefile.shade
new file mode 100644
index 0000000..562494d
--- /dev/null
+++ b/makefile.shade
@@ -0,0 +1,7 @@
+
+var VERSION='0.1'
+var FULL_VERSION='0.1'
+var AUTHORS='Microsoft Open Technologies, Inc.'
+
+use-standard-lifecycle
+k-standard-goals
diff --git a/samples/ResponseCachingSample/ResponseCachingSample.xproj b/samples/ResponseCachingSample/ResponseCachingSample.xproj
new file mode 100644
index 0000000..cec78a3
--- /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)
+ .\bin\
+
+
+ 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 0000000..78685d6
--- /dev/null
+++ b/samples/ResponseCachingSample/Startup.cs
@@ -0,0 +1,45 @@
+// 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.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Net.Http.Headers;
+
+namespace ResponseCachingSample
+{
+ public class Startup
+ {
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services.AddMemoryCache();
+ }
+
+ 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);
+ });
+ }
+
+ public static void Main(string[] args)
+ {
+ var host = new WebHostBuilder()
+ .UseIISIntegration()
+ .UseKestrel()
+ .UseStartup()
+ .Build();
+
+ host.Run();
+ }
+ }
+}
diff --git a/samples/ResponseCachingSample/project.json b/samples/ResponseCachingSample/project.json
new file mode 100644
index 0000000..a759eb8
--- /dev/null
+++ b/samples/ResponseCachingSample/project.json
@@ -0,0 +1,36 @@
+{
+ "version": "0.1.0-*",
+ "buildOptions": {
+ "emitEntryPoint": true,
+ "preserveCompilationContext": true
+ },
+
+ "dependencies": {
+ "Microsoft.NETCore.Platforms": "1.0.1-*",
+ "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0-*",
+ "Microsoft.AspNetCore.Server.Kestrel": "1.0.0-*",
+ "Microsoft.Extensions.Caching.Memory": "1.0.0-*",
+ "Microsoft.AspNetCore.Http.Abstractions": "1.0.0-*",
+ "Microsoft.AspNetCore.Http.Extensions": "1.0.0-*",
+ "Microsoft.AspNetCore.ResponseCaching": "0.1.0-*"
+ },
+
+ "frameworks": {
+ "net451": {},
+ "netcoreapp1.0": {
+ "dependencies": {
+ "Microsoft.NETCore.App": {
+ "version": "1.0.0-*",
+ "type": "platform"
+ },
+ "System.Console": "4.0.0-*"
+ }
+ }
+ },
+
+ "runtimeOptions": {
+ "configProperties": {
+ "System.GC.Server": true
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.ResponseCaching/CachingContext.cs b/src/Microsoft.AspNetCore.ResponseCaching/CachingContext.cs
new file mode 100644
index 0000000..e5db372
--- /dev/null
+++ b/src/Microsoft.AspNetCore.ResponseCaching/CachingContext.cs
@@ -0,0 +1,210 @@
+// 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.IO;
+using System.Globalization;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Caching.Memory;
+using Microsoft.Extensions.Primitives;
+
+namespace Microsoft.AspNetCore.ResponseCaching
+{
+ public struct CachingContext
+ {
+ private string _cacheKey;
+ private RequestType _requestType;
+ private bool _isProxied;
+
+ public CachingContext(HttpContext httpContext, IMemoryCache cache)
+ {
+ HttpContext = httpContext;
+ Cache = cache;
+
+ _requestType = RequestType.NotCached;
+ _isProxied = false;
+ _cacheKey = null;
+ OriginalResponseStream = null;
+ Buffer = null;
+ ResponseStarted = false;
+ CacheResponse = false;
+ }
+
+ 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; }
+
+ public bool CheckRequestAllowsCaching()
+ {
+ // Verify the method
+ // TODO: What other methods should be supported?
+ if (string.Equals("GET", HttpContext.Request.Method, StringComparison.OrdinalIgnoreCase))
+ {
+ _requestType = RequestType.FullReponse;
+ }
+ else if (string.Equals("HEAD", HttpContext.Request.Method, StringComparison.OrdinalIgnoreCase) ||
+ string.Equals("OPTIONS", HttpContext.Request.Method, StringComparison.OrdinalIgnoreCase))
+ {
+ _requestType = RequestType.HeadersOnly;
+ }
+ else
+ {
+ _requestType = RequestType.NotCached;
+ 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;
+
+ var headers = cacheEntry.Headers;
+ for (var i = 0; i < headers.Count; i++)
+ {
+ var entry = headers[i];
+ response.Headers[entry.Key] = entry.Value;
+ }
+
+ // TODO: Allow setting proxied _isProxied
+ var age = Math.Max((DateTimeOffset.UtcNow - cacheEntry.Created).TotalSeconds, 0.0);
+ var ageString = (age > int.MaxValue ? int.MaxValue : (int)age).ToString(CultureInfo.InvariantCulture);
+ response.Headers[_isProxied ? "Age" : "X-Cache-Age"] = ageString;
+
+ if (_requestType == RequestType.HeadersOnly)
+ {
+ response.Headers["Content-Length"] = "0";
+ }
+ else
+ {
+ // Copy the cached response body
+ var body = cacheEntry.Body;
+ response.Headers["Content-Length"] = body.Length.ToString(CultureInfo.InvariantCulture);
+ 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()
+ {
+ // Don't cache errors? 404 etc
+ if (CacheResponse && HttpContext.Response.StatusCode == 200)
+ {
+ // Store the buffer to cache
+ var cacheEntry = new ResponseCacheEntry()
+ {
+ Created = DateTimeOffset.UtcNow,
+ StatusCode = HttpContext.Response.StatusCode
+ };
+
+ var headers = HttpContext.Response.Headers;
+ var count = headers.Count
+ - (headers.ContainsKey("Date") ? 1 : 0)
+ - (headers.ContainsKey("Content-Length") ? 1 : 0)
+ - (headers.ContainsKey("Age") ? 1 : 0);
+ var cachedHeaders = new List>(count);
+ int age = 0;
+ foreach (var entry in headers)
+ {
+ // Reduce create date by Age
+ if (entry.Key == "Age" && int.TryParse(entry.Value, out age) && age > 0)
+ {
+ cacheEntry.Created -= new TimeSpan(0, 0, age);
+ }
+ // Don't copy Date header or Content-Length
+ else if (entry.Key != "Date" && entry.Key != "Content-Length")
+ {
+ cachedHeaders.Add(entry);
+ }
+ }
+
+ 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;
+ }
+
+ private enum RequestType
+ {
+ NotCached,
+ HeadersOnly,
+ FullReponse
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.ResponseCaching/IResponseCacheOptions.cs b/src/Microsoft.AspNetCore.ResponseCaching/IResponseCacheOptions.cs
new file mode 100644
index 0000000..556c33e
--- /dev/null
+++ b/src/Microsoft.AspNetCore.ResponseCaching/IResponseCacheOptions.cs
@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace Microsoft.AspNetCore.ResponseCaching
+{
+ interface IResponseCacheOptions
+ {
+ int MaxCachedItemBytes { get; set; }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.xproj b/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.xproj
new file mode 100644
index 0000000..5fa1019
--- /dev/null
+++ b/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.xproj
@@ -0,0 +1,18 @@
+
+
+
+ 14.0
+ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
+
+
+
+ d1031270-dbd3-4f02-a3dc-3e7dade8ebe6
+ Microsoft.AspNetCore.ResponseCaching
+ ..\artifacts\obj\$(MSBuildProjectName)
+ .\bin\
+
+
+ 2.0
+
+
+
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.ResponseCaching/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000..09bbb58
--- /dev/null
+++ b/src/Microsoft.AspNetCore.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.AspNetCore.ResponseCaching/ResponseCacheEntry.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheEntry.cs
new file mode 100644
index 0000000..dc749f7
--- /dev/null
+++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheEntry.cs
@@ -0,0 +1,17 @@
+// 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;
+using Microsoft.Extensions.Primitives;
+using System;
+
+namespace Microsoft.AspNetCore.ResponseCaching
+{
+ public class ResponseCacheEntry
+ {
+ public int StatusCode { get; set; }
+ public List> Headers { get; set; }
+ public byte[] Body { get; set; }
+ public DateTimeOffset Created { get; set; }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingExtensions.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingExtensions.cs
new file mode 100644
index 0000000..60817a6
--- /dev/null
+++ b/src/Microsoft.AspNetCore.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.AspNetCore.ResponseCaching;
+
+namespace Microsoft.AspNetCore.Builder
+{
+ public static class ResponseCachingExtensions
+ {
+ public static IApplicationBuilder UseResponseCaching(this IApplicationBuilder app)
+ {
+ return app.UseMiddleware();
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs
new file mode 100644
index 0000000..fb7e4ce
--- /dev/null
+++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.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.Threading.Tasks;
+using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Caching.Memory;
+
+namespace Microsoft.AspNetCore.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.AspNetCore.ResponseCaching/project.json b/src/Microsoft.AspNetCore.ResponseCaching/project.json
new file mode 100644
index 0000000..76c316a
--- /dev/null
+++ b/src/Microsoft.AspNetCore.ResponseCaching/project.json
@@ -0,0 +1,31 @@
+{
+ "version": "0.1.0-*",
+ "description": "Middleware that automatically caches HTTP responses on the server.",
+ "packOptions": {
+ "repository": {
+ "type": "git",
+ "url": "git://github.com/aspnet/ResponseCaching"
+ },
+ "tags": [
+ "aspnetcore",
+ "response cache"
+ ]
+ },
+ "buildOptions": {
+ "warningsAsErrors": true
+ },
+ "dependencies": {
+ "Microsoft.AspNetCore.Http.Abstractions": "1.0.0-*",
+ "Microsoft.Extensions.Caching.Abstractions": "1.0.0-*",
+ "System.Buffers": "4.0.0-*"
+ },
+ "frameworks": {
+ "net451": {
+ "frameworkAssemblies": {
+ "System.Runtime": { "type": "build" }
+ }
+ },
+ "netstandard1.3": {
+ }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/CachingContextTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/CachingContextTests.cs
new file mode 100644
index 0000000..d191932
--- /dev/null
+++ b/test/Microsoft.AspNetCore.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.AspNetCore.Http;
+using Microsoft.Extensions.Caching.Memory;
+using Xunit;
+
+namespace Microsoft.AspNetCore.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.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.xproj b/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.xproj
new file mode 100644
index 0000000..cefc297
--- /dev/null
+++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.xproj
@@ -0,0 +1,21 @@
+
+
+
+ 14.0
+ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
+
+
+
+ 151b2027-3936-44b9-a4a0-e1e5902125ab
+ Microsoft.AspNetCore.ResponseCaching.Tests
+ ..\..\artifacts\obj\$(MSBuildProjectName)
+ .\bin\
+
+
+ 2.0
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json b/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json
new file mode 100644
index 0000000..0a036ec
--- /dev/null
+++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json
@@ -0,0 +1,34 @@
+{
+ "buildOptions": {
+ "warningsAsErrors": true
+ },
+ "dependencies": {
+ "dotnet-test-xunit": "1.0.0-*",
+ "Microsoft.AspNetCore.Http": "1.0.0-*",
+ "Microsoft.Extensions.Caching.Memory": "1.0.0-*",
+ "Microsoft.AspNetCore.ResponseCaching": "0.1.0-*",
+ "xunit": "2.1.0"
+ },
+ "frameworks": {
+
+ "netcoreapp1.0": {
+ "dependencies": {
+ "Microsoft.NETCore.App": {
+ "version": "1.0.0-*",
+ "type": "platform"
+ }
+ },
+ "imports": [
+ "dnxcore50",
+ "portable-net451+win8"
+ ]
+ },
+
+ "net451": {
+ "dependencies": {
+ "xunit.runner.console": "2.1.0"
+ }
+ }
+ },
+ "testRunner": "xunit"
+}