diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..bdaa5ba --- /dev/null +++ b/.gitattributes @@ -0,0 +1,50 @@ +*.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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..718941c --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ +[Oo]bj/ +[Bb]in/ +TestResults/ +.nuget/ +_ReSharper.*/ +packages/ +artifacts/ +PublishProfiles/ +*.user +*.suo +*.cache +*.docstates +_ReSharper.* +nuget.exe +*net45.csproj +*net451.csproj +*k10.csproj +*.psess +*.vsp +*.pidb +*.userprefs +*DS_Store +*.ncrunchsolution +*.*sdf +*.ipch +*.sln.ide +project.lock.json +/.vs/ +.build/ +.testPublish/ \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..ceb3c7b --- /dev/null +++ b/.travis.yml @@ -0,0 +1,28 @@ +language: csharp +sudo: required +dist: trusty +addons: + apt: + packages: + - gettext + - libcurl4-openssl-dev + - libicu-dev + - libssl-dev + - libunwind8 + - zlib1g +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 verify \ No newline at end of file diff --git a/NuGet.Config b/NuGet.Config new file mode 100644 index 0000000..5500f6d --- /dev/null +++ b/NuGet.Config @@ -0,0 +1,7 @@ + + + + + + + diff --git a/NuGetPackageVerifier.json b/NuGetPackageVerifier.json new file mode 100644 index 0000000..27720e2 --- /dev/null +++ b/NuGetPackageVerifier.json @@ -0,0 +1,15 @@ +{ + "adx": { // Packages written by the ADX team and that ship on NuGet.org + "rules": [ + "AdxVerificationCompositeRule" + ], + "packages": { + "Microsoft.AspNetCore.ResponseCaching": { }, + } + }, + "Default": { // Rules to run for packages not listed in any other set. + "rules": [ + "DefaultCompositeRule" + ] + } +} \ No newline at end of file diff --git a/README.md b/README.md index 4a181c9..cf7ba9f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -ASP.NET Response Caching +ASP.NET Core Response Caching ======== This repo hosts the ASP.NET Core middleware for response caching. diff --git a/ResponseCaching.sln b/ResponseCaching.sln new file mode 100644 index 0000000..c052708 --- /dev/null +++ b/ResponseCaching.sln @@ -0,0 +1,50 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 14 +VisualStudioVersion = 14.0.25420.1 +MinimumVisualStudioVersion = 10.0.40219.1 +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 +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.ResponseCaching", "src\Microsoft.AspNetCore.ResponseCaching\Microsoft.AspNetCore.ResponseCaching.xproj", "{D1031270-DBD3-4F02-A3DC-3E7DADE8EBE6}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {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 + {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 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {1139BDEE-FA15-474D-8855-0AB91F23CF26} = {C51DF5BD-B53D-4795-BC01-A9AB066BF286} + {151B2027-3936-44B9-A4A0-E1E5902125AB} = {89A50974-E9D4-4F87-ACF2-6A6005E64931} + {D1031270-DBD3-4F02-A3DC-3E7DADE8EBE6} = {367AABAF-E03C-4491-A9A7-BDDE8903D1B4} + EndGlobalSection +EndGlobal diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 0000000..b9a9bcd --- /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 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 100755 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..829daad --- /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 0000000..43167a3 --- /dev/null +++ b/samples/ResponseCachingSample/ResponseCachingSample.xproj @@ -0,0 +1,19 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 1139bdee-fa15-474d-8855-0ab91f23cf26 + ResponseCachingSample + .\obj + .\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..eca8af2 --- /dev/null +++ b/samples/ResponseCachingSample/Startup.cs @@ -0,0 +1,47 @@ +// 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.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() + .UseKestrel() + .UseContentRoot(Directory.GetCurrentDirectory()) + .UseIISIntegration() + .UseStartup() + .Build(); + + host.Run(); + } + } +} diff --git a/samples/ResponseCachingSample/project.json b/samples/ResponseCachingSample/project.json new file mode 100644 index 0000000..a32ead3 --- /dev/null +++ b/samples/ResponseCachingSample/project.json @@ -0,0 +1,34 @@ +{ + "version": "1.1.0-*", + "dependencies": { + "Microsoft.AspNetCore.ResponseCaching": "0.1.0-*", + "Microsoft.AspNetCore.Server.IISIntegration": "1.1.0-*", + "Microsoft.AspNetCore.Server.Kestrel": "1.1.0-*", + "Microsoft.Extensions.Caching.Memory": "1.1.0-*" + }, + "buildOptions": { + "emitEntryPoint": true + }, + "frameworks": { + "net451": {}, + "netcoreapp1.0": { + "dependencies": { + "Microsoft.NETCore.App": { + "version": "1.0.0-*", + "type": "platform" + } + } + } + }, + "publishOptions": { + "include": [ + "web.config" + ] + }, + "tools": { + "Microsoft.AspNetCore.Server.IISIntegration.Tools": "1.0.0-*" + }, + "scripts": { + "postpublish": "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" + } +} diff --git a/samples/ResponseCachingSample/web.config b/samples/ResponseCachingSample/web.config new file mode 100644 index 0000000..f7ac679 --- /dev/null +++ b/samples/ResponseCachingSample/web.config @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file 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..a4d4533 --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.xproj @@ -0,0 +1,19 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + d1031270-dbd3-4f02-a3dc-3e7dade8ebe6 + + + .\obj + .\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..32dcddf --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/Properties/AssemblyInfo.cs @@ -0,0 +1,11 @@ +// 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.Resources; + +[assembly: AssemblyMetadata("Serviceable", "True")] +[assembly: NeutralResourcesLanguage("en-us")] +[assembly: AssemblyCompany("Microsoft Corporation.")] +[assembly: AssemblyCopyright("© Microsoft Corporation. All rights reserved.")] +[assembly: AssemblyProduct("Microsoft ASP.NET Core")] \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs new file mode 100644 index 0000000..89927a1 --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs @@ -0,0 +1,145 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Caching.Memory; + +namespace Microsoft.AspNetCore.ResponseCaching +{ + public class ResponseCachingContext + { + private string _cacheKey; + + public ResponseCachingContext(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; } + + public 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(); + ResponseCachingEntry 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[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 ResponseCachingEntry(); + cacheEntry.StatusCode = HttpContext.Response.StatusCode; + foreach (var pair in HttpContext.Response.Headers) + { + cacheEntry.Headers[pair.Key] = pair.Value; + } + 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.AspNetCore.ResponseCaching/ResponseCachingEntry.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingEntry.cs new file mode 100644 index 0000000..c372e9a --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingEntry.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 Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.ResponseCaching +{ + internal class ResponseCachingEntry + { + public int StatusCode { get; set; } + internal IHeaderDictionary Headers { get; set; } = new HeaderDictionary(); + internal byte[] Body { 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..6e64205 --- /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 ResponseCachingContext(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..8023c1b --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/project.json @@ -0,0 +1,31 @@ +{ + "version": "0.1.0-*", + "buildOptions": { + "warningsAsErrors": true, + "keyFile": "../../tools/Key.snk", + "nowarn": [ + "CS1591" + ], + "xmlDoc": true + }, + "description": "ASP.NET Core middleware for caching HTTP responses on the server.", + "packOptions": { + "repository": { + "type": "git", + "url": "git://github.com/aspnet/ResponseCaching" + }, + "tags": [ + "aspnetcore", + "cache", + "caching" + ] + }, + "dependencies": { + "Microsoft.AspNetCore.Http": "1.1.0-*", + "Microsoft.Extensions.Caching.Abstractions": "1.1.0-*" + }, + "frameworks": { + "net451": {}, + "netstandard1.3": {} + } +} 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..fce0536 --- /dev/null +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.xproj @@ -0,0 +1,18 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + 151b2027-3936-44b9-a4a0-e1e5902125ab + Microsoft.AspNet.ResponseCaching.Tests + .\obj + .\bin\ + + + 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 new file mode 100644 index 0000000..dc9462d --- /dev/null +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.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 ResponseCachingContextTests + { + [Fact] + public void CheckRequestAllowsCaching_Method_GET_Allowed() + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.Method = "GET"; + var context = new ResponseCachingContext(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 ResponseCachingContext(httpContext, new MemoryCache(new MemoryCacheOptions())); + + Assert.False(context.CheckRequestAllowsCaching()); + } + } +} diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json b/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json new file mode 100644 index 0000000..be74f33 --- /dev/null +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json @@ -0,0 +1,25 @@ +{ + "buildOptions": { + "warningsAsErrors": true, + "keyFile": "../../tools/Key.snk" + }, + "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-*", + "xunit": "2.2.0-*" + }, + "frameworks": { + "netcoreapp1.0": { + "dependencies": { + "Microsoft.NETCore.App": { + "version": "1.0.0-*", + "type": "platform" + } + } + }, + "net451": {} + }, + "testRunner": "xunit" +} diff --git a/tools/Key.snk b/tools/Key.snk new file mode 100644 index 0000000..e10e488 Binary files /dev/null and b/tools/Key.snk differ