From 703fa4279eb75691018336994c9d70e3ee8c5170 Mon Sep 17 00:00:00 2001 From: Murat Girgin Date: Fri, 15 May 2015 10:01:00 -0700 Subject: [PATCH 001/188] Initial commit --- CONTRIBUTING.md | 4 ++++ LICENSE.txt | 12 ++++++++++++ README.md | 6 ++++++ 3 files changed, 22 insertions(+) create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE.txt create mode 100644 README.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..64ff041d5c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,4 @@ +Contributing +====== + +Information on contributing to this repo is in the [Contributing Guide](https://github.com/aspnet/Home/blob/dev/CONTRIBUTING.md) in the Home repo. diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000000..0bdc1962b6 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,12 @@ +Copyright (c) .NET Foundation. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +these files except in compliance with the License. You may obtain a copy of the +License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software distributed +under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +CONDITIONS OF ANY KIND, either express or implied. See the License for the +specific language governing permissions and limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000000..64dce69e3e --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +ASP.NET Response Caching +======== + +This repo hosts the ASP.NET 5 middleware for response caching. + +This project is part of ASP.NET vNext. You can find samples, documentation and getting started instructions for ASP.NET vNext at the [Home](https://github.com/aspnet/home) repo. From 47f24598f447b346899c204c9aa6bb5ff6b5cff1 Mon Sep 17 00:00:00 2001 From: Murat Girgin Date: Fri, 15 May 2015 10:04:22 -0700 Subject: [PATCH 002/188] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 64dce69e3e..4e0aa44a0f 100644 --- a/README.md +++ b/README.md @@ -3,4 +3,4 @@ ASP.NET Response Caching This repo hosts the ASP.NET 5 middleware for response caching. -This project is part of ASP.NET vNext. You can find samples, documentation and getting started instructions for ASP.NET vNext at the [Home](https://github.com/aspnet/home) repo. +This project is part of ASP.NET 5. You can find samples, documentation and getting started instructions for ASP.NET 5 at the [Home](https://github.com/aspnet/home) repo. From 2ec664f32591fa729802d602f8417f9006cadf1c Mon Sep 17 00:00:00 2001 From: Cesar Blum Silveira Date: Mon, 14 Mar 2016 22:01:49 -0700 Subject: [PATCH 003/188] ASP.NET 5 -> ASP.NET Core --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4e0aa44a0f..4a181c96e8 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ASP.NET Response Caching ======== -This repo hosts the ASP.NET 5 middleware for response caching. +This repo hosts the ASP.NET Core middleware for response caching. -This project is part of ASP.NET 5. You can find samples, documentation and getting started instructions for ASP.NET 5 at the [Home](https://github.com/aspnet/home) repo. +This project is part of ASP.NET Core. You can find samples, documentation and getting started instructions for ASP.NET Core at the [Home](https://github.com/aspnet/home) repo. From ed9f238edade41379e577a713e232a921a1ec130 Mon Sep 17 00:00:00 2001 From: Chris R Date: Wed, 20 May 2015 09:10:28 -0700 Subject: [PATCH 004/188] Add build infrastructure. --- .gitattributes | 50 ++++++++++++++++++++++++++++++++++++++++++++ .gitignore | 26 +++++++++++++++++++++++ NuGet.Config | 7 +++++++ NuGet.master.config | 7 +++++++ NuGet.release.config | 7 +++++++ build.cmd | 28 +++++++++++++++++++++++++ build.sh | 38 +++++++++++++++++++++++++++++++++ makefile.shade | 7 +++++++ 8 files changed, 170 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 NuGet.Config create mode 100644 NuGet.master.config create mode 100644 NuGet.release.config create mode 100644 build.cmd create mode 100644 build.sh create mode 100644 makefile.shade diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..bdaa5ba982 --- /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 0000000000..216e8d9c58 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +[Oo]bj/ +[Bb]in/ +TestResults/ +.nuget/ +*.sln.ide/ +_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 \ No newline at end of file diff --git a/NuGet.Config b/NuGet.Config new file mode 100644 index 0000000000..46c3b3e36c --- /dev/null +++ b/NuGet.Config @@ -0,0 +1,7 @@ + + + + + + + diff --git a/NuGet.master.config b/NuGet.master.config new file mode 100644 index 0000000000..e2edffce48 --- /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 0000000000..1978dc065a --- /dev/null +++ b/NuGet.release.config @@ -0,0 +1,7 @@ + + + + + + + diff --git a/build.cmd b/build.cmd new file mode 100644 index 0000000000..41025afb26 --- /dev/null +++ b/build.cmd @@ -0,0 +1,28 @@ +@echo off +cd %~dp0 + +SETLOCAL +SET CACHED_NUGET=%LocalAppData%\NuGet\NuGet.exe + +IF EXIST %CACHED_NUGET% goto copynuget +echo Downloading latest version of NuGet.exe... +IF NOT EXIST %LocalAppData%\NuGet md %LocalAppData%\NuGet +@powershell -NoProfile -ExecutionPolicy unrestricted -Command "$ProgressPreference = 'SilentlyContinue'; Invoke-WebRequest 'https://www.nuget.org/nuget.exe' -OutFile '%CACHED_NUGET%'" + +:copynuget +IF EXIST .nuget\nuget.exe goto restore +md .nuget +copy %CACHED_NUGET% .nuget\nuget.exe > nul + +:restore +IF EXIST packages\KoreBuild goto run +.nuget\NuGet.exe install KoreBuild -ExcludeVersion -o packages -nocache -pre +.nuget\NuGet.exe install Sake -version 0.2 -o packages -ExcludeVersion + +IF "%SKIP_DNX_INSTALL%"=="1" goto run +CALL packages\KoreBuild\build\dnvm upgrade -runtime CLR -arch x86 +CALL packages\KoreBuild\build\dnvm install default -runtime CoreCLR -arch x86 + +:run +CALL packages\KoreBuild\build\dnvm use default -runtime CLR -arch x86 +packages\Sake\tools\Sake.exe -I packages\KoreBuild\build -f makefile.shade %* diff --git a/build.sh b/build.sh new file mode 100644 index 0000000000..c8cc2a72e1 --- /dev/null +++ b/build.sh @@ -0,0 +1,38 @@ +#!/bin/bash + +if test `uname` = Darwin; then + cachedir=~/Library/Caches/KBuild +else + if [ -z $XDG_DATA_HOME ]; then + cachedir=$HOME/.local/share + else + cachedir=$XDG_DATA_HOME; + fi +fi +mkdir -p $cachedir + +url=https://www.nuget.org/nuget.exe + +if test ! -f $cachedir/nuget.exe; then + wget -O $cachedir/nuget.exe $url 2>/dev/null || curl -o $cachedir/nuget.exe --location $url /dev/null +fi + +if test ! -e .nuget; then + mkdir .nuget + cp $cachedir/nuget.exe .nuget/nuget.exe +fi + +if test ! -d packages/KoreBuild; then + mono .nuget/nuget.exe install KoreBuild -ExcludeVersion -o packages -nocache -pre + mono .nuget/nuget.exe install Sake -version 0.2 -o packages -ExcludeVersion +fi + +if ! type dnvm > /dev/null 2>&1; then + source packages/KoreBuild/build/dnvm.sh +fi + +if ! type dnx > /dev/null 2>&1; then + dnvm upgrade +fi + +mono packages/Sake/tools/Sake.exe -I packages/KoreBuild/build -f makefile.shade "$@" diff --git a/makefile.shade b/makefile.shade new file mode 100644 index 0000000000..562494d144 --- /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 From a14bb69d6a5d519b729879715269d768e10e39fc Mon Sep 17 00:00:00 2001 From: Chris R Date: Fri, 22 May 2015 12:53:34 -0700 Subject: [PATCH 005/188] Rough outline of middleware, sample, and test projects. --- .gitignore | 3 +- ResponseCaching.sln | 50 ++++++ global.json | 3 + .../ResponseCachingSample.xproj | 19 +++ samples/ResponseCachingSample/Startup.cs | 33 ++++ samples/ResponseCachingSample/project.json | 33 ++++ .../CachingContext.cs | 143 ++++++++++++++++++ .../Microsoft.AspNet.ResponseCaching.xproj | 20 +++ .../Properties/AssemblyInfo.cs | 8 + .../ResponseCacheEntry.cs | 14 ++ .../ResponseCachingExtensions.cs | 15 ++ .../ResponseCachingMiddleware.cs | 61 ++++++++ .../project.json | 15 ++ .../CachingContextTests.cs | 33 ++++ ...crosoft.AspNet.ResponseCaching.Tests.xproj | 20 +++ .../project.json | 15 ++ 16 files changed, 484 insertions(+), 1 deletion(-) create mode 100644 ResponseCaching.sln create mode 100644 global.json create mode 100644 samples/ResponseCachingSample/ResponseCachingSample.xproj create mode 100644 samples/ResponseCachingSample/Startup.cs create mode 100644 samples/ResponseCachingSample/project.json create mode 100644 src/Microsoft.AspNet.ResponseCaching/CachingContext.cs create mode 100644 src/Microsoft.AspNet.ResponseCaching/Microsoft.AspNet.ResponseCaching.xproj create mode 100644 src/Microsoft.AspNet.ResponseCaching/Properties/AssemblyInfo.cs create mode 100644 src/Microsoft.AspNet.ResponseCaching/ResponseCacheEntry.cs create mode 100644 src/Microsoft.AspNet.ResponseCaching/ResponseCachingExtensions.cs create mode 100644 src/Microsoft.AspNet.ResponseCaching/ResponseCachingMiddleware.cs create mode 100644 src/Microsoft.AspNet.ResponseCaching/project.json create mode 100644 test/Microsoft.AspNet.ResponseCaching.Tests/CachingContextTests.cs create mode 100644 test/Microsoft.AspNet.ResponseCaching.Tests/Microsoft.AspNet.ResponseCaching.Tests.xproj create mode 100644 test/Microsoft.AspNet.ResponseCaching.Tests/project.json 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": { } + } +} From 1628d42b4369572acaaa43d1d7e48926b3b3c619 Mon Sep 17 00:00:00 2001 From: John Luo Date: Wed, 20 Jul 2016 17:03:59 -0700 Subject: [PATCH 006/188] Clean up and update to post RTM --- .gitignore | 7 +- .travis.yml | 28 +++++++ NuGet.Config | 4 +- NuGet.master.config | 7 -- NuGet.release.config | 7 -- NuGetPackageVerifier.json | 15 ++++ README.md | 2 +- ResponseCaching.sln | 18 ++--- appveyor.yml | 13 +++ build.cmd | 30 +------ build.ps1 | 67 +++++++++++++++ build.sh | 76 ++++++++++-------- global.json | 2 +- makefile.shade | 7 -- .../ResponseCachingSample.xproj | 8 +- samples/ResponseCachingSample/Startup.cs | 22 ++++- samples/ResponseCachingSample/project.json | 53 ++++++------ samples/ResponseCachingSample/web.config | 9 +++ .../Properties/AssemblyInfo.cs | 8 -- .../project.json | 15 ---- ...icrosoft.AspNetCore.ResponseCaching.xproj} | 15 ++-- .../Properties/AssemblyInfo.cs | 11 +++ .../ResponseCachingContext.cs} | 24 +++--- .../ResponseCachingEntry.cs} | 8 +- .../ResponseCachingExtensions.cs | 4 +- .../ResponseCachingMiddleware.cs | 9 +-- .../project.json | 31 +++++++ .../project.json | 15 ---- ...ft.AspNetCore.ResponseCaching.Tests.xproj} | 12 ++- .../ResponseCachingContextTests.cs} | 12 +-- .../project.json | 25 ++++++ tools/Key.snk | Bin 0 -> 596 bytes 32 files changed, 351 insertions(+), 213 deletions(-) create mode 100644 .travis.yml delete mode 100644 NuGet.master.config delete mode 100644 NuGet.release.config create mode 100644 NuGetPackageVerifier.json create mode 100644 appveyor.yml create mode 100644 build.ps1 mode change 100644 => 100755 build.sh delete mode 100644 makefile.shade create mode 100644 samples/ResponseCachingSample/web.config delete mode 100644 src/Microsoft.AspNet.ResponseCaching/Properties/AssemblyInfo.cs delete mode 100644 src/Microsoft.AspNet.ResponseCaching/project.json rename src/{Microsoft.AspNet.ResponseCaching/Microsoft.AspNet.ResponseCaching.xproj => Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.xproj} (59%) create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Properties/AssemblyInfo.cs rename src/{Microsoft.AspNet.ResponseCaching/CachingContext.cs => Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs} (87%) rename src/{Microsoft.AspNet.ResponseCaching/ResponseCacheEntry.cs => Microsoft.AspNetCore.ResponseCaching/ResponseCachingEntry.cs} (57%) rename src/{Microsoft.AspNet.ResponseCaching => Microsoft.AspNetCore.ResponseCaching}/ResponseCachingExtensions.cs (83%) rename src/{Microsoft.AspNet.ResponseCaching => Microsoft.AspNetCore.ResponseCaching}/ResponseCachingMiddleware.cs (89%) create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/project.json delete mode 100644 test/Microsoft.AspNet.ResponseCaching.Tests/project.json rename test/{Microsoft.AspNet.ResponseCaching.Tests/Microsoft.AspNet.ResponseCaching.Tests.xproj => Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.xproj} (65%) rename test/{Microsoft.AspNet.ResponseCaching.Tests/CachingContextTests.cs => Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs} (67%) create mode 100644 test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json create mode 100644 tools/Key.snk diff --git a/.gitignore b/.gitignore index be311a1f7d..718941ca08 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,6 @@ [Bb]in/ TestResults/ .nuget/ -*.sln.ide/ _ReSharper.*/ packages/ artifacts/ @@ -24,4 +23,8 @@ nuget.exe *.ncrunchsolution *.*sdf *.ipch -project.lock.json \ No newline at end of file +*.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 0000000000..ceb3c7b67b --- /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 index 46c3b3e36c..5500f6d507 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -1,7 +1,7 @@  - - + + diff --git a/NuGet.master.config b/NuGet.master.config deleted file mode 100644 index e2edffce48..0000000000 --- a/NuGet.master.config +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/NuGet.release.config b/NuGet.release.config deleted file mode 100644 index 1978dc065a..0000000000 --- a/NuGet.release.config +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/NuGetPackageVerifier.json b/NuGetPackageVerifier.json new file mode 100644 index 0000000000..27720e2307 --- /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 4a181c96e8..cf7ba9f96d 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 index 358a798cbb..c052708fa0 100644 --- a/ResponseCaching.sln +++ b/ResponseCaching.sln @@ -1,10 +1,8 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 14 -VisualStudioVersion = 14.0.22823.1 +VisualStudioVersion = 14.0.25420.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}" @@ -18,7 +16,9 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution 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}" +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 @@ -26,10 +26,6 @@ Global 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 @@ -38,13 +34,17 @@ Global {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 - {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} + {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 0000000000..b9a9bcd1e6 --- /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 index 41025afb26..7d4894cb4a 100644 --- a/build.cmd +++ b/build.cmd @@ -1,28 +1,2 @@ -@echo off -cd %~dp0 - -SETLOCAL -SET CACHED_NUGET=%LocalAppData%\NuGet\NuGet.exe - -IF EXIST %CACHED_NUGET% goto copynuget -echo Downloading latest version of NuGet.exe... -IF NOT EXIST %LocalAppData%\NuGet md %LocalAppData%\NuGet -@powershell -NoProfile -ExecutionPolicy unrestricted -Command "$ProgressPreference = 'SilentlyContinue'; Invoke-WebRequest 'https://www.nuget.org/nuget.exe' -OutFile '%CACHED_NUGET%'" - -:copynuget -IF EXIST .nuget\nuget.exe goto restore -md .nuget -copy %CACHED_NUGET% .nuget\nuget.exe > nul - -:restore -IF EXIST packages\KoreBuild goto run -.nuget\NuGet.exe install KoreBuild -ExcludeVersion -o packages -nocache -pre -.nuget\NuGet.exe install Sake -version 0.2 -o packages -ExcludeVersion - -IF "%SKIP_DNX_INSTALL%"=="1" goto run -CALL packages\KoreBuild\build\dnvm upgrade -runtime CLR -arch x86 -CALL packages\KoreBuild\build\dnvm install default -runtime CoreCLR -arch x86 - -:run -CALL packages\KoreBuild\build\dnvm use default -runtime CLR -arch x86 -packages\Sake\tools\Sake.exe -I packages\KoreBuild\build -f makefile.shade %* +@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 0000000000..8f2f99691a --- /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 old mode 100644 new mode 100755 index c8cc2a72e1..f4208100eb --- a/build.sh +++ b/build.sh @@ -1,38 +1,46 @@ -#!/bin/bash +#!/usr/bin/env bash +repoFolder="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +cd $repoFolder -if test `uname` = Darwin; then - cachedir=~/Library/Caches/KBuild -else - if [ -z $XDG_DATA_HOME ]; then - cachedir=$HOME/.local/share - else - cachedir=$XDG_DATA_HOME; +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 -mkdir -p $cachedir -url=https://www.nuget.org/nuget.exe - -if test ! -f $cachedir/nuget.exe; then - wget -O $cachedir/nuget.exe $url 2>/dev/null || curl -o $cachedir/nuget.exe --location $url /dev/null -fi - -if test ! -e .nuget; then - mkdir .nuget - cp $cachedir/nuget.exe .nuget/nuget.exe -fi - -if test ! -d packages/KoreBuild; then - mono .nuget/nuget.exe install KoreBuild -ExcludeVersion -o packages -nocache -pre - mono .nuget/nuget.exe install Sake -version 0.2 -o packages -ExcludeVersion -fi - -if ! type dnvm > /dev/null 2>&1; then - source packages/KoreBuild/build/dnvm.sh -fi - -if ! type dnx > /dev/null 2>&1; then - dnvm upgrade -fi - -mono packages/Sake/tools/Sake.exe -I packages/KoreBuild/build -f makefile.shade "$@" +$buildFile -r $repoFolder "$@" \ No newline at end of file diff --git a/global.json b/global.json index 397ac5fc98..829daadd5b 100644 --- a/global.json +++ b/global.json @@ -1,3 +1,3 @@ { - "projects": ["src"] + "projects": ["src"] } \ No newline at end of file diff --git a/makefile.shade b/makefile.shade deleted file mode 100644 index 562494d144..0000000000 --- a/makefile.shade +++ /dev/null @@ -1,7 +0,0 @@ - -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 index 2403e53763..43167a3606 100644 --- a/samples/ResponseCachingSample/ResponseCachingSample.xproj +++ b/samples/ResponseCachingSample/ResponseCachingSample.xproj @@ -4,16 +4,16 @@ 14.0 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - + 1139bdee-fa15-474d-8855-0ab91f23cf26 ResponseCachingSample - ..\..\artifacts\obj\$(MSBuildProjectName) - ..\..\artifacts\bin\$(MSBuildProjectName)\ + .\obj + .\bin\ 2.0 2931 - + \ No newline at end of file diff --git a/samples/ResponseCachingSample/Startup.cs b/samples/ResponseCachingSample/Startup.cs index 1ca6d9ac4c..eca8af2ae0 100644 --- a/samples/ResponseCachingSample/Startup.cs +++ b/samples/ResponseCachingSample/Startup.cs @@ -2,9 +2,11 @@ // 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 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 @@ -13,7 +15,7 @@ namespace ResponseCachingSample { public void ConfigureServices(IServiceCollection services) { - services.AddCaching(); + services.AddMemoryCache(); } public void Configure(IApplicationBuilder app) @@ -29,5 +31,17 @@ namespace ResponseCachingSample 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 index ca19e78857..a32ead3d3a 100644 --- a/samples/ResponseCachingSample/project.json +++ b/samples/ResponseCachingSample/project.json @@ -1,33 +1,34 @@ { - "webroot": "wwwroot", - "version": "1.0.0-*", - + "version": "1.1.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-*" + "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-*" }, - - "commands": { - "web": "Microsoft.AspNet.Hosting --server Microsoft.AspNet.Server.WebListener --server.urls http://localhost:5000" + "buildOptions": { + "emitEntryPoint": true }, - "frameworks": { - "dnx451": { }, - "dnxcore50": { } + "net451": {}, + "netcoreapp1.0": { + "dependencies": { + "Microsoft.NETCore.App": { + "version": "1.0.0-*", + "type": "platform" + } + } + } }, - - "publishExclude": [ - "node_modules", - "bower_components", - "**.xproj", - "**.user", - "**.vspscc" - ], - "exclude": [ - "wwwroot", - "node_modules", - "bower_components" - ] + "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 0000000000..f7ac679334 --- /dev/null +++ b/samples/ResponseCachingSample/web.config @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/src/Microsoft.AspNet.ResponseCaching/Properties/AssemblyInfo.cs b/src/Microsoft.AspNet.ResponseCaching/Properties/AssemblyInfo.cs deleted file mode 100644 index 09bbb58941..0000000000 --- a/src/Microsoft.AspNet.ResponseCaching/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,8 +0,0 @@ -// 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/project.json b/src/Microsoft.AspNet.ResponseCaching/project.json deleted file mode 100644 index b7f364b6cc..0000000000 --- a/src/Microsoft.AspNet.ResponseCaching/project.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "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/src/Microsoft.AspNet.ResponseCaching/Microsoft.AspNet.ResponseCaching.xproj b/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.xproj similarity index 59% rename from src/Microsoft.AspNet.ResponseCaching/Microsoft.AspNet.ResponseCaching.xproj rename to src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.xproj index bb65aad14e..a4d4533df7 100644 --- a/src/Microsoft.AspNet.ResponseCaching/Microsoft.AspNet.ResponseCaching.xproj +++ b/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.xproj @@ -4,17 +4,16 @@ 14.0 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - + d1031270-dbd3-4f02-a3dc-3e7dade8ebe6 - Microsoft.AspNet.ResponseCaching - ..\artifacts\obj\$(MSBuildProjectName) - ..\artifacts\bin\$(MSBuildProjectName)\ + + + .\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 0000000000..32dcddfc57 --- /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.AspNet.ResponseCaching/CachingContext.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs similarity index 87% rename from src/Microsoft.AspNet.ResponseCaching/CachingContext.cs rename to src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs index c5586ce04e..89927a1783 100644 --- a/src/Microsoft.AspNet.ResponseCaching/CachingContext.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs @@ -3,18 +3,17 @@ using System; using System.IO; -using System.Linq; using System.Threading.Tasks; -using Microsoft.AspNet.Http; -using Microsoft.Framework.Caching.Memory; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Caching.Memory; -namespace Microsoft.AspNet.ResponseCaching +namespace Microsoft.AspNetCore.ResponseCaching { - internal class CachingContext + public class ResponseCachingContext { private string _cacheKey; - public CachingContext(HttpContext httpContext, IMemoryCache cache) + public ResponseCachingContext(HttpContext httpContext, IMemoryCache cache) { HttpContext = httpContext; Cache = cache; @@ -32,7 +31,7 @@ namespace Microsoft.AspNet.ResponseCaching private bool CacheResponse { get; set; } - internal bool CheckRequestAllowsCaching() + public bool CheckRequestAllowsCaching() { // Verify the method // TODO: What other methods should be supported? @@ -64,7 +63,7 @@ namespace Microsoft.AspNet.ResponseCaching internal async Task TryServeFromCacheAsync() { _cacheKey = CreateCacheKey(); - ResponseCacheEntry cacheEntry; + ResponseCachingEntry cacheEntry; if (Cache.TryGetValue(_cacheKey, out cacheEntry)) { // TODO: Compare cached request headers @@ -80,7 +79,7 @@ namespace Microsoft.AspNet.ResponseCaching response.StatusCode = cacheEntry.StatusCode; foreach (var pair in cacheEntry.Headers) { - response.Headers.SetValues(pair.Key, pair.Value); + response.Headers[pair.Key] = pair.Value; } // TODO: Update cache headers (Age) @@ -122,9 +121,12 @@ namespace Microsoft.AspNet.ResponseCaching if (CacheResponse) { // Store the buffer to cache - var cacheEntry = new ResponseCacheEntry(); + var cacheEntry = new ResponseCachingEntry(); cacheEntry.StatusCode = HttpContext.Response.StatusCode; - cacheEntry.Headers = HttpContext.Response.Headers.ToList(); + foreach (var pair in HttpContext.Response.Headers) + { + cacheEntry.Headers[pair.Key] = pair.Value; + } cacheEntry.Body = Buffer.ToArray(); Cache.Set(_cacheKey, cacheEntry); // TODO: Timeouts } diff --git a/src/Microsoft.AspNet.ResponseCaching/ResponseCacheEntry.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingEntry.cs similarity index 57% rename from src/Microsoft.AspNet.ResponseCaching/ResponseCacheEntry.cs rename to src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingEntry.cs index f23685de33..c372e9a1ac 100644 --- a/src/Microsoft.AspNet.ResponseCaching/ResponseCacheEntry.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingEntry.cs @@ -1,14 +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; +using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNet.ResponseCaching +namespace Microsoft.AspNetCore.ResponseCaching { - internal class ResponseCacheEntry + internal class ResponseCachingEntry { public int StatusCode { get; set; } - internal IEnumerable> Headers { get; set; } + internal IHeaderDictionary Headers { get; set; } = new HeaderDictionary(); internal byte[] Body { get; set; } } } diff --git a/src/Microsoft.AspNet.ResponseCaching/ResponseCachingExtensions.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingExtensions.cs similarity index 83% rename from src/Microsoft.AspNet.ResponseCaching/ResponseCachingExtensions.cs rename to src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingExtensions.cs index 6632ad0d2b..60817a6511 100644 --- a/src/Microsoft.AspNet.ResponseCaching/ResponseCachingExtensions.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingExtensions.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 Microsoft.AspNet.ResponseCaching; +using Microsoft.AspNetCore.ResponseCaching; -namespace Microsoft.AspNet.Builder +namespace Microsoft.AspNetCore.Builder { public static class ResponseCachingExtensions { diff --git a/src/Microsoft.AspNet.ResponseCaching/ResponseCachingMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs similarity index 89% rename from src/Microsoft.AspNet.ResponseCaching/ResponseCachingMiddleware.cs rename to src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs index 0c0fda42a2..6e642057da 100644 --- a/src/Microsoft.AspNet.ResponseCaching/ResponseCachingMiddleware.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs @@ -2,11 +2,10 @@ // 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; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Caching.Memory; -namespace Microsoft.AspNet.ResponseCaching +namespace Microsoft.AspNetCore.ResponseCaching { // http://tools.ietf.org/html/rfc7234 public class ResponseCachingMiddleware @@ -22,7 +21,7 @@ namespace Microsoft.AspNet.ResponseCaching public async Task Invoke(HttpContext context) { - var cachingContext = new CachingContext(context, _cache); + var cachingContext = new ResponseCachingContext(context, _cache); // Should we attempt any caching logic? if (cachingContext.CheckRequestAllowsCaching()) { diff --git a/src/Microsoft.AspNetCore.ResponseCaching/project.json b/src/Microsoft.AspNetCore.ResponseCaching/project.json new file mode 100644 index 0000000000..8023c1b533 --- /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.AspNet.ResponseCaching.Tests/project.json b/test/Microsoft.AspNet.ResponseCaching.Tests/project.json deleted file mode 100644 index 5ad3ebb2d2..0000000000 --- a/test/Microsoft.AspNet.ResponseCaching.Tests/project.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "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": { } - } -} diff --git a/test/Microsoft.AspNet.ResponseCaching.Tests/Microsoft.AspNet.ResponseCaching.Tests.xproj b/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.xproj similarity index 65% rename from test/Microsoft.AspNet.ResponseCaching.Tests/Microsoft.AspNet.ResponseCaching.Tests.xproj rename to test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.xproj index ee98d4b4cd..fce053679a 100644 --- a/test/Microsoft.AspNet.ResponseCaching.Tests/Microsoft.AspNet.ResponseCaching.Tests.xproj +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.xproj @@ -4,17 +4,15 @@ 14.0 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - + 151b2027-3936-44b9-a4a0-e1e5902125ab Microsoft.AspNet.ResponseCaching.Tests - ..\..\artifacts\obj\$(MSBuildProjectName) - ..\..\artifacts\bin\$(MSBuildProjectName)\ + .\obj + .\bin\ - 2.0 - - + + \ No newline at end of file diff --git a/test/Microsoft.AspNet.ResponseCaching.Tests/CachingContextTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs similarity index 67% rename from test/Microsoft.AspNet.ResponseCaching.Tests/CachingContextTests.cs rename to test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs index d0315b5a58..dc9462d295 100644 --- a/test/Microsoft.AspNet.ResponseCaching.Tests/CachingContextTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs @@ -1,20 +1,20 @@ // 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 Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Caching.Memory; using Xunit; -namespace Microsoft.AspNet.ResponseCaching +namespace Microsoft.AspNetCore.ResponseCaching { - public class CachingContextTests + public class ResponseCachingContextTests { [Fact] public void CheckRequestAllowsCaching_Method_GET_Allowed() { var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "GET"; - var context = new CachingContext(httpContext, new MemoryCache(new MemoryCacheOptions())); + var context = new ResponseCachingContext(httpContext, new MemoryCache(new MemoryCacheOptions())); Assert.True(context.CheckRequestAllowsCaching()); } @@ -25,7 +25,7 @@ namespace Microsoft.AspNet.ResponseCaching { var httpContext = new DefaultHttpContext(); httpContext.Request.Method = method; - var context = new CachingContext(httpContext, new MemoryCache(new MemoryCacheOptions())); + 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 0000000000..be74f330a0 --- /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 0000000000000000000000000000000000000000..e10e4889c125d3120cd9e81582243d70f7cbb806 GIT binary patch literal 596 zcmV-a0;~N80ssI2Bme+XQ$aES1ONa50098=Iw=HCsnz~#iVhm& zj%TU(_THUee?3yHBjk$37ysB?i5#7WD$={H zV4B!OxRPrb|8)HPg~A}8P>^=#y<)56#=E&NzcjOtPK~<4n6GHt=K$ro*T(lhby_@U zEk(hLzk1H)0yXj{A_5>fk-TgNoP|q6(tP2xo8zt8i%212CWM#AeCd?`hS|4~L({h~Moo(~vy&3Z z1uI}`fd^*>o=rwbAGymj6RM^pZm(*Kfhs+Y1#`-2JPWZMK8@;ZWCk2+9bX4YP);~fj-BU*R zQPvWv$89!{Rl9wM+zR>_TSkn^voYxA?2G iKnV#iZ6Ah`K>b=@=IjYJXrxL124zR(38)nxe+&q_$QXwJ literal 0 HcmV?d00001 From d204adafd2560852acf82b2c16c624caba6aba8a Mon Sep 17 00:00:00 2001 From: John Luo Date: Wed, 27 Jul 2016 14:58:58 -0700 Subject: [PATCH 007/188] Add CI status to readme --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index cf7ba9f96d..5c6ea8d9c8 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ ASP.NET Core Response Caching ======== +AppVeyor: [![AppVeyor](https://ci.appveyor.com/api/projects/status/p52yj0kghdyicvwu/branch/dev?svg=true)](https://ci.appveyor.com/project/aspnetci/ResponseCaching/branch/dev) + +Travis: [![Travis](https://travis-ci.org/aspnet/ResponseCaching.svg?branch=dev)](https://travis-ci.org/aspnet/ResponseCaching) This repo hosts the ASP.NET Core middleware for response caching. From 8e7eb5d99e07b872fea6b49fe6e2cd7028f34dd1 Mon Sep 17 00:00:00 2001 From: BrennanConroy Date: Tue, 2 Aug 2016 13:07:17 -0700 Subject: [PATCH 008/188] Update .travis.yml --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index ceb3c7b67b..efc1a57214 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,6 +23,6 @@ branches: - dev - /^(.*\/)?ci-.*$/ before_install: - - if test "$TRAVIS_OS_NAME" == "osx"; then brew update; brew install openssl; brew link --force openssl; fi + - if test "$TRAVIS_OS_NAME" == "osx"; then brew update; brew install openssl; ln -s /usr/local/opt/openssl/lib/libcrypto.1.0.0.dylib /usr/local/lib/; ln -s /usr/local/opt/openssl/lib/libssl.1.0.0.dylib /usr/local/lib/; fi script: - - ./build.sh verify \ No newline at end of file + - ./build.sh verify From a40cc8816975590d78f81fef3d695043340bd935 Mon Sep 17 00:00:00 2001 From: John Luo Date: Thu, 21 Jul 2016 19:25:51 -0700 Subject: [PATCH 009/188] IResponseCache adapter and support for vary by header --- samples/ResponseCachingSample/Startup.cs | 7 +- .../IResponseCache.cs | 13 ++ .../CachedResponse.cs} | 8 +- .../Internal/CachedVaryBy.cs | 12 ++ .../DefaultResponseCacheEntrySerializer.cs | 157 ++++++++++++++++++ .../Internal/DistributedResponseCache.cs | 60 +++++++ .../Internal/MemoryResponseCache.cs | 38 +++++ .../ResponseCachingContext.cs | 124 +++++++++++--- .../ResponseCachingMiddleware.cs | 16 +- ...ponseCachingServiceCollectionExtensions.cs | 39 +++++ .../project.json | 2 +- ...oft.AspNetCore.ResponseCaching.Tests.xproj | 3 + .../ResponseCachingContextTests.cs | 23 ++- .../ResponseCachingTests.cs | 50 ++++++ .../project.json | 3 +- 15 files changed, 516 insertions(+), 39 deletions(-) create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/IResponseCache.cs rename src/Microsoft.AspNetCore.ResponseCaching/{ResponseCachingEntry.cs => Internal/CachedResponse.cs} (71%) create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Internal/CachedVaryBy.cs create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Internal/DefaultResponseCacheEntrySerializer.cs create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Internal/DistributedResponseCache.cs create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingServiceCollectionExtensions.cs create mode 100644 test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs 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": { From 91d57c72bf0a1e32e1380b25ad12f18b8b5feba1 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Tue, 9 Aug 2016 15:10:17 -0700 Subject: [PATCH 010/188] Switching to dotnet.myget.org feed --- NuGet.Config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NuGet.Config b/NuGet.Config index 5500f6d507..0fd623ffdd 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -1,7 +1,7 @@  - + From 7d716d20072b50ea7c6f9e56cc31ed204de1cd85 Mon Sep 17 00:00:00 2001 From: Ben Adams Date: Wed, 27 Jul 2016 20:06:14 +0100 Subject: [PATCH 011/188] Date & Age handling --- .../IResponseCachingOptions.cs | 10 +++ .../Internal/CachedResponse.cs | 2 + .../ResponseCachingContext.cs | 71 ++++++++++++++++--- 3 files changed, 73 insertions(+), 10 deletions(-) create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/IResponseCachingOptions.cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/IResponseCachingOptions.cs b/src/Microsoft.AspNetCore.ResponseCaching/IResponseCachingOptions.cs new file mode 100644 index 0000000000..4c121c8a63 --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/IResponseCachingOptions.cs @@ -0,0 +1,10 @@ +// 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 +{ + interface IResponseCachingOptions + { + int MaxCachedItemBytes { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/CachedResponse.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/CachedResponse.cs index 2c43a562a0..63caf4bcc8 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/CachedResponse.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/CachedResponse.cs @@ -1,6 +1,7 @@ // 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; namespace Microsoft.AspNetCore.ResponseCaching.Internal @@ -12,5 +13,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal internal IHeaderDictionary Headers { get; set; } = new HeaderDictionary(); internal byte[] Body { get; set; } + public DateTimeOffset Created { get; set; } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs index a493e47e4d..a163e79cb6 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs @@ -2,7 +2,9 @@ // 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.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; @@ -14,6 +16,7 @@ namespace Microsoft.AspNetCore.ResponseCaching public class ResponseCachingContext { private string _cacheKey; + private RequestType _requestType; public ResponseCachingContext(HttpContext httpContext, IResponseCache cache) { @@ -43,12 +46,24 @@ namespace Microsoft.AspNetCore.ResponseCaching private bool CacheResponse { get; set; } + private bool IsProxied { get; set; } + public bool CheckRequestAllowsCaching() { // Verify the method // TODO: What other methods should be supported? - if (!string.Equals("GET", HttpContext.Request.Method, StringComparison.OrdinalIgnoreCase)) + 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; } @@ -137,14 +152,24 @@ namespace Microsoft.AspNetCore.ResponseCaching response.Headers[pair.Key] = pair.Value; } - // TODO: Update cache headers (Age) - response.Headers["Served_From_Cache"] = DateTime.Now.ToString(); + // 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; - // Copy the cached response body - var body = cachedResponse.Body; - if (body.Length > 0) + if (_requestType == RequestType.HeadersOnly) { - await response.Body.WriteAsync(body, 0, body.Length); + response.Headers["Content-Length"] = "0"; + } + else + { + // Copy the cached response body + var body = cachedResponse.Body; + response.Headers["Content-Length"] = body.Length.ToString(CultureInfo.InvariantCulture); + if (body.Length > 0) + { + await response.Body.WriteAsync(body, 0, body.Length); + } } return true; } @@ -173,7 +198,8 @@ namespace Microsoft.AspNetCore.ResponseCaching internal void FinalizeCaching() { - if (CacheResponse) + // Don't cache errors? 404 etc + if (CacheResponse && HttpContext.Response.StatusCode == 200) { var response = HttpContext.Response; var varyHeaderValue = response.Headers["Vary"]; @@ -194,12 +220,30 @@ namespace Microsoft.AspNetCore.ResponseCaching // Store the response to cache var cachedResponse = new CachedResponse { + Created = DateTimeOffset.UtcNow, StatusCode = HttpContext.Response.StatusCode, Body = Buffer.ToArray() }; - foreach (var pair in HttpContext.Response.Headers) + + 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); + var age = 0; + foreach (var entry in headers) { - cachedResponse.Headers[pair.Key] = pair.Value; + // 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); + } } Cache.Set(_cacheKey, cachedResponse); // TODO: Timeouts @@ -215,5 +259,12 @@ namespace Microsoft.AspNetCore.ResponseCaching // Unhook the response stream. HttpContext.Response.Body = OriginalResponseStream; } + + private enum RequestType + { + NotCached = 0, + HeadersOnly, + FullReponse + } } } From 62aabc1bae963d8f1d14b3069bdfee816db27446 Mon Sep 17 00:00:00 2001 From: John Luo Date: Sat, 30 Jul 2016 17:47:05 -0700 Subject: [PATCH 012/188] Add implementation for HTTP caching --- samples/ResponseCachingSample/Startup.cs | 2 +- .../IResponseCache.cs | 5 +- .../IResponseCachingOptions.cs | 10 - .../Internal/CachedResponse.cs | 3 +- .../DefaultResponseCacheEntrySerializer.cs | 5 +- .../Internal/DistributedResponseCache.cs | 10 +- .../Internal/ISystemClock.cs | 18 + .../Internal/MemoryResponseCache.cs | 10 +- .../Internal/ResponseCacheStream.cs | 164 ++++++ .../Internal/SendFileFeatureWrapper.cs | 28 + .../Internal/SystemClock.cs | 24 + .../Properties/AssemblyInfo.cs | 4 +- .../ResponseCachingContext.cs | 504 +++++++++++++----- .../ResponseCachingMiddleware.cs | 26 +- .../project.json | 1 + ...efaultResponseCacheEntrySerializerTests.cs | 92 ++++ ...oft.AspNetCore.ResponseCaching.Tests.xproj | 2 +- .../ResponseCachingContextTests.cs | 467 +++++++++++++++- .../ResponseCachingTests.cs | 352 +++++++++++- 19 files changed, 1543 insertions(+), 184 deletions(-) delete mode 100644 src/Microsoft.AspNetCore.ResponseCaching/IResponseCachingOptions.cs create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Internal/ISystemClock.cs create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCacheStream.cs create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Internal/SendFileFeatureWrapper.cs create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Internal/SystemClock.cs create mode 100644 test/Microsoft.AspNetCore.ResponseCaching.Tests/DefaultResponseCacheEntrySerializerTests.cs diff --git a/samples/ResponseCachingSample/Startup.cs b/samples/ResponseCachingSample/Startup.cs index 6dc0ffc99e..510f0d0e0e 100644 --- a/samples/ResponseCachingSample/Startup.cs +++ b/samples/ResponseCachingSample/Startup.cs @@ -29,7 +29,7 @@ namespace ResponseCachingSample Public = true, MaxAge = TimeSpan.FromSeconds(10) }; - context.Response.Headers["Vary"] = new string[] { "Accept-Encoding", "Non-Existent" }; + context.Response.Headers[HeaderNames.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 index a3ba37eeac..dd120e6ec6 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/IResponseCache.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/IResponseCache.cs @@ -1,13 +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; + 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 Set(string key, object entry, TimeSpan validFor); void Remove(string key); } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/IResponseCachingOptions.cs b/src/Microsoft.AspNetCore.ResponseCaching/IResponseCachingOptions.cs deleted file mode 100644 index 4c121c8a63..0000000000 --- a/src/Microsoft.AspNetCore.ResponseCaching/IResponseCachingOptions.cs +++ /dev/null @@ -1,10 +0,0 @@ -// 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 -{ - interface IResponseCachingOptions - { - int MaxCachedItemBytes { get; set; } - } -} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/CachedResponse.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/CachedResponse.cs index 63caf4bcc8..4743d370b5 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/CachedResponse.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/CachedResponse.cs @@ -8,11 +8,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal { internal class CachedResponse { + internal DateTimeOffset Created { get; set; } + internal int StatusCode { get; set; } internal IHeaderDictionary Headers { get; set; } = new HeaderDictionary(); internal byte[] Body { get; set; } - public DateTimeOffset Created { get; set; } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/DefaultResponseCacheEntrySerializer.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/DefaultResponseCacheEntrySerializer.cs index 4885a6bfcf..5112081b18 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/DefaultResponseCacheEntrySerializer.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/DefaultResponseCacheEntrySerializer.cs @@ -69,6 +69,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal } // Serialization Format + // Creation time - UtcTicks (long) // Status code (int) // Header count (int) // Header(s) @@ -78,6 +79,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal // Body (byte[]) private static CachedResponse ReadCachedResponse(BinaryReader reader) { + var created = new DateTimeOffset(reader.ReadInt64(), TimeSpan.Zero); var statusCode = reader.ReadInt32(); var headerCount = reader.ReadInt32(); var headers = new HeaderDictionary(); @@ -90,7 +92,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal var bodyLength = reader.ReadInt32(); var body = reader.ReadBytes(bodyLength); - return new CachedResponse { StatusCode = statusCode, Headers = headers, Body = body }; + return new CachedResponse { Created = created, StatusCode = statusCode, Headers = headers, Body = body }; } // Serialization Format @@ -135,6 +137,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal private static void WriteCachedResponse(BinaryWriter writer, CachedResponse entry) { writer.Write(nameof(CachedResponse)); + writer.Write(entry.Created.UtcTicks); writer.Write(entry.StatusCode); writer.Write(entry.Headers.Count); foreach (var header in entry.Headers) diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/DistributedResponseCache.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/DistributedResponseCache.cs index d87a4be2b1..c9068b6288 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/DistributedResponseCache.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/DistributedResponseCache.cs @@ -45,11 +45,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal } } - public void Set(string key, object entry) + public void Set(string key, object entry, TimeSpan validFor) { try { - _cache.Set(key, DefaultResponseCacheSerializer.Serialize(entry)); + _cache.Set( + key, + DefaultResponseCacheSerializer.Serialize(entry), + new DistributedCacheEntryOptions() + { + AbsoluteExpirationRelativeToNow = validFor + }); } catch { diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ISystemClock.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ISystemClock.cs new file mode 100644 index 0000000000..4b560e3dad --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ISystemClock.cs @@ -0,0 +1,18 @@ +// 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; + +namespace Microsoft.AspNetCore.ResponseCaching.Internal +{ + /// + /// Abstracts the system clock to facilitate testing. + /// + internal interface ISystemClock + { + /// + /// Retrieves the current system time in UTC. + /// + DateTimeOffset UtcNow { get; } + } +} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs index 2937ad439d..117d112db9 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs @@ -30,9 +30,15 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal _cache.Remove(key); } - public void Set(string key, object entry) + public void Set(string key, object entry, TimeSpan validFor) { - _cache.Set(key, entry); + _cache.Set( + key, + entry, + new MemoryCacheEntryOptions() + { + AbsoluteExpirationRelativeToNow = validFor + }); } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCacheStream.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCacheStream.cs new file mode 100644 index 0000000000..bf9d90ee21 --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCacheStream.cs @@ -0,0 +1,164 @@ +// 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; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.ResponseCaching.Internal +{ + internal class ResponseCacheStream : Stream + { + private readonly Stream _innerStream; + + public ResponseCacheStream(Stream innerStream) + { + _innerStream = innerStream; + } + + public MemoryStream BufferedStream { get; } = new MemoryStream(); + + public bool BufferingEnabled { get; set; } = true; + + public override bool CanRead => _innerStream.CanRead; + + public override bool CanSeek => _innerStream.CanSeek; + + public override bool CanWrite => _innerStream.CanWrite; + + public override long Length => _innerStream.Length; + + public override long Position + { + get { return _innerStream.Position; } + set { _innerStream.Position = value; } + } + + public void DisableBuffering() + { + BufferingEnabled = false; + BufferedStream.Dispose(); + } + + public override void SetLength(long value) + { + DisableBuffering(); + _innerStream.SetLength(value); + } + + public override long Seek(long offset, SeekOrigin origin) + { + DisableBuffering(); + return _innerStream.Seek(offset, origin); + } + + public override void Flush() + => _innerStream.Flush(); + + public override Task FlushAsync(CancellationToken cancellationToken) + => _innerStream.FlushAsync(); + + // Underlying stream is write-only, no need to override other read related methods + public override int Read(byte[] buffer, int offset, int count) + => _innerStream.Read(buffer, offset, count); + + public override void Write(byte[] buffer, int offset, int count) + { + try + { + _innerStream.Write(buffer, offset, count); + } + catch + { + DisableBuffering(); + throw; + } + + if (BufferingEnabled) + { + BufferedStream.Write(buffer, offset, count); + } + } + + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + try + { + await _innerStream.WriteAsync(buffer, offset, count, cancellationToken); + } + catch + { + DisableBuffering(); + throw; + } + + if (BufferingEnabled) + { + await BufferedStream.WriteAsync(buffer, offset, count, cancellationToken); + } + } + + public override void WriteByte(byte value) + { + try + { + _innerStream.WriteByte(value); + } + catch + { + DisableBuffering(); + throw; + } + + if (BufferingEnabled) + { + BufferedStream.WriteByte(value); + } + } + +#if NETSTANDARD1_3 + public IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) +#else + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) +#endif + { + return ToIAsyncResult(WriteAsync(buffer, offset, count), callback, state); + } +#if NETSTANDARD1_3 + public void EndWrite(IAsyncResult asyncResult) +#else + public override void EndWrite(IAsyncResult asyncResult) +#endif + { + if (asyncResult == null) + { + throw new ArgumentNullException(nameof(asyncResult)); + } + ((Task)asyncResult).GetAwaiter().GetResult(); + } + + private static IAsyncResult ToIAsyncResult(Task task, AsyncCallback callback, object state) + { + var tcs = new TaskCompletionSource(state); + task.ContinueWith(t => + { + if (t.IsFaulted) + { + tcs.TrySetException(t.Exception.InnerExceptions); + } + else if (t.IsCanceled) + { + tcs.TrySetCanceled(); + } + else + { + tcs.TrySetResult(0); + } + + callback?.Invoke(tcs.Task); + }, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default); + return tcs.Task; + } + } +} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/SendFileFeatureWrapper.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/SendFileFeatureWrapper.cs new file mode 100644 index 0000000000..5d6264228e --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/SendFileFeatureWrapper.cs @@ -0,0 +1,28 @@ +// 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; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.ResponseCaching.Internal +{ + internal class SendFileFeatureWrapper : IHttpSendFileFeature + { + private readonly IHttpSendFileFeature _originalSendFileFeature; + private readonly ResponseCacheStream _responseCacheStream; + + public SendFileFeatureWrapper(IHttpSendFileFeature originalSendFileFeature, ResponseCacheStream responseCacheStream) + { + _originalSendFileFeature = originalSendFileFeature; + _responseCacheStream = responseCacheStream; + } + + // Flush and disable the buffer if anyone tries to call the SendFile feature. + public Task SendFileAsync(string path, long offset, long? length, CancellationToken cancellation) + { + _responseCacheStream.DisableBuffering(); + return _originalSendFileFeature.SendFileAsync(path, offset, length, cancellation); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/SystemClock.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/SystemClock.cs new file mode 100644 index 0000000000..39b6e4735a --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/SystemClock.cs @@ -0,0 +1,24 @@ +// 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; + +namespace Microsoft.AspNetCore.ResponseCaching.Internal +{ + /// + /// Provides access to the normal system clock. + /// + internal class SystemClock : ISystemClock + { + /// + /// Retrieves the current system time in UTC. + /// + public DateTimeOffset UtcNow + { + get + { + return DateTimeOffset.UtcNow; + } + } + } +} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.ResponseCaching/Properties/AssemblyInfo.cs index 32dcddfc57..f6017986e9 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Properties/AssemblyInfo.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Properties/AssemblyInfo.cs @@ -3,9 +3,11 @@ using System.Reflection; using System.Resources; +using System.Runtime.CompilerServices; [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 +[assembly: AssemblyProduct("Microsoft ASP.NET Core")] +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.ResponseCaching.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs index a163e79cb6..4eb18dc0af 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs @@ -2,84 +2,144 @@ // 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.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Http.Headers; using Microsoft.AspNetCore.ResponseCaching.Internal; using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.ResponseCaching { public class ResponseCachingContext { + private static readonly CacheControlHeaderValue EmptyCacheControl = new CacheControlHeaderValue(); private string _cacheKey; - private RequestType _requestType; + private ResponseType? _responseType; + private RequestHeaders _requestHeaders; + private ResponseHeaders _responseHeaders; + private CacheControlHeaderValue _requestCacheControl; + private CacheControlHeaderValue _responseCacheControl; + private bool? _cacheResponse; + private CachedResponse _cachedResponse; + private TimeSpan _cachedResponseValidFor; + internal DateTimeOffset _responseTime; public ResponseCachingContext(HttpContext httpContext, IResponseCache cache) + : this(httpContext, cache, new SystemClock()) + { + } + + // Internal for testing + internal ResponseCachingContext(HttpContext httpContext, IResponseCache cache, ISystemClock clock) { if (cache == null) { throw new ArgumentNullException(nameof(cache)); } - if (httpContext == null) { throw new ArgumentNullException(nameof(httpContext)); } + if (clock == null) + { + throw new ArgumentNullException(nameof(clock)); + } HttpContext = httpContext; Cache = cache; + Clock = clock; } + internal bool CacheResponse + { + get + { + if (_cacheResponse == null) + { + // TODO: apparent age vs corrected age value + var responseAge = _responseTime - ResponseHeaders.Date ?? TimeSpan.Zero; + + _cacheResponse = ResponseIsCacheable() && EntryIsFresh(ResponseHeaders, responseAge, verifyAgainstRequest: false); + } + return _cacheResponse.Value; + } + } + + internal bool ResponseStarted { get; set; } + + private ISystemClock Clock { get; } + private HttpContext HttpContext { get; } private IResponseCache Cache { get; } private Stream OriginalResponseStream { get; set; } - private MemoryStream Buffer { get; set; } + private ResponseCacheStream ResponseCacheStream { get; set; } - internal bool ResponseStarted { get; set; } + private IHttpSendFileFeature OriginalSendFileFeature { get; set; } - private bool CacheResponse { get; set; } - - private bool IsProxied { get; set; } - - public bool CheckRequestAllowsCaching() + private RequestHeaders RequestHeaders { - // Verify the method - // TODO: What other methods should be supported? - if (string.Equals("GET", HttpContext.Request.Method, StringComparison.OrdinalIgnoreCase)) + get { - _requestType = RequestType.FullReponse; + if (_requestHeaders == null) + { + _requestHeaders = HttpContext.Request.GetTypedHeaders(); + } + return _requestHeaders; + } + } + + private ResponseHeaders ResponseHeaders + { + get + { + if (_responseHeaders == null) + { + _responseHeaders = HttpContext.Response.GetTypedHeaders(); + } + return _responseHeaders; + } + } + + private CacheControlHeaderValue RequestCacheControl + { + get + { + if (_requestCacheControl == null) + { + _requestCacheControl = RequestHeaders.CacheControl ?? EmptyCacheControl; + } + return _requestCacheControl; + } + } + + private CacheControlHeaderValue ResponseCacheControl + { + get + { + if (_responseCacheControl == null) + { + _responseCacheControl = ResponseHeaders.CacheControl ?? EmptyCacheControl; + } + return _responseCacheControl; } - 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;/PATH;VaryBy - private string CreateCacheKey() + // TODO: Method invariant retrieval? E.g. HEAD after GET to the same resource. + internal string CreateCacheKey() { return CreateCacheKey(varyBy: null); } - private string CreateCacheKey(CachedVaryBy varyBy) + internal string CreateCacheKey(CachedVaryBy varyBy) { var request = HttpContext.Request; var builder = new StringBuilder() @@ -94,18 +154,19 @@ namespace Microsoft.AspNetCore.ResponseCaching private string CreateVaryByCacheKey(CachedVaryBy varyBy) { // TODO: resolve key format and delimiters - if (varyBy == null) + if (varyBy == null || varyBy.Headers.Count == 0) { 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? + // TODO: Normalization of order, case? + var value = HttpContext.Request.Headers[header].ToString(); + + // TODO: How to handle null/empty string? if (string.IsNullOrEmpty(value)) { value = "null"; @@ -122,10 +183,159 @@ namespace Microsoft.AspNetCore.ResponseCaching return builder.ToString(); } + internal bool RequestIsCacheable() + { + // Verify the method + // TODO: RFC lists POST as a cacheable method when explicit freshness information is provided, but this is not widely implemented. Will revisit. + var request = HttpContext.Request; + if (string.Equals("GET", request.Method, StringComparison.OrdinalIgnoreCase)) + { + _responseType = ResponseType.FullReponse; + } + else if (string.Equals("HEAD", request.Method, StringComparison.OrdinalIgnoreCase)) + { + _responseType = ResponseType.HeadersOnly; + } + else + { + return false; + } + + // Verify existence of authorization headers + // TODO: The server may indicate that the response to these request are cacheable + if (!string.IsNullOrEmpty(request.Headers[HeaderNames.Authorization])) + { + return false; + } + + // Verify request cache-control parameters + // TODO: no-cache requests can be retrieved upon validation with origin + if (!string.IsNullOrEmpty(request.Headers[HeaderNames.CacheControl])) + { + if (RequestCacheControl.NoCache || RequestCacheControl.NoStore) + { + return false; + } + } + else + { + // Support for legacy HTTP 1.0 cache directive + var pragmaHeaderValues = request.Headers[HeaderNames.Pragma]; + foreach (var directive in pragmaHeaderValues) + { + if (string.Equals("no-cache", directive, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + } + } + + // TODO: Verify global middleware settings? Explicit ignore list, range requests, etc. + return true; + } + + internal bool ResponseIsCacheable() + { + // Only cache pages explicitly marked with public + // TODO: Consider caching responses that are not marked as public but otherwise cacheable? + if (!ResponseCacheControl.Public) + { + return false; + } + + // Check no-store + if (ResponseCacheControl.NoStore) + { + return false; + } + + // Check no-cache + // TODO: Handle no-cache with headers + if (ResponseCacheControl.NoCache) + { + return false; + } + + var response = HttpContext.Response; + + // Do not cache responses varying by * + if (string.Equals(response.Headers[HeaderNames.Vary], "*", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + // TODO: public MAY override the cacheability checks for private and status codes + + // Check private + if (ResponseCacheControl.Private) + { + return false; + } + + // Check response code + // TODO: RFC also lists 203, 204, 206, 300, 301, 404, 405, 410, 414, and 501 as cacheable by default + if (response.StatusCode != StatusCodes.Status200OK) + { + return false; + } + + return true; + } + + internal bool EntryIsFresh(ResponseHeaders responseHeaders, TimeSpan age, bool verifyAgainstRequest) + { + var responseCacheControl = responseHeaders.CacheControl ?? EmptyCacheControl; + + // Add min-fresh requirements + if (verifyAgainstRequest) + { + age += RequestCacheControl.MinFresh ?? TimeSpan.Zero; + } + + // Validate shared max age, this overrides any max age settings for shared caches + if (age > responseCacheControl.SharedMaxAge) + { + // shared max age implies must revalidate + return false; + } + else if (responseCacheControl.SharedMaxAge == null) + { + // Validate max age + if (age > responseCacheControl.MaxAge || (verifyAgainstRequest && age > RequestCacheControl.MaxAge)) + { + // Must revalidate + if (responseCacheControl.MustRevalidate) + { + return false; + } + + // Request allows stale values + if (verifyAgainstRequest && age < RequestCacheControl.MaxStaleLimit) + { + // TODO: Add warning header indicating the response is stale + return true; + } + + return false; + } + else if (responseCacheControl.MaxAge == null && (!verifyAgainstRequest || RequestCacheControl.MaxAge == null)) + { + // Validate expiration + if (_responseTime > responseHeaders.Expires) + { + return false; + } + } + } + + return true; + } + internal async Task TryServeFromCacheAsync() { _cacheKey = CreateCacheKey(); var cacheEntry = Cache.Get(_cacheKey); + var responseServed = false; if (cacheEntry is CachedVaryBy) { @@ -136,73 +346,80 @@ namespace Microsoft.AspNetCore.ResponseCaching if (cacheEntry is CachedResponse) { - // TODO: Compare cached request headers - - // 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 cachedResponseHeaders = new ResponseHeaders(cachedResponse.Headers); - var response = HttpContext.Response; - // Copy the cached status code and response headers - response.StatusCode = cachedResponse.StatusCode; - foreach (var pair in cachedResponse.Headers) + _responseTime = Clock.UtcNow; + var age = _responseTime - cachedResponse.Created; + age = age > TimeSpan.Zero ? age : TimeSpan.Zero; + + if (EntryIsFresh(cachedResponseHeaders, age, verifyAgainstRequest: true)) { - response.Headers[pair.Key] = pair.Value; - } + var response = HttpContext.Response; + // Copy the cached status code and response headers + response.StatusCode = cachedResponse.StatusCode; + foreach (var header in cachedResponse.Headers) + { + response.Headers.Add(header); + } - // 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; + response.Headers[HeaderNames.Age] = age.TotalSeconds.ToString("F0", CultureInfo.InvariantCulture); - if (_requestType == RequestType.HeadersOnly) - { - response.Headers["Content-Length"] = "0"; + if (_responseType == ResponseType.HeadersOnly) + { + responseServed = true; + } + else if (_responseType == ResponseType.FullReponse) + { + // Copy the cached response body + var body = cachedResponse.Body; + + // Add a content-length if required + if (response.ContentLength == null && string.IsNullOrEmpty(response.Headers[HeaderNames.TransferEncoding])) + { + response.ContentLength = body.Length; + } + + if (body.Length > 0) + { + await response.Body.WriteAsync(body, 0, body.Length); + } + + responseServed = true; + } + else + { + throw new InvalidOperationException($"{nameof(_responseType)} not specified or is unrecognized."); + } } else { - // Copy the cached response body - var body = cachedResponse.Body; - response.Headers["Content-Length"] = body.Length.ToString(CultureInfo.InvariantCulture); - if (body.Length > 0) - { - await response.Body.WriteAsync(body, 0, body.Length); - } + // TODO: Validate with endpoint instead } - 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) + if (!responseServed && RequestCacheControl.OnlyIfCached) { + HttpContext.Response.StatusCode = StatusCodes.Status504GatewayTimeout; + + responseServed = true; + } + + return responseServed; + } + + internal void FinalizeCachingHeaders() + { + if (CacheResponse) + { + // Create the cache entry now var response = HttpContext.Response; - var varyHeaderValue = response.Headers["Vary"]; + var varyHeaderValue = response.Headers[HeaderNames.Vary]; + _cachedResponseValidFor = ResponseCacheControl.SharedMaxAge + ?? ResponseCacheControl.MaxAge + ?? (ResponseHeaders.Expires - _responseTime) + // TODO: Heuristics for expiration? + ?? TimeSpan.FromSeconds(10); // Check if any VaryBy rules exist if (!StringValues.IsNullOrEmpty(varyHeaderValue)) @@ -210,61 +427,94 @@ namespace Microsoft.AspNetCore.ResponseCaching var cachedVaryBy = new CachedVaryBy { // Only vary by headers for now + // TODO: VaryBy Encoding Headers = varyHeaderValue }; - Cache.Set(_cacheKey, cachedVaryBy); + // TODO: Overwrite? + Cache.Set(_cacheKey, cachedVaryBy, _cachedResponseValidFor); _cacheKey = CreateCacheKey(cachedVaryBy); } - // Store the response to cache - var cachedResponse = new CachedResponse + // Ensure date header is set + if (ResponseHeaders.Date == null) { - Created = DateTimeOffset.UtcNow, - StatusCode = HttpContext.Response.StatusCode, - Body = Buffer.ToArray() - }; - - 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); - var 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); - } + ResponseHeaders.Date = _responseTime; } - Cache.Set(_cacheKey, cachedResponse); // TODO: Timeouts + // Store the response to cache + _cachedResponse = new CachedResponse + { + Created = ResponseHeaders.Date.Value, + StatusCode = HttpContext.Response.StatusCode + }; + + foreach (var header in ResponseHeaders.Headers) + { + if (!string.Equals(header.Key, HeaderNames.Age, StringComparison.OrdinalIgnoreCase) + && !string.Equals(header.Key, HeaderNames.SetCookie, StringComparison.OrdinalIgnoreCase)) + { + _cachedResponse.Headers.Add(header); + } + } + } + else + { + ResponseCacheStream.DisableBuffering(); } - - // TODO: TEMP, flush the buffer to the client - Buffer.Seek(0, SeekOrigin.Begin); - Buffer.CopyTo(OriginalResponseStream); } - internal void UnhookResponseStream() + internal void FinalizeCachingBody() { - // Unhook the response stream. + if (CacheResponse && ResponseCacheStream.BufferingEnabled) + { + _cachedResponse.Body = ResponseCacheStream.BufferedStream.ToArray(); + + Cache.Set(_cacheKey, _cachedResponse, _cachedResponseValidFor); + } + } + + internal void OnResponseStarting() + { + if (!ResponseStarted) + { + ResponseStarted = true; + _responseTime = Clock.UtcNow; + + FinalizeCachingHeaders(); + } + } + + internal void ShimResponseStream() + { + // TODO: Consider caching large responses on disk and serving them from there. + + // Shim response stream + OriginalResponseStream = HttpContext.Response.Body; + ResponseCacheStream = new ResponseCacheStream(OriginalResponseStream); + HttpContext.Response.Body = ResponseCacheStream; + + // Shim IHttpSendFileFeature + OriginalSendFileFeature = HttpContext.Features.Get(); + if (OriginalSendFileFeature != null) + { + HttpContext.Features.Set(new SendFileFeatureWrapper(OriginalSendFileFeature, ResponseCacheStream)); + } + } + + internal void UnshimResponseStream() + { + // Unshim response stream HttpContext.Response.Body = OriginalResponseStream; + + // Unshim IHttpSendFileFeature + HttpContext.Features.Set(OriginalSendFileFeature); } - private enum RequestType + private enum ResponseType { - NotCached = 0, - HeadersOnly, - FullReponse + HeadersOnly = 0, + FullReponse = 1 } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs index c18f66ecd9..5c43ff15fe 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs @@ -4,12 +4,19 @@ using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; namespace Microsoft.AspNetCore.ResponseCaching { // http://tools.ietf.org/html/rfc7234 public class ResponseCachingMiddleware { + private static readonly Func OnStartingCallback = state => + { + ((ResponseCachingContext)state).OnResponseStarting(); + return Task.FromResult(0); + }; + private readonly RequestDelegate _next; private readonly IResponseCache _cache; @@ -32,8 +39,9 @@ namespace Microsoft.AspNetCore.ResponseCaching public async Task Invoke(HttpContext context) { var cachingContext = new ResponseCachingContext(context, _cache); + // Should we attempt any caching logic? - if (cachingContext.CheckRequestAllowsCaching()) + if (cachingContext.RequestIsCacheable()) { // Can this request be served from cache? if (await cachingContext.TryServeFromCacheAsync()) @@ -42,27 +50,29 @@ namespace Microsoft.AspNetCore.ResponseCaching } // Hook up to listen to the response stream - cachingContext.HookResponseStream(); + cachingContext.ShimResponseStream(); try { + // Subscribe to OnStarting event + context.Response.OnStarting(OnStartingCallback, cachingContext); + 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(); - } + cachingContext.OnResponseStarting(); + // Finalize the cache entry - cachingContext.FinalizeCaching(); + cachingContext.FinalizeCachingBody(); } finally { - cachingContext.UnhookResponseStream(); + cachingContext.UnshimResponseStream(); } } else { + // TODO: Invalidate resources for successful unsafe methods? Required by RFC await _next(context); } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/project.json b/src/Microsoft.AspNetCore.ResponseCaching/project.json index 1fd13308ab..ae02d9e641 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/project.json +++ b/src/Microsoft.AspNetCore.ResponseCaching/project.json @@ -22,6 +22,7 @@ }, "dependencies": { "Microsoft.AspNetCore.Http": "1.1.0-*", + "Microsoft.AspNetCore.Http.Extensions": "1.1.0-*", "Microsoft.Extensions.Caching.Memory": "1.1.0-*" }, "frameworks": { diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/DefaultResponseCacheEntrySerializerTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/DefaultResponseCacheEntrySerializerTests.cs new file mode 100644 index 0000000000..b0da702792 --- /dev/null +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/DefaultResponseCacheEntrySerializerTests.cs @@ -0,0 +1,92 @@ +// 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.Linq; +using Microsoft.AspNetCore.ResponseCaching.Internal; +using Xunit; +using Microsoft.AspNetCore.Http; +using System.Text; + +namespace Microsoft.AspNetCore.ResponseCaching.Tests +{ + public class DefaultResponseCacheEntrySerializerTests + { + [Fact] + public void SerializeNullObjectThrows() + { + Assert.Throws(() => DefaultResponseCacheSerializer.Serialize(null)); + } + + [Fact] + public void SerializeUnknownObjectThrows() + { + Assert.Throws(() => DefaultResponseCacheSerializer.Serialize(new object())); + } + + [Fact] + public void RoundTripCachedResponsesSucceeds() + { + var headers = new HeaderDictionary(); + headers["keyA"] = "valueA"; + headers["keyB"] = "valueB"; + var cachedEntry = new CachedResponse() + { + Created = DateTimeOffset.UtcNow, + StatusCode = StatusCodes.Status200OK, + Body = Encoding.ASCII.GetBytes("Hello world"), + Headers = headers + }; + + AssertCachedResponsesEqual(cachedEntry, (CachedResponse)DefaultResponseCacheSerializer.Deserialize(DefaultResponseCacheSerializer.Serialize(cachedEntry))); + } + + [Fact] + public void RoundTripCachedVaryBySucceeds() + { + var headers = new[] { "headerA", "headerB" }; + var cachedVaryBy = new CachedVaryBy() + { + Headers = headers + }; + + AssertCachedVarybyEqual(cachedVaryBy, (CachedVaryBy)DefaultResponseCacheSerializer.Deserialize(DefaultResponseCacheSerializer.Serialize(cachedVaryBy))); + } + + + [Fact] + public void DeserializeInvalidEntriesReturnsNull() + { + var headers = new[] { "headerA", "headerB" }; + var cachedVaryBy = new CachedVaryBy() + { + Headers = headers + }; + var serializedEntry = DefaultResponseCacheSerializer.Serialize(cachedVaryBy); + Array.Reverse(serializedEntry); + + Assert.Null(DefaultResponseCacheSerializer.Deserialize(serializedEntry)); + } + + private static void AssertCachedResponsesEqual(CachedResponse expected, CachedResponse actual) + { + Assert.NotNull(actual); + Assert.NotNull(expected); + Assert.Equal(expected.Created, actual.Created); + Assert.Equal(expected.StatusCode, actual.StatusCode); + Assert.Equal(expected.Headers.Count, actual.Headers.Count); + foreach (var expectedHeader in expected.Headers) + { + Assert.Equal(expectedHeader.Value, actual.Headers[expectedHeader.Key]); + } + Assert.True(expected.Body.SequenceEqual(actual.Body)); + } + + private static void AssertCachedVarybyEqual(CachedVaryBy expected, CachedVaryBy actual) + { + Assert.NotNull(actual); + Assert.NotNull(expected); + Assert.Equal(expected.Headers, actual.Headers); + } + } +} 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 81a4f88eab..ea971b7729 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.xproj +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.xproj @@ -7,7 +7,7 @@ 151b2027-3936-44b9-a4a0-e1e5902125ab - Microsoft.AspNet.ResponseCaching.Tests + Microsoft.AspNetCore.ResponseCaching.Tests .\obj .\bin\ diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs index 5c83622de7..067d6c91b5 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs @@ -2,33 +2,462 @@ // 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.Threading; +using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Caching.Memory; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Http.Headers; +using Microsoft.AspNetCore.ResponseCaching.Internal; +using Microsoft.Net.Http.Headers; using Xunit; namespace Microsoft.AspNetCore.ResponseCaching.Tests { public class ResponseCachingContextTests { - [Fact] - public void CheckRequestAllowsCaching_Method_GET_Allowed() - { - var httpContext = new DefaultHttpContext(); - httpContext.Request.Method = "GET"; - var context = new ResponseCachingContext(httpContext, new TestResponseCache()); - - Assert.True(context.CheckRequestAllowsCaching()); - } - [Theory] - [InlineData("POST")] - public void CheckRequestAllowsCaching_Method_Unsafe_NotAllowed(string method) + [InlineData("GET")] + [InlineData("HEAD")] + public void RequestIsCacheable_CacheableMethods_Allowed(string method) { var httpContext = new DefaultHttpContext(); httpContext.Request.Method = method; var context = new ResponseCachingContext(httpContext, new TestResponseCache()); - Assert.False(context.CheckRequestAllowsCaching()); + Assert.True(context.RequestIsCacheable()); + } + + [Theory] + [InlineData("POST")] + [InlineData("OPTIONS")] + [InlineData("PUT")] + [InlineData("DELETE")] + [InlineData("TRACE")] + [InlineData("CONNECT")] + [InlineData("")] + [InlineData(null)] + public void RequestIsCacheable_UncacheableMethods_NotAllowed(string method) + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.Method = method; + var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + + Assert.False(context.RequestIsCacheable()); + } + + [Fact] + public void RequestIsCacheable_AuthorizationHeaders_NotAllowed() + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.Method = "GET"; + httpContext.Request.Headers[HeaderNames.Authorization] = "Basic plaintextUN:plaintextPW"; + var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + + Assert.False(context.RequestIsCacheable()); + } + + [Theory] + [InlineData("no-cache")] + [InlineData("no-store")] + [InlineData("no-cache, no-store")] + public void RequestIsCacheable_ExplicitDisablingDirectives_NotAllowed(string directive) + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.Method = "GET"; + httpContext.Request.Headers[HeaderNames.CacheControl] = directive; + var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + + Assert.False(context.RequestIsCacheable()); + } + + [Fact] + public void RequestIsCacheable_LegacyDirectives_NotAllowed() + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.Method = "GET"; + httpContext.Request.Headers[HeaderNames.Pragma] = "no-cache"; + var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + + Assert.False(context.RequestIsCacheable()); + } + + [Fact] + public void RequestIsCacheable_LegacyDirectives_OverridenByCacheControl() + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.Method = "GET"; + httpContext.Request.Headers[HeaderNames.Pragma] = "no-cache"; + httpContext.Request.Headers[HeaderNames.CacheControl] = "max-age=10"; + var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + + Assert.True(context.RequestIsCacheable()); + } + + [Fact] + public void CreateCacheKey_Includes_UppercaseMethodAndPath() + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.Method = "head"; + httpContext.Request.Path = "/path/subpath"; + httpContext.Request.Scheme = "https"; + httpContext.Request.Host = new HostString("example.com", 80); + httpContext.Request.PathBase = "/pathBase"; + httpContext.Request.QueryString = new QueryString("?query.Key=a&query.Value=b"); + var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + + Assert.Equal("HEAD;/PATH/SUBPATH", context.CreateCacheKey()); + } + + [Fact] + public void CreateCacheKey_Includes_ListedVaryByHeadersOnly() + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.Method = "GET"; + httpContext.Request.Path = "/"; + httpContext.Request.Headers["HeaderA"] = "ValueA"; + httpContext.Request.Headers["HeaderB"] = "ValueB"; + var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + + Assert.Equal("GET;/;HeaderA=ValueA;HeaderC=null;", context.CreateCacheKey(new CachedVaryBy() + { + Headers = new string[] { "HeaderA", "HeaderC" } + })); + } + + [Fact] + public void ResponseIsCacheable_NoPublic_NotAllowed() + { + var httpContext = new DefaultHttpContext(); + var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + + Assert.False(context.ResponseIsCacheable()); + } + + [Fact] + public void ResponseIsCacheable_Public_Allowed() + { + var httpContext = new DefaultHttpContext(); + httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + Public = true + }; + var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + + Assert.True(context.ResponseIsCacheable()); + } + + [Fact] + public void ResponseIsCacheable_NoCache_NotAllowed() + { + var httpContext = new DefaultHttpContext(); + httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + Public = true, + NoCache = true + }; + var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + + Assert.False(context.ResponseIsCacheable()); + } + + [Fact] + public void ResponseIsCacheable_NoStore_NotAllowed() + { + var httpContext = new DefaultHttpContext(); + httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + Public = true, + NoStore = true + }; + var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + + Assert.False(context.ResponseIsCacheable()); + } + + [Fact] + public void ResponseIsCacheable_VaryByStar_NotAllowed() + { + var httpContext = new DefaultHttpContext(); + httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + Public = true + }; + httpContext.Response.Headers[HeaderNames.Vary] = "*"; + var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + + Assert.False(context.ResponseIsCacheable()); + } + + [Fact] + public void ResponseIsCacheable_Private_NotAllowed() + { + var httpContext = new DefaultHttpContext(); + httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + Public = true, + Private = true + }; + var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + + Assert.False(context.ResponseIsCacheable()); + } + + [Theory] + [InlineData(StatusCodes.Status200OK)] + public void ResponseIsCacheable_SuccessStatusCodes_Allowed(int statusCode) + { + var httpContext = new DefaultHttpContext(); + httpContext.Response.StatusCode = statusCode; + httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + Public = true + }; + var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + + Assert.True(context.ResponseIsCacheable()); + } + + [Theory] + [InlineData(StatusCodes.Status201Created)] + [InlineData(StatusCodes.Status202Accepted)] + [InlineData(StatusCodes.Status203NonAuthoritative)] + [InlineData(StatusCodes.Status204NoContent)] + [InlineData(StatusCodes.Status205ResetContent)] + [InlineData(StatusCodes.Status206PartialContent)] + [InlineData(StatusCodes.Status207MultiStatus)] + [InlineData(StatusCodes.Status300MultipleChoices)] + [InlineData(StatusCodes.Status301MovedPermanently)] + [InlineData(StatusCodes.Status302Found)] + [InlineData(StatusCodes.Status303SeeOther)] + [InlineData(StatusCodes.Status304NotModified)] + [InlineData(StatusCodes.Status305UseProxy)] + [InlineData(StatusCodes.Status306SwitchProxy)] + [InlineData(StatusCodes.Status307TemporaryRedirect)] + [InlineData(StatusCodes.Status308PermanentRedirect)] + [InlineData(StatusCodes.Status400BadRequest)] + [InlineData(StatusCodes.Status401Unauthorized)] + [InlineData(StatusCodes.Status402PaymentRequired)] + [InlineData(StatusCodes.Status403Forbidden)] + [InlineData(StatusCodes.Status404NotFound)] + [InlineData(StatusCodes.Status405MethodNotAllowed)] + [InlineData(StatusCodes.Status406NotAcceptable)] + [InlineData(StatusCodes.Status407ProxyAuthenticationRequired)] + [InlineData(StatusCodes.Status408RequestTimeout)] + [InlineData(StatusCodes.Status409Conflict)] + [InlineData(StatusCodes.Status410Gone)] + [InlineData(StatusCodes.Status411LengthRequired)] + [InlineData(StatusCodes.Status412PreconditionFailed)] + [InlineData(StatusCodes.Status413RequestEntityTooLarge)] + [InlineData(StatusCodes.Status414RequestUriTooLong)] + [InlineData(StatusCodes.Status415UnsupportedMediaType)] + [InlineData(StatusCodes.Status416RequestedRangeNotSatisfiable)] + [InlineData(StatusCodes.Status417ExpectationFailed)] + [InlineData(StatusCodes.Status418ImATeapot)] + [InlineData(StatusCodes.Status419AuthenticationTimeout)] + [InlineData(StatusCodes.Status422UnprocessableEntity)] + [InlineData(StatusCodes.Status423Locked)] + [InlineData(StatusCodes.Status424FailedDependency)] + [InlineData(StatusCodes.Status451UnavailableForLegalReasons)] + [InlineData(StatusCodes.Status500InternalServerError)] + [InlineData(StatusCodes.Status501NotImplemented)] + [InlineData(StatusCodes.Status502BadGateway)] + [InlineData(StatusCodes.Status503ServiceUnavailable)] + [InlineData(StatusCodes.Status504GatewayTimeout)] + [InlineData(StatusCodes.Status505HttpVersionNotsupported)] + [InlineData(StatusCodes.Status506VariantAlsoNegotiates)] + [InlineData(StatusCodes.Status507InsufficientStorage)] + public void ResponseIsCacheable_NonSuccessStatusCodes_NotAllowed(int statusCode) + { + var httpContext = new DefaultHttpContext(); + httpContext.Response.StatusCode = statusCode; + httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + Public = true + }; + var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + + Assert.False(context.ResponseIsCacheable()); + } + + [Fact] + public void EntryIsFresh_NoExpiryRequirements_IsFresh() + { + var httpContext = new DefaultHttpContext(); + var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + + Assert.True(context.EntryIsFresh(new ResponseHeaders(new HeaderDictionary()), TimeSpan.MaxValue, verifyAgainstRequest: false)); + } + + [Fact] + public void EntryIsFresh_PastExpiry_IsNotFresh() + { + var httpContext = new DefaultHttpContext(); + var utcNow = DateTimeOffset.UtcNow; + httpContext.Response.GetTypedHeaders().Expires = utcNow; + var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + context._responseTime = utcNow; + + Assert.False(context.EntryIsFresh(httpContext.Response.GetTypedHeaders(), TimeSpan.MaxValue, verifyAgainstRequest: false)); + } + + [Fact] + public void EntryIsFresh_MaxAgeOverridesExpiry_ToFresh() + { + var utcNow = DateTimeOffset.UtcNow; + var httpContext = new DefaultHttpContext(); + + var responseHeaders = httpContext.Response.GetTypedHeaders(); + responseHeaders.Expires = utcNow; + responseHeaders.CacheControl = new CacheControlHeaderValue() + { + MaxAge = TimeSpan.FromSeconds(10) + }; + + var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + context._responseTime = utcNow; + + Assert.True(context.EntryIsFresh(httpContext.Response.GetTypedHeaders(), TimeSpan.FromSeconds(10), verifyAgainstRequest: false)); + } + + [Fact] + public void EntryIsFresh_MaxAgeOverridesExpiry_ToNotFresh() + { + var utcNow = DateTimeOffset.UtcNow; + var httpContext = new DefaultHttpContext(); + + var responseHeaders = httpContext.Response.GetTypedHeaders(); + responseHeaders.Expires = utcNow; + responseHeaders.CacheControl = new CacheControlHeaderValue() + { + MaxAge = TimeSpan.FromSeconds(10) + }; + + var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + context._responseTime = utcNow; + + Assert.False(context.EntryIsFresh(httpContext.Response.GetTypedHeaders(), TimeSpan.FromSeconds(11), verifyAgainstRequest: false)); + } + + [Fact] + public void EntryIsFresh_SharedMaxAgeOverridesMaxAge_ToFresh() + { + var httpContext = new DefaultHttpContext(); + httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + MaxAge = TimeSpan.FromSeconds(10), + SharedMaxAge = TimeSpan.FromSeconds(15) + }; + var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + + Assert.True(context.EntryIsFresh(httpContext.Response.GetTypedHeaders(), TimeSpan.FromSeconds(11), verifyAgainstRequest: false)); + } + + [Fact] + public void EntryIsFresh_SharedMaxAgeOverridesMaxAge_ToNotFresh() + { + var httpContext = new DefaultHttpContext(); + httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + MaxAge = TimeSpan.FromSeconds(10), + SharedMaxAge = TimeSpan.FromSeconds(5) + }; + var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + + Assert.False(context.EntryIsFresh(httpContext.Response.GetTypedHeaders(), TimeSpan.FromSeconds(6), verifyAgainstRequest: false)); + } + + [Fact] + public void EntryIsFresh_MinFreshReducesFreshness_ToNotFresh() + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + MinFresh = TimeSpan.FromSeconds(3) + }; + httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + MaxAge = TimeSpan.FromSeconds(10), + SharedMaxAge = TimeSpan.FromSeconds(5) + }; + var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + + Assert.False(context.EntryIsFresh(httpContext.Response.GetTypedHeaders(), TimeSpan.FromSeconds(3), verifyAgainstRequest: true)); + } + + [Fact] + public void EntryIsFresh_RequestMaxAgeRestrictAge_ToNotFresh() + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + MaxAge = TimeSpan.FromSeconds(5) + }; + httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + MaxAge = TimeSpan.FromSeconds(10), + }; + var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + + Assert.False(context.EntryIsFresh(httpContext.Response.GetTypedHeaders(), TimeSpan.FromSeconds(6), verifyAgainstRequest: true)); + } + + [Fact] + public void EntryIsFresh_MaxStaleOverridesFreshness_ToFresh() + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + MaxAge = TimeSpan.FromSeconds(5), + MaxStale = true, // This value must be set to true in order to specify MaxStaleLimit + MaxStaleLimit = TimeSpan.FromSeconds(10) + }; + httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + MaxAge = TimeSpan.FromSeconds(5), + }; + var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + + Assert.True(context.EntryIsFresh(httpContext.Response.GetTypedHeaders(), TimeSpan.FromSeconds(6), verifyAgainstRequest: true)); + } + + [Fact] + public void EntryIsFresh_MustRevalidateOverridesRequestMaxStale_ToNotFresh() + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + MaxAge = TimeSpan.FromSeconds(5), + MaxStale = true, // This value must be set to true in order to specify MaxStaleLimit + MaxStaleLimit = TimeSpan.FromSeconds(10) + }; + httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + MaxAge = TimeSpan.FromSeconds(5), + MustRevalidate = true + }; + var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + + Assert.False(context.EntryIsFresh(httpContext.Response.GetTypedHeaders(), TimeSpan.FromSeconds(6), verifyAgainstRequest: true)); + } + + [Fact] + public void EntryIsFresh_IgnoresRequestVerificationWhenSpecified() + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + MinFresh = TimeSpan.FromSeconds(1), + MaxAge = TimeSpan.FromSeconds(3) + }; + httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + MaxAge = TimeSpan.FromSeconds(10), + SharedMaxAge = TimeSpan.FromSeconds(5) + }; + var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + + Assert.True(context.EntryIsFresh(httpContext.Response.GetTypedHeaders(), TimeSpan.FromSeconds(3), verifyAgainstRequest: false)); } private class TestResponseCache : IResponseCache @@ -42,9 +471,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { } - public void Set(string key, object entry) + public void Set(string key, object entry, TimeSpan validFor) { } } + + private class TestHttpSendFileFeature : IHttpSendFileFeature + { + public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellation) + { + return Task.FromResult(0); + } + } } } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs index e66933d97d..c21c2dab11 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs @@ -1,12 +1,16 @@ // 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; +using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; -using System; +using Microsoft.Net.Http.Headers; using Xunit; namespace Microsoft.AspNetCore.ResponseCaching.Tests @@ -16,19 +20,272 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContentIfAvailable() { - var builder = new WebHostBuilder() - .ConfigureServices(services => + var builder = CreateBuilderWithResponseCaching(async (context) => + { + var uniqueId = Guid.NewGuid().ToString(); + var headers = context.Response.GetTypedHeaders(); + headers.CacheControl = new CacheControlHeaderValue() { - services.AddDistributedResponseCache(); - }) - .Configure(app => + Public = true, + MaxAge = TimeSpan.FromSeconds(10) + }; + headers.Date = DateTimeOffset.UtcNow; + headers.Headers["X-Value"] = uniqueId; + await context.Response.WriteAsync(uniqueId); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + var subsequentResponse = await client.GetAsync(""); + + initialResponse.EnsureSuccessStatusCode(); + subsequentResponse.EnsureSuccessStatusCode(); + + foreach (var header in initialResponse.Headers) { - app.UseResponseCaching(); - app.Run(async (context) => + Assert.Equal(initialResponse.Headers.GetValues(header.Key), subsequentResponse.Headers.GetValues(header.Key)); + } + Assert.True(subsequentResponse.Headers.Contains(HeaderNames.Age)); + Assert.Equal(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync()); + } + } + + [Fact] + public async void ServesFreshContentIfNotAvailable() + { + var builder = CreateBuilderWithResponseCaching(async (context) => + { + var uniqueId = Guid.NewGuid().ToString(); + var headers = context.Response.GetTypedHeaders(); + headers.CacheControl = new CacheControlHeaderValue() + { + Public = true, + MaxAge = TimeSpan.FromSeconds(10) + }; + headers.Date = DateTimeOffset.UtcNow; + headers.Headers["X-Value"] = uniqueId; + await context.Response.WriteAsync(uniqueId); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + var subsequentResponse = await client.GetAsync("/different"); + + initialResponse.EnsureSuccessStatusCode(); + subsequentResponse.EnsureSuccessStatusCode(); + + Assert.False(subsequentResponse.Headers.Contains(HeaderNames.Age)); + Assert.NotEqual(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync()); + } + } + + [Fact] + public async void ServesCachedContentIfVaryByMatches() + { + var builder = CreateBuilderWithResponseCaching(async (context) => + { + var uniqueId = Guid.NewGuid().ToString(); + var headers = context.Response.GetTypedHeaders(); + headers.CacheControl = new CacheControlHeaderValue() + { + Public = true, + MaxAge = TimeSpan.FromSeconds(10) + }; + headers.Date = DateTimeOffset.UtcNow; + headers.Headers["X-Value"] = uniqueId; + context.Response.Headers[HeaderNames.Vary] = HeaderNames.From; + await context.Response.WriteAsync(uniqueId); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + client.DefaultRequestHeaders.From = "user@example.com"; + var initialResponse = await client.GetAsync(""); + var subsequentResponse = await client.GetAsync(""); + + initialResponse.EnsureSuccessStatusCode(); + subsequentResponse.EnsureSuccessStatusCode(); + + foreach (var header in initialResponse.Headers) + { + Assert.Equal(initialResponse.Headers.GetValues(header.Key), subsequentResponse.Headers.GetValues(header.Key)); + } + Assert.True(subsequentResponse.Headers.Contains(HeaderNames.Age)); + Assert.Equal(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync()); + } + } + + [Fact] + public async void ServesFreshContentIfRequestRequirementsNotMet() + { + var builder = CreateBuilderWithResponseCaching(async (context) => + { + var uniqueId = Guid.NewGuid().ToString(); + var headers = context.Response.GetTypedHeaders(); + headers.CacheControl = new CacheControlHeaderValue() + { + Public = true, + MaxAge = TimeSpan.FromSeconds(10) + }; + headers.Date = DateTimeOffset.UtcNow; + headers.Headers["X-Value"] = uniqueId; + await context.Response.WriteAsync(uniqueId); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue() + { + MaxAge = TimeSpan.FromSeconds(0) + }; + var subsequentResponse = await client.GetAsync(""); + + initialResponse.EnsureSuccessStatusCode(); + subsequentResponse.EnsureSuccessStatusCode(); + + Assert.False(subsequentResponse.Headers.Contains(HeaderNames.Age)); + Assert.NotEqual(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync()); + } + } + + [Fact] + public async void ServesFreshContentIfVaryByMismatches() + { + var builder = CreateBuilderWithResponseCaching(async (context) => + { + var uniqueId = Guid.NewGuid().ToString(); + var headers = context.Response.GetTypedHeaders(); + headers.CacheControl = new CacheControlHeaderValue() + { + Public = true, + MaxAge = TimeSpan.FromSeconds(10) + }; + headers.Date = DateTimeOffset.UtcNow; + headers.Headers["X-Value"] = uniqueId; + context.Response.Headers[HeaderNames.Vary] = HeaderNames.From; + await context.Response.WriteAsync(uniqueId); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + client.DefaultRequestHeaders.From = "user@example.com"; + var initialResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.From = "user2@example.com"; + var subsequentResponse = await client.GetAsync(""); + + initialResponse.EnsureSuccessStatusCode(); + subsequentResponse.EnsureSuccessStatusCode(); + + Assert.False(subsequentResponse.Headers.Contains(HeaderNames.Age)); + Assert.NotEqual(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync()); + } + } + + [Fact] + public async void Serves504IfOnlyIfCachedHeaderIsSpecified() + { + var builder = CreateBuilderWithResponseCaching(async (context) => + { + var uniqueId = Guid.NewGuid().ToString(); + var headers = context.Response.GetTypedHeaders(); + headers.CacheControl = new CacheControlHeaderValue() + { + Public = true, + MaxAge = TimeSpan.FromSeconds(10) + }; + headers.Date = DateTimeOffset.UtcNow; + headers.Headers["X-Value"] = uniqueId; + await context.Response.WriteAsync(uniqueId); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue() + { + OnlyIfCached = true + }; + var subsequentResponse = await client.GetAsync("/different"); + + initialResponse.EnsureSuccessStatusCode(); + Assert.Equal(System.Net.HttpStatusCode.GatewayTimeout, subsequentResponse.StatusCode); + } + } + + [Fact] + public async void ServesCachedContentWithoutSetCookie() + { + var builder = CreateBuilderWithResponseCaching(async (context) => + { + var uniqueId = Guid.NewGuid().ToString(); + var headers = context.Response.GetTypedHeaders(); + headers.CacheControl = new CacheControlHeaderValue() + { + Public = true, + MaxAge = TimeSpan.FromSeconds(10) + }; + headers.Date = DateTimeOffset.UtcNow; + headers.Headers["X-Value"] = uniqueId; + headers.Headers[HeaderNames.SetCookie] = "cookieName=cookieValue"; + await context.Response.WriteAsync(uniqueId); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + var subsequentResponse = await client.GetAsync(""); + + initialResponse.EnsureSuccessStatusCode(); + subsequentResponse.EnsureSuccessStatusCode(); + + foreach (var header in initialResponse.Headers) + { + if (!string.Equals(HeaderNames.SetCookie, header.Key, StringComparison.OrdinalIgnoreCase)) { - context.Response.Headers["Cache-Control"] = "public"; - await context.Response.WriteAsync(DateTime.UtcNow.ToString()); + Assert.Equal(initialResponse.Headers.GetValues(header.Key), subsequentResponse.Headers.GetValues(header.Key)); + } + } + Assert.True(initialResponse.Headers.Contains(HeaderNames.SetCookie)); + Assert.True(subsequentResponse.Headers.Contains(HeaderNames.Age)); + Assert.False(subsequentResponse.Headers.Contains(HeaderNames.SetCookie)); + Assert.Equal(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync()); + } + } + + [Fact] + public async void ServesCachedContentIfIHttpSendFileFeatureNotUsed() + { + var builder = CreateBuilderWithResponseCaching( + app => + { + app.Use(async (context, next) => + { + context.Features.Set(new DummySendFileFeature()); + await next.Invoke(); }); + }, + async (context) => + { + var uniqueId = Guid.NewGuid().ToString(); + var headers = context.Response.GetTypedHeaders(); + headers.CacheControl = new CacheControlHeaderValue() + { + Public = true, + MaxAge = TimeSpan.FromSeconds(10) + }; + headers.Date = DateTimeOffset.UtcNow; + headers.Headers["X-Value"] = uniqueId; + await context.Response.WriteAsync(uniqueId); }); using (var server = new TestServer(builder)) @@ -40,11 +297,80 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests 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")); + foreach (var header in initialResponse.Headers) + { + Assert.Equal(initialResponse.Headers.GetValues(header.Key), subsequentResponse.Headers.GetValues(header.Key)); + } + Assert.True(subsequentResponse.Headers.Contains(HeaderNames.Age)); Assert.Equal(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync()); } } + + [Fact] + public async void ServesFreshContentIfIHttpSendFileFeatureUsed() + { + var builder = CreateBuilderWithResponseCaching( + app => + { + app.Use(async (context, next) => + { + context.Features.Set(new DummySendFileFeature()); + await next.Invoke(); + }); + }, + async (context) => + { + var uniqueId = Guid.NewGuid().ToString(); + var headers = context.Response.GetTypedHeaders(); + headers.CacheControl = new CacheControlHeaderValue() + { + Public = true, + MaxAge = TimeSpan.FromSeconds(10) + }; + headers.Date = DateTimeOffset.UtcNow; + headers.Headers["X-Value"] = uniqueId; + await context.Features.Get().SendFileAsync("dummy", 0, 0, CancellationToken.None); + await context.Response.WriteAsync(uniqueId); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + var subsequentResponse = await client.GetAsync(""); + + initialResponse.EnsureSuccessStatusCode(); + subsequentResponse.EnsureSuccessStatusCode(); + + Assert.False(subsequentResponse.Headers.Contains(HeaderNames.Age)); + Assert.NotEqual(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync()); + } + } + + private static IWebHostBuilder CreateBuilderWithResponseCaching(RequestDelegate requestDelegate) => + CreateBuilderWithResponseCaching(app => { }, requestDelegate); + + private static IWebHostBuilder CreateBuilderWithResponseCaching(Action configureDelegate, RequestDelegate requestDelegate) + { + return new WebHostBuilder() + .ConfigureServices(services => + { + services.AddDistributedResponseCache(); + }) + .Configure(app => + { + configureDelegate(app); + app.UseResponseCaching(); + app.Run(requestDelegate); + }); + } + + private class DummySendFileFeature : IHttpSendFileFeature + { + public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellation) + { + return Task.FromResult(0); + } + } } } From 4f61c65931de17c1d4a7312c1017ba9368a646df Mon Sep 17 00:00:00 2001 From: John Luo Date: Tue, 23 Aug 2016 17:11:18 -0700 Subject: [PATCH 013/188] Update no-store behaviour --- .../ResponseCachingContext.cs | 4 +- .../ResponseCachingContextTests.cs | 45 +++++++++-- .../ResponseCachingTests.cs | 76 +++++++++++++++++++ 3 files changed, 116 insertions(+), 9 deletions(-) diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs index 4eb18dc0af..54f6384d4b 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs @@ -212,7 +212,7 @@ namespace Microsoft.AspNetCore.ResponseCaching // TODO: no-cache requests can be retrieved upon validation with origin if (!string.IsNullOrEmpty(request.Headers[HeaderNames.CacheControl])) { - if (RequestCacheControl.NoCache || RequestCacheControl.NoStore) + if (RequestCacheControl.NoCache) { return false; } @@ -244,7 +244,7 @@ namespace Microsoft.AspNetCore.ResponseCaching } // Check no-store - if (ResponseCacheControl.NoStore) + if (RequestCacheControl.NoStore || ResponseCacheControl.NoStore) { return false; } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs index 067d6c91b5..beeff86721 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs @@ -57,20 +57,34 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Assert.False(context.RequestIsCacheable()); } - [Theory] - [InlineData("no-cache")] - [InlineData("no-store")] - [InlineData("no-cache, no-store")] - public void RequestIsCacheable_ExplicitDisablingDirectives_NotAllowed(string directive) + [Fact] + public void RequestIsCacheable_NoCache_NotAllowed() { var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "GET"; - httpContext.Request.Headers[HeaderNames.CacheControl] = directive; + httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + NoCache = true + }; var context = new ResponseCachingContext(httpContext, new TestResponseCache()); Assert.False(context.RequestIsCacheable()); } + [Fact] + public void RequestIsCacheable_NoStore_Allowed() + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.Method = "GET"; + httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + NoStore = true + }; + var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + + Assert.True(context.RequestIsCacheable()); + } + [Fact] public void RequestIsCacheable_LegacyDirectives_NotAllowed() { @@ -162,7 +176,24 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public void ResponseIsCacheable_NoStore_NotAllowed() + public void ResponseIsCacheable_RequestNoStore_NotAllowed() + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + NoStore = true + }; + httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + Public = true + }; + var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + + Assert.False(context.ResponseIsCacheable()); + } + + [Fact] + public void ResponseIsCacheable_ResponseNoStore_NotAllowed() { var httpContext = new DefaultHttpContext(); httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs index c21c2dab11..9c700e6917 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs @@ -347,6 +347,82 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } } + [Fact] + public async void ServesCachedContentIfSubsequentRequestContainsNoStore() + { + var builder = CreateBuilderWithResponseCaching( + async (context) => + { + var uniqueId = Guid.NewGuid().ToString(); + var headers = context.Response.GetTypedHeaders(); + headers.CacheControl = new CacheControlHeaderValue() + { + Public = true, + MaxAge = TimeSpan.FromSeconds(10) + }; + headers.Date = DateTimeOffset.UtcNow; + headers.Headers["X-Value"] = uniqueId; + await context.Response.WriteAsync(uniqueId); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue() + { + NoStore = true + }; + var subsequentResponse = await client.GetAsync(""); + + initialResponse.EnsureSuccessStatusCode(); + subsequentResponse.EnsureSuccessStatusCode(); + + foreach (var header in initialResponse.Headers) + { + Assert.Equal(initialResponse.Headers.GetValues(header.Key), subsequentResponse.Headers.GetValues(header.Key)); + } + Assert.True(subsequentResponse.Headers.Contains(HeaderNames.Age)); + Assert.Equal(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync()); + } + } + + [Fact] + public async void ServesFreshContentIfInitialRequestContainsNoStore() + { + var builder = CreateBuilderWithResponseCaching( + async (context) => + { + var uniqueId = Guid.NewGuid().ToString(); + var headers = context.Response.GetTypedHeaders(); + headers.CacheControl = new CacheControlHeaderValue() + { + Public = true, + MaxAge = TimeSpan.FromSeconds(10) + }; + headers.Date = DateTimeOffset.UtcNow; + headers.Headers["X-Value"] = uniqueId; + await context.Response.WriteAsync(uniqueId); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + client.DefaultRequestHeaders.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue() + { + NoStore = true + }; + var initialResponse = await client.GetAsync(""); + var subsequentResponse = await client.GetAsync(""); + + initialResponse.EnsureSuccessStatusCode(); + subsequentResponse.EnsureSuccessStatusCode(); + + Assert.False(subsequentResponse.Headers.Contains(HeaderNames.Age)); + Assert.NotEqual(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync()); + } + } + private static IWebHostBuilder CreateBuilderWithResponseCaching(RequestDelegate requestDelegate) => CreateBuilderWithResponseCaching(app => { }, requestDelegate); From fb724a71de9f35a352cc29d2876bf95ee4e2bf16 Mon Sep 17 00:00:00 2001 From: John Luo Date: Mon, 22 Aug 2016 18:40:21 -0700 Subject: [PATCH 014/188] Add configurable options for ResponseCaching Override RequestIsCacheable Override ResponseIsCacheable Append customized cache key --- .../{ => Interfaces}/IResponseCache.cs | 0 .../IResponseCachingCacheKeySuffixProvider.cs | 17 ++ .../IResponseCachingCacheabilityValidator.cs | 24 ++ .../Internal/NoopCacheKeySuffixProvider.cs | 12 + .../Internal/NoopCacheabilityValidator.cs | 14 ++ .../OverrideResult.cs | 23 ++ .../ResponseCachingContext.cs | 119 +++++---- .../ResponseCachingExtensions.cs | 7 + .../ResponseCachingMiddleware.cs | 27 +- ...ponseCachingServiceCollectionExtensions.cs | 10 + .../ResponseCachingContextTests.cs | 233 +++++++++++++++--- 11 files changed, 402 insertions(+), 84 deletions(-) rename src/Microsoft.AspNetCore.ResponseCaching/{ => Interfaces}/IResponseCache.cs (100%) create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCachingCacheKeySuffixProvider.cs create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCachingCacheabilityValidator.cs create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Internal/NoopCacheKeySuffixProvider.cs create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Internal/NoopCacheabilityValidator.cs create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/OverrideResult.cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/IResponseCache.cs b/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCache.cs similarity index 100% rename from src/Microsoft.AspNetCore.ResponseCaching/IResponseCache.cs rename to src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCache.cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCachingCacheKeySuffixProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCachingCacheKeySuffixProvider.cs new file mode 100644 index 0000000000..7514fa4254 --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCachingCacheKeySuffixProvider.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 Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.ResponseCaching +{ + public interface IResponseCachingCacheKeySuffixProvider + { + /// + /// Create a key segment that is appended to the default cache key. + /// + /// The . + /// The key segment that will be appended to the default cache key. + string CreateCustomKeySuffix(HttpContext httpContext); + } +} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCachingCacheabilityValidator.cs b/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCachingCacheabilityValidator.cs new file mode 100644 index 0000000000..11b46560c8 --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCachingCacheabilityValidator.cs @@ -0,0 +1,24 @@ +// 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 +{ + public interface IResponseCachingCacheabilityValidator + { + /// + /// Override default behavior for determining cacheability of an HTTP request. + /// + /// The . + /// The . + OverrideResult RequestIsCacheableOverride(HttpContext httpContext); + + /// + /// Override default behavior for determining cacheability of an HTTP response. + /// + /// The . + /// The . + OverrideResult ResponseIsCacheableOverride(HttpContext httpContext); + } +} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/NoopCacheKeySuffixProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/NoopCacheKeySuffixProvider.cs new file mode 100644 index 0000000000..43daeb5a5d --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/NoopCacheKeySuffixProvider.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.AspNetCore.Http; + +namespace Microsoft.AspNetCore.ResponseCaching.Internal +{ + internal class NoopCacheKeySuffixProvider : IResponseCachingCacheKeySuffixProvider + { + public string CreateCustomKeySuffix(HttpContext httpContext) => null; + } +} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/NoopCacheabilityValidator.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/NoopCacheabilityValidator.cs new file mode 100644 index 0000000000..309e900f4b --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/NoopCacheabilityValidator.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 +{ + internal class NoopCacheabilityValidator : IResponseCachingCacheabilityValidator + { + public OverrideResult RequestIsCacheableOverride(HttpContext httpContext) => OverrideResult.UseDefaultLogic; + + public OverrideResult ResponseIsCacheableOverride(HttpContext httpContext) => OverrideResult.UseDefaultLogic; + } +} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/OverrideResult.cs b/src/Microsoft.AspNetCore.ResponseCaching/OverrideResult.cs new file mode 100644 index 0000000000..e2a168aaeb --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/OverrideResult.cs @@ -0,0 +1,23 @@ +// 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 enum OverrideResult + { + /// + /// Use the default logic for determining cacheability. + /// + UseDefaultLogic, + + /// + /// Ignore default logic and do not cache. + /// + DoNotCache, + + /// + /// Ignore default logic and cache. + /// + Cache + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs index 54f6384d4b..394d8f9c20 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs @@ -28,31 +28,29 @@ namespace Microsoft.AspNetCore.ResponseCaching private CachedResponse _cachedResponse; private TimeSpan _cachedResponseValidFor; internal DateTimeOffset _responseTime; - - public ResponseCachingContext(HttpContext httpContext, IResponseCache cache) - : this(httpContext, cache, new SystemClock()) + + public ResponseCachingContext( + HttpContext httpContext, + IResponseCache cache, + IResponseCachingCacheabilityValidator cacheabilityValidator, + IResponseCachingCacheKeySuffixProvider cacheKeySuffixProvider) + : this(httpContext, cache, new SystemClock(), cacheabilityValidator, cacheKeySuffixProvider) { } // Internal for testing - internal ResponseCachingContext(HttpContext httpContext, IResponseCache cache, ISystemClock clock) + internal ResponseCachingContext( + HttpContext httpContext, + IResponseCache cache, + ISystemClock clock, + IResponseCachingCacheabilityValidator cacheabilityValidator, + IResponseCachingCacheKeySuffixProvider cacheKeySuffixProvider) { - if (cache == null) - { - throw new ArgumentNullException(nameof(cache)); - } - if (httpContext == null) - { - throw new ArgumentNullException(nameof(httpContext)); - } - if (clock == null) - { - throw new ArgumentNullException(nameof(clock)); - } - HttpContext = httpContext; Cache = cache; Clock = clock; + CacheabilityValidator = cacheabilityValidator; + CacheKeySuffixProvider = cacheKeySuffixProvider; } internal bool CacheResponse @@ -72,12 +70,16 @@ namespace Microsoft.AspNetCore.ResponseCaching internal bool ResponseStarted { get; set; } - private ISystemClock Clock { get; } - private HttpContext HttpContext { get; } private IResponseCache Cache { get; } + private ISystemClock Clock { get; } + + private IResponseCachingCacheabilityValidator CacheabilityValidator { get; } + + private IResponseCachingCacheKeySuffixProvider CacheKeySuffixProvider { get; } + private Stream OriginalResponseStream { get; set; } private ResponseCacheStream ResponseCacheStream { get; set; } @@ -145,46 +147,56 @@ namespace Microsoft.AspNetCore.ResponseCaching var builder = new StringBuilder() .Append(request.Method.ToUpperInvariant()) .Append(";") - .Append(request.Path.Value.ToUpperInvariant()) - .Append(CreateVaryByCacheKey(varyBy)); + .Append(request.Path.Value.ToUpperInvariant()); - return builder.ToString(); - } - - private string CreateVaryByCacheKey(CachedVaryBy varyBy) - { - // TODO: resolve key format and delimiters - if (varyBy == null || varyBy.Headers.Count == 0) + if (varyBy?.Headers.Count > 0) { - return string.Empty; - } - - var builder = new StringBuilder(";"); - - foreach (var header in varyBy.Headers) - { - // TODO: Normalization of order, case? - var value = HttpContext.Request.Headers[header].ToString(); - - // TODO: How to handle null/empty string? - if (string.IsNullOrEmpty(value)) + // TODO: resolve key format and delimiters + foreach (var header in varyBy.Headers) { - value = "null"; + // TODO: Normalization of order, case? + var value = HttpContext.Request.Headers[header]; + + // TODO: How to handle null/empty string? + if (StringValues.IsNullOrEmpty(value)) + { + value = "null"; + } + + builder.Append(";") + .Append(header) + .Append("=") + .Append(value); } - - builder.Append(header) - .Append("=") - .Append(value) - .Append(";"); } + // TODO: Parse querystring params - // Parse querystring params + // Append custom cache key segment + var customKey = CacheKeySuffixProvider.CreateCustomKeySuffix(HttpContext); + if (!string.IsNullOrEmpty(customKey)) + { + builder.Append(";") + .Append(customKey); + } return builder.ToString(); } internal bool RequestIsCacheable() { + // Use optional override if specified by user + switch(CacheabilityValidator.RequestIsCacheableOverride(HttpContext)) + { + case OverrideResult.UseDefaultLogic: + break; + case OverrideResult.DoNotCache: + return false; + case OverrideResult.Cache: + return true; + default: + throw new NotSupportedException($"Unrecognized result from {nameof(CacheabilityValidator.RequestIsCacheableOverride)}."); + } + // Verify the method // TODO: RFC lists POST as a cacheable method when explicit freshness information is provided, but this is not widely implemented. Will revisit. var request = HttpContext.Request; @@ -236,6 +248,19 @@ namespace Microsoft.AspNetCore.ResponseCaching internal bool ResponseIsCacheable() { + // Use optional override if specified by user + switch (CacheabilityValidator.ResponseIsCacheableOverride(HttpContext)) + { + case OverrideResult.UseDefaultLogic: + break; + case OverrideResult.DoNotCache: + return false; + case OverrideResult.Cache: + return true; + default: + throw new NotSupportedException($"Unrecognized result from {nameof(CacheabilityValidator.ResponseIsCacheableOverride)}."); + } + // Only cache pages explicitly marked with public // TODO: Consider caching responses that are not marked as public but otherwise cacheable? if (!ResponseCacheControl.Public) diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingExtensions.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingExtensions.cs index 60817a6511..76b81dbccb 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingExtensions.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingExtensions.cs @@ -1,7 +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 Microsoft.AspNetCore.ResponseCaching; +using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Builder { @@ -9,6 +11,11 @@ namespace Microsoft.AspNetCore.Builder { public static IApplicationBuilder UseResponseCaching(this IApplicationBuilder app) { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + return app.UseMiddleware(); } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs index 5c43ff15fe..c22ef33fb5 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs @@ -4,11 +4,9 @@ using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; namespace Microsoft.AspNetCore.ResponseCaching { - // http://tools.ietf.org/html/rfc7234 public class ResponseCachingMiddleware { private static readonly Func OnStartingCallback = state => @@ -19,26 +17,45 @@ namespace Microsoft.AspNetCore.ResponseCaching private readonly RequestDelegate _next; private readonly IResponseCache _cache; + IResponseCachingCacheabilityValidator _cacheabilityValidator; + IResponseCachingCacheKeySuffixProvider _cacheKeySuffixProvider; - public ResponseCachingMiddleware(RequestDelegate next, IResponseCache cache) + public ResponseCachingMiddleware( + RequestDelegate next, + IResponseCache cache, + IResponseCachingCacheabilityValidator cacheabilityValidator, + IResponseCachingCacheKeySuffixProvider cacheKeySuffixProvider) { if (cache == null) { throw new ArgumentNullException(nameof(cache)); } - if (next == null) { throw new ArgumentNullException(nameof(next)); } + if (cacheabilityValidator == null) + { + throw new ArgumentNullException(nameof(cacheabilityValidator)); + } + if (cacheKeySuffixProvider == null) + { + throw new ArgumentNullException(nameof(cacheKeySuffixProvider)); + } _next = next; _cache = cache; + _cacheabilityValidator = cacheabilityValidator; + _cacheKeySuffixProvider = cacheKeySuffixProvider; } public async Task Invoke(HttpContext context) { - var cachingContext = new ResponseCachingContext(context, _cache); + var cachingContext = new ResponseCachingContext( + context, + _cache, + _cacheabilityValidator, + _cacheKeySuffixProvider); // Should we attempt any caching logic? if (cachingContext.RequestIsCacheable()) diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingServiceCollectionExtensions.cs index b1e72631c7..b6e18f7874 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingServiceCollectionExtensions.cs @@ -18,6 +18,7 @@ namespace Microsoft.Extensions.DependencyInjection } services.AddMemoryCache(); + services.AddResponseCachingServices(); services.TryAdd(ServiceDescriptor.Singleton()); return services; @@ -31,9 +32,18 @@ namespace Microsoft.Extensions.DependencyInjection } services.AddDistributedMemoryCache(); + services.AddResponseCachingServices(); services.TryAdd(ServiceDescriptor.Singleton()); return services; } + + private static IServiceCollection AddResponseCachingServices(this IServiceCollection services) + { + services.TryAdd(ServiceDescriptor.Singleton()); + services.TryAdd(ServiceDescriptor.Singleton()); + + return services; + } } } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs index beeff86721..a6aaefee31 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs @@ -2,7 +2,6 @@ // 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.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; @@ -23,7 +22,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var httpContext = new DefaultHttpContext(); httpContext.Request.Method = method; - var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + var context = CreateTestContext(httpContext); Assert.True(context.RequestIsCacheable()); } @@ -41,7 +40,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var httpContext = new DefaultHttpContext(); httpContext.Request.Method = method; - var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + var context = CreateTestContext(httpContext); Assert.False(context.RequestIsCacheable()); } @@ -52,7 +51,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "GET"; httpContext.Request.Headers[HeaderNames.Authorization] = "Basic plaintextUN:plaintextPW"; - var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + var context = CreateTestContext(httpContext); Assert.False(context.RequestIsCacheable()); } @@ -66,7 +65,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { NoCache = true }; - var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + var context = CreateTestContext(httpContext); Assert.False(context.RequestIsCacheable()); } @@ -80,7 +79,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { NoStore = true }; - var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + var context = CreateTestContext(httpContext); Assert.True(context.RequestIsCacheable()); } @@ -91,7 +90,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "GET"; httpContext.Request.Headers[HeaderNames.Pragma] = "no-cache"; - var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + var context = CreateTestContext(httpContext); Assert.False(context.RequestIsCacheable()); } @@ -103,11 +102,61 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests httpContext.Request.Method = "GET"; httpContext.Request.Headers[HeaderNames.Pragma] = "no-cache"; httpContext.Request.Headers[HeaderNames.CacheControl] = "max-age=10"; - var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + var context = CreateTestContext(httpContext); Assert.True(context.RequestIsCacheable()); } + private class AllowUnrecognizedHTTPMethodRequests : IResponseCachingCacheabilityValidator + { + public OverrideResult RequestIsCacheableOverride(HttpContext httpContext) => + httpContext.Request.Method == "UNRECOGNIZED" ? OverrideResult.Cache : OverrideResult.DoNotCache; + + public OverrideResult ResponseIsCacheableOverride(HttpContext httpContext) => OverrideResult.UseDefaultLogic; + } + + [Fact] + public void RequestIsCacheableOverride_OverridesDefaultBehavior_ToAllowed() + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.Method = "UNRECOGNIZED"; + var responseCachingContext = CreateTestContext(httpContext, new AllowUnrecognizedHTTPMethodRequests()); + + Assert.True(responseCachingContext.RequestIsCacheable()); + } + + private class DisallowGetHTTPMethodRequests : IResponseCachingCacheabilityValidator + { + public OverrideResult RequestIsCacheableOverride(HttpContext httpContext) => + httpContext.Request.Method == "GET" ? OverrideResult.DoNotCache : OverrideResult.Cache; + + public OverrideResult ResponseIsCacheableOverride(HttpContext httpContext) => OverrideResult.UseDefaultLogic; + } + + [Fact] + public void RequestIsCacheableOverride_OverridesDefaultBehavior_ToNotAllowed() + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.Method = "GET"; + var responseCachingContext = CreateTestContext(httpContext, new DisallowGetHTTPMethodRequests()); + + Assert.False(responseCachingContext.RequestIsCacheable()); + } + + [Fact] + public void RequestIsCacheableOverride_IgnoreFallsBackToDefaultBehavior() + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.Method = "GET"; + var responseCachingContext = CreateTestContext(httpContext, new NoopCacheabilityValidator()); + + Assert.True(responseCachingContext.RequestIsCacheable()); + + httpContext.Request.Method = "UNRECOGNIZED"; + + Assert.False(responseCachingContext.RequestIsCacheable()); + } + [Fact] public void CreateCacheKey_Includes_UppercaseMethodAndPath() { @@ -118,7 +167,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests httpContext.Request.Host = new HostString("example.com", 80); httpContext.Request.PathBase = "/pathBase"; httpContext.Request.QueryString = new QueryString("?query.Key=a&query.Value=b"); - var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + var context = CreateTestContext(httpContext); Assert.Equal("HEAD;/PATH/SUBPATH", context.CreateCacheKey()); } @@ -131,9 +180,30 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests httpContext.Request.Path = "/"; httpContext.Request.Headers["HeaderA"] = "ValueA"; httpContext.Request.Headers["HeaderB"] = "ValueB"; - var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + var context = CreateTestContext(httpContext); - Assert.Equal("GET;/;HeaderA=ValueA;HeaderC=null;", context.CreateCacheKey(new CachedVaryBy() + Assert.Equal("GET;/;HeaderA=ValueA;HeaderC=null", context.CreateCacheKey(new CachedVaryBy() + { + Headers = new string[] { "HeaderA", "HeaderC" } + })); + } + + private class CustomizeKeySuffixProvider : IResponseCachingCacheKeySuffixProvider + { + public string CreateCustomKeySuffix(HttpContext httpContext) => "CustomizedKey"; + } + + [Fact] + public void CreateCacheKey_OptionalCacheKey_AppendedToDefaultKey() + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.Method = "GET"; + httpContext.Request.Path = "/"; + httpContext.Request.Headers["HeaderA"] = "ValueA"; + httpContext.Request.Headers["HeaderB"] = "ValueB"; + var responseCachingContext = CreateTestContext(httpContext, new CustomizeKeySuffixProvider()); + + Assert.Equal("GET;/;HeaderA=ValueA;HeaderC=null;CustomizedKey", responseCachingContext.CreateCacheKey(new CachedVaryBy() { Headers = new string[] { "HeaderA", "HeaderC" } })); @@ -143,7 +213,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests public void ResponseIsCacheable_NoPublic_NotAllowed() { var httpContext = new DefaultHttpContext(); - var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + var context = CreateTestContext(httpContext); Assert.False(context.ResponseIsCacheable()); } @@ -156,7 +226,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { Public = true }; - var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + var context = CreateTestContext(httpContext); Assert.True(context.ResponseIsCacheable()); } @@ -170,7 +240,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Public = true, NoCache = true }; - var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + var context = CreateTestContext(httpContext); Assert.False(context.ResponseIsCacheable()); } @@ -187,7 +257,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { Public = true }; - var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + var context = CreateTestContext(httpContext); Assert.False(context.ResponseIsCacheable()); } @@ -201,7 +271,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Public = true, NoStore = true }; - var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + var context = CreateTestContext(httpContext); Assert.False(context.ResponseIsCacheable()); } @@ -215,7 +285,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Public = true }; httpContext.Response.Headers[HeaderNames.Vary] = "*"; - var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + var context = CreateTestContext(httpContext); Assert.False(context.ResponseIsCacheable()); } @@ -229,7 +299,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Public = true, Private = true }; - var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + var context = CreateTestContext(httpContext); Assert.False(context.ResponseIsCacheable()); } @@ -244,7 +314,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { Public = true }; - var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + var context = CreateTestContext(httpContext); Assert.True(context.ResponseIsCacheable()); } @@ -306,16 +376,78 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { Public = true }; - var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + var context = CreateTestContext(httpContext); Assert.False(context.ResponseIsCacheable()); } - + + private class Allow500Response : IResponseCachingCacheabilityValidator + { + public OverrideResult RequestIsCacheableOverride(HttpContext httpContext) => OverrideResult.UseDefaultLogic; + + public OverrideResult ResponseIsCacheableOverride(HttpContext httpContext) => + httpContext.Response.StatusCode == StatusCodes.Status500InternalServerError ? OverrideResult.Cache : OverrideResult.DoNotCache; + } + + [Fact] + public void ResponseIsCacheableOverride_OverridesDefaultBehavior_ToAllowed() + { + var httpContext = new DefaultHttpContext(); + httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError; + httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + Public = true + }; + var responseCachingContext = CreateTestContext(httpContext, new Allow500Response()); + + Assert.True(responseCachingContext.ResponseIsCacheable()); + } + + private class Disallow200Response : IResponseCachingCacheabilityValidator + { + public OverrideResult RequestIsCacheableOverride(HttpContext httpContext) => OverrideResult.UseDefaultLogic; + + public OverrideResult ResponseIsCacheableOverride(HttpContext httpContext) => + httpContext.Response.StatusCode == StatusCodes.Status200OK ? OverrideResult.DoNotCache : OverrideResult.Cache; + } + + [Fact] + public void ResponseIsCacheableOverride_OverridesDefaultBehavior_ToNotAllowed() + { + var httpContext = new DefaultHttpContext(); + httpContext.Response.StatusCode = StatusCodes.Status200OK; + httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + Public = true + }; + var responseCachingContext = CreateTestContext(httpContext, new Disallow200Response()); + + Assert.False(responseCachingContext.ResponseIsCacheable()); + } + + [Fact] + public void ResponseIsCacheableOverride_IgnoreFallsBackToDefaultBehavior() + { + var httpContext = new DefaultHttpContext(); + httpContext.Response.StatusCode = StatusCodes.Status200OK; + httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + Public = true + }; + var responseCachingContext = CreateTestContext(httpContext, new NoopCacheabilityValidator()); + + Assert.True(responseCachingContext.ResponseIsCacheable()); + + httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError; + + Assert.False(responseCachingContext.ResponseIsCacheable()); + } + [Fact] public void EntryIsFresh_NoExpiryRequirements_IsFresh() { var httpContext = new DefaultHttpContext(); - var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + var context = CreateTestContext(httpContext); Assert.True(context.EntryIsFresh(new ResponseHeaders(new HeaderDictionary()), TimeSpan.MaxValue, verifyAgainstRequest: false)); } @@ -326,7 +458,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var httpContext = new DefaultHttpContext(); var utcNow = DateTimeOffset.UtcNow; httpContext.Response.GetTypedHeaders().Expires = utcNow; - var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + var context = CreateTestContext(httpContext); context._responseTime = utcNow; Assert.False(context.EntryIsFresh(httpContext.Response.GetTypedHeaders(), TimeSpan.MaxValue, verifyAgainstRequest: false)); @@ -345,7 +477,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests MaxAge = TimeSpan.FromSeconds(10) }; - var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + var context = CreateTestContext(httpContext); context._responseTime = utcNow; Assert.True(context.EntryIsFresh(httpContext.Response.GetTypedHeaders(), TimeSpan.FromSeconds(10), verifyAgainstRequest: false)); @@ -364,7 +496,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests MaxAge = TimeSpan.FromSeconds(10) }; - var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + var context = CreateTestContext(httpContext); context._responseTime = utcNow; Assert.False(context.EntryIsFresh(httpContext.Response.GetTypedHeaders(), TimeSpan.FromSeconds(11), verifyAgainstRequest: false)); @@ -379,7 +511,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests MaxAge = TimeSpan.FromSeconds(10), SharedMaxAge = TimeSpan.FromSeconds(15) }; - var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + var context = CreateTestContext(httpContext); Assert.True(context.EntryIsFresh(httpContext.Response.GetTypedHeaders(), TimeSpan.FromSeconds(11), verifyAgainstRequest: false)); } @@ -393,7 +525,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests MaxAge = TimeSpan.FromSeconds(10), SharedMaxAge = TimeSpan.FromSeconds(5) }; - var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + var context = CreateTestContext(httpContext); Assert.False(context.EntryIsFresh(httpContext.Response.GetTypedHeaders(), TimeSpan.FromSeconds(6), verifyAgainstRequest: false)); } @@ -411,7 +543,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests MaxAge = TimeSpan.FromSeconds(10), SharedMaxAge = TimeSpan.FromSeconds(5) }; - var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + var context = CreateTestContext(httpContext); Assert.False(context.EntryIsFresh(httpContext.Response.GetTypedHeaders(), TimeSpan.FromSeconds(3), verifyAgainstRequest: true)); } @@ -428,7 +560,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { MaxAge = TimeSpan.FromSeconds(10), }; - var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + var context = CreateTestContext(httpContext); Assert.False(context.EntryIsFresh(httpContext.Response.GetTypedHeaders(), TimeSpan.FromSeconds(6), verifyAgainstRequest: true)); } @@ -447,7 +579,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { MaxAge = TimeSpan.FromSeconds(5), }; - var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + var context = CreateTestContext(httpContext); Assert.True(context.EntryIsFresh(httpContext.Response.GetTypedHeaders(), TimeSpan.FromSeconds(6), verifyAgainstRequest: true)); } @@ -467,7 +599,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests MaxAge = TimeSpan.FromSeconds(5), MustRevalidate = true }; - var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + var context = CreateTestContext(httpContext); Assert.False(context.EntryIsFresh(httpContext.Response.GetTypedHeaders(), TimeSpan.FromSeconds(6), verifyAgainstRequest: true)); } @@ -486,11 +618,48 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests MaxAge = TimeSpan.FromSeconds(10), SharedMaxAge = TimeSpan.FromSeconds(5) }; - var context = new ResponseCachingContext(httpContext, new TestResponseCache()); + var context = CreateTestContext(httpContext); Assert.True(context.EntryIsFresh(httpContext.Response.GetTypedHeaders(), TimeSpan.FromSeconds(3), verifyAgainstRequest: false)); } + private static ResponseCachingContext CreateTestContext(HttpContext httpContext) + { + return CreateTestContext( + httpContext, + new NoopCacheKeySuffixProvider(), + new NoopCacheabilityValidator()); + } + + private static ResponseCachingContext CreateTestContext(HttpContext httpContext, IResponseCachingCacheKeySuffixProvider cacheKeySuffixProvider) + { + return CreateTestContext( + httpContext, + cacheKeySuffixProvider, + new NoopCacheabilityValidator()); + } + + private static ResponseCachingContext CreateTestContext(HttpContext httpContext, IResponseCachingCacheabilityValidator cacheabilityValidator) + { + return CreateTestContext( + httpContext, + new NoopCacheKeySuffixProvider(), + cacheabilityValidator); + } + + private static ResponseCachingContext CreateTestContext( + HttpContext httpContext, + IResponseCachingCacheKeySuffixProvider cacheKeySuffixProvider, + IResponseCachingCacheabilityValidator cacheabilityValidator) + { + return new ResponseCachingContext( + httpContext, + new TestResponseCache(), + new SystemClock(), + cacheabilityValidator, + cacheKeySuffixProvider); + } + private class TestResponseCache : IResponseCache { public object Get(string key) From 8c5a5f7394c17e66f1dddd1c0975bb2d1bf8517c Mon Sep 17 00:00:00 2001 From: John Luo Date: Wed, 24 Aug 2016 17:42:43 -0700 Subject: [PATCH 015/188] Use ObjectPool for StringBuilder --- .../ResponseCachingContext.cs | 151 ++++++++++-------- .../ResponseCachingMiddleware.cs | 21 ++- .../ResponseCachingContextTests.cs | 3 + 3 files changed, 100 insertions(+), 75 deletions(-) diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs index 394d8f9c20..b7c9b7a626 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs @@ -10,14 +10,23 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Headers; using Microsoft.AspNetCore.ResponseCaching.Internal; +using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.ResponseCaching { - public class ResponseCachingContext + internal class ResponseCachingContext { private static readonly CacheControlHeaderValue EmptyCacheControl = new CacheControlHeaderValue(); + + private readonly HttpContext _httpContext; + private readonly IResponseCache _cache; + private readonly ISystemClock _clock; + private readonly ObjectPool _builderPool; + private readonly IResponseCachingCacheabilityValidator _cacheabilityValidator; + private readonly IResponseCachingCacheKeySuffixProvider _cacheKeySuffixProvider; + private string _cacheKey; private ResponseType? _responseType; private RequestHeaders _requestHeaders; @@ -29,12 +38,13 @@ namespace Microsoft.AspNetCore.ResponseCaching private TimeSpan _cachedResponseValidFor; internal DateTimeOffset _responseTime; - public ResponseCachingContext( + internal ResponseCachingContext( HttpContext httpContext, IResponseCache cache, + ObjectPool builderPool, IResponseCachingCacheabilityValidator cacheabilityValidator, IResponseCachingCacheKeySuffixProvider cacheKeySuffixProvider) - : this(httpContext, cache, new SystemClock(), cacheabilityValidator, cacheKeySuffixProvider) + : this(httpContext, cache, new SystemClock(), builderPool, cacheabilityValidator, cacheKeySuffixProvider) { } @@ -43,14 +53,16 @@ namespace Microsoft.AspNetCore.ResponseCaching HttpContext httpContext, IResponseCache cache, ISystemClock clock, + ObjectPool builderPool, IResponseCachingCacheabilityValidator cacheabilityValidator, IResponseCachingCacheKeySuffixProvider cacheKeySuffixProvider) { - HttpContext = httpContext; - Cache = cache; - Clock = clock; - CacheabilityValidator = cacheabilityValidator; - CacheKeySuffixProvider = cacheKeySuffixProvider; + _httpContext = httpContext; + _cache = cache; + _clock = clock; + _builderPool = builderPool; + _cacheabilityValidator = cacheabilityValidator; + _cacheKeySuffixProvider = cacheKeySuffixProvider; } internal bool CacheResponse @@ -70,16 +82,6 @@ namespace Microsoft.AspNetCore.ResponseCaching internal bool ResponseStarted { get; set; } - private HttpContext HttpContext { get; } - - private IResponseCache Cache { get; } - - private ISystemClock Clock { get; } - - private IResponseCachingCacheabilityValidator CacheabilityValidator { get; } - - private IResponseCachingCacheKeySuffixProvider CacheKeySuffixProvider { get; } - private Stream OriginalResponseStream { get; set; } private ResponseCacheStream ResponseCacheStream { get; set; } @@ -92,7 +94,7 @@ namespace Microsoft.AspNetCore.ResponseCaching { if (_requestHeaders == null) { - _requestHeaders = HttpContext.Request.GetTypedHeaders(); + _requestHeaders = _httpContext.Request.GetTypedHeaders(); } return _requestHeaders; } @@ -104,7 +106,7 @@ namespace Microsoft.AspNetCore.ResponseCaching { if (_responseHeaders == null) { - _responseHeaders = HttpContext.Response.GetTypedHeaders(); + _responseHeaders = _httpContext.Response.GetTypedHeaders(); } return _responseHeaders; } @@ -143,49 +145,58 @@ namespace Microsoft.AspNetCore.ResponseCaching internal string CreateCacheKey(CachedVaryBy varyBy) { - var request = HttpContext.Request; - var builder = new StringBuilder() - .Append(request.Method.ToUpperInvariant()) - .Append(";") - .Append(request.Path.Value.ToUpperInvariant()); + var request = _httpContext.Request; + var builder = _builderPool.Get(); - if (varyBy?.Headers.Count > 0) + try { - // TODO: resolve key format and delimiters - foreach (var header in varyBy.Headers) + builder + .Append(request.Method.ToUpperInvariant()) + .Append(";") + .Append(request.Path.Value.ToUpperInvariant()); + + if (varyBy?.Headers.Count > 0) { - // TODO: Normalization of order, case? - var value = HttpContext.Request.Headers[header]; - - // TODO: How to handle null/empty string? - if (StringValues.IsNullOrEmpty(value)) + // TODO: resolve key format and delimiters + foreach (var header in varyBy.Headers) { - value = "null"; + // TODO: Normalization of order, case? + var value = _httpContext.Request.Headers[header]; + + // TODO: How to handle null/empty string? + if (StringValues.IsNullOrEmpty(value)) + { + value = "null"; + } + + builder.Append(";") + .Append(header) + .Append("=") + .Append(value); } - - builder.Append(";") - .Append(header) - .Append("=") - .Append(value); } - } - // TODO: Parse querystring params + // TODO: Parse querystring params - // Append custom cache key segment - var customKey = CacheKeySuffixProvider.CreateCustomKeySuffix(HttpContext); - if (!string.IsNullOrEmpty(customKey)) + // Append custom cache key segment + var customKey = _cacheKeySuffixProvider.CreateCustomKeySuffix(_httpContext); + if (!string.IsNullOrEmpty(customKey)) + { + builder.Append(";") + .Append(customKey); + } + + return builder.ToString(); + } + finally { - builder.Append(";") - .Append(customKey); + _builderPool.Return(builder); } - - return builder.ToString(); } internal bool RequestIsCacheable() { // Use optional override if specified by user - switch(CacheabilityValidator.RequestIsCacheableOverride(HttpContext)) + switch(_cacheabilityValidator.RequestIsCacheableOverride(_httpContext)) { case OverrideResult.UseDefaultLogic: break; @@ -194,12 +205,12 @@ namespace Microsoft.AspNetCore.ResponseCaching case OverrideResult.Cache: return true; default: - throw new NotSupportedException($"Unrecognized result from {nameof(CacheabilityValidator.RequestIsCacheableOverride)}."); + throw new NotSupportedException($"Unrecognized result from {nameof(_cacheabilityValidator.RequestIsCacheableOverride)}."); } // Verify the method // TODO: RFC lists POST as a cacheable method when explicit freshness information is provided, but this is not widely implemented. Will revisit. - var request = HttpContext.Request; + var request = _httpContext.Request; if (string.Equals("GET", request.Method, StringComparison.OrdinalIgnoreCase)) { _responseType = ResponseType.FullReponse; @@ -249,7 +260,7 @@ namespace Microsoft.AspNetCore.ResponseCaching internal bool ResponseIsCacheable() { // Use optional override if specified by user - switch (CacheabilityValidator.ResponseIsCacheableOverride(HttpContext)) + switch (_cacheabilityValidator.ResponseIsCacheableOverride(_httpContext)) { case OverrideResult.UseDefaultLogic: break; @@ -258,7 +269,7 @@ namespace Microsoft.AspNetCore.ResponseCaching case OverrideResult.Cache: return true; default: - throw new NotSupportedException($"Unrecognized result from {nameof(CacheabilityValidator.ResponseIsCacheableOverride)}."); + throw new NotSupportedException($"Unrecognized result from {nameof(_cacheabilityValidator.ResponseIsCacheableOverride)}."); } // Only cache pages explicitly marked with public @@ -281,7 +292,7 @@ namespace Microsoft.AspNetCore.ResponseCaching return false; } - var response = HttpContext.Response; + var response = _httpContext.Response; // Do not cache responses varying by * if (string.Equals(response.Headers[HeaderNames.Vary], "*", StringComparison.OrdinalIgnoreCase)) @@ -359,14 +370,14 @@ namespace Microsoft.AspNetCore.ResponseCaching internal async Task TryServeFromCacheAsync() { _cacheKey = CreateCacheKey(); - var cacheEntry = Cache.Get(_cacheKey); + var cacheEntry = _cache.Get(_cacheKey); var responseServed = false; if (cacheEntry is CachedVaryBy) { // Request contains VaryBy rules, recompute key and try again _cacheKey = CreateCacheKey(cacheEntry as CachedVaryBy); - cacheEntry = Cache.Get(_cacheKey); + cacheEntry = _cache.Get(_cacheKey); } if (cacheEntry is CachedResponse) @@ -374,13 +385,13 @@ namespace Microsoft.AspNetCore.ResponseCaching var cachedResponse = cacheEntry as CachedResponse; var cachedResponseHeaders = new ResponseHeaders(cachedResponse.Headers); - _responseTime = Clock.UtcNow; + _responseTime = _clock.UtcNow; var age = _responseTime - cachedResponse.Created; age = age > TimeSpan.Zero ? age : TimeSpan.Zero; if (EntryIsFresh(cachedResponseHeaders, age, verifyAgainstRequest: true)) { - var response = HttpContext.Response; + var response = _httpContext.Response; // Copy the cached status code and response headers response.StatusCode = cachedResponse.StatusCode; foreach (var header in cachedResponse.Headers) @@ -425,7 +436,7 @@ namespace Microsoft.AspNetCore.ResponseCaching if (!responseServed && RequestCacheControl.OnlyIfCached) { - HttpContext.Response.StatusCode = StatusCodes.Status504GatewayTimeout; + _httpContext.Response.StatusCode = StatusCodes.Status504GatewayTimeout; responseServed = true; } @@ -438,7 +449,7 @@ namespace Microsoft.AspNetCore.ResponseCaching if (CacheResponse) { // Create the cache entry now - var response = HttpContext.Response; + var response = _httpContext.Response; var varyHeaderValue = response.Headers[HeaderNames.Vary]; _cachedResponseValidFor = ResponseCacheControl.SharedMaxAge ?? ResponseCacheControl.MaxAge @@ -457,7 +468,7 @@ namespace Microsoft.AspNetCore.ResponseCaching }; // TODO: Overwrite? - Cache.Set(_cacheKey, cachedVaryBy, _cachedResponseValidFor); + _cache.Set(_cacheKey, cachedVaryBy, _cachedResponseValidFor); _cacheKey = CreateCacheKey(cachedVaryBy); } @@ -471,7 +482,7 @@ namespace Microsoft.AspNetCore.ResponseCaching _cachedResponse = new CachedResponse { Created = ResponseHeaders.Date.Value, - StatusCode = HttpContext.Response.StatusCode + StatusCode = _httpContext.Response.StatusCode }; foreach (var header in ResponseHeaders.Headers) @@ -495,7 +506,7 @@ namespace Microsoft.AspNetCore.ResponseCaching { _cachedResponse.Body = ResponseCacheStream.BufferedStream.ToArray(); - Cache.Set(_cacheKey, _cachedResponse, _cachedResponseValidFor); + _cache.Set(_cacheKey, _cachedResponse, _cachedResponseValidFor); } } @@ -504,7 +515,7 @@ namespace Microsoft.AspNetCore.ResponseCaching if (!ResponseStarted) { ResponseStarted = true; - _responseTime = Clock.UtcNow; + _responseTime = _clock.UtcNow; FinalizeCachingHeaders(); } @@ -515,25 +526,25 @@ namespace Microsoft.AspNetCore.ResponseCaching // TODO: Consider caching large responses on disk and serving them from there. // Shim response stream - OriginalResponseStream = HttpContext.Response.Body; + OriginalResponseStream = _httpContext.Response.Body; ResponseCacheStream = new ResponseCacheStream(OriginalResponseStream); - HttpContext.Response.Body = ResponseCacheStream; + _httpContext.Response.Body = ResponseCacheStream; // Shim IHttpSendFileFeature - OriginalSendFileFeature = HttpContext.Features.Get(); + OriginalSendFileFeature = _httpContext.Features.Get(); if (OriginalSendFileFeature != null) { - HttpContext.Features.Set(new SendFileFeatureWrapper(OriginalSendFileFeature, ResponseCacheStream)); + _httpContext.Features.Set(new SendFileFeatureWrapper(OriginalSendFileFeature, ResponseCacheStream)); } } internal void UnshimResponseStream() { // Unshim response stream - HttpContext.Response.Body = OriginalResponseStream; + _httpContext.Response.Body = OriginalResponseStream; // Unshim IHttpSendFileFeature - HttpContext.Features.Set(OriginalSendFileFeature); + _httpContext.Features.Set(OriginalSendFileFeature); } private enum ResponseType diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs index c22ef33fb5..9c6a922eb8 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs @@ -2,8 +2,11 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.ResponseCaching { @@ -17,22 +20,28 @@ namespace Microsoft.AspNetCore.ResponseCaching private readonly RequestDelegate _next; private readonly IResponseCache _cache; - IResponseCachingCacheabilityValidator _cacheabilityValidator; - IResponseCachingCacheKeySuffixProvider _cacheKeySuffixProvider; + private readonly ObjectPool _builderPool; + private readonly IResponseCachingCacheabilityValidator _cacheabilityValidator; + private readonly IResponseCachingCacheKeySuffixProvider _cacheKeySuffixProvider; public ResponseCachingMiddleware( RequestDelegate next, - IResponseCache cache, + IResponseCache cache, + ObjectPoolProvider poolProvider, IResponseCachingCacheabilityValidator cacheabilityValidator, IResponseCachingCacheKeySuffixProvider cacheKeySuffixProvider) { + if (next == null) + { + throw new ArgumentNullException(nameof(next)); + } if (cache == null) { throw new ArgumentNullException(nameof(cache)); } - if (next == null) + if (poolProvider == null) { - throw new ArgumentNullException(nameof(next)); + throw new ArgumentNullException(nameof(poolProvider)); } if (cacheabilityValidator == null) { @@ -45,6 +54,7 @@ namespace Microsoft.AspNetCore.ResponseCaching _next = next; _cache = cache; + _builderPool = poolProvider.CreateStringBuilderPool(); _cacheabilityValidator = cacheabilityValidator; _cacheKeySuffixProvider = cacheKeySuffixProvider; } @@ -54,6 +64,7 @@ namespace Microsoft.AspNetCore.ResponseCaching var cachingContext = new ResponseCachingContext( context, _cache, + _builderPool, _cacheabilityValidator, _cacheKeySuffixProvider); diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs index a6aaefee31..74e3819763 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs @@ -2,12 +2,14 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Headers; using Microsoft.AspNetCore.ResponseCaching.Internal; +using Microsoft.Extensions.ObjectPool; using Microsoft.Net.Http.Headers; using Xunit; @@ -656,6 +658,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests httpContext, new TestResponseCache(), new SystemClock(), + new DefaultObjectPool(new StringBuilderPooledObjectPolicy()), cacheabilityValidator, cacheKeySuffixProvider); } From 1d6c5af72c1fc6722dae7f7915d2c1dfa8e5e698 Mon Sep 17 00:00:00 2001 From: John Luo Date: Fri, 26 Aug 2016 12:39:27 -0700 Subject: [PATCH 016/188] Add option for VaryBy query string params --- .../Internal/CachedVaryBy.cs | 1 + .../DefaultResponseCacheEntrySerializer.cs | 33 +- .../ResponseCachingContext.cs | 78 +++- .../ResponseCachingExtensions.cs | 1 - .../ResponseCachingFeature.cs | 13 + .../ResponseCachingHttpContextExtensions.cs | 26 ++ ...efaultResponseCacheEntrySerializerTests.cs | 44 +- .../ResponseCachingContextTests.cs | 73 +++- .../ResponseCachingTests.cs | 413 +++++++++++++----- 9 files changed, 562 insertions(+), 120 deletions(-) create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingFeature.cs create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingHttpContextExtensions.cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/CachedVaryBy.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/CachedVaryBy.cs index 8ff382cd09..1fb1a4501d 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/CachedVaryBy.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/CachedVaryBy.cs @@ -8,5 +8,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal internal class CachedVaryBy { internal StringValues Headers { get; set; } + internal StringValues Params { get; set; } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/DefaultResponseCacheEntrySerializer.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/DefaultResponseCacheEntrySerializer.cs index 5112081b18..23b06f26a7 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/DefaultResponseCacheEntrySerializer.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/DefaultResponseCacheEntrySerializer.cs @@ -96,12 +96,26 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal } // Serialization Format - // Headers (comma separated string) + // Headers count + // Headers if count > 0 (comma separated string) + // Params count + // Params if count > 0 (comma separated string) private static CachedVaryBy ReadCachedVaryBy(BinaryReader reader) { - var headers = reader.ReadString().Split(','); + var headerCount = reader.ReadInt32(); + var headers = new string[headerCount]; + for (var index = 0; index < headerCount; index++) + { + headers[index] = reader.ReadString(); + } + var paramCount = reader.ReadInt32(); + var param = new string[paramCount]; + for (var index = 0; index < paramCount; index++) + { + param[index] = reader.ReadString(); + } - return new CachedVaryBy { Headers = headers }; + return new CachedVaryBy { Headers = headers, Params = param }; } // See serialization format above @@ -154,7 +168,18 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal private static void WriteCachedVaryBy(BinaryWriter writer, CachedVaryBy entry) { writer.Write(nameof(CachedVaryBy)); - writer.Write(entry.Headers); + + writer.Write(entry.Headers.Count); + foreach (var header in entry.Headers) + { + writer.Write(header); + } + + writer.Write(entry.Params.Count); + foreach (var param in entry.Params) + { + writer.Write(param); + } } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs index b7c9b7a626..1fcb159239 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs @@ -4,6 +4,7 @@ using System; using System.IO; using System.Globalization; +using System.Linq; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; @@ -19,6 +20,8 @@ namespace Microsoft.AspNetCore.ResponseCaching internal class ResponseCachingContext { private static readonly CacheControlHeaderValue EmptyCacheControl = new CacheControlHeaderValue(); + // Use the record separator for delimiting components of the cache key to avoid possible collisions + private static readonly char KeyDelimiter = '\x1e'; private readonly HttpContext _httpContext; private readonly IResponseCache _cache; @@ -150,13 +153,19 @@ namespace Microsoft.AspNetCore.ResponseCaching try { + // Default key builder .Append(request.Method.ToUpperInvariant()) - .Append(";") + .Append(KeyDelimiter) .Append(request.Path.Value.ToUpperInvariant()); + // Vary by headers if (varyBy?.Headers.Count > 0) { + // Append a group separator for the header segment of the cache key + builder.Append(KeyDelimiter) + .Append('H'); + // TODO: resolve key format and delimiters foreach (var header in varyBy.Headers) { @@ -169,19 +178,62 @@ namespace Microsoft.AspNetCore.ResponseCaching value = "null"; } - builder.Append(";") + builder.Append(KeyDelimiter) .Append(header) .Append("=") .Append(value); } } - // TODO: Parse querystring params + + // Vary by query params + if (varyBy?.Params.Count > 0) + { + // Append a group separator for the query parameter segment of the cache key + builder.Append(KeyDelimiter) + .Append('Q'); + + if (varyBy.Params.Count == 1 && string.Equals(varyBy.Params[0], "*")) + { + // Vary by all available query params + foreach (var query in _httpContext.Request.Query.OrderBy(q => q.Key, StringComparer.OrdinalIgnoreCase)) + { + builder.Append(KeyDelimiter) + .Append(query.Key.ToUpperInvariant()) + .Append("=") + .Append(query.Value); + } + } + else + { + // TODO: resolve key format and delimiters + foreach (var param in varyBy.Params) + { + // TODO: Normalization of order, case? + var value = _httpContext.Request.Query[param]; + + // TODO: How to handle null/empty string? + if (StringValues.IsNullOrEmpty(value)) + { + value = "null"; + } + + builder.Append(KeyDelimiter) + .Append(param) + .Append("=") + .Append(value); + } + } + } // Append custom cache key segment var customKey = _cacheKeySuffixProvider.CreateCustomKeySuffix(_httpContext); if (!string.IsNullOrEmpty(customKey)) { - builder.Append(";") + // Append a group separator for the custom segment of the cache key + builder.Append(KeyDelimiter) + .Append('C'); + + builder.Append(KeyDelimiter) .Append(customKey); } @@ -451,6 +503,7 @@ namespace Microsoft.AspNetCore.ResponseCaching // Create the cache entry now var response = _httpContext.Response; var varyHeaderValue = response.Headers[HeaderNames.Vary]; + var varyParamsValue = _httpContext.GetResponseCachingFeature().VaryByParams; _cachedResponseValidFor = ResponseCacheControl.SharedMaxAge ?? ResponseCacheControl.MaxAge ?? (ResponseHeaders.Expires - _responseTime) @@ -458,13 +511,18 @@ namespace Microsoft.AspNetCore.ResponseCaching ?? TimeSpan.FromSeconds(10); // Check if any VaryBy rules exist - if (!StringValues.IsNullOrEmpty(varyHeaderValue)) + if (!StringValues.IsNullOrEmpty(varyHeaderValue) || !StringValues.IsNullOrEmpty(varyParamsValue)) { + if (varyParamsValue.Count > 1) + { + Array.Sort(varyParamsValue.ToArray(), StringComparer.OrdinalIgnoreCase); + } + var cachedVaryBy = new CachedVaryBy { - // Only vary by headers for now // TODO: VaryBy Encoding - Headers = varyHeaderValue + Headers = varyHeaderValue, + Params = varyParamsValue }; // TODO: Overwrite? @@ -536,6 +594,9 @@ namespace Microsoft.AspNetCore.ResponseCaching { _httpContext.Features.Set(new SendFileFeatureWrapper(OriginalSendFileFeature, ResponseCacheStream)); } + + // TODO: Move this temporary interface with endpoint to HttpAbstractions + _httpContext.AddResponseCachingFeature(); } internal void UnshimResponseStream() @@ -545,6 +606,9 @@ namespace Microsoft.AspNetCore.ResponseCaching // Unshim IHttpSendFileFeature _httpContext.Features.Set(OriginalSendFileFeature); + + // TODO: Move this temporary interface with endpoint to HttpAbstractions + _httpContext.RemoveResponseCachingFeature(); } private enum ResponseType diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingExtensions.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingExtensions.cs index 76b81dbccb..45d905cea6 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingExtensions.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingExtensions.cs @@ -3,7 +3,6 @@ using System; using Microsoft.AspNetCore.ResponseCaching; -using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Builder { diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingFeature.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingFeature.cs new file mode 100644 index 0000000000..4341f20ae7 --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingFeature.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. + +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.ResponseCaching +{ + // TODO: Temporary interface for endpoints to specify options for response caching + public class ResponseCachingFeature + { + public StringValues VaryByParams { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingHttpContextExtensions.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingHttpContextExtensions.cs new file mode 100644 index 0000000000..e446b4491b --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingHttpContextExtensions.cs @@ -0,0 +1,26 @@ +// 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 +{ + // TODO: Temporary interface for endpoints to specify options for response caching + public static class ResponseCachingHttpContextExtensions + { + public static void AddResponseCachingFeature(this HttpContext httpContext) + { + httpContext.Features.Set(new ResponseCachingFeature()); + } + + public static void RemoveResponseCachingFeature(this HttpContext httpContext) + { + httpContext.Features.Set(null); + } + + public static ResponseCachingFeature GetResponseCachingFeature(this HttpContext httpContext) + { + return httpContext.Features.Get() ?? new ResponseCachingFeature(); + } + } +} diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/DefaultResponseCacheEntrySerializerTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/DefaultResponseCacheEntrySerializerTests.cs index b0da702792..2469af5ff8 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/DefaultResponseCacheEntrySerializerTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/DefaultResponseCacheEntrySerializerTests.cs @@ -13,19 +13,19 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests public class DefaultResponseCacheEntrySerializerTests { [Fact] - public void SerializeNullObjectThrows() + public void Serialize_NullObject_Throws() { Assert.Throws(() => DefaultResponseCacheSerializer.Serialize(null)); } [Fact] - public void SerializeUnknownObjectThrows() + public void Serialize_UnknownObject_Throws() { Assert.Throws(() => DefaultResponseCacheSerializer.Serialize(new object())); } [Fact] - public void RoundTripCachedResponsesSucceeds() + public void RoundTrip_CachedResponses_Succeeds() { var headers = new HeaderDictionary(); headers["keyA"] = "valueA"; @@ -42,7 +42,15 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public void RoundTripCachedVaryBySucceeds() + public void RoundTrip_Empty_CachedVaryBy_Succeeds() + { + var cachedVaryBy = new CachedVaryBy(); + + AssertCachedVarybyEqual(cachedVaryBy, (CachedVaryBy)DefaultResponseCacheSerializer.Deserialize(DefaultResponseCacheSerializer.Serialize(cachedVaryBy))); + } + + [Fact] + public void RoundTrip_HeadersOnly_CachedVaryBy_Succeeds() { var headers = new[] { "headerA", "headerB" }; var cachedVaryBy = new CachedVaryBy() @@ -53,9 +61,34 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests AssertCachedVarybyEqual(cachedVaryBy, (CachedVaryBy)DefaultResponseCacheSerializer.Deserialize(DefaultResponseCacheSerializer.Serialize(cachedVaryBy))); } + [Fact] + public void RoundTrip_ParamsOnly_CachedVaryBy_Succeeds() + { + var param = new[] { "paramA", "paramB" }; + var cachedVaryBy = new CachedVaryBy() + { + Params = param + }; + + AssertCachedVarybyEqual(cachedVaryBy, (CachedVaryBy)DefaultResponseCacheSerializer.Deserialize(DefaultResponseCacheSerializer.Serialize(cachedVaryBy))); + } [Fact] - public void DeserializeInvalidEntriesReturnsNull() + public void RoundTrip_HeadersAndParams_CachedVaryBy_Succeeds() + { + var headers = new[] { "headerA", "headerB" }; + var param = new[] { "paramA", "paramB" }; + var cachedVaryBy = new CachedVaryBy() + { + Headers = headers, + Params = param + }; + + AssertCachedVarybyEqual(cachedVaryBy, (CachedVaryBy)DefaultResponseCacheSerializer.Deserialize(DefaultResponseCacheSerializer.Serialize(cachedVaryBy))); + } + + [Fact] + public void Deserialize_InvalidEntries_ReturnsNull() { var headers = new[] { "headerA", "headerB" }; var cachedVaryBy = new CachedVaryBy() @@ -87,6 +120,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Assert.NotNull(actual); Assert.NotNull(expected); Assert.Equal(expected.Headers, actual.Headers); + Assert.Equal(expected.Params, actual.Params); } } } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs index 74e3819763..fa47d71309 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs @@ -17,6 +17,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { public class ResponseCachingContextTests { + private static readonly char KeyDelimiter = '\x1e'; + [Theory] [InlineData("GET")] [InlineData("HEAD")] @@ -171,7 +173,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests httpContext.Request.QueryString = new QueryString("?query.Key=a&query.Value=b"); var context = CreateTestContext(httpContext); - Assert.Equal("HEAD;/PATH/SUBPATH", context.CreateCacheKey()); + Assert.Equal($"HEAD{KeyDelimiter}/PATH/SUBPATH", context.CreateCacheKey()); } [Fact] @@ -184,12 +186,77 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests httpContext.Request.Headers["HeaderB"] = "ValueB"; var context = CreateTestContext(httpContext); - Assert.Equal("GET;/;HeaderA=ValueA;HeaderC=null", context.CreateCacheKey(new CachedVaryBy() + Assert.Equal($"GET{KeyDelimiter}/{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=null", context.CreateCacheKey(new CachedVaryBy() { Headers = new string[] { "HeaderA", "HeaderC" } })); } + [Fact] + public void CreateCacheKey_Includes_ListedVaryByParamsOnly() + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.Method = "GET"; + httpContext.Request.Path = "/"; + httpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB"); + var context = CreateTestContext(httpContext); + + Assert.Equal($"GET{KeyDelimiter}/{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=null", context.CreateCacheKey(new CachedVaryBy() + { + Params = new string[] { "ParamA", "ParamC" } + })); + } + + [Fact] + public void CreateCacheKey_Includes_VaryByParams_ParamNameCaseInsensitive_UseVaryByCasing() + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.Method = "GET"; + httpContext.Request.Path = "/"; + httpContext.Request.QueryString = new QueryString("?parama=ValueA¶mB=ValueB"); + var context = CreateTestContext(httpContext); + + Assert.Equal($"GET{KeyDelimiter}/{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=null", context.CreateCacheKey(new CachedVaryBy() + { + Params = new string[] { "ParamA", "ParamC" } + })); + } + + [Fact] + public void CreateCacheKey_Includes_AllQueryParamsGivenAsterisk() + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.Method = "GET"; + httpContext.Request.Path = "/"; + httpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB"); + var context = CreateTestContext(httpContext); + + // To support case insensitivity, all param keys are converted to lower case. + // Explicit VaryBy uses the casing specified in the setting. + Assert.Equal($"GET{KeyDelimiter}/{KeyDelimiter}Q{KeyDelimiter}PARAMA=ValueA{KeyDelimiter}PARAMB=ValueB", context.CreateCacheKey(new CachedVaryBy() + { + Params = new string[] { "*" } + })); + } + + [Fact] + public void CreateCacheKey_Includes_ListedVaryByHeadersAndParams() + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.Method = "GET"; + httpContext.Request.Path = "/"; + httpContext.Request.Headers["HeaderA"] = "ValueA"; + httpContext.Request.Headers["HeaderB"] = "ValueB"; + httpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB"); + var context = CreateTestContext(httpContext); + + Assert.Equal($"GET{KeyDelimiter}/{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=null{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=null", context.CreateCacheKey(new CachedVaryBy() + { + Headers = new string[] { "HeaderA", "HeaderC" }, + Params = new string[] { "ParamA", "ParamC" } + })); + } + private class CustomizeKeySuffixProvider : IResponseCachingCacheKeySuffixProvider { public string CreateCustomKeySuffix(HttpContext httpContext) => "CustomizedKey"; @@ -205,7 +272,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests httpContext.Request.Headers["HeaderB"] = "ValueB"; var responseCachingContext = CreateTestContext(httpContext, new CustomizeKeySuffixProvider()); - Assert.Equal("GET;/;HeaderA=ValueA;HeaderC=null;CustomizedKey", responseCachingContext.CreateCacheKey(new CachedVaryBy() + Assert.Equal($"GET{KeyDelimiter}/{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=null{KeyDelimiter}C{KeyDelimiter}CustomizedKey", responseCachingContext.CreateCacheKey(new CachedVaryBy() { Headers = new string[] { "HeaderA", "HeaderC" } })); diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs index 9c700e6917..2648025acd 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; @@ -18,7 +19,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests public class ResponseCachingTests { [Fact] - public async void ServesCachedContentIfAvailable() + public async void ServesCachedContent_IfAvailable() { var builder = CreateBuilderWithResponseCaching(async (context) => { @@ -40,20 +41,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var initialResponse = await client.GetAsync(""); var subsequentResponse = await client.GetAsync(""); - initialResponse.EnsureSuccessStatusCode(); - subsequentResponse.EnsureSuccessStatusCode(); - - foreach (var header in initialResponse.Headers) - { - Assert.Equal(initialResponse.Headers.GetValues(header.Key), subsequentResponse.Headers.GetValues(header.Key)); - } - Assert.True(subsequentResponse.Headers.Contains(HeaderNames.Age)); - Assert.Equal(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync()); + await AssertResponseCachedAsync(initialResponse, subsequentResponse); } } [Fact] - public async void ServesFreshContentIfNotAvailable() + public async void ServesFreshContent_IfNotAvailable() { var builder = CreateBuilderWithResponseCaching(async (context) => { @@ -75,16 +68,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var initialResponse = await client.GetAsync(""); var subsequentResponse = await client.GetAsync("/different"); - initialResponse.EnsureSuccessStatusCode(); - subsequentResponse.EnsureSuccessStatusCode(); - - Assert.False(subsequentResponse.Headers.Contains(HeaderNames.Age)); - Assert.NotEqual(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync()); + await AssertResponseNotCachedAsync(initialResponse, subsequentResponse); } } [Fact] - public async void ServesCachedContentIfVaryByMatches() + public async void ServesCachedContent_IfVaryByHeader_Matches() { var builder = CreateBuilderWithResponseCaching(async (context) => { @@ -108,20 +97,284 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var initialResponse = await client.GetAsync(""); var subsequentResponse = await client.GetAsync(""); - initialResponse.EnsureSuccessStatusCode(); - subsequentResponse.EnsureSuccessStatusCode(); - - foreach (var header in initialResponse.Headers) - { - Assert.Equal(initialResponse.Headers.GetValues(header.Key), subsequentResponse.Headers.GetValues(header.Key)); - } - Assert.True(subsequentResponse.Headers.Contains(HeaderNames.Age)); - Assert.Equal(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync()); + await AssertResponseCachedAsync(initialResponse, subsequentResponse); } } [Fact] - public async void ServesFreshContentIfRequestRequirementsNotMet() + public async void ServesFreshContent_IfVaryByHeader_Mismatches() + { + var builder = CreateBuilderWithResponseCaching(async (context) => + { + var uniqueId = Guid.NewGuid().ToString(); + var headers = context.Response.GetTypedHeaders(); + headers.CacheControl = new CacheControlHeaderValue() + { + Public = true, + MaxAge = TimeSpan.FromSeconds(10) + }; + headers.Date = DateTimeOffset.UtcNow; + headers.Headers["X-Value"] = uniqueId; + context.Response.Headers[HeaderNames.Vary] = HeaderNames.From; + await context.Response.WriteAsync(uniqueId); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + client.DefaultRequestHeaders.From = "user@example.com"; + var initialResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.From = "user2@example.com"; + var subsequentResponse = await client.GetAsync(""); + + await AssertResponseNotCachedAsync(initialResponse, subsequentResponse); + } + } + + [Fact] + public async void ServesCachedContent_IfVaryByParams_Matches() + { + var builder = CreateBuilderWithResponseCaching(async (context) => + { + var uniqueId = Guid.NewGuid().ToString(); + var headers = context.Response.GetTypedHeaders(); + headers.CacheControl = new CacheControlHeaderValue() + { + Public = true, + MaxAge = TimeSpan.FromSeconds(10) + }; + headers.Date = DateTimeOffset.UtcNow; + headers.Headers["X-Value"] = uniqueId; + context.GetResponseCachingFeature().VaryByParams = "param"; + await context.Response.WriteAsync(uniqueId); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync("?param=value"); + var subsequentResponse = await client.GetAsync("?param=value"); + + await AssertResponseCachedAsync(initialResponse, subsequentResponse); + } + } + + [Fact] + public async void ServesCachedContent_IfVaryByParamsExplicit_Matches_ParamNameCaseInsensitive() + { + var builder = CreateBuilderWithResponseCaching( + app => + { + app.Use(async (context, next) => + { + context.Features.Set(new DummySendFileFeature()); + await next.Invoke(); + }); + }, + async (context) => + { + var uniqueId = Guid.NewGuid().ToString(); + var headers = context.Response.GetTypedHeaders(); + headers.CacheControl = new CacheControlHeaderValue() + { + Public = true, + MaxAge = TimeSpan.FromSeconds(10) + }; + headers.Date = DateTimeOffset.UtcNow; + headers.Headers["X-Value"] = uniqueId; + context.GetResponseCachingFeature().VaryByParams = new[] { "ParamA", "paramb" }; + await context.Response.WriteAsync(uniqueId); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync("?parama=valuea¶mb=valueb"); + var subsequentResponse = await client.GetAsync("?ParamA=valuea&ParamB=valueb"); + + await AssertResponseCachedAsync(initialResponse, subsequentResponse); + } + } + + [Fact] + public async void ServesCachedContent_IfVaryByParamsStar_Matches_ParamNameCaseInsensitive() + { + var builder = CreateBuilderWithResponseCaching( + app => + { + app.Use(async (context, next) => + { + context.Features.Set(new DummySendFileFeature()); + await next.Invoke(); + }); + }, + async (context) => + { + var uniqueId = Guid.NewGuid().ToString(); + var headers = context.Response.GetTypedHeaders(); + headers.CacheControl = new CacheControlHeaderValue() + { + Public = true, + MaxAge = TimeSpan.FromSeconds(10) + }; + headers.Date = DateTimeOffset.UtcNow; + headers.Headers["X-Value"] = uniqueId; + context.GetResponseCachingFeature().VaryByParams = new[] { "*" }; + await context.Response.WriteAsync(uniqueId); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync("?parama=valuea¶mb=valueb"); + var subsequentResponse = await client.GetAsync("?ParamA=valuea&ParamB=valueb"); + + await AssertResponseCachedAsync(initialResponse, subsequentResponse); + } + } + + [Fact] + public async void ServesCachedContent_IfVaryByParamsExplicit_Matches_OrderInsensitive() + { + var builder = CreateBuilderWithResponseCaching(async (context) => + { + var uniqueId = Guid.NewGuid().ToString(); + var headers = context.Response.GetTypedHeaders(); + headers.CacheControl = new CacheControlHeaderValue() + { + Public = true, + MaxAge = TimeSpan.FromSeconds(10) + }; + headers.Date = DateTimeOffset.UtcNow; + headers.Headers["X-Value"] = uniqueId; + context.GetResponseCachingFeature().VaryByParams = new[] { "ParamB", "ParamA" }; + await context.Response.WriteAsync(uniqueId); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync("?ParamA=ValueA&ParamB=ValueB"); + var subsequentResponse = await client.GetAsync("?ParamB=ValueB&ParamA=ValueA"); + + await AssertResponseCachedAsync(initialResponse, subsequentResponse); + } + } + + [Fact] + public async void ServesCachedContent_IfVaryByParamsStar_Matches_OrderInsensitive() + { + var builder = CreateBuilderWithResponseCaching(async (context) => + { + var uniqueId = Guid.NewGuid().ToString(); + var headers = context.Response.GetTypedHeaders(); + headers.CacheControl = new CacheControlHeaderValue() + { + Public = true, + MaxAge = TimeSpan.FromSeconds(10) + }; + headers.Date = DateTimeOffset.UtcNow; + headers.Headers["X-Value"] = uniqueId; + context.GetResponseCachingFeature().VaryByParams = new[] { "*" }; + await context.Response.WriteAsync(uniqueId); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync("?ParamA=ValueA&ParamB=ValueB"); + var subsequentResponse = await client.GetAsync("?ParamB=ValueB&ParamA=ValueA"); + + await AssertResponseCachedAsync(initialResponse, subsequentResponse); + } + } + + [Fact] + public async void ServesFreshContent_IfVaryByParams_Mismatches() + { + var builder = CreateBuilderWithResponseCaching(async (context) => + { + var uniqueId = Guid.NewGuid().ToString(); + var headers = context.Response.GetTypedHeaders(); + headers.CacheControl = new CacheControlHeaderValue() + { + Public = true, + MaxAge = TimeSpan.FromSeconds(10) + }; + headers.Date = DateTimeOffset.UtcNow; + headers.Headers["X-Value"] = uniqueId; + context.GetResponseCachingFeature().VaryByParams = "param"; + await context.Response.WriteAsync(uniqueId); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync("?param=value"); + var subsequentResponse = await client.GetAsync("?param=value2"); + + await AssertResponseNotCachedAsync(initialResponse, subsequentResponse); + } + } + + [Fact] + public async void ServesFreshContent_IfVaryByParamsExplicit_Mismatch_ParamValueCaseSensitive() + { + var builder = CreateBuilderWithResponseCaching(async (context) => + { + var uniqueId = Guid.NewGuid().ToString(); + var headers = context.Response.GetTypedHeaders(); + headers.CacheControl = new CacheControlHeaderValue() + { + Public = true, + MaxAge = TimeSpan.FromSeconds(10) + }; + headers.Date = DateTimeOffset.UtcNow; + headers.Headers["X-Value"] = uniqueId; + context.GetResponseCachingFeature().VaryByParams = new[] { "ParamA", "ParamB" }; + await context.Response.WriteAsync(uniqueId); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync("?parama=valuea¶mb=valueb"); + var subsequentResponse = await client.GetAsync("?parama=ValueA¶mb=ValueB"); + + await AssertResponseNotCachedAsync(initialResponse, subsequentResponse); + } + } + + [Fact] + public async void ServesFreshContent_IfVaryByParamsStar_Mismatch_ParamValueCaseSensitive() + { + var builder = CreateBuilderWithResponseCaching(async (context) => + { + var uniqueId = Guid.NewGuid().ToString(); + var headers = context.Response.GetTypedHeaders(); + headers.CacheControl = new CacheControlHeaderValue() + { + Public = true, + MaxAge = TimeSpan.FromSeconds(10) + }; + headers.Date = DateTimeOffset.UtcNow; + headers.Headers["X-Value"] = uniqueId; + context.GetResponseCachingFeature().VaryByParams = new[] { "*" }; + await context.Response.WriteAsync(uniqueId); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync("?parama=valuea¶mb=valueb"); + var subsequentResponse = await client.GetAsync("?parama=ValueA¶mb=ValueB"); + + await AssertResponseNotCachedAsync(initialResponse, subsequentResponse); + } + } + + [Fact] + public async void ServesFreshContent_IfRequestRequirements_NotMet() { var builder = CreateBuilderWithResponseCaching(async (context) => { @@ -147,50 +400,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests }; var subsequentResponse = await client.GetAsync(""); - initialResponse.EnsureSuccessStatusCode(); - subsequentResponse.EnsureSuccessStatusCode(); - - Assert.False(subsequentResponse.Headers.Contains(HeaderNames.Age)); - Assert.NotEqual(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync()); + await AssertResponseNotCachedAsync(initialResponse, subsequentResponse); } } [Fact] - public async void ServesFreshContentIfVaryByMismatches() - { - var builder = CreateBuilderWithResponseCaching(async (context) => - { - var uniqueId = Guid.NewGuid().ToString(); - var headers = context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }; - headers.Date = DateTimeOffset.UtcNow; - headers.Headers["X-Value"] = uniqueId; - context.Response.Headers[HeaderNames.Vary] = HeaderNames.From; - await context.Response.WriteAsync(uniqueId); - }); - - using (var server = new TestServer(builder)) - { - var client = server.CreateClient(); - client.DefaultRequestHeaders.From = "user@example.com"; - var initialResponse = await client.GetAsync(""); - client.DefaultRequestHeaders.From = "user2@example.com"; - var subsequentResponse = await client.GetAsync(""); - - initialResponse.EnsureSuccessStatusCode(); - subsequentResponse.EnsureSuccessStatusCode(); - - Assert.False(subsequentResponse.Headers.Contains(HeaderNames.Age)); - Assert.NotEqual(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync()); - } - } - - [Fact] - public async void Serves504IfOnlyIfCachedHeaderIsSpecified() + public async void Serves504_IfOnlyIfCachedHeader_IsSpecified() { var builder = CreateBuilderWithResponseCaching(async (context) => { @@ -222,7 +437,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public async void ServesCachedContentWithoutSetCookie() + public async void ServesCachedContent_WithoutSetCookie() { var builder = CreateBuilderWithResponseCaching(async (context) => { @@ -263,7 +478,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public async void ServesCachedContentIfIHttpSendFileFeatureNotUsed() + public async void ServesCachedContent_IfIHttpSendFileFeature_NotUsed() { var builder = CreateBuilderWithResponseCaching( app => @@ -294,20 +509,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var initialResponse = await client.GetAsync(""); var subsequentResponse = await client.GetAsync(""); - initialResponse.EnsureSuccessStatusCode(); - subsequentResponse.EnsureSuccessStatusCode(); - - foreach (var header in initialResponse.Headers) - { - Assert.Equal(initialResponse.Headers.GetValues(header.Key), subsequentResponse.Headers.GetValues(header.Key)); - } - Assert.True(subsequentResponse.Headers.Contains(HeaderNames.Age)); - Assert.Equal(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync()); + await AssertResponseCachedAsync(initialResponse, subsequentResponse); } } [Fact] - public async void ServesFreshContentIfIHttpSendFileFeatureUsed() + public async void ServesFreshContent_IfIHttpSendFileFeature_Used() { var builder = CreateBuilderWithResponseCaching( app => @@ -339,16 +546,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var initialResponse = await client.GetAsync(""); var subsequentResponse = await client.GetAsync(""); - initialResponse.EnsureSuccessStatusCode(); - subsequentResponse.EnsureSuccessStatusCode(); - - Assert.False(subsequentResponse.Headers.Contains(HeaderNames.Age)); - Assert.NotEqual(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync()); + await AssertResponseNotCachedAsync(initialResponse, subsequentResponse); } } [Fact] - public async void ServesCachedContentIfSubsequentRequestContainsNoStore() + public async void ServesCachedContent_IfSubsequentRequest_ContainsNoStore() { var builder = CreateBuilderWithResponseCaching( async (context) => @@ -375,20 +578,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests }; var subsequentResponse = await client.GetAsync(""); - initialResponse.EnsureSuccessStatusCode(); - subsequentResponse.EnsureSuccessStatusCode(); - - foreach (var header in initialResponse.Headers) - { - Assert.Equal(initialResponse.Headers.GetValues(header.Key), subsequentResponse.Headers.GetValues(header.Key)); - } - Assert.True(subsequentResponse.Headers.Contains(HeaderNames.Age)); - Assert.Equal(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync()); + await AssertResponseCachedAsync(initialResponse, subsequentResponse); } } [Fact] - public async void ServesFreshContentIfInitialRequestContainsNoStore() + public async void ServesFreshContent_IfInitialRequestContains_NoStore() { var builder = CreateBuilderWithResponseCaching( async (context) => @@ -415,14 +610,32 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var initialResponse = await client.GetAsync(""); var subsequentResponse = await client.GetAsync(""); - initialResponse.EnsureSuccessStatusCode(); - subsequentResponse.EnsureSuccessStatusCode(); - - Assert.False(subsequentResponse.Headers.Contains(HeaderNames.Age)); - Assert.NotEqual(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync()); + await AssertResponseNotCachedAsync(initialResponse, subsequentResponse); } } + private static async Task AssertResponseCachedAsync(HttpResponseMessage initialResponse, HttpResponseMessage subsequentResponse) + { + initialResponse.EnsureSuccessStatusCode(); + subsequentResponse.EnsureSuccessStatusCode(); + + foreach (var header in initialResponse.Headers) + { + Assert.Equal(initialResponse.Headers.GetValues(header.Key), subsequentResponse.Headers.GetValues(header.Key)); + } + Assert.True(subsequentResponse.Headers.Contains(HeaderNames.Age)); + Assert.Equal(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync()); + } + + private static async Task AssertResponseNotCachedAsync(HttpResponseMessage initialResponse, HttpResponseMessage subsequentResponse) + { + initialResponse.EnsureSuccessStatusCode(); + subsequentResponse.EnsureSuccessStatusCode(); + + Assert.False(subsequentResponse.Headers.Contains(HeaderNames.Age)); + Assert.NotEqual(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync()); + } + private static IWebHostBuilder CreateBuilderWithResponseCaching(RequestDelegate requestDelegate) => CreateBuilderWithResponseCaching(app => { }, requestDelegate); From a5e92159826b3f68f8230ff50389275e12ba6ed7 Mon Sep 17 00:00:00 2001 From: John Luo Date: Tue, 30 Aug 2016 12:29:27 -0700 Subject: [PATCH 017/188] Update calls for string comparison --- .../Internal/DefaultResponseCacheEntrySerializer.cs | 12 ++++++------ .../ResponseCachingContext.cs | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/DefaultResponseCacheEntrySerializer.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/DefaultResponseCacheEntrySerializer.cs index 23b06f26a7..e0079b1a2c 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/DefaultResponseCacheEntrySerializer.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/DefaultResponseCacheEntrySerializer.cs @@ -37,7 +37,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal // Serialization Format // Format version (int) - // Type (string) + // Type (char: 'R' for CachedResponse, 'V' for CachedVaryBy) // Type-dependent data (see CachedResponse and CachedVaryBy) public static object Read(BinaryReader reader) { @@ -51,14 +51,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal return null; } - var type = reader.ReadString(); + var type = reader.ReadChar(); - if (string.Equals(nameof(CachedResponse), type)) + if (type == 'R') { var cachedResponse = ReadCachedResponse(reader); return cachedResponse; } - else if (string.Equals(nameof(CachedVaryBy), type)) + else if (type == 'V') { var cachedResponse = ReadCachedVaryBy(reader); return cachedResponse; @@ -150,7 +150,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal // See serialization format above private static void WriteCachedResponse(BinaryWriter writer, CachedResponse entry) { - writer.Write(nameof(CachedResponse)); + writer.Write('R'); writer.Write(entry.Created.UtcTicks); writer.Write(entry.StatusCode); writer.Write(entry.Headers.Count); @@ -167,7 +167,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal // See serialization format above private static void WriteCachedVaryBy(BinaryWriter writer, CachedVaryBy entry) { - writer.Write(nameof(CachedVaryBy)); + writer.Write('V'); writer.Write(entry.Headers.Count); foreach (var header in entry.Headers) diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs index 1fcb159239..01ec34915d 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs @@ -192,7 +192,7 @@ namespace Microsoft.AspNetCore.ResponseCaching builder.Append(KeyDelimiter) .Append('Q'); - if (varyBy.Params.Count == 1 && string.Equals(varyBy.Params[0], "*")) + if (varyBy.Params.Count == 1 && string.Equals(varyBy.Params[0], "*", StringComparison.Ordinal)) { // Vary by all available query params foreach (var query in _httpContext.Request.Query.OrderBy(q => q.Key, StringComparer.OrdinalIgnoreCase)) From 52f219b16ebd167941ffc0b0bea77ca7e4241722 Mon Sep 17 00:00:00 2001 From: John Luo Date: Mon, 29 Aug 2016 12:55:38 -0700 Subject: [PATCH 018/188] Support conditional requests and send 304 when possible --- .../ResponseCachingContext.cs | 100 +++++++++---- .../ResponseCachingContextTests.cs | 135 ++++++++++++++++++ .../ResponseCachingTests.cs | 116 +++++++++++++++ 3 files changed, 320 insertions(+), 31 deletions(-) diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs index 01ec34915d..7caa7b285b 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs @@ -443,41 +443,50 @@ namespace Microsoft.AspNetCore.ResponseCaching if (EntryIsFresh(cachedResponseHeaders, age, verifyAgainstRequest: true)) { - var response = _httpContext.Response; - // Copy the cached status code and response headers - response.StatusCode = cachedResponse.StatusCode; - foreach (var header in cachedResponse.Headers) + // Check conditional request rules + if (ConditionalRequestSatisfied(cachedResponseHeaders)) { - response.Headers.Add(header); - } - - response.Headers[HeaderNames.Age] = age.TotalSeconds.ToString("F0", CultureInfo.InvariantCulture); - - if (_responseType == ResponseType.HeadersOnly) - { - responseServed = true; - } - else if (_responseType == ResponseType.FullReponse) - { - // Copy the cached response body - var body = cachedResponse.Body; - - // Add a content-length if required - if (response.ContentLength == null && string.IsNullOrEmpty(response.Headers[HeaderNames.TransferEncoding])) - { - response.ContentLength = body.Length; - } - - if (body.Length > 0) - { - await response.Body.WriteAsync(body, 0, body.Length); - } - + _httpContext.Response.StatusCode = StatusCodes.Status304NotModified; responseServed = true; } else { - throw new InvalidOperationException($"{nameof(_responseType)} not specified or is unrecognized."); + var response = _httpContext.Response; + // Copy the cached status code and response headers + response.StatusCode = cachedResponse.StatusCode; + foreach (var header in cachedResponse.Headers) + { + response.Headers.Add(header); + } + + response.Headers[HeaderNames.Age] = age.TotalSeconds.ToString("F0", CultureInfo.InvariantCulture); + + if (_responseType == ResponseType.HeadersOnly) + { + responseServed = true; + } + else if (_responseType == ResponseType.FullReponse) + { + // Copy the cached response body + var body = cachedResponse.Body; + + // Add a content-length if required + if (response.ContentLength == null && string.IsNullOrEmpty(response.Headers[HeaderNames.TransferEncoding])) + { + response.ContentLength = body.Length; + } + + if (body.Length > 0) + { + await response.Body.WriteAsync(body, 0, body.Length); + } + + responseServed = true; + } + else + { + throw new InvalidOperationException($"{nameof(_responseType)} not specified or is unrecognized."); + } } } else @@ -489,13 +498,42 @@ namespace Microsoft.AspNetCore.ResponseCaching if (!responseServed && RequestCacheControl.OnlyIfCached) { _httpContext.Response.StatusCode = StatusCodes.Status504GatewayTimeout; - responseServed = true; } return responseServed; } + internal bool ConditionalRequestSatisfied(ResponseHeaders cachedResponseHeaders) + { + var ifNoneMatchHeader = RequestHeaders.IfNoneMatch; + + if (ifNoneMatchHeader != null) + { + if (ifNoneMatchHeader.Count == 1 && ifNoneMatchHeader[0].Equals(EntityTagHeaderValue.Any)) + { + return true; + } + + if (cachedResponseHeaders.ETag != null) + { + foreach (var tag in ifNoneMatchHeader) + { + if (cachedResponseHeaders.ETag.Compare(tag, useStrongComparison: true)) + { + return true; + } + } + } + } + else if ((cachedResponseHeaders.LastModified ?? cachedResponseHeaders.Date) <= RequestHeaders.IfUnmodifiedSince) + { + return true; + } + + return false; + } + internal void FinalizeCachingHeaders() { if (CacheResponse) diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs index fa47d71309..54bd5a55ba 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs @@ -2,6 +2,7 @@ // 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.Text; using System.Threading; using System.Threading.Tasks; @@ -692,6 +693,140 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Assert.True(context.EntryIsFresh(httpContext.Response.GetTypedHeaders(), TimeSpan.FromSeconds(3), verifyAgainstRequest: false)); } + [Fact] + public void ConditionalRequestSatisfied_NotConditionalRequest_Fails() + { + var context = CreateTestContext(new DefaultHttpContext()); + var cachedHeaders = new ResponseHeaders(new HeaderDictionary()); + + Assert.False(context.ConditionalRequestSatisfied(cachedHeaders)); + } + + [Fact] + public void ConditionalRequestSatisfied_IfUnmodifiedSince_FallsbackToDateHeader() + { + var utcNow = DateTimeOffset.UtcNow; + var cachedHeaders = new ResponseHeaders(new HeaderDictionary()); + var httpContext = new DefaultHttpContext(); + var context = CreateTestContext(httpContext); + + httpContext.Request.GetTypedHeaders().IfUnmodifiedSince = utcNow; + + // Verify modifications in the past succeeds + cachedHeaders.Date = utcNow - TimeSpan.FromSeconds(10); + Assert.True(context.ConditionalRequestSatisfied(cachedHeaders)); + + // Verify modifications at present succeeds + cachedHeaders.Date = utcNow; + Assert.True(context.ConditionalRequestSatisfied(cachedHeaders)); + + // Verify modifications in the future fails + cachedHeaders.Date = utcNow + TimeSpan.FromSeconds(10); + Assert.False(context.ConditionalRequestSatisfied(cachedHeaders)); + } + + [Fact] + public void ConditionalRequestSatisfied_IfUnmodifiedSince_LastModifiedOverridesDateHeader() + { + var utcNow = DateTimeOffset.UtcNow; + var cachedHeaders = new ResponseHeaders(new HeaderDictionary()); + var httpContext = new DefaultHttpContext(); + var context = CreateTestContext(httpContext); + + httpContext.Request.GetTypedHeaders().IfUnmodifiedSince = utcNow; + + // Verify modifications in the past succeeds + cachedHeaders.Date = utcNow + TimeSpan.FromSeconds(10); + cachedHeaders.LastModified = utcNow - TimeSpan.FromSeconds(10); + Assert.True(context.ConditionalRequestSatisfied(cachedHeaders)); + + // Verify modifications at present + cachedHeaders.Date = utcNow + TimeSpan.FromSeconds(10); + cachedHeaders.LastModified = utcNow; + Assert.True(context.ConditionalRequestSatisfied(cachedHeaders)); + + // Verify modifications in the future fails + cachedHeaders.Date = utcNow - TimeSpan.FromSeconds(10); + cachedHeaders.LastModified = utcNow + TimeSpan.FromSeconds(10); + Assert.False(context.ConditionalRequestSatisfied(cachedHeaders)); + } + + [Fact] + public void ConditionalRequestSatisfied_IfNoneMatch_Overrides_IfUnmodifiedSince_ToPass() + { + var utcNow = DateTimeOffset.UtcNow; + var cachedHeaders = new ResponseHeaders(new HeaderDictionary()); + var httpContext = new DefaultHttpContext(); + var requestHeaders = httpContext.Request.GetTypedHeaders(); + var context = CreateTestContext(httpContext); + + // This would fail the IfUnmodifiedSince checks + requestHeaders.IfUnmodifiedSince = utcNow; + cachedHeaders.LastModified = utcNow + TimeSpan.FromSeconds(10); + + requestHeaders.IfNoneMatch = new List(new[] { EntityTagHeaderValue.Any }); + Assert.True(context.ConditionalRequestSatisfied(cachedHeaders)); + } + + [Fact] + public void ConditionalRequestSatisfied_IfNoneMatch_Overrides_IfUnmodifiedSince_ToFail() + { + var utcNow = DateTimeOffset.UtcNow; + var cachedHeaders = new ResponseHeaders(new HeaderDictionary()); + var httpContext = new DefaultHttpContext(); + var requestHeaders = httpContext.Request.GetTypedHeaders(); + var context = CreateTestContext(httpContext); + + // This would pass the IfUnmodifiedSince checks + requestHeaders.IfUnmodifiedSince = utcNow; + cachedHeaders.LastModified = utcNow - TimeSpan.FromSeconds(10); + + requestHeaders.IfNoneMatch = new List(new[] { new EntityTagHeaderValue("\"E1\"") }); + Assert.False(context.ConditionalRequestSatisfied(cachedHeaders)); + } + + [Fact] + public void ConditionalRequestSatisfied_IfNoneMatch_AnyWithoutETagInResponse_Passes() + { + var cachedHeaders = new ResponseHeaders(new HeaderDictionary()); + var httpContext = new DefaultHttpContext(); + var context = CreateTestContext(httpContext); + + httpContext.Request.GetTypedHeaders().IfNoneMatch = new List(new[] { new EntityTagHeaderValue("\"E1\"") }); + + Assert.False(context.ConditionalRequestSatisfied(cachedHeaders)); + } + + [Fact] + public void ConditionalRequestSatisfied_IfNoneMatch_ExplicitWithMatch_Passes() + { + var cachedHeaders = new ResponseHeaders(new HeaderDictionary()) + { + ETag = new EntityTagHeaderValue("\"E1\"") + }; + var httpContext = new DefaultHttpContext(); + var context = CreateTestContext(httpContext); + + httpContext.Request.GetTypedHeaders().IfNoneMatch = new List(new[] { new EntityTagHeaderValue("\"E1\"") }); + + Assert.True(context.ConditionalRequestSatisfied(cachedHeaders)); + } + + [Fact] + public void ConditionalRequestSatisfied_IfNoneMatch_ExplicitWithoutMatch_Fails() + { + var cachedHeaders = new ResponseHeaders(new HeaderDictionary()) + { + ETag = new EntityTagHeaderValue("\"E2\"") + }; + var httpContext = new DefaultHttpContext(); + var context = CreateTestContext(httpContext); + + httpContext.Request.GetTypedHeaders().IfNoneMatch = new List(new[] { new EntityTagHeaderValue("\"E1\"") }); + + Assert.False(context.ConditionalRequestSatisfied(cachedHeaders)); + } + private static ResponseCachingContext CreateTestContext(HttpContext httpContext) { return CreateTestContext( diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs index 2648025acd..400e5af249 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs @@ -636,6 +636,122 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Assert.NotEqual(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync()); } + [Fact] + public async void Serves304_IfIfModifiedSince_Satisfied() + { + var builder = CreateBuilderWithResponseCaching(async (context) => + { + var uniqueId = Guid.NewGuid().ToString(); + var headers = context.Response.GetTypedHeaders(); + headers.CacheControl = new CacheControlHeaderValue() + { + Public = true, + MaxAge = TimeSpan.FromSeconds(10) + }; + headers.Date = DateTimeOffset.UtcNow; + headers.Headers["X-Value"] = uniqueId; + await context.Response.WriteAsync(uniqueId); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.IfUnmodifiedSince = DateTimeOffset.MaxValue; + var subsequentResponse = await client.GetAsync(""); + + initialResponse.EnsureSuccessStatusCode(); + Assert.Equal(System.Net.HttpStatusCode.NotModified, subsequentResponse.StatusCode); + } + } + + [Fact] + public async void ServesCachedContent_IfIfModifiedSince_NotSatisfied() + { + var builder = CreateBuilderWithResponseCaching(async (context) => + { + var uniqueId = Guid.NewGuid().ToString(); + var headers = context.Response.GetTypedHeaders(); + headers.CacheControl = new CacheControlHeaderValue() + { + Public = true, + MaxAge = TimeSpan.FromSeconds(10) + }; + headers.Date = DateTimeOffset.UtcNow; + headers.Headers["X-Value"] = uniqueId; + await context.Response.WriteAsync(uniqueId); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue; + var subsequentResponse = await client.GetAsync(""); + + await AssertResponseCachedAsync(initialResponse, subsequentResponse); + } + } + + [Fact] + public async void Serves304_IfIfNoneMatch_Satisfied() + { + var builder = CreateBuilderWithResponseCaching(async (context) => + { + var uniqueId = Guid.NewGuid().ToString(); + var headers = context.Response.GetTypedHeaders(); + headers.CacheControl = new CacheControlHeaderValue() + { + Public = true, + MaxAge = TimeSpan.FromSeconds(10) + }; + headers.Date = DateTimeOffset.UtcNow; + headers.Headers["X-Value"] = uniqueId; + headers.ETag = new EntityTagHeaderValue("\"E1\""); + await context.Response.WriteAsync(uniqueId); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.IfNoneMatch.Add(new System.Net.Http.Headers.EntityTagHeaderValue("\"E1\"")); + var subsequentResponse = await client.GetAsync(""); + + initialResponse.EnsureSuccessStatusCode(); + Assert.Equal(System.Net.HttpStatusCode.NotModified, subsequentResponse.StatusCode); + } + } + + [Fact] + public async void ServesCachedContent_IfIfNoneMatch_NotSatisfied() + { + var builder = CreateBuilderWithResponseCaching(async (context) => + { + var uniqueId = Guid.NewGuid().ToString(); + var headers = context.Response.GetTypedHeaders(); + headers.CacheControl = new CacheControlHeaderValue() + { + Public = true, + MaxAge = TimeSpan.FromSeconds(10) + }; + headers.Date = DateTimeOffset.UtcNow; + headers.Headers["X-Value"] = uniqueId; + headers.ETag = new EntityTagHeaderValue("\"E1\""); + await context.Response.WriteAsync(uniqueId); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.IfNoneMatch.Add(new System.Net.Http.Headers.EntityTagHeaderValue("\"E2\"")); + var subsequentResponse = await client.GetAsync(""); + + await AssertResponseCachedAsync(initialResponse, subsequentResponse); + } + } + private static IWebHostBuilder CreateBuilderWithResponseCaching(RequestDelegate requestDelegate) => CreateBuilderWithResponseCaching(app => { }, requestDelegate); From 3b0f01a8eccc394110fd5324ef74a813f5d9b6d9 Mon Sep 17 00:00:00 2001 From: John Luo Date: Wed, 31 Aug 2016 17:52:05 -0700 Subject: [PATCH 019/188] Customize key prefix instead of suffix --- ...cs => IResponseCachingCacheKeyModifier.cs} | 8 ++--- ...fixProvider.cs => NoopCacheKeyModifier.cs} | 4 +-- .../ResponseCachingContext.cs | 30 ++++++++----------- .../ResponseCachingMiddleware.cs | 12 ++++---- ...ponseCachingServiceCollectionExtensions.cs | 2 +- .../ResponseCachingContextTests.cs | 22 +++++++------- 6 files changed, 37 insertions(+), 41 deletions(-) rename src/Microsoft.AspNetCore.ResponseCaching/Interfaces/{IResponseCachingCacheKeySuffixProvider.cs => IResponseCachingCacheKeyModifier.cs} (57%) rename src/Microsoft.AspNetCore.ResponseCaching/Internal/{NoopCacheKeySuffixProvider.cs => NoopCacheKeyModifier.cs} (62%) diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCachingCacheKeySuffixProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCachingCacheKeyModifier.cs similarity index 57% rename from src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCachingCacheKeySuffixProvider.cs rename to src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCachingCacheKeyModifier.cs index 7514fa4254..d78c27cd90 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCachingCacheKeySuffixProvider.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCachingCacheKeyModifier.cs @@ -5,13 +5,13 @@ using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.ResponseCaching { - public interface IResponseCachingCacheKeySuffixProvider + public interface IResponseCachingCacheKeyModifier { /// - /// Create a key segment that is appended to the default cache key. + /// Create a key segment that is prepended to the default cache key. /// /// The . - /// The key segment that will be appended to the default cache key. - string CreateCustomKeySuffix(HttpContext httpContext); + /// The key segment that will be prepended to the default cache key. + string CreatKeyPrefix(HttpContext httpContext); } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/NoopCacheKeySuffixProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/NoopCacheKeyModifier.cs similarity index 62% rename from src/Microsoft.AspNetCore.ResponseCaching/Internal/NoopCacheKeySuffixProvider.cs rename to src/Microsoft.AspNetCore.ResponseCaching/Internal/NoopCacheKeyModifier.cs index 43daeb5a5d..a32332c366 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/NoopCacheKeySuffixProvider.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/NoopCacheKeyModifier.cs @@ -5,8 +5,8 @@ using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.ResponseCaching.Internal { - internal class NoopCacheKeySuffixProvider : IResponseCachingCacheKeySuffixProvider + internal class NoopCacheKeyModifier : IResponseCachingCacheKeyModifier { - public string CreateCustomKeySuffix(HttpContext httpContext) => null; + public string CreatKeyPrefix(HttpContext httpContext) => null; } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs index 7caa7b285b..17ebe49caf 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs @@ -28,7 +28,7 @@ namespace Microsoft.AspNetCore.ResponseCaching private readonly ISystemClock _clock; private readonly ObjectPool _builderPool; private readonly IResponseCachingCacheabilityValidator _cacheabilityValidator; - private readonly IResponseCachingCacheKeySuffixProvider _cacheKeySuffixProvider; + private readonly IResponseCachingCacheKeyModifier _cacheKeyModifier; private string _cacheKey; private ResponseType? _responseType; @@ -46,8 +46,8 @@ namespace Microsoft.AspNetCore.ResponseCaching IResponseCache cache, ObjectPool builderPool, IResponseCachingCacheabilityValidator cacheabilityValidator, - IResponseCachingCacheKeySuffixProvider cacheKeySuffixProvider) - : this(httpContext, cache, new SystemClock(), builderPool, cacheabilityValidator, cacheKeySuffixProvider) + IResponseCachingCacheKeyModifier cacheKeyModifier) + : this(httpContext, cache, new SystemClock(), builderPool, cacheabilityValidator, cacheKeyModifier) { } @@ -58,14 +58,14 @@ namespace Microsoft.AspNetCore.ResponseCaching ISystemClock clock, ObjectPool builderPool, IResponseCachingCacheabilityValidator cacheabilityValidator, - IResponseCachingCacheKeySuffixProvider cacheKeySuffixProvider) + IResponseCachingCacheKeyModifier cacheKeyModifier) { _httpContext = httpContext; _cache = cache; _clock = clock; _builderPool = builderPool; _cacheabilityValidator = cacheabilityValidator; - _cacheKeySuffixProvider = cacheKeySuffixProvider; + _cacheKeyModifier = cacheKeyModifier; } internal bool CacheResponse @@ -153,6 +153,14 @@ namespace Microsoft.AspNetCore.ResponseCaching try { + // Prepend custom cache key prefix + var customKeyPrefix = _cacheKeyModifier.CreatKeyPrefix(_httpContext); + if (!string.IsNullOrEmpty(customKeyPrefix)) + { + builder.Append(customKeyPrefix) + .Append(KeyDelimiter); + } + // Default key builder .Append(request.Method.ToUpperInvariant()) @@ -225,18 +233,6 @@ namespace Microsoft.AspNetCore.ResponseCaching } } - // Append custom cache key segment - var customKey = _cacheKeySuffixProvider.CreateCustomKeySuffix(_httpContext); - if (!string.IsNullOrEmpty(customKey)) - { - // Append a group separator for the custom segment of the cache key - builder.Append(KeyDelimiter) - .Append('C'); - - builder.Append(KeyDelimiter) - .Append(customKey); - } - return builder.ToString(); } finally diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs index 9c6a922eb8..5fe8718134 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs @@ -22,14 +22,14 @@ namespace Microsoft.AspNetCore.ResponseCaching private readonly IResponseCache _cache; private readonly ObjectPool _builderPool; private readonly IResponseCachingCacheabilityValidator _cacheabilityValidator; - private readonly IResponseCachingCacheKeySuffixProvider _cacheKeySuffixProvider; + private readonly IResponseCachingCacheKeyModifier _cacheKeyModifier; public ResponseCachingMiddleware( RequestDelegate next, IResponseCache cache, ObjectPoolProvider poolProvider, IResponseCachingCacheabilityValidator cacheabilityValidator, - IResponseCachingCacheKeySuffixProvider cacheKeySuffixProvider) + IResponseCachingCacheKeyModifier cacheKeyModifier) { if (next == null) { @@ -47,16 +47,16 @@ namespace Microsoft.AspNetCore.ResponseCaching { throw new ArgumentNullException(nameof(cacheabilityValidator)); } - if (cacheKeySuffixProvider == null) + if (cacheKeyModifier == null) { - throw new ArgumentNullException(nameof(cacheKeySuffixProvider)); + throw new ArgumentNullException(nameof(cacheKeyModifier)); } _next = next; _cache = cache; _builderPool = poolProvider.CreateStringBuilderPool(); _cacheabilityValidator = cacheabilityValidator; - _cacheKeySuffixProvider = cacheKeySuffixProvider; + _cacheKeyModifier = cacheKeyModifier; } public async Task Invoke(HttpContext context) @@ -66,7 +66,7 @@ namespace Microsoft.AspNetCore.ResponseCaching _cache, _builderPool, _cacheabilityValidator, - _cacheKeySuffixProvider); + _cacheKeyModifier); // Should we attempt any caching logic? if (cachingContext.RequestIsCacheable()) diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingServiceCollectionExtensions.cs index b6e18f7874..c2ad4a050d 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingServiceCollectionExtensions.cs @@ -40,7 +40,7 @@ namespace Microsoft.Extensions.DependencyInjection private static IServiceCollection AddResponseCachingServices(this IServiceCollection services) { - services.TryAdd(ServiceDescriptor.Singleton()); + services.TryAdd(ServiceDescriptor.Singleton()); services.TryAdd(ServiceDescriptor.Singleton()); return services; diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs index 54bd5a55ba..e66a33af06 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs @@ -258,22 +258,22 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests })); } - private class CustomizeKeySuffixProvider : IResponseCachingCacheKeySuffixProvider + private class KeyModifier : IResponseCachingCacheKeyModifier { - public string CreateCustomKeySuffix(HttpContext httpContext) => "CustomizedKey"; + public string CreatKeyPrefix(HttpContext httpContext) => "CustomizedKeyPrefix"; } [Fact] - public void CreateCacheKey_OptionalCacheKey_AppendedToDefaultKey() + public void CreateCacheKey_CacheKeyModifier_AddsPrefix() { var httpContext = new DefaultHttpContext(); httpContext.Request.Method = "GET"; httpContext.Request.Path = "/"; httpContext.Request.Headers["HeaderA"] = "ValueA"; httpContext.Request.Headers["HeaderB"] = "ValueB"; - var responseCachingContext = CreateTestContext(httpContext, new CustomizeKeySuffixProvider()); + var responseCachingContext = CreateTestContext(httpContext, new KeyModifier()); - Assert.Equal($"GET{KeyDelimiter}/{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=null{KeyDelimiter}C{KeyDelimiter}CustomizedKey", responseCachingContext.CreateCacheKey(new CachedVaryBy() + Assert.Equal($"CustomizedKeyPrefix{KeyDelimiter}GET{KeyDelimiter}/{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=null", responseCachingContext.CreateCacheKey(new CachedVaryBy() { Headers = new string[] { "HeaderA", "HeaderC" } })); @@ -831,15 +831,15 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { return CreateTestContext( httpContext, - new NoopCacheKeySuffixProvider(), + new NoopCacheKeyModifier(), new NoopCacheabilityValidator()); } - private static ResponseCachingContext CreateTestContext(HttpContext httpContext, IResponseCachingCacheKeySuffixProvider cacheKeySuffixProvider) + private static ResponseCachingContext CreateTestContext(HttpContext httpContext, IResponseCachingCacheKeyModifier cacheKeyModifier) { return CreateTestContext( httpContext, - cacheKeySuffixProvider, + cacheKeyModifier, new NoopCacheabilityValidator()); } @@ -847,13 +847,13 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { return CreateTestContext( httpContext, - new NoopCacheKeySuffixProvider(), + new NoopCacheKeyModifier(), cacheabilityValidator); } private static ResponseCachingContext CreateTestContext( HttpContext httpContext, - IResponseCachingCacheKeySuffixProvider cacheKeySuffixProvider, + IResponseCachingCacheKeyModifier cacheKeyModifier, IResponseCachingCacheabilityValidator cacheabilityValidator) { return new ResponseCachingContext( @@ -862,7 +862,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests new SystemClock(), new DefaultObjectPool(new StringBuilderPooledObjectPolicy()), cacheabilityValidator, - cacheKeySuffixProvider); + cacheKeyModifier); } private class TestResponseCache : IResponseCache From d72ef128dd5b066428aa5fa6e4603961d50560bc Mon Sep 17 00:00:00 2001 From: John Luo Date: Wed, 31 Aug 2016 17:33:01 -0700 Subject: [PATCH 020/188] Adding options to specify maximum response body size --- .../Internal/ResponseCacheStream.cs | 33 +- .../ResponseCachingContext.cs | 36 +- .../ResponseCachingExtensions.cs | 15 + .../ResponseCachingMiddleware.cs | 11 +- .../ResponseCachingOptions.cs | 22 + .../ResponseCachingContextTests.cs | 3 +- .../ResponseCachingTests.cs | 516 ++++++------------ 7 files changed, 255 insertions(+), 381 deletions(-) create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingOptions.cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCacheStream.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCacheStream.cs index bf9d90ee21..b8921b85ba 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCacheStream.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCacheStream.cs @@ -11,10 +11,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal internal class ResponseCacheStream : Stream { private readonly Stream _innerStream; + private readonly long _maxBufferSize; - public ResponseCacheStream(Stream innerStream) + public ResponseCacheStream(Stream innerStream, long maxBufferSize) { _innerStream = innerStream; + _maxBufferSize = maxBufferSize; } public MemoryStream BufferedStream { get; } = new MemoryStream(); @@ -38,6 +40,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal public void DisableBuffering() { BufferingEnabled = false; + BufferedStream.SetLength(0); + BufferedStream.Capacity = 0; BufferedStream.Dispose(); } @@ -77,7 +81,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal if (BufferingEnabled) { - BufferedStream.Write(buffer, offset, count); + if (BufferedStream.Length + count > _maxBufferSize) + { + DisableBuffering(); + } + else + { + BufferedStream.Write(buffer, offset, count); + } } } @@ -95,7 +106,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal if (BufferingEnabled) { - await BufferedStream.WriteAsync(buffer, offset, count, cancellationToken); + if (BufferedStream.Length + count > _maxBufferSize) + { + DisableBuffering(); + } + else + { + await BufferedStream.WriteAsync(buffer, offset, count, cancellationToken); + } } } @@ -113,7 +131,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal if (BufferingEnabled) { - BufferedStream.WriteByte(value); + if (BufferedStream.Length + 1 > _maxBufferSize) + { + DisableBuffering(); + } + else + { + BufferedStream.WriteByte(value); + } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs index 17ebe49caf..2733172960 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs @@ -7,6 +7,7 @@ using System.Globalization; using System.Linq; using System.Text; using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Headers; @@ -25,7 +26,7 @@ namespace Microsoft.AspNetCore.ResponseCaching private readonly HttpContext _httpContext; private readonly IResponseCache _cache; - private readonly ISystemClock _clock; + private readonly ResponseCachingOptions _options; private readonly ObjectPool _builderPool; private readonly IResponseCachingCacheabilityValidator _cacheabilityValidator; private readonly IResponseCachingCacheKeyModifier _cacheKeyModifier; @@ -40,29 +41,19 @@ namespace Microsoft.AspNetCore.ResponseCaching private CachedResponse _cachedResponse; private TimeSpan _cachedResponseValidFor; internal DateTimeOffset _responseTime; - - internal ResponseCachingContext( - HttpContext httpContext, - IResponseCache cache, - ObjectPool builderPool, - IResponseCachingCacheabilityValidator cacheabilityValidator, - IResponseCachingCacheKeyModifier cacheKeyModifier) - : this(httpContext, cache, new SystemClock(), builderPool, cacheabilityValidator, cacheKeyModifier) - { - } // Internal for testing internal ResponseCachingContext( HttpContext httpContext, IResponseCache cache, - ISystemClock clock, + ResponseCachingOptions options, ObjectPool builderPool, IResponseCachingCacheabilityValidator cacheabilityValidator, IResponseCachingCacheKeyModifier cacheKeyModifier) { _httpContext = httpContext; _cache = cache; - _clock = clock; + _options = options; _builderPool = builderPool; _cacheabilityValidator = cacheabilityValidator; _cacheKeyModifier = cacheKeyModifier; @@ -74,10 +65,7 @@ namespace Microsoft.AspNetCore.ResponseCaching { if (_cacheResponse == null) { - // TODO: apparent age vs corrected age value - var responseAge = _responseTime - ResponseHeaders.Date ?? TimeSpan.Zero; - - _cacheResponse = ResponseIsCacheable() && EntryIsFresh(ResponseHeaders, responseAge, verifyAgainstRequest: false); + _cacheResponse = ResponseIsCacheable(); } return _cacheResponse.Value; } @@ -363,6 +351,14 @@ namespace Microsoft.AspNetCore.ResponseCaching return false; } + // Check response freshness + // TODO: apparent age vs corrected age value + var responseAge = _responseTime - ResponseHeaders.Date ?? TimeSpan.Zero; + if (!EntryIsFresh(ResponseHeaders, responseAge, verifyAgainstRequest: false)) + { + return false; + } + return true; } @@ -433,7 +429,7 @@ namespace Microsoft.AspNetCore.ResponseCaching var cachedResponse = cacheEntry as CachedResponse; var cachedResponseHeaders = new ResponseHeaders(cachedResponse.Headers); - _responseTime = _clock.UtcNow; + _responseTime = _options.SystemClock.UtcNow; var age = _responseTime - cachedResponse.Created; age = age > TimeSpan.Zero ? age : TimeSpan.Zero; @@ -607,7 +603,7 @@ namespace Microsoft.AspNetCore.ResponseCaching if (!ResponseStarted) { ResponseStarted = true; - _responseTime = _clock.UtcNow; + _responseTime = _options.SystemClock.UtcNow; FinalizeCachingHeaders(); } @@ -619,7 +615,7 @@ namespace Microsoft.AspNetCore.ResponseCaching // Shim response stream OriginalResponseStream = _httpContext.Response.Body; - ResponseCacheStream = new ResponseCacheStream(OriginalResponseStream); + ResponseCacheStream = new ResponseCacheStream(OriginalResponseStream, _options.MaximumCachedBodySize); _httpContext.Response.Body = ResponseCacheStream; // Shim IHttpSendFileFeature diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingExtensions.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingExtensions.cs index 45d905cea6..037863e4cf 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingExtensions.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingExtensions.cs @@ -3,6 +3,7 @@ using System; using Microsoft.AspNetCore.ResponseCaching; +using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Builder { @@ -17,5 +18,19 @@ namespace Microsoft.AspNetCore.Builder return app.UseMiddleware(); } + + public static IApplicationBuilder UseResponseCaching(this IApplicationBuilder app, ResponseCachingOptions options) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + return app.UseMiddleware(Options.Create(options)); + } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs index 5fe8718134..96c1a713a3 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs @@ -4,6 +4,7 @@ using System; using System.Text; using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.Options; @@ -20,13 +21,15 @@ namespace Microsoft.AspNetCore.ResponseCaching private readonly RequestDelegate _next; private readonly IResponseCache _cache; + private readonly ResponseCachingOptions _options; private readonly ObjectPool _builderPool; private readonly IResponseCachingCacheabilityValidator _cacheabilityValidator; private readonly IResponseCachingCacheKeyModifier _cacheKeyModifier; public ResponseCachingMiddleware( - RequestDelegate next, + RequestDelegate next, IResponseCache cache, + IOptions options, ObjectPoolProvider poolProvider, IResponseCachingCacheabilityValidator cacheabilityValidator, IResponseCachingCacheKeyModifier cacheKeyModifier) @@ -39,6 +42,10 @@ namespace Microsoft.AspNetCore.ResponseCaching { throw new ArgumentNullException(nameof(cache)); } + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } if (poolProvider == null) { throw new ArgumentNullException(nameof(poolProvider)); @@ -54,6 +61,7 @@ namespace Microsoft.AspNetCore.ResponseCaching _next = next; _cache = cache; + _options = options.Value; _builderPool = poolProvider.CreateStringBuilderPool(); _cacheabilityValidator = cacheabilityValidator; _cacheKeyModifier = cacheKeyModifier; @@ -64,6 +72,7 @@ namespace Microsoft.AspNetCore.ResponseCaching var cachingContext = new ResponseCachingContext( context, _cache, + _options, _builderPool, _cacheabilityValidator, _cacheKeyModifier); diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingOptions.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingOptions.cs new file mode 100644 index 0000000000..753b9b6b72 --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingOptions.cs @@ -0,0 +1,22 @@ +// 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.ComponentModel; +using Microsoft.AspNetCore.ResponseCaching.Internal; + +namespace Microsoft.AspNetCore.Builder +{ + public class ResponseCachingOptions + { + /// + /// The largest cacheable size for the response body in bytes. + /// + public long MaximumCachedBodySize { get; set; } = 1024 * 1024; + + /// + /// For testing purposes only. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + internal ISystemClock SystemClock { get; set; } = new SystemClock(); + } +} diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs index e66a33af06..2662c56b78 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Text; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Headers; @@ -859,7 +860,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests return new ResponseCachingContext( httpContext, new TestResponseCache(), - new SystemClock(), + new ResponseCachingOptions(), new DefaultObjectPool(new StringBuilderPooledObjectPolicy()), cacheabilityValidator, cacheKeyModifier); diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs index 400e5af249..783dfd8f86 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs @@ -21,19 +21,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfAvailable() { - var builder = CreateBuilderWithResponseCaching(async (context) => - { - var uniqueId = Guid.NewGuid().ToString(); - var headers = context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }; - headers.Date = DateTimeOffset.UtcNow; - headers.Headers["X-Value"] = uniqueId; - await context.Response.WriteAsync(uniqueId); - }); + var builder = CreateBuilderWithResponseCaching(); using (var server = new TestServer(builder)) { @@ -48,19 +36,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfNotAvailable() { - var builder = CreateBuilderWithResponseCaching(async (context) => - { - var uniqueId = Guid.NewGuid().ToString(); - var headers = context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }; - headers.Date = DateTimeOffset.UtcNow; - headers.Headers["X-Value"] = uniqueId; - await context.Response.WriteAsync(uniqueId); - }); + var builder = CreateBuilderWithResponseCaching(); using (var server = new TestServer(builder)) { @@ -77,17 +53,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var builder = CreateBuilderWithResponseCaching(async (context) => { - var uniqueId = Guid.NewGuid().ToString(); - var headers = context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }; - headers.Date = DateTimeOffset.UtcNow; - headers.Headers["X-Value"] = uniqueId; context.Response.Headers[HeaderNames.Vary] = HeaderNames.From; - await context.Response.WriteAsync(uniqueId); + await DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -106,17 +73,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var builder = CreateBuilderWithResponseCaching(async (context) => { - var uniqueId = Guid.NewGuid().ToString(); - var headers = context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }; - headers.Date = DateTimeOffset.UtcNow; - headers.Headers["X-Value"] = uniqueId; context.Response.Headers[HeaderNames.Vary] = HeaderNames.From; - await context.Response.WriteAsync(uniqueId); + await DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -136,17 +94,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var builder = CreateBuilderWithResponseCaching(async (context) => { - var uniqueId = Guid.NewGuid().ToString(); - var headers = context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }; - headers.Date = DateTimeOffset.UtcNow; - headers.Headers["X-Value"] = uniqueId; context.GetResponseCachingFeature().VaryByParams = "param"; - await context.Response.WriteAsync(uniqueId); + await DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -162,28 +111,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfVaryByParamsExplicit_Matches_ParamNameCaseInsensitive() { - var builder = CreateBuilderWithResponseCaching( - app => + var builder = CreateBuilderWithResponseCaching(async (context) => { - app.Use(async (context, next) => - { - context.Features.Set(new DummySendFileFeature()); - await next.Invoke(); - }); - }, - async (context) => - { - var uniqueId = Guid.NewGuid().ToString(); - var headers = context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }; - headers.Date = DateTimeOffset.UtcNow; - headers.Headers["X-Value"] = uniqueId; context.GetResponseCachingFeature().VaryByParams = new[] { "ParamA", "paramb" }; - await context.Response.WriteAsync(uniqueId); + await DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -199,28 +130,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfVaryByParamsStar_Matches_ParamNameCaseInsensitive() { - var builder = CreateBuilderWithResponseCaching( - app => + var builder = CreateBuilderWithResponseCaching(async (context) => { - app.Use(async (context, next) => - { - context.Features.Set(new DummySendFileFeature()); - await next.Invoke(); - }); - }, - async (context) => - { - var uniqueId = Guid.NewGuid().ToString(); - var headers = context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }; - headers.Date = DateTimeOffset.UtcNow; - headers.Headers["X-Value"] = uniqueId; context.GetResponseCachingFeature().VaryByParams = new[] { "*" }; - await context.Response.WriteAsync(uniqueId); + await DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -238,17 +151,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var builder = CreateBuilderWithResponseCaching(async (context) => { - var uniqueId = Guid.NewGuid().ToString(); - var headers = context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }; - headers.Date = DateTimeOffset.UtcNow; - headers.Headers["X-Value"] = uniqueId; context.GetResponseCachingFeature().VaryByParams = new[] { "ParamB", "ParamA" }; - await context.Response.WriteAsync(uniqueId); + await DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -266,17 +170,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var builder = CreateBuilderWithResponseCaching(async (context) => { - var uniqueId = Guid.NewGuid().ToString(); - var headers = context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }; - headers.Date = DateTimeOffset.UtcNow; - headers.Headers["X-Value"] = uniqueId; context.GetResponseCachingFeature().VaryByParams = new[] { "*" }; - await context.Response.WriteAsync(uniqueId); + await DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -294,17 +189,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var builder = CreateBuilderWithResponseCaching(async (context) => { - var uniqueId = Guid.NewGuid().ToString(); - var headers = context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }; - headers.Date = DateTimeOffset.UtcNow; - headers.Headers["X-Value"] = uniqueId; context.GetResponseCachingFeature().VaryByParams = "param"; - await context.Response.WriteAsync(uniqueId); + await DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -322,17 +208,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var builder = CreateBuilderWithResponseCaching(async (context) => { - var uniqueId = Guid.NewGuid().ToString(); - var headers = context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }; - headers.Date = DateTimeOffset.UtcNow; - headers.Headers["X-Value"] = uniqueId; context.GetResponseCachingFeature().VaryByParams = new[] { "ParamA", "ParamB" }; - await context.Response.WriteAsync(uniqueId); + await DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -350,17 +227,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var builder = CreateBuilderWithResponseCaching(async (context) => { - var uniqueId = Guid.NewGuid().ToString(); - var headers = context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }; - headers.Date = DateTimeOffset.UtcNow; - headers.Headers["X-Value"] = uniqueId; context.GetResponseCachingFeature().VaryByParams = new[] { "*" }; - await context.Response.WriteAsync(uniqueId); + await DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -376,19 +244,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfRequestRequirements_NotMet() { - var builder = CreateBuilderWithResponseCaching(async (context) => - { - var uniqueId = Guid.NewGuid().ToString(); - var headers = context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }; - headers.Date = DateTimeOffset.UtcNow; - headers.Headers["X-Value"] = uniqueId; - await context.Response.WriteAsync(uniqueId); - }); + var builder = CreateBuilderWithResponseCaching(); using (var server = new TestServer(builder)) { @@ -407,19 +263,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void Serves504_IfOnlyIfCachedHeader_IsSpecified() { - var builder = CreateBuilderWithResponseCaching(async (context) => - { - var uniqueId = Guid.NewGuid().ToString(); - var headers = context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }; - headers.Date = DateTimeOffset.UtcNow; - headers.Headers["X-Value"] = uniqueId; - await context.Response.WriteAsync(uniqueId); - }); + var builder = CreateBuilderWithResponseCaching(); using (var server = new TestServer(builder)) { @@ -441,17 +285,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var builder = CreateBuilderWithResponseCaching(async (context) => { - var uniqueId = Guid.NewGuid().ToString(); - var headers = context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }; - headers.Date = DateTimeOffset.UtcNow; - headers.Headers["X-Value"] = uniqueId; - headers.Headers[HeaderNames.SetCookie] = "cookieName=cookieValue"; - await context.Response.WriteAsync(uniqueId); + var headers = context.Response.Headers[HeaderNames.SetCookie] = "cookieName=cookieValue"; + await DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -480,28 +315,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfIHttpSendFileFeature_NotUsed() { - var builder = CreateBuilderWithResponseCaching( - app => + var builder = CreateBuilderWithResponseCaching(app => + { + app.Use(async (context, next) => { - app.Use(async (context, next) => - { - context.Features.Set(new DummySendFileFeature()); - await next.Invoke(); - }); - }, - async (context) => - { - var uniqueId = Guid.NewGuid().ToString(); - var headers = context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }; - headers.Date = DateTimeOffset.UtcNow; - headers.Headers["X-Value"] = uniqueId; - await context.Response.WriteAsync(uniqueId); + context.Features.Set(new DummySendFileFeature()); + await next.Invoke(); }); + }); using (var server = new TestServer(builder)) { @@ -527,17 +348,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests }, async (context) => { - var uniqueId = Guid.NewGuid().ToString(); - var headers = context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }; - headers.Date = DateTimeOffset.UtcNow; - headers.Headers["X-Value"] = uniqueId; await context.Features.Get().SendFileAsync("dummy", 0, 0, CancellationToken.None); - await context.Response.WriteAsync(uniqueId); + await DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -553,20 +365,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfSubsequentRequest_ContainsNoStore() { - var builder = CreateBuilderWithResponseCaching( - async (context) => - { - var uniqueId = Guid.NewGuid().ToString(); - var headers = context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }; - headers.Date = DateTimeOffset.UtcNow; - headers.Headers["X-Value"] = uniqueId; - await context.Response.WriteAsync(uniqueId); - }); + var builder = CreateBuilderWithResponseCaching(); using (var server = new TestServer(builder)) { @@ -585,20 +384,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfInitialRequestContains_NoStore() { - var builder = CreateBuilderWithResponseCaching( - async (context) => - { - var uniqueId = Guid.NewGuid().ToString(); - var headers = context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }; - headers.Date = DateTimeOffset.UtcNow; - headers.Headers["X-Value"] = uniqueId; - await context.Response.WriteAsync(uniqueId); - }); + var builder = CreateBuilderWithResponseCaching(); using (var server = new TestServer(builder)) { @@ -614,6 +400,116 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } } + [Fact] + public async void Serves304_IfIfModifiedSince_Satisfied() + { + var builder = CreateBuilderWithResponseCaching(); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.IfUnmodifiedSince = DateTimeOffset.MaxValue; + var subsequentResponse = await client.GetAsync(""); + + initialResponse.EnsureSuccessStatusCode(); + Assert.Equal(System.Net.HttpStatusCode.NotModified, subsequentResponse.StatusCode); + } + } + + [Fact] + public async void ServesCachedContent_IfIfModifiedSince_NotSatisfied() + { + var builder = CreateBuilderWithResponseCaching(); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue; + var subsequentResponse = await client.GetAsync(""); + + await AssertResponseCachedAsync(initialResponse, subsequentResponse); + } + } + + [Fact] + public async void Serves304_IfIfNoneMatch_Satisfied() + { + var builder = CreateBuilderWithResponseCaching(async (context) => + { + var headers = context.Response.GetTypedHeaders().ETag = new EntityTagHeaderValue("\"E1\""); + await DefaultRequestDelegate(context); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.IfNoneMatch.Add(new System.Net.Http.Headers.EntityTagHeaderValue("\"E1\"")); + var subsequentResponse = await client.GetAsync(""); + + initialResponse.EnsureSuccessStatusCode(); + Assert.Equal(System.Net.HttpStatusCode.NotModified, subsequentResponse.StatusCode); + } + } + + [Fact] + public async void ServesCachedContent_IfIfNoneMatch_NotSatisfied() + { + var builder = CreateBuilderWithResponseCaching(async (context) => + { + var headers = context.Response.GetTypedHeaders().ETag = new EntityTagHeaderValue("\"E1\""); + await DefaultRequestDelegate(context); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.IfNoneMatch.Add(new System.Net.Http.Headers.EntityTagHeaderValue("\"E2\"")); + var subsequentResponse = await client.GetAsync(""); + + await AssertResponseCachedAsync(initialResponse, subsequentResponse); + } + } + + [Fact] + public async void ServesCachedContent_IfBodySize_IsCacheable() + { + var builder = CreateBuilderWithResponseCaching(new ResponseCachingOptions() + { + MaximumCachedBodySize = 100 + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + var subsequentResponse = await client.GetAsync(""); + + await AssertResponseCachedAsync(initialResponse, subsequentResponse); + } + } + + [Fact] + public async void ServesFreshContent_IfBodySize_IsNotCacheable() + { + var builder = CreateBuilderWithResponseCaching(new ResponseCachingOptions() + { + MaximumCachedBodySize = 1 + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + var subsequentResponse = await client.GetAsync("/different"); + + await AssertResponseNotCachedAsync(initialResponse, subsequentResponse); + } + } + private static async Task AssertResponseCachedAsync(HttpResponseMessage initialResponse, HttpResponseMessage subsequentResponse) { initialResponse.EnsureSuccessStatusCode(); @@ -636,126 +532,36 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Assert.NotEqual(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync()); } - [Fact] - public async void Serves304_IfIfModifiedSince_Satisfied() + private static RequestDelegate DefaultRequestDelegate = async (context) => { - var builder = CreateBuilderWithResponseCaching(async (context) => + var uniqueId = Guid.NewGuid().ToString(); + var headers = context.Response.GetTypedHeaders(); + headers.CacheControl = new CacheControlHeaderValue() { - var uniqueId = Guid.NewGuid().ToString(); - var headers = context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }; - headers.Date = DateTimeOffset.UtcNow; - headers.Headers["X-Value"] = uniqueId; - await context.Response.WriteAsync(uniqueId); - }); + Public = true, + MaxAge = TimeSpan.FromSeconds(10) + }; + headers.Date = DateTimeOffset.UtcNow; + headers.Headers["X-Value"] = uniqueId; + await context.Response.WriteAsync(uniqueId); + }; - using (var server = new TestServer(builder)) - { - var client = server.CreateClient(); - var initialResponse = await client.GetAsync(""); - client.DefaultRequestHeaders.IfUnmodifiedSince = DateTimeOffset.MaxValue; - var subsequentResponse = await client.GetAsync(""); + private static IWebHostBuilder CreateBuilderWithResponseCaching() => + CreateBuilderWithResponseCaching(app => { }, new ResponseCachingOptions(), DefaultRequestDelegate); - initialResponse.EnsureSuccessStatusCode(); - Assert.Equal(System.Net.HttpStatusCode.NotModified, subsequentResponse.StatusCode); - } - } - - [Fact] - public async void ServesCachedContent_IfIfModifiedSince_NotSatisfied() - { - var builder = CreateBuilderWithResponseCaching(async (context) => - { - var uniqueId = Guid.NewGuid().ToString(); - var headers = context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }; - headers.Date = DateTimeOffset.UtcNow; - headers.Headers["X-Value"] = uniqueId; - await context.Response.WriteAsync(uniqueId); - }); - - using (var server = new TestServer(builder)) - { - var client = server.CreateClient(); - var initialResponse = await client.GetAsync(""); - client.DefaultRequestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue; - var subsequentResponse = await client.GetAsync(""); - - await AssertResponseCachedAsync(initialResponse, subsequentResponse); - } - } - - [Fact] - public async void Serves304_IfIfNoneMatch_Satisfied() - { - var builder = CreateBuilderWithResponseCaching(async (context) => - { - var uniqueId = Guid.NewGuid().ToString(); - var headers = context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }; - headers.Date = DateTimeOffset.UtcNow; - headers.Headers["X-Value"] = uniqueId; - headers.ETag = new EntityTagHeaderValue("\"E1\""); - await context.Response.WriteAsync(uniqueId); - }); - - using (var server = new TestServer(builder)) - { - var client = server.CreateClient(); - var initialResponse = await client.GetAsync(""); - client.DefaultRequestHeaders.IfNoneMatch.Add(new System.Net.Http.Headers.EntityTagHeaderValue("\"E1\"")); - var subsequentResponse = await client.GetAsync(""); - - initialResponse.EnsureSuccessStatusCode(); - Assert.Equal(System.Net.HttpStatusCode.NotModified, subsequentResponse.StatusCode); - } - } - - [Fact] - public async void ServesCachedContent_IfIfNoneMatch_NotSatisfied() - { - var builder = CreateBuilderWithResponseCaching(async (context) => - { - var uniqueId = Guid.NewGuid().ToString(); - var headers = context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }; - headers.Date = DateTimeOffset.UtcNow; - headers.Headers["X-Value"] = uniqueId; - headers.ETag = new EntityTagHeaderValue("\"E1\""); - await context.Response.WriteAsync(uniqueId); - }); - - using (var server = new TestServer(builder)) - { - var client = server.CreateClient(); - var initialResponse = await client.GetAsync(""); - client.DefaultRequestHeaders.IfNoneMatch.Add(new System.Net.Http.Headers.EntityTagHeaderValue("\"E2\"")); - var subsequentResponse = await client.GetAsync(""); - - await AssertResponseCachedAsync(initialResponse, subsequentResponse); - } - } + private static IWebHostBuilder CreateBuilderWithResponseCaching(ResponseCachingOptions options) => + CreateBuilderWithResponseCaching(app => { }, options, DefaultRequestDelegate); private static IWebHostBuilder CreateBuilderWithResponseCaching(RequestDelegate requestDelegate) => - CreateBuilderWithResponseCaching(app => { }, requestDelegate); + CreateBuilderWithResponseCaching(app => { }, new ResponseCachingOptions(), requestDelegate); - private static IWebHostBuilder CreateBuilderWithResponseCaching(Action configureDelegate, RequestDelegate requestDelegate) + private static IWebHostBuilder CreateBuilderWithResponseCaching(Action configureDelegate) => + CreateBuilderWithResponseCaching(configureDelegate, new ResponseCachingOptions(), DefaultRequestDelegate); + + private static IWebHostBuilder CreateBuilderWithResponseCaching(Action configureDelegate, RequestDelegate requestDelegate) => + CreateBuilderWithResponseCaching(configureDelegate, new ResponseCachingOptions(), requestDelegate); + + private static IWebHostBuilder CreateBuilderWithResponseCaching(Action configureDelegate, ResponseCachingOptions options, RequestDelegate requestDelegate) { return new WebHostBuilder() .ConfigureServices(services => @@ -765,7 +571,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests .Configure(app => { configureDelegate(app); - app.UseResponseCaching(); + app.UseResponseCaching(options); app.Run(requestDelegate); }); } From 0f711ce27922b626cf1df26658ab925c24b8230f Mon Sep 17 00:00:00 2001 From: Doug Bunting Date: Sun, 4 Sep 2016 17:58:40 -0700 Subject: [PATCH 021/188] Increase .travis.yml consistency between repos - aspnet/Universe#349 - minimize `dotnet` setup time; no need for caching - build with `--quiet` --- .travis.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index efc1a57214..d7636fa329 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,6 +10,10 @@ addons: - libssl-dev - libunwind8 - zlib1g +env: + global: + - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + - DOTNET_CLI_TELEMETRY_OPTOUT: 1 mono: - 4.0.5 os: @@ -25,4 +29,4 @@ branches: before_install: - if test "$TRAVIS_OS_NAME" == "osx"; then brew update; brew install openssl; ln -s /usr/local/opt/openssl/lib/libcrypto.1.0.0.dylib /usr/local/lib/; ln -s /usr/local/opt/openssl/lib/libssl.1.0.0.dylib /usr/local/lib/; fi script: - - ./build.sh verify + - ./build.sh --quiet verify From 411681e8d62d954825ff34d5443e3245ade93e38 Mon Sep 17 00:00:00 2001 From: John Luo Date: Fri, 2 Sep 2016 16:03:07 -0700 Subject: [PATCH 022/188] Add options to configure case sensitivity of request paths --- .../ResponseCachingContext.cs | 10 ++--- .../ResponseCachingOptions.cs | 7 ++- .../ResponseCachingContextTests.cs | 43 ++++++++++++++++++- 3 files changed, 53 insertions(+), 7 deletions(-) diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs index 2733172960..142ded2e5a 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs @@ -44,7 +44,7 @@ namespace Microsoft.AspNetCore.ResponseCaching // Internal for testing internal ResponseCachingContext( - HttpContext httpContext, + HttpContext httpContext, IResponseCache cache, ResponseCachingOptions options, ObjectPool builderPool, @@ -149,11 +149,11 @@ namespace Microsoft.AspNetCore.ResponseCaching .Append(KeyDelimiter); } - // Default key + // Default key builder .Append(request.Method.ToUpperInvariant()) .Append(KeyDelimiter) - .Append(request.Path.Value.ToUpperInvariant()); + .Append(_options.CaseSensitivePaths ? request.Path.Value : request.Path.Value.ToUpperInvariant()); // Vary by headers if (varyBy?.Headers.Count > 0) @@ -337,7 +337,7 @@ namespace Microsoft.AspNetCore.ResponseCaching } // TODO: public MAY override the cacheability checks for private and status codes - + // Check private if (ResponseCacheControl.Private) { @@ -365,7 +365,7 @@ namespace Microsoft.AspNetCore.ResponseCaching internal bool EntryIsFresh(ResponseHeaders responseHeaders, TimeSpan age, bool verifyAgainstRequest) { var responseCacheControl = responseHeaders.CacheControl ?? EmptyCacheControl; - + // Add min-fresh requirements if (verifyAgainstRequest) { diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingOptions.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingOptions.cs index 753b9b6b72..fefe65c4e2 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingOptions.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingOptions.cs @@ -9,10 +9,15 @@ namespace Microsoft.AspNetCore.Builder public class ResponseCachingOptions { /// - /// The largest cacheable size for the response body in bytes. + /// The largest cacheable size for the response body in bytes. The default is set to 1 MB. /// public long MaximumCachedBodySize { get; set; } = 1024 * 1024; + /// + /// true if request paths are case-sensitive; otherwise false. The default is to treat paths as case-insensitive. + /// + public bool CaseSensitivePaths { get; set; } = false; + /// /// For testing purposes only. /// diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs index 2662c56b78..1227eb9b80 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs @@ -280,6 +280,34 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests })); } + [Fact] + public void CreateCacheKey_CaseInsensitivePath_NormalizesPath() + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.Method = "GET"; + httpContext.Request.Path = "/Path"; + var context = CreateTestContext(httpContext, new ResponseCachingOptions() + { + CaseSensitivePaths = false + }); + + Assert.Equal($"GET{KeyDelimiter}/PATH", context.CreateCacheKey()); + } + + [Fact] + public void CreateCacheKey_CaseSensitivePath_PreservesPathCase() + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.Method = "GET"; + httpContext.Request.Path = "/Path"; + var context = CreateTestContext(httpContext, new ResponseCachingOptions() + { + CaseSensitivePaths = true + }); + + Assert.Equal($"GET{KeyDelimiter}/Path", context.CreateCacheKey()); + } + [Fact] public void ResponseIsCacheable_NoPublic_NotAllowed() { @@ -832,6 +860,16 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { return CreateTestContext( httpContext, + new ResponseCachingOptions(), + new NoopCacheKeyModifier(), + new NoopCacheabilityValidator()); + } + + private static ResponseCachingContext CreateTestContext(HttpContext httpContext, ResponseCachingOptions options) + { + return CreateTestContext( + httpContext, + options, new NoopCacheKeyModifier(), new NoopCacheabilityValidator()); } @@ -840,6 +878,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { return CreateTestContext( httpContext, + new ResponseCachingOptions(), cacheKeyModifier, new NoopCacheabilityValidator()); } @@ -848,19 +887,21 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { return CreateTestContext( httpContext, + new ResponseCachingOptions(), new NoopCacheKeyModifier(), cacheabilityValidator); } private static ResponseCachingContext CreateTestContext( HttpContext httpContext, + ResponseCachingOptions options, IResponseCachingCacheKeyModifier cacheKeyModifier, IResponseCachingCacheabilityValidator cacheabilityValidator) { return new ResponseCachingContext( httpContext, new TestResponseCache(), - new ResponseCachingOptions(), + options, new DefaultObjectPool(new StringBuilderPooledObjectPolicy()), cacheabilityValidator, cacheKeyModifier); From 02b8fb3bbc241fc9e0dc9c39c1f6badb7e2c584c Mon Sep 17 00:00:00 2001 From: John Luo Date: Wed, 7 Sep 2016 11:33:05 -0700 Subject: [PATCH 023/188] Use TaskCache class from Microsoft.Extensions.TaskCache.Sources --- .../ResponseCachingMiddleware.cs | 3 ++- src/Microsoft.AspNetCore.ResponseCaching/project.json | 6 +++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs index 96c1a713a3..240c44b4d5 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs @@ -6,6 +6,7 @@ using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Internal; using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.Options; @@ -16,7 +17,7 @@ namespace Microsoft.AspNetCore.ResponseCaching private static readonly Func OnStartingCallback = state => { ((ResponseCachingContext)state).OnResponseStarting(); - return Task.FromResult(0); + return TaskCache.CompletedTask; }; private readonly RequestDelegate _next; diff --git a/src/Microsoft.AspNetCore.ResponseCaching/project.json b/src/Microsoft.AspNetCore.ResponseCaching/project.json index ae02d9e641..70fa887b15 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/project.json +++ b/src/Microsoft.AspNetCore.ResponseCaching/project.json @@ -23,7 +23,11 @@ "dependencies": { "Microsoft.AspNetCore.Http": "1.1.0-*", "Microsoft.AspNetCore.Http.Extensions": "1.1.0-*", - "Microsoft.Extensions.Caching.Memory": "1.1.0-*" + "Microsoft.Extensions.Caching.Memory": "1.1.0-*", + "Microsoft.Extensions.TaskCache.Sources": { + "version": "1.1.0-*", + "type": "build" + } }, "frameworks": { "net451": {}, From 7300d9e9366d5dbca9bfbde97db14d6ff3ba2ce1 Mon Sep 17 00:00:00 2001 From: John Luo Date: Fri, 2 Sep 2016 20:32:45 -0700 Subject: [PATCH 024/188] Refactoring ResponseCacheContext - Extract key creationg into a service - Extract cacheability checks into a service - Add a ResponseCachingState feature to preserve response cache context between operations - Recognize Set-Cookie as not-cacheable --- .../CacheabilityValidator.cs | 206 +++++ .../ResponseCachingExtensions.cs | 0 .../ResponseCachingHttpContextExtensions.cs | 12 +- ...ponseCachingServiceCollectionExtensions.cs | 8 +- .../Interfaces/ICacheabilityValidator.cs | 33 + .../Interfaces/IKeyProvider.cs | 25 + .../IResponseCachingCacheKeyModifier.cs | 17 - .../IResponseCachingCacheabilityValidator.cs | 24 - ...ySerializer.cs => CacheEntrySerializer.cs} | 66 +- ...CacheKeyModifier.cs => CachedVaryRules.cs} | 6 +- .../Internal/DistributedResponseCache.cs | 4 +- .../Internal/HttpContextInternalExtensions.cs | 44 ++ .../Internal/NoopCacheabilityValidator.cs | 14 - .../Internal/ResponseCachingState.cs | 89 +++ .../KeyProvider.cs | 157 ++++ .../OverrideResult.cs | 23 - .../ResponseCachingContext.cs | 493 ++---------- .../ResponseCachingFeature.cs | 2 +- .../ResponseCachingMiddleware.cs | 98 +-- .../CachedVaryBy.cs => VaryRules.cs} | 4 +- .../CacheEntrySerializerTests.cs | 157 ++++ .../CacheabilityValidatorTests.cs | 624 +++++++++++++++ ...efaultResponseCacheEntrySerializerTests.cs | 126 --- .../HttpContextInternalExtensionTests.cs | 37 + .../KeyProviderTests.cs | 155 ++++ .../ResponseCachingContextTests.cs | 730 +----------------- .../ResponseCachingTests.cs | 53 +- 27 files changed, 1720 insertions(+), 1487 deletions(-) create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/CacheabilityValidator.cs rename src/Microsoft.AspNetCore.ResponseCaching/{ => Extensions}/ResponseCachingExtensions.cs (100%) rename src/Microsoft.AspNetCore.ResponseCaching/{ => Extensions}/ResponseCachingHttpContextExtensions.cs (63%) rename src/Microsoft.AspNetCore.ResponseCaching/{ => Extensions}/ResponseCachingServiceCollectionExtensions.cs (85%) create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Interfaces/ICacheabilityValidator.cs create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IKeyProvider.cs delete mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCachingCacheKeyModifier.cs delete mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCachingCacheabilityValidator.cs rename src/Microsoft.AspNetCore.ResponseCaching/Internal/{DefaultResponseCacheEntrySerializer.cs => CacheEntrySerializer.cs} (74%) rename src/Microsoft.AspNetCore.ResponseCaching/Internal/{NoopCacheKeyModifier.cs => CachedVaryRules.cs} (57%) create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Internal/HttpContextInternalExtensions.cs delete mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Internal/NoopCacheabilityValidator.cs create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingState.cs create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/KeyProvider.cs delete mode 100644 src/Microsoft.AspNetCore.ResponseCaching/OverrideResult.cs rename src/Microsoft.AspNetCore.ResponseCaching/{Internal/CachedVaryBy.cs => VaryRules.cs} (78%) create mode 100644 test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheEntrySerializerTests.cs create mode 100644 test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheabilityValidatorTests.cs delete mode 100644 test/Microsoft.AspNetCore.ResponseCaching.Tests/DefaultResponseCacheEntrySerializerTests.cs create mode 100644 test/Microsoft.AspNetCore.ResponseCaching.Tests/HttpContextInternalExtensionTests.cs create mode 100644 test/Microsoft.AspNetCore.ResponseCaching.Tests/KeyProviderTests.cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/CacheabilityValidator.cs b/src/Microsoft.AspNetCore.ResponseCaching/CacheabilityValidator.cs new file mode 100644 index 0000000000..0e46bd470d --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/CacheabilityValidator.cs @@ -0,0 +1,206 @@ +// 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.AspNetCore.Http.Headers; +using Microsoft.AspNetCore.ResponseCaching.Internal; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.ResponseCaching +{ + public class CacheabilityValidator : ICacheabilityValidator + { + public virtual bool RequestIsCacheable(HttpContext httpContext) + { + var state = httpContext.GetResponseCachingState(); + + // Verify the method + // TODO: RFC lists POST as a cacheable method when explicit freshness information is provided, but this is not widely implemented. Will revisit. + var request = httpContext.Request; + if (!string.Equals("GET", request.Method, StringComparison.OrdinalIgnoreCase) && + !string.Equals("HEAD", request.Method, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + // Verify existence of authorization headers + // TODO: The server may indicate that the response to these request are cacheable + if (!StringValues.IsNullOrEmpty(request.Headers[HeaderNames.Authorization])) + { + return false; + } + + // Verify request cache-control parameters + // TODO: no-cache requests can be retrieved upon validation with origin + if (!StringValues.IsNullOrEmpty(request.Headers[HeaderNames.CacheControl])) + { + if (state.RequestCacheControl.NoCache) + { + return false; + } + } + else + { + // Support for legacy HTTP 1.0 cache directive + var pragmaHeaderValues = request.Headers[HeaderNames.Pragma]; + foreach (var directive in pragmaHeaderValues) + { + if (string.Equals("no-cache", directive, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + } + } + + // TODO: Verify global middleware settings? Explicit ignore list, range requests, etc. + return true; + } + + public virtual bool ResponseIsCacheable(HttpContext httpContext) + { + var state = httpContext.GetResponseCachingState(); + + // Only cache pages explicitly marked with public + // TODO: Consider caching responses that are not marked as public but otherwise cacheable? + if (!state.ResponseCacheControl.Public) + { + return false; + } + + // Check no-store + if (state.RequestCacheControl.NoStore || state.ResponseCacheControl.NoStore) + { + return false; + } + + // Check no-cache + // TODO: Handle no-cache with headers + if (state.ResponseCacheControl.NoCache) + { + return false; + } + + var response = httpContext.Response; + + // Do not cache responses with Set-Cookie headers + if (!StringValues.IsNullOrEmpty(response.Headers[HeaderNames.SetCookie])) + { + return false; + } + + // Do not cache responses varying by * + var varyHeader = response.Headers[HeaderNames.Vary]; + if (varyHeader.Count == 1 && string.Equals(varyHeader, "*", StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + // TODO: public MAY override the cacheability checks for private and status codes + + // Check private + if (state.ResponseCacheControl.Private) + { + return false; + } + + // Check response code + // TODO: RFC also lists 203, 204, 206, 300, 301, 404, 405, 410, 414, and 501 as cacheable by default + if (response.StatusCode != StatusCodes.Status200OK) + { + return false; + } + + // Check response freshness + // TODO: apparent age vs corrected age value + if (state.ResponseHeaders.Date == null) + { + if (state.ResponseCacheControl.SharedMaxAge == null && + state.ResponseCacheControl.MaxAge == null && + state.ResponseTime > state.ResponseHeaders.Expires) + { + return false; + } + } + else + { + var age = state.ResponseTime - state.ResponseHeaders.Date.Value; + + // Validate shared max age + if (age > state.ResponseCacheControl.SharedMaxAge) + { + return false; + } + else if (state.ResponseCacheControl.SharedMaxAge == null) + { + // Validate max age + if (age > state.ResponseCacheControl.MaxAge) + { + return false; + } + else if (state.ResponseCacheControl.MaxAge == null) + { + // Validate expiration + if (state.ResponseTime > state.ResponseHeaders.Expires) + { + return false; + } + } + } + } + + return true; + } + + public virtual bool CachedEntryIsFresh(HttpContext httpContext, ResponseHeaders cachedResponseHeaders) + { + var state = httpContext.GetResponseCachingState(); + var age = state.CachedEntryAge; + + // Add min-fresh requirements + if (state.RequestCacheControl.MinFresh != null) + { + age += state.RequestCacheControl.MinFresh.Value; + } + + // Validate shared max age, this overrides any max age settings for shared caches + if (age > cachedResponseHeaders.CacheControl.SharedMaxAge) + { + // shared max age implies must revalidate + return false; + } + else if (cachedResponseHeaders.CacheControl.SharedMaxAge == null) + { + // Validate max age + if (age > cachedResponseHeaders.CacheControl.MaxAge || age > state.RequestCacheControl.MaxAge) + { + // Must revalidate + if (cachedResponseHeaders.CacheControl.MustRevalidate) + { + return false; + } + + // Request allows stale values + if (age < state.RequestCacheControl.MaxStaleLimit) + { + // TODO: Add warning header indicating the response is stale + return true; + } + + return false; + } + else if (cachedResponseHeaders.CacheControl.MaxAge == null && state.RequestCacheControl.MaxAge == null) + { + // Validate expiration + if (state.ResponseTime > cachedResponseHeaders.Expires) + { + return false; + } + } + } + + return true; + } + } +} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingExtensions.cs b/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCachingExtensions.cs similarity index 100% rename from src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingExtensions.cs rename to src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCachingExtensions.cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingHttpContextExtensions.cs b/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCachingHttpContextExtensions.cs similarity index 63% rename from src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingHttpContextExtensions.cs rename to src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCachingHttpContextExtensions.cs index e446b4491b..ae6481949c 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingHttpContextExtensions.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCachingHttpContextExtensions.cs @@ -2,25 +2,21 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.ResponseCaching.Internal; namespace Microsoft.AspNetCore.ResponseCaching { // TODO: Temporary interface for endpoints to specify options for response caching public static class ResponseCachingHttpContextExtensions { - public static void AddResponseCachingFeature(this HttpContext httpContext) + public static ResponseCachingState GetResponseCachingState(this HttpContext httpContext) { - httpContext.Features.Set(new ResponseCachingFeature()); - } - - public static void RemoveResponseCachingFeature(this HttpContext httpContext) - { - httpContext.Features.Set(null); + return httpContext.Features.Get(); } public static ResponseCachingFeature GetResponseCachingFeature(this HttpContext httpContext) { - return httpContext.Features.Get() ?? new ResponseCachingFeature(); + return httpContext.Features.Get(); } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCachingServiceCollectionExtensions.cs similarity index 85% rename from src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingServiceCollectionExtensions.cs rename to src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCachingServiceCollectionExtensions.cs index c2ad4a050d..4877d34bdc 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCachingServiceCollectionExtensions.cs @@ -16,7 +16,7 @@ namespace Microsoft.Extensions.DependencyInjection { throw new ArgumentNullException(nameof(services)); } - + services.AddMemoryCache(); services.AddResponseCachingServices(); services.TryAdd(ServiceDescriptor.Singleton()); @@ -30,7 +30,7 @@ namespace Microsoft.Extensions.DependencyInjection { throw new ArgumentNullException(nameof(services)); } - + services.AddDistributedMemoryCache(); services.AddResponseCachingServices(); services.TryAdd(ServiceDescriptor.Singleton()); @@ -40,8 +40,8 @@ namespace Microsoft.Extensions.DependencyInjection private static IServiceCollection AddResponseCachingServices(this IServiceCollection services) { - services.TryAdd(ServiceDescriptor.Singleton()); - services.TryAdd(ServiceDescriptor.Singleton()); + services.TryAdd(ServiceDescriptor.Singleton()); + services.TryAdd(ServiceDescriptor.Singleton()); return services; } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/ICacheabilityValidator.cs b/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/ICacheabilityValidator.cs new file mode 100644 index 0000000000..01ded6818a --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/ICacheabilityValidator.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.AspNetCore.Http.Headers; + +namespace Microsoft.AspNetCore.ResponseCaching +{ + public interface ICacheabilityValidator + { + /// + /// Determine the cacheability of an HTTP request. + /// + /// The . + /// true if the request is cacheable; otherwise false. + bool RequestIsCacheable(HttpContext httpContext); + + /// + /// Determine the cacheability of an HTTP response. + /// + /// The . + /// true if the response is cacheable; otherwise false. + bool ResponseIsCacheable(HttpContext httpContext); + + /// + /// Determine the freshness of the cached entry. + /// + /// The . + /// The of the cached entry. + /// true if the cached entry is fresh; otherwise false. + bool CachedEntryIsFresh(HttpContext httpContext, ResponseHeaders cachedResponseHeaders); + } +} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IKeyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IKeyProvider.cs new file mode 100644 index 0000000000..888498f908 --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IKeyProvider.cs @@ -0,0 +1,25 @@ +// 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 +{ + public interface IKeyProvider + { + /// + /// Create a key using the HTTP request. + /// + /// The . + /// The created base key. + string CreateBaseKey(HttpContext httpContext); + + /// + /// Create a key using the HTTP context and vary rules. + /// + /// The . + /// The . + /// The created base key. + string CreateVaryKey(HttpContext httpContext, VaryRules varyRules); + } +} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCachingCacheKeyModifier.cs b/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCachingCacheKeyModifier.cs deleted file mode 100644 index d78c27cd90..0000000000 --- a/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCachingCacheKeyModifier.cs +++ /dev/null @@ -1,17 +0,0 @@ -// 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 -{ - public interface IResponseCachingCacheKeyModifier - { - /// - /// Create a key segment that is prepended to the default cache key. - /// - /// The . - /// The key segment that will be prepended to the default cache key. - string CreatKeyPrefix(HttpContext httpContext); - } -} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCachingCacheabilityValidator.cs b/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCachingCacheabilityValidator.cs deleted file mode 100644 index 11b46560c8..0000000000 --- a/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCachingCacheabilityValidator.cs +++ /dev/null @@ -1,24 +0,0 @@ -// 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 -{ - public interface IResponseCachingCacheabilityValidator - { - /// - /// Override default behavior for determining cacheability of an HTTP request. - /// - /// The . - /// The . - OverrideResult RequestIsCacheableOverride(HttpContext httpContext); - - /// - /// Override default behavior for determining cacheability of an HTTP response. - /// - /// The . - /// The . - OverrideResult ResponseIsCacheableOverride(HttpContext httpContext); - } -} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/DefaultResponseCacheEntrySerializer.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntrySerializer.cs similarity index 74% rename from src/Microsoft.AspNetCore.ResponseCaching/Internal/DefaultResponseCacheEntrySerializer.cs rename to src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntrySerializer.cs index e0079b1a2c..0f4f3f5a37 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/DefaultResponseCacheEntrySerializer.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntrySerializer.cs @@ -7,7 +7,7 @@ using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.ResponseCaching.Internal { - internal static class DefaultResponseCacheSerializer + internal static class CacheEntrySerializer { private const int FormatVersion = 1; @@ -37,8 +37,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal // Serialization Format // Format version (int) - // Type (char: 'R' for CachedResponse, 'V' for CachedVaryBy) - // Type-dependent data (see CachedResponse and CachedVaryBy) + // Type (char: 'R' for CachedResponse, 'V' for CachedVaryRules) + // Type-dependent data (see CachedResponse and CachedVaryRules) public static object Read(BinaryReader reader) { if (reader == null) @@ -60,11 +60,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal } else if (type == 'V') { - var cachedResponse = ReadCachedVaryBy(reader); - return cachedResponse; + var cachedVaryRules = ReadCachedVaryRules(reader); + return cachedVaryRules; } - // Unable to read as CachedResponse or CachedVaryBy + // Unable to read as CachedResponse or CachedVaryRules return null; } @@ -96,12 +96,19 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal } // Serialization Format - // Headers count - // Headers if count > 0 (comma separated string) - // Params count - // Params if count > 0 (comma separated string) - private static CachedVaryBy ReadCachedVaryBy(BinaryReader reader) + // ContainsVaryRules (bool) + // If containing vary rules: + // Headers count + // Headers if count > 0 (comma separated string) + // Params count + // Params if count > 0 (comma separated string) + private static CachedVaryRules ReadCachedVaryRules(BinaryReader reader) { + if (!reader.ReadBoolean()) + { + return new CachedVaryRules(); + } + var headerCount = reader.ReadInt32(); var headers = new string[headerCount]; for (var index = 0; index < headerCount; index++) @@ -115,7 +122,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal param[index] = reader.ReadString(); } - return new CachedVaryBy { Headers = headers, Params = param }; + return new CachedVaryRules { VaryRules = new VaryRules() { Headers = headers, Params = param } }; } // See serialization format above @@ -132,14 +139,16 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal } writer.Write(FormatVersion); - + if (entry is CachedResponse) { + writer.Write('R'); WriteCachedResponse(writer, entry as CachedResponse); } - else if (entry is CachedVaryBy) + else if (entry is CachedVaryRules) { - WriteCachedVaryBy(writer, entry as CachedVaryBy); + writer.Write('V'); + WriteCachedVaryRules(writer, entry as CachedVaryRules); } else { @@ -150,7 +159,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal // See serialization format above private static void WriteCachedResponse(BinaryWriter writer, CachedResponse entry) { - writer.Write('R'); writer.Write(entry.Created.UtcTicks); writer.Write(entry.StatusCode); writer.Write(entry.Headers.Count); @@ -165,20 +173,26 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal } // See serialization format above - private static void WriteCachedVaryBy(BinaryWriter writer, CachedVaryBy entry) + private static void WriteCachedVaryRules(BinaryWriter writer, CachedVaryRules varyRules) { - writer.Write('V'); - - writer.Write(entry.Headers.Count); - foreach (var header in entry.Headers) + if (varyRules.VaryRules == null) { - writer.Write(header); + writer.Write(false); } - - writer.Write(entry.Params.Count); - foreach (var param in entry.Params) + else { - writer.Write(param); + writer.Write(true); + writer.Write(varyRules.VaryRules.Headers.Count); + foreach (var header in varyRules.VaryRules.Headers) + { + writer.Write(header); + } + + writer.Write(varyRules.VaryRules.Params.Count); + foreach (var param in varyRules.VaryRules.Params) + { + writer.Write(param); + } } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/NoopCacheKeyModifier.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/CachedVaryRules.cs similarity index 57% rename from src/Microsoft.AspNetCore.ResponseCaching/Internal/NoopCacheKeyModifier.cs rename to src/Microsoft.AspNetCore.ResponseCaching/Internal/CachedVaryRules.cs index a32332c366..bd74a08c8c 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/NoopCacheKeyModifier.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/CachedVaryRules.cs @@ -1,12 +1,10 @@ // 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 { - internal class NoopCacheKeyModifier : IResponseCachingCacheKeyModifier + internal class CachedVaryRules { - public string CreatKeyPrefix(HttpContext httpContext) => null; + internal VaryRules VaryRules; } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/DistributedResponseCache.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/DistributedResponseCache.cs index c9068b6288..7de5d8b946 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/DistributedResponseCache.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/DistributedResponseCache.cs @@ -24,7 +24,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal { try { - return DefaultResponseCacheSerializer.Deserialize(_cache.Get(key)); + return CacheEntrySerializer.Deserialize(_cache.Get(key)); } catch { @@ -51,7 +51,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal { _cache.Set( key, - DefaultResponseCacheSerializer.Serialize(entry), + CacheEntrySerializer.Serialize(entry), new DistributedCacheEntryOptions() { AbsoluteExpirationRelativeToNow = validFor diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/HttpContextInternalExtensions.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/HttpContextInternalExtensions.cs new file mode 100644 index 0000000000..3c148ef340 --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/HttpContextInternalExtensions.cs @@ -0,0 +1,44 @@ +// 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; + +namespace Microsoft.AspNetCore.ResponseCaching.Internal +{ + internal static class HttpContextInternalExtensions + { + internal static void AddResponseCachingFeature(this HttpContext httpContext) + { + if (httpContext.GetResponseCachingFeature() != null) + { + throw new InvalidOperationException($"Another instance of {nameof(ResponseCachingFeature)} already exists. Only one instance of {nameof(ResponseCachingMiddleware)} can be configured for an application."); + } + httpContext.Features.Set(new ResponseCachingFeature()); + } + + internal static void RemoveResponseCachingFeature(this HttpContext httpContext) + { + httpContext.Features.Set(null); + } + + internal static void AddResponseCachingState(this HttpContext httpContext) + { + if (httpContext.GetResponseCachingState() != null) + { + throw new InvalidOperationException($"Another instance of {nameof(ResponseCachingState)} already exists. Only one instance of {nameof(ResponseCachingMiddleware)} can be configured for an application."); + } + httpContext.Features.Set(new ResponseCachingState(httpContext)); + } + + internal static void RemoveResponseCachingState(this HttpContext httpContext) + { + httpContext.Features.Set(null); + } + + internal static ResponseCachingState GetResponseCachingState(this HttpContext httpContext) + { + return httpContext.Features.Get(); + } + } +} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/NoopCacheabilityValidator.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/NoopCacheabilityValidator.cs deleted file mode 100644 index 309e900f4b..0000000000 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/NoopCacheabilityValidator.cs +++ /dev/null @@ -1,14 +0,0 @@ -// 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 -{ - internal class NoopCacheabilityValidator : IResponseCachingCacheabilityValidator - { - public OverrideResult RequestIsCacheableOverride(HttpContext httpContext) => OverrideResult.UseDefaultLogic; - - public OverrideResult ResponseIsCacheableOverride(HttpContext httpContext) => OverrideResult.UseDefaultLogic; - } -} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingState.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingState.cs new file mode 100644 index 0000000000..3ff862a502 --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingState.cs @@ -0,0 +1,89 @@ +// 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.AspNetCore.Http.Headers; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.ResponseCaching.Internal +{ + public class ResponseCachingState + { + private static readonly CacheControlHeaderValue EmptyCacheControl = new CacheControlHeaderValue(); + + private readonly HttpContext _httpContext; + + private RequestHeaders _requestHeaders; + private ResponseHeaders _responseHeaders; + private CacheControlHeaderValue _requestCacheControl; + private CacheControlHeaderValue _responseCacheControl; + + internal ResponseCachingState(HttpContext httpContext) + { + _httpContext = httpContext; + } + + public bool ShouldCacheResponse { get; internal set; } + + public string BaseKey { get; internal set; } + + public string VaryKey { get; internal set; } + + public DateTimeOffset ResponseTime { get; internal set; } + + public TimeSpan CachedEntryAge { get; internal set; } + + public TimeSpan CachedResponseValidFor { get; internal set; } + + internal CachedResponse CachedResponse { get; set; } + + public RequestHeaders RequestHeaders + { + get + { + if (_requestHeaders == null) + { + _requestHeaders = _httpContext.Request.GetTypedHeaders(); + } + return _requestHeaders; + } + } + + public ResponseHeaders ResponseHeaders + { + get + { + if (_responseHeaders == null) + { + _responseHeaders = _httpContext.Response.GetTypedHeaders(); + } + return _responseHeaders; + } + } + + public CacheControlHeaderValue RequestCacheControl + { + get + { + if (_requestCacheControl == null) + { + _requestCacheControl = RequestHeaders.CacheControl ?? EmptyCacheControl; + } + return _requestCacheControl; + } + } + + public CacheControlHeaderValue ResponseCacheControl + { + get + { + if (_responseCacheControl == null) + { + _responseCacheControl = ResponseHeaders.CacheControl ?? EmptyCacheControl; + } + return _responseCacheControl; + } + } + } +} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/KeyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/KeyProvider.cs new file mode 100644 index 0000000000..6eac525b33 --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/KeyProvider.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.Linq; +using System.Text; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.ResponseCaching.Internal; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.ResponseCaching +{ + public class KeyProvider : IKeyProvider + { + // Use the record separator for delimiting components of the cache key to avoid possible collisions + private static readonly char KeyDelimiter = '\x1e'; + + private readonly ObjectPool _builderPool; + private readonly ResponseCachingOptions _options; + + public KeyProvider(ObjectPoolProvider poolProvider, IOptions options) + { + if (poolProvider == null) + { + throw new ArgumentNullException(nameof(poolProvider)); + } + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + _builderPool = poolProvider.CreateStringBuilderPool(); + _options = options.Value; + } + + // GET/PATH + // TODO: Method invariant retrieval? E.g. HEAD after GET to the same resource. + public virtual string CreateBaseKey(HttpContext httpContext) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + + var request = httpContext.Request; + var builder = _builderPool.Get(); + + try + { + builder + .Append(request.Method.ToUpperInvariant()) + .Append(KeyDelimiter) + .Append(_options.CaseSensitivePaths ? request.Path.Value : request.Path.Value.ToUpperInvariant()); + + return builder.ToString();; + } + finally + { + _builderPool.Return(builder); + } + } + + // BaseKeyHHeaderName=HeaderValueQQueryName=QueryValue + public virtual string CreateVaryKey(HttpContext httpContext, VaryRules varyRules) + { + if (httpContext == null) + { + throw new ArgumentNullException(nameof(httpContext)); + } + if (varyRules == null) + { + // TODO: replace this with a GUID + return httpContext.GetResponseCachingState()?.BaseKey ?? CreateBaseKey(httpContext); + } + + var request = httpContext.Request; + var builder = _builderPool.Get(); + + try + { + // TODO: replace this with a GUID + builder.Append(httpContext.GetResponseCachingState()?.BaseKey ?? CreateBaseKey(httpContext)); + + // Vary by headers + if (varyRules?.Headers.Count > 0) + { + // Append a group separator for the header segment of the cache key + builder.Append(KeyDelimiter) + .Append('H'); + + foreach (var header in varyRules.Headers) + { + var value = httpContext.Request.Headers[header]; + + // TODO: How to handle null/empty string? + if (StringValues.IsNullOrEmpty(value)) + { + value = "null"; + } + + builder.Append(KeyDelimiter) + .Append(header) + .Append("=") + .Append(value); + } + } + + // Vary by query params + if (varyRules?.Params.Count > 0) + { + // Append a group separator for the query parameter segment of the cache key + builder.Append(KeyDelimiter) + .Append('Q'); + + if (varyRules.Params.Count == 1 && string.Equals(varyRules.Params[0], "*", StringComparison.Ordinal)) + { + // Vary by all available query params + foreach (var query in httpContext.Request.Query.OrderBy(q => q.Key, StringComparer.OrdinalIgnoreCase)) + { + builder.Append(KeyDelimiter) + .Append(query.Key.ToUpperInvariant()) + .Append("=") + .Append(query.Value); + } + } + else + { + foreach (var param in varyRules.Params) + { + var value = httpContext.Request.Query[param]; + + // TODO: How to handle null/empty string? + if (StringValues.IsNullOrEmpty(value)) + { + value = "null"; + } + + builder.Append(KeyDelimiter) + .Append(param) + .Append("=") + .Append(value); + } + } + } + + return builder.ToString(); + } + finally + { + _builderPool.Return(builder); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/OverrideResult.cs b/src/Microsoft.AspNetCore.ResponseCaching/OverrideResult.cs deleted file mode 100644 index e2a168aaeb..0000000000 --- a/src/Microsoft.AspNetCore.ResponseCaching/OverrideResult.cs +++ /dev/null @@ -1,23 +0,0 @@ -// 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 enum OverrideResult - { - /// - /// Use the default logic for determining cacheability. - /// - UseDefaultLogic, - - /// - /// Ignore default logic and do not cache. - /// - DoNotCache, - - /// - /// Ignore default logic and cache. - /// - Cache - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs index 142ded2e5a..cd34857e84 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs @@ -4,15 +4,12 @@ using System; using System.IO; using System.Globalization; -using System.Linq; -using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Headers; using Microsoft.AspNetCore.ResponseCaching.Internal; -using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; @@ -20,54 +17,37 @@ namespace Microsoft.AspNetCore.ResponseCaching { internal class ResponseCachingContext { - private static readonly CacheControlHeaderValue EmptyCacheControl = new CacheControlHeaderValue(); - // Use the record separator for delimiting components of the cache key to avoid possible collisions - private static readonly char KeyDelimiter = '\x1e'; - private readonly HttpContext _httpContext; private readonly IResponseCache _cache; private readonly ResponseCachingOptions _options; - private readonly ObjectPool _builderPool; - private readonly IResponseCachingCacheabilityValidator _cacheabilityValidator; - private readonly IResponseCachingCacheKeyModifier _cacheKeyModifier; + private readonly ICacheabilityValidator _cacheabilityValidator; + private readonly IKeyProvider _keyProvider; - private string _cacheKey; - private ResponseType? _responseType; - private RequestHeaders _requestHeaders; - private ResponseHeaders _responseHeaders; - private CacheControlHeaderValue _requestCacheControl; - private CacheControlHeaderValue _responseCacheControl; - private bool? _cacheResponse; - private CachedResponse _cachedResponse; - private TimeSpan _cachedResponseValidFor; - internal DateTimeOffset _responseTime; + private ResponseCachingState _state; - // Internal for testing internal ResponseCachingContext( HttpContext httpContext, IResponseCache cache, ResponseCachingOptions options, - ObjectPool builderPool, - IResponseCachingCacheabilityValidator cacheabilityValidator, - IResponseCachingCacheKeyModifier cacheKeyModifier) + ICacheabilityValidator cacheabilityValidator, + IKeyProvider keyProvider) { _httpContext = httpContext; _cache = cache; _options = options; - _builderPool = builderPool; _cacheabilityValidator = cacheabilityValidator; - _cacheKeyModifier = cacheKeyModifier; + _keyProvider = keyProvider; } - internal bool CacheResponse + internal ResponseCachingState State { get { - if (_cacheResponse == null) + if (_state == null) { - _cacheResponse = ResponseIsCacheable(); + _state = _httpContext.GetResponseCachingState(); } - return _cacheResponse.Value; + return _state; } } @@ -79,349 +59,17 @@ namespace Microsoft.AspNetCore.ResponseCaching private IHttpSendFileFeature OriginalSendFileFeature { get; set; } - private RequestHeaders RequestHeaders - { - get - { - if (_requestHeaders == null) - { - _requestHeaders = _httpContext.Request.GetTypedHeaders(); - } - return _requestHeaders; - } - } - - private ResponseHeaders ResponseHeaders - { - get - { - if (_responseHeaders == null) - { - _responseHeaders = _httpContext.Response.GetTypedHeaders(); - } - return _responseHeaders; - } - } - - private CacheControlHeaderValue RequestCacheControl - { - get - { - if (_requestCacheControl == null) - { - _requestCacheControl = RequestHeaders.CacheControl ?? EmptyCacheControl; - } - return _requestCacheControl; - } - } - - private CacheControlHeaderValue ResponseCacheControl - { - get - { - if (_responseCacheControl == null) - { - _responseCacheControl = ResponseHeaders.CacheControl ?? EmptyCacheControl; - } - return _responseCacheControl; - } - } - - // GET;/PATH;VaryBy - // TODO: Method invariant retrieval? E.g. HEAD after GET to the same resource. - internal string CreateCacheKey() - { - return CreateCacheKey(varyBy: null); - } - - internal string CreateCacheKey(CachedVaryBy varyBy) - { - var request = _httpContext.Request; - var builder = _builderPool.Get(); - - try - { - // Prepend custom cache key prefix - var customKeyPrefix = _cacheKeyModifier.CreatKeyPrefix(_httpContext); - if (!string.IsNullOrEmpty(customKeyPrefix)) - { - builder.Append(customKeyPrefix) - .Append(KeyDelimiter); - } - - // Default key - builder - .Append(request.Method.ToUpperInvariant()) - .Append(KeyDelimiter) - .Append(_options.CaseSensitivePaths ? request.Path.Value : request.Path.Value.ToUpperInvariant()); - - // Vary by headers - if (varyBy?.Headers.Count > 0) - { - // Append a group separator for the header segment of the cache key - builder.Append(KeyDelimiter) - .Append('H'); - - // TODO: resolve key format and delimiters - foreach (var header in varyBy.Headers) - { - // TODO: Normalization of order, case? - var value = _httpContext.Request.Headers[header]; - - // TODO: How to handle null/empty string? - if (StringValues.IsNullOrEmpty(value)) - { - value = "null"; - } - - builder.Append(KeyDelimiter) - .Append(header) - .Append("=") - .Append(value); - } - } - - // Vary by query params - if (varyBy?.Params.Count > 0) - { - // Append a group separator for the query parameter segment of the cache key - builder.Append(KeyDelimiter) - .Append('Q'); - - if (varyBy.Params.Count == 1 && string.Equals(varyBy.Params[0], "*", StringComparison.Ordinal)) - { - // Vary by all available query params - foreach (var query in _httpContext.Request.Query.OrderBy(q => q.Key, StringComparer.OrdinalIgnoreCase)) - { - builder.Append(KeyDelimiter) - .Append(query.Key.ToUpperInvariant()) - .Append("=") - .Append(query.Value); - } - } - else - { - // TODO: resolve key format and delimiters - foreach (var param in varyBy.Params) - { - // TODO: Normalization of order, case? - var value = _httpContext.Request.Query[param]; - - // TODO: How to handle null/empty string? - if (StringValues.IsNullOrEmpty(value)) - { - value = "null"; - } - - builder.Append(KeyDelimiter) - .Append(param) - .Append("=") - .Append(value); - } - } - } - - return builder.ToString(); - } - finally - { - _builderPool.Return(builder); - } - } - - internal bool RequestIsCacheable() - { - // Use optional override if specified by user - switch(_cacheabilityValidator.RequestIsCacheableOverride(_httpContext)) - { - case OverrideResult.UseDefaultLogic: - break; - case OverrideResult.DoNotCache: - return false; - case OverrideResult.Cache: - return true; - default: - throw new NotSupportedException($"Unrecognized result from {nameof(_cacheabilityValidator.RequestIsCacheableOverride)}."); - } - - // Verify the method - // TODO: RFC lists POST as a cacheable method when explicit freshness information is provided, but this is not widely implemented. Will revisit. - var request = _httpContext.Request; - if (string.Equals("GET", request.Method, StringComparison.OrdinalIgnoreCase)) - { - _responseType = ResponseType.FullReponse; - } - else if (string.Equals("HEAD", request.Method, StringComparison.OrdinalIgnoreCase)) - { - _responseType = ResponseType.HeadersOnly; - } - else - { - return false; - } - - // Verify existence of authorization headers - // TODO: The server may indicate that the response to these request are cacheable - if (!string.IsNullOrEmpty(request.Headers[HeaderNames.Authorization])) - { - return false; - } - - // Verify request cache-control parameters - // TODO: no-cache requests can be retrieved upon validation with origin - if (!string.IsNullOrEmpty(request.Headers[HeaderNames.CacheControl])) - { - if (RequestCacheControl.NoCache) - { - return false; - } - } - else - { - // Support for legacy HTTP 1.0 cache directive - var pragmaHeaderValues = request.Headers[HeaderNames.Pragma]; - foreach (var directive in pragmaHeaderValues) - { - if (string.Equals("no-cache", directive, StringComparison.OrdinalIgnoreCase)) - { - return false; - } - } - } - - // TODO: Verify global middleware settings? Explicit ignore list, range requests, etc. - return true; - } - - internal bool ResponseIsCacheable() - { - // Use optional override if specified by user - switch (_cacheabilityValidator.ResponseIsCacheableOverride(_httpContext)) - { - case OverrideResult.UseDefaultLogic: - break; - case OverrideResult.DoNotCache: - return false; - case OverrideResult.Cache: - return true; - default: - throw new NotSupportedException($"Unrecognized result from {nameof(_cacheabilityValidator.ResponseIsCacheableOverride)}."); - } - - // Only cache pages explicitly marked with public - // TODO: Consider caching responses that are not marked as public but otherwise cacheable? - if (!ResponseCacheControl.Public) - { - return false; - } - - // Check no-store - if (RequestCacheControl.NoStore || ResponseCacheControl.NoStore) - { - return false; - } - - // Check no-cache - // TODO: Handle no-cache with headers - if (ResponseCacheControl.NoCache) - { - return false; - } - - var response = _httpContext.Response; - - // Do not cache responses varying by * - if (string.Equals(response.Headers[HeaderNames.Vary], "*", StringComparison.OrdinalIgnoreCase)) - { - return false; - } - - // TODO: public MAY override the cacheability checks for private and status codes - - // Check private - if (ResponseCacheControl.Private) - { - return false; - } - - // Check response code - // TODO: RFC also lists 203, 204, 206, 300, 301, 404, 405, 410, 414, and 501 as cacheable by default - if (response.StatusCode != StatusCodes.Status200OK) - { - return false; - } - - // Check response freshness - // TODO: apparent age vs corrected age value - var responseAge = _responseTime - ResponseHeaders.Date ?? TimeSpan.Zero; - if (!EntryIsFresh(ResponseHeaders, responseAge, verifyAgainstRequest: false)) - { - return false; - } - - return true; - } - - internal bool EntryIsFresh(ResponseHeaders responseHeaders, TimeSpan age, bool verifyAgainstRequest) - { - var responseCacheControl = responseHeaders.CacheControl ?? EmptyCacheControl; - - // Add min-fresh requirements - if (verifyAgainstRequest) - { - age += RequestCacheControl.MinFresh ?? TimeSpan.Zero; - } - - // Validate shared max age, this overrides any max age settings for shared caches - if (age > responseCacheControl.SharedMaxAge) - { - // shared max age implies must revalidate - return false; - } - else if (responseCacheControl.SharedMaxAge == null) - { - // Validate max age - if (age > responseCacheControl.MaxAge || (verifyAgainstRequest && age > RequestCacheControl.MaxAge)) - { - // Must revalidate - if (responseCacheControl.MustRevalidate) - { - return false; - } - - // Request allows stale values - if (verifyAgainstRequest && age < RequestCacheControl.MaxStaleLimit) - { - // TODO: Add warning header indicating the response is stale - return true; - } - - return false; - } - else if (responseCacheControl.MaxAge == null && (!verifyAgainstRequest || RequestCacheControl.MaxAge == null)) - { - // Validate expiration - if (_responseTime > responseHeaders.Expires) - { - return false; - } - } - } - - return true; - } - internal async Task TryServeFromCacheAsync() { - _cacheKey = CreateCacheKey(); - var cacheEntry = _cache.Get(_cacheKey); + State.BaseKey = _keyProvider.CreateBaseKey(_httpContext); + var cacheEntry = _cache.Get(State.BaseKey); var responseServed = false; - if (cacheEntry is CachedVaryBy) + if (cacheEntry is CachedVaryRules) { - // Request contains VaryBy rules, recompute key and try again - _cacheKey = CreateCacheKey(cacheEntry as CachedVaryBy); - cacheEntry = _cache.Get(_cacheKey); + // Request contains vary rules, recompute key and try again + var varyKey = _keyProvider.CreateVaryKey(_httpContext, ((CachedVaryRules)cacheEntry).VaryRules); + cacheEntry = _cache.Get(varyKey); } if (cacheEntry is CachedResponse) @@ -429,17 +77,18 @@ namespace Microsoft.AspNetCore.ResponseCaching var cachedResponse = cacheEntry as CachedResponse; var cachedResponseHeaders = new ResponseHeaders(cachedResponse.Headers); - _responseTime = _options.SystemClock.UtcNow; - var age = _responseTime - cachedResponse.Created; - age = age > TimeSpan.Zero ? age : TimeSpan.Zero; + State.ResponseTime = _options.SystemClock.UtcNow; + var cachedEntryAge = State.ResponseTime - cachedResponse.Created; + State.CachedEntryAge = cachedEntryAge > TimeSpan.Zero ? cachedEntryAge : TimeSpan.Zero; - if (EntryIsFresh(cachedResponseHeaders, age, verifyAgainstRequest: true)) + if (_cacheabilityValidator.CachedEntryIsFresh(_httpContext, cachedResponseHeaders)) { + responseServed = true; + // Check conditional request rules if (ConditionalRequestSatisfied(cachedResponseHeaders)) { _httpContext.Response.StatusCode = StatusCodes.Status304NotModified; - responseServed = true; } else { @@ -451,33 +100,20 @@ namespace Microsoft.AspNetCore.ResponseCaching response.Headers.Add(header); } - response.Headers[HeaderNames.Age] = age.TotalSeconds.ToString("F0", CultureInfo.InvariantCulture); + response.Headers[HeaderNames.Age] = State.CachedEntryAge.TotalSeconds.ToString("F0", CultureInfo.InvariantCulture); - if (_responseType == ResponseType.HeadersOnly) - { - responseServed = true; - } - else if (_responseType == ResponseType.FullReponse) - { - // Copy the cached response body - var body = cachedResponse.Body; + var body = cachedResponse.Body; + + // Copy the cached response body + if (body.Length > 0) + { // Add a content-length if required - if (response.ContentLength == null && string.IsNullOrEmpty(response.Headers[HeaderNames.TransferEncoding])) + if (response.ContentLength == null && StringValues.IsNullOrEmpty(response.Headers[HeaderNames.TransferEncoding])) { response.ContentLength = body.Length; } - - if (body.Length > 0) - { - await response.Body.WriteAsync(body, 0, body.Length); - } - - responseServed = true; - } - else - { - throw new InvalidOperationException($"{nameof(_responseType)} not specified or is unrecognized."); + await response.Body.WriteAsync(body, 0, body.Length); } } } @@ -487,7 +123,7 @@ namespace Microsoft.AspNetCore.ResponseCaching } } - if (!responseServed && RequestCacheControl.OnlyIfCached) + if (!responseServed && State.RequestCacheControl.OnlyIfCached) { _httpContext.Response.StatusCode = StatusCodes.Status504GatewayTimeout; responseServed = true; @@ -498,7 +134,7 @@ namespace Microsoft.AspNetCore.ResponseCaching internal bool ConditionalRequestSatisfied(ResponseHeaders cachedResponseHeaders) { - var ifNoneMatchHeader = RequestHeaders.IfNoneMatch; + var ifNoneMatchHeader = State.RequestHeaders.IfNoneMatch; if (ifNoneMatchHeader != null) { @@ -518,7 +154,7 @@ namespace Microsoft.AspNetCore.ResponseCaching } } } - else if ((cachedResponseHeaders.LastModified ?? cachedResponseHeaders.Date) <= RequestHeaders.IfUnmodifiedSince) + else if ((cachedResponseHeaders.LastModified ?? cachedResponseHeaders.Date) <= State.RequestHeaders.IfUnmodifiedSince) { return true; } @@ -528,57 +164,56 @@ namespace Microsoft.AspNetCore.ResponseCaching internal void FinalizeCachingHeaders() { - if (CacheResponse) + if (_cacheabilityValidator.ResponseIsCacheable(_httpContext)) { + State.ShouldCacheResponse = true; + // Create the cache entry now var response = _httpContext.Response; var varyHeaderValue = response.Headers[HeaderNames.Vary]; - var varyParamsValue = _httpContext.GetResponseCachingFeature().VaryByParams; - _cachedResponseValidFor = ResponseCacheControl.SharedMaxAge - ?? ResponseCacheControl.MaxAge - ?? (ResponseHeaders.Expires - _responseTime) + var varyParamsValue = _httpContext.GetResponseCachingFeature()?.VaryParams ?? StringValues.Empty; + State.CachedResponseValidFor = State.ResponseCacheControl.SharedMaxAge + ?? State.ResponseCacheControl.MaxAge + ?? (State.ResponseHeaders.Expires - State.ResponseTime) // TODO: Heuristics for expiration? ?? TimeSpan.FromSeconds(10); - // Check if any VaryBy rules exist + // Check if any vary rules exist if (!StringValues.IsNullOrEmpty(varyHeaderValue) || !StringValues.IsNullOrEmpty(varyParamsValue)) { - if (varyParamsValue.Count > 1) + var cachedVaryRules = new CachedVaryRules { - Array.Sort(varyParamsValue.ToArray(), StringComparer.OrdinalIgnoreCase); - } - - var cachedVaryBy = new CachedVaryBy - { - // TODO: VaryBy Encoding - Headers = varyHeaderValue, - Params = varyParamsValue + VaryRules = new VaryRules() + { + // TODO: Vary Encoding + Headers = varyHeaderValue, + Params = varyParamsValue + } }; // TODO: Overwrite? - _cache.Set(_cacheKey, cachedVaryBy, _cachedResponseValidFor); - _cacheKey = CreateCacheKey(cachedVaryBy); + _cache.Set(State.BaseKey, cachedVaryRules, State.CachedResponseValidFor); + State.VaryKey = _keyProvider.CreateVaryKey(_httpContext, cachedVaryRules.VaryRules); } // Ensure date header is set - if (ResponseHeaders.Date == null) + if (State.ResponseHeaders.Date == null) { - ResponseHeaders.Date = _responseTime; + State.ResponseHeaders.Date = State.ResponseTime; } // Store the response to cache - _cachedResponse = new CachedResponse + State.CachedResponse = new CachedResponse { - Created = ResponseHeaders.Date.Value, + Created = State.ResponseHeaders.Date.Value, StatusCode = _httpContext.Response.StatusCode }; - foreach (var header in ResponseHeaders.Headers) + foreach (var header in State.ResponseHeaders.Headers) { - if (!string.Equals(header.Key, HeaderNames.Age, StringComparison.OrdinalIgnoreCase) - && !string.Equals(header.Key, HeaderNames.SetCookie, StringComparison.OrdinalIgnoreCase)) + if (!string.Equals(header.Key, HeaderNames.Age, StringComparison.OrdinalIgnoreCase)) { - _cachedResponse.Headers.Add(header); + State.CachedResponse.Headers.Add(header); } } } @@ -590,11 +225,11 @@ namespace Microsoft.AspNetCore.ResponseCaching internal void FinalizeCachingBody() { - if (CacheResponse && ResponseCacheStream.BufferingEnabled) + if (State.ShouldCacheResponse && ResponseCacheStream.BufferingEnabled) { - _cachedResponse.Body = ResponseCacheStream.BufferedStream.ToArray(); + State.CachedResponse.Body = ResponseCacheStream.BufferedStream.ToArray(); - _cache.Set(_cacheKey, _cachedResponse, _cachedResponseValidFor); + _cache.Set(State.VaryKey ?? State.BaseKey, State.CachedResponse, State.CachedResponseValidFor); } } @@ -603,7 +238,7 @@ namespace Microsoft.AspNetCore.ResponseCaching if (!ResponseStarted) { ResponseStarted = true; - _responseTime = _options.SystemClock.UtcNow; + State.ResponseTime = _options.SystemClock.UtcNow; FinalizeCachingHeaders(); } @@ -640,11 +275,5 @@ namespace Microsoft.AspNetCore.ResponseCaching // TODO: Move this temporary interface with endpoint to HttpAbstractions _httpContext.RemoveResponseCachingFeature(); } - - private enum ResponseType - { - HeadersOnly = 0, - FullReponse = 1 - } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingFeature.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingFeature.cs index 4341f20ae7..e02c8e28ec 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingFeature.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingFeature.cs @@ -8,6 +8,6 @@ namespace Microsoft.AspNetCore.ResponseCaching // TODO: Temporary interface for endpoints to specify options for response caching public class ResponseCachingFeature { - public StringValues VaryByParams { get; set; } + public StringValues VaryParams { get; set; } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs index 240c44b4d5..f5b62fd2ae 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs @@ -2,12 +2,11 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.ResponseCaching.Internal; using Microsoft.Extensions.Internal; -using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.ResponseCaching @@ -23,17 +22,15 @@ namespace Microsoft.AspNetCore.ResponseCaching private readonly RequestDelegate _next; private readonly IResponseCache _cache; private readonly ResponseCachingOptions _options; - private readonly ObjectPool _builderPool; - private readonly IResponseCachingCacheabilityValidator _cacheabilityValidator; - private readonly IResponseCachingCacheKeyModifier _cacheKeyModifier; + private readonly ICacheabilityValidator _cacheabilityValidator; + private readonly IKeyProvider _keyProvider; public ResponseCachingMiddleware( RequestDelegate next, IResponseCache cache, IOptions options, - ObjectPoolProvider poolProvider, - IResponseCachingCacheabilityValidator cacheabilityValidator, - IResponseCachingCacheKeyModifier cacheKeyModifier) + ICacheabilityValidator cacheabilityValidator, + IKeyProvider keyProvider) { if (next == null) { @@ -47,71 +44,74 @@ namespace Microsoft.AspNetCore.ResponseCaching { throw new ArgumentNullException(nameof(options)); } - if (poolProvider == null) - { - throw new ArgumentNullException(nameof(poolProvider)); - } if (cacheabilityValidator == null) { throw new ArgumentNullException(nameof(cacheabilityValidator)); } - if (cacheKeyModifier == null) + if (keyProvider == null) { - throw new ArgumentNullException(nameof(cacheKeyModifier)); + throw new ArgumentNullException(nameof(keyProvider)); } _next = next; _cache = cache; _options = options.Value; - _builderPool = poolProvider.CreateStringBuilderPool(); _cacheabilityValidator = cacheabilityValidator; - _cacheKeyModifier = cacheKeyModifier; + _keyProvider = keyProvider; } public async Task Invoke(HttpContext context) { - var cachingContext = new ResponseCachingContext( - context, - _cache, - _options, - _builderPool, - _cacheabilityValidator, - _cacheKeyModifier); + context.AddResponseCachingState(); - // Should we attempt any caching logic? - if (cachingContext.RequestIsCacheable()) + try { - // Can this request be served from cache? - if (await cachingContext.TryServeFromCacheAsync()) + var cachingContext = new ResponseCachingContext( + context, + _cache, + _options, + _cacheabilityValidator, + _keyProvider); + + // Should we attempt any caching logic? + if (_cacheabilityValidator.RequestIsCacheable(context)) { - return; + // Can this request be served from cache? + if (await cachingContext.TryServeFromCacheAsync()) + { + return; + } + + // Hook up to listen to the response stream + cachingContext.ShimResponseStream(); + + try + { + // Subscribe to OnStarting event + context.Response.OnStarting(OnStartingCallback, cachingContext); + + await _next(context); + + // If there was no response body, check the response headers now. We can cache things like redirects. + cachingContext.OnResponseStarting(); + + // Finalize the cache entry + cachingContext.FinalizeCachingBody(); + } + finally + { + cachingContext.UnshimResponseStream(); + } } - - // Hook up to listen to the response stream - cachingContext.ShimResponseStream(); - - try + else { - // Subscribe to OnStarting event - context.Response.OnStarting(OnStartingCallback, cachingContext); - + // TODO: Invalidate resources for successful unsafe methods? Required by RFC await _next(context); - - // If there was no response body, check the response headers now. We can cache things like redirects. - cachingContext.OnResponseStarting(); - - // Finalize the cache entry - cachingContext.FinalizeCachingBody(); - } - finally - { - cachingContext.UnshimResponseStream(); } } - else + finally { - // TODO: Invalidate resources for successful unsafe methods? Required by RFC - await _next(context); + context.RemoveResponseCachingState(); } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/CachedVaryBy.cs b/src/Microsoft.AspNetCore.ResponseCaching/VaryRules.cs similarity index 78% rename from src/Microsoft.AspNetCore.ResponseCaching/Internal/CachedVaryBy.cs rename to src/Microsoft.AspNetCore.ResponseCaching/VaryRules.cs index 1fb1a4501d..46d9419528 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/CachedVaryBy.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/VaryRules.cs @@ -3,9 +3,9 @@ using Microsoft.Extensions.Primitives; -namespace Microsoft.AspNetCore.ResponseCaching.Internal +namespace Microsoft.AspNetCore.ResponseCaching { - internal class CachedVaryBy + public class VaryRules { internal StringValues Headers { get; set; } internal StringValues Params { get; set; } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheEntrySerializerTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheEntrySerializerTests.cs new file mode 100644 index 0000000000..e85dabe9f6 --- /dev/null +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheEntrySerializerTests.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.Linq; +using System.Text; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.ResponseCaching.Internal; +using Xunit; + +namespace Microsoft.AspNetCore.ResponseCaching.Tests +{ + public class CacheEntrySerializerTests + { + [Fact] + public void Serialize_NullObject_Throws() + { + Assert.Throws(() => CacheEntrySerializer.Serialize(null)); + } + + [Fact] + public void Serialize_UnknownObject_Throws() + { + Assert.Throws(() => CacheEntrySerializer.Serialize(new object())); + } + + [Fact] + public void RoundTrip_CachedResponses_Succeeds() + { + var headers = new HeaderDictionary(); + headers["keyA"] = "valueA"; + headers["keyB"] = "valueB"; + var cachedEntry = new CachedResponse() + { + Created = DateTimeOffset.UtcNow, + StatusCode = StatusCodes.Status200OK, + Body = Encoding.ASCII.GetBytes("Hello world"), + Headers = headers + }; + + AssertCachedResponsesEqual(cachedEntry, (CachedResponse)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedEntry))); + } + + [Fact] + public void RoundTrip_Empty_CachedVaryRules_Succeeds() + { + var cachedVaryRules = new CachedVaryRules(); + + AssertCachedVaryRulesEqual(cachedVaryRules, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRules))); + } + + [Fact] + public void RoundTrip_CachedVaryRules_EmptyRules_Succeeds() + { + var cachedVaryRules = new CachedVaryRules() + { + VaryRules = new VaryRules() + }; + + AssertCachedVaryRulesEqual(cachedVaryRules, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRules))); + } + + [Fact] + public void RoundTrip_HeadersOnly_CachedVaryRules_Succeeds() + { + var headers = new[] { "headerA", "headerB" }; + var cachedVaryRules = new CachedVaryRules() + { + VaryRules = new VaryRules() + { + Headers = headers + } + }; + + AssertCachedVaryRulesEqual(cachedVaryRules, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRules))); + } + + [Fact] + public void RoundTrip_ParamsOnly_CachedVaryRules_Succeeds() + { + var param = new[] { "paramA", "paramB" }; + var cachedVaryRules = new CachedVaryRules() + { + VaryRules = new VaryRules() + { + Params = param + } + }; + + AssertCachedVaryRulesEqual(cachedVaryRules, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRules))); + } + + [Fact] + public void RoundTrip_HeadersAndParams_CachedVaryRules_Succeeds() + { + var headers = new[] { "headerA", "headerB" }; + var param = new[] { "paramA", "paramB" }; + var cachedVaryRules = new CachedVaryRules() + { + VaryRules = new VaryRules() + { + Headers = headers, + Params = param + } + }; + + AssertCachedVaryRulesEqual(cachedVaryRules, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRules))); + } + + [Fact] + public void Deserialize_InvalidEntries_ReturnsNull() + { + var headers = new[] { "headerA", "headerB" }; + var cachedVaryRules = new CachedVaryRules() + { + VaryRules = new VaryRules() + { + Headers = headers + } + }; + var serializedEntry = CacheEntrySerializer.Serialize(cachedVaryRules); + Array.Reverse(serializedEntry); + + Assert.Null(CacheEntrySerializer.Deserialize(serializedEntry)); + } + + private static void AssertCachedResponsesEqual(CachedResponse expected, CachedResponse actual) + { + Assert.NotNull(actual); + Assert.NotNull(expected); + Assert.Equal(expected.Created, actual.Created); + Assert.Equal(expected.StatusCode, actual.StatusCode); + Assert.Equal(expected.Headers.Count, actual.Headers.Count); + foreach (var expectedHeader in expected.Headers) + { + Assert.Equal(expectedHeader.Value, actual.Headers[expectedHeader.Key]); + } + Assert.True(expected.Body.SequenceEqual(actual.Body)); + } + + private static void AssertCachedVaryRulesEqual(CachedVaryRules expected, CachedVaryRules actual) + { + Assert.NotNull(actual); + Assert.NotNull(expected); + if (expected.VaryRules == null) + { + Assert.Null(actual.VaryRules); + } + else + { + Assert.NotNull(actual.VaryRules); + Assert.Equal(expected.VaryRules.Headers, actual.VaryRules.Headers); + Assert.Equal(expected.VaryRules.Params, actual.VaryRules.Params); + } + } + } +} diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheabilityValidatorTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheabilityValidatorTests.cs new file mode 100644 index 0000000000..5ead7ab80e --- /dev/null +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheabilityValidatorTests.cs @@ -0,0 +1,624 @@ +// 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.AspNetCore.Http.Headers; +using Microsoft.AspNetCore.ResponseCaching.Internal; +using Microsoft.Net.Http.Headers; +using Xunit; + +namespace Microsoft.AspNetCore.ResponseCaching.Tests +{ + public class CacheabilityValidatorTests + { + [Theory] + [InlineData("GET")] + [InlineData("HEAD")] + public void RequestIsCacheable_CacheableMethods_Allowed(string method) + { + var httpContext = CreateDefaultContext(); + httpContext.Request.Method = method; + + Assert.True(new CacheabilityValidator().RequestIsCacheable(httpContext)); + } + + [Theory] + [InlineData("POST")] + [InlineData("OPTIONS")] + [InlineData("PUT")] + [InlineData("DELETE")] + [InlineData("TRACE")] + [InlineData("CONNECT")] + [InlineData("")] + [InlineData(null)] + public void RequestIsCacheable_UncacheableMethods_NotAllowed(string method) + { + var httpContext = CreateDefaultContext(); + httpContext.Request.Method = method; + + Assert.False(new CacheabilityValidator().RequestIsCacheable(httpContext)); + } + + [Fact] + public void RequestIsCacheable_AuthorizationHeaders_NotAllowed() + { + var httpContext = CreateDefaultContext(); + httpContext.Request.Method = "GET"; + httpContext.Request.Headers[HeaderNames.Authorization] = "Basic plaintextUN:plaintextPW"; + + Assert.False(new CacheabilityValidator().RequestIsCacheable(httpContext)); + } + + [Fact] + public void RequestIsCacheable_NoCache_NotAllowed() + { + var httpContext = CreateDefaultContext(); + httpContext.Request.Method = "GET"; + httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + NoCache = true + }; + + Assert.False(new CacheabilityValidator().RequestIsCacheable(httpContext)); + } + + [Fact] + public void RequestIsCacheable_NoStore_Allowed() + { + var httpContext = CreateDefaultContext(); + httpContext.Request.Method = "GET"; + httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + NoStore = true + }; + + Assert.True(new CacheabilityValidator().RequestIsCacheable(httpContext)); + } + + [Fact] + public void RequestIsCacheable_LegacyDirectives_NotAllowed() + { + var httpContext = CreateDefaultContext(); + httpContext.Request.Method = "GET"; + httpContext.Request.Headers[HeaderNames.Pragma] = "no-cache"; + + Assert.False(new CacheabilityValidator().RequestIsCacheable(httpContext)); + } + + [Fact] + public void RequestIsCacheable_LegacyDirectives_OverridenByCacheControl() + { + var httpContext = CreateDefaultContext(); + httpContext.Request.Method = "GET"; + httpContext.Request.Headers[HeaderNames.Pragma] = "no-cache"; + httpContext.Request.Headers[HeaderNames.CacheControl] = "max-age=10"; + + Assert.True(new CacheabilityValidator().RequestIsCacheable(httpContext)); + } + + [Fact] + public void ResponseIsCacheable_NoPublic_NotAllowed() + { + var httpContext = CreateDefaultContext(); + + Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext)); + } + + [Fact] + public void ResponseIsCacheable_Public_Allowed() + { + var httpContext = CreateDefaultContext(); + httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + Public = true + }; + + Assert.True(new CacheabilityValidator().ResponseIsCacheable(httpContext)); + } + + [Fact] + public void ResponseIsCacheable_NoCache_NotAllowed() + { + var httpContext = CreateDefaultContext(); + httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + Public = true, + NoCache = true + }; + + Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext)); + } + + [Fact] + public void ResponseIsCacheable_RequestNoStore_NotAllowed() + { + var httpContext = CreateDefaultContext(); + httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + NoStore = true + }; + httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + Public = true + }; + + Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext)); + } + + [Fact] + public void ResponseIsCacheable_ResponseNoStore_NotAllowed() + { + var httpContext = CreateDefaultContext(); + httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + Public = true, + NoStore = true + }; + + Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext)); + } + + [Fact] + public void ResponseIsCacheable_SetCookieHeader_NotAllowed() + { + var httpContext = CreateDefaultContext(); + httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + Public = true + }; + httpContext.Response.Headers[HeaderNames.SetCookie] = "cookieName=cookieValue"; + + Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext)); + } + + [Fact] + public void ResponseIsCacheable_VaryHeaderByStar_NotAllowed() + { + var httpContext = CreateDefaultContext(); + httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + Public = true + }; + httpContext.Response.Headers[HeaderNames.Vary] = "*"; + + Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext)); + } + + [Fact] + public void ResponseIsCacheable_Private_NotAllowed() + { + var httpContext = CreateDefaultContext(); + httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + Public = true, + Private = true + }; + + Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext)); + } + + [Theory] + [InlineData(StatusCodes.Status200OK)] + public void ResponseIsCacheable_SuccessStatusCodes_Allowed(int statusCode) + { + var httpContext = CreateDefaultContext(); + httpContext.Response.StatusCode = statusCode; + httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + Public = true + }; + + Assert.True(new CacheabilityValidator().ResponseIsCacheable(httpContext)); + } + + [Theory] + [InlineData(StatusCodes.Status201Created)] + [InlineData(StatusCodes.Status202Accepted)] + [InlineData(StatusCodes.Status203NonAuthoritative)] + [InlineData(StatusCodes.Status204NoContent)] + [InlineData(StatusCodes.Status205ResetContent)] + [InlineData(StatusCodes.Status206PartialContent)] + [InlineData(StatusCodes.Status207MultiStatus)] + [InlineData(StatusCodes.Status300MultipleChoices)] + [InlineData(StatusCodes.Status301MovedPermanently)] + [InlineData(StatusCodes.Status302Found)] + [InlineData(StatusCodes.Status303SeeOther)] + [InlineData(StatusCodes.Status304NotModified)] + [InlineData(StatusCodes.Status305UseProxy)] + [InlineData(StatusCodes.Status306SwitchProxy)] + [InlineData(StatusCodes.Status307TemporaryRedirect)] + [InlineData(StatusCodes.Status308PermanentRedirect)] + [InlineData(StatusCodes.Status400BadRequest)] + [InlineData(StatusCodes.Status401Unauthorized)] + [InlineData(StatusCodes.Status402PaymentRequired)] + [InlineData(StatusCodes.Status403Forbidden)] + [InlineData(StatusCodes.Status404NotFound)] + [InlineData(StatusCodes.Status405MethodNotAllowed)] + [InlineData(StatusCodes.Status406NotAcceptable)] + [InlineData(StatusCodes.Status407ProxyAuthenticationRequired)] + [InlineData(StatusCodes.Status408RequestTimeout)] + [InlineData(StatusCodes.Status409Conflict)] + [InlineData(StatusCodes.Status410Gone)] + [InlineData(StatusCodes.Status411LengthRequired)] + [InlineData(StatusCodes.Status412PreconditionFailed)] + [InlineData(StatusCodes.Status413RequestEntityTooLarge)] + [InlineData(StatusCodes.Status414RequestUriTooLong)] + [InlineData(StatusCodes.Status415UnsupportedMediaType)] + [InlineData(StatusCodes.Status416RequestedRangeNotSatisfiable)] + [InlineData(StatusCodes.Status417ExpectationFailed)] + [InlineData(StatusCodes.Status418ImATeapot)] + [InlineData(StatusCodes.Status419AuthenticationTimeout)] + [InlineData(StatusCodes.Status422UnprocessableEntity)] + [InlineData(StatusCodes.Status423Locked)] + [InlineData(StatusCodes.Status424FailedDependency)] + [InlineData(StatusCodes.Status451UnavailableForLegalReasons)] + [InlineData(StatusCodes.Status500InternalServerError)] + [InlineData(StatusCodes.Status501NotImplemented)] + [InlineData(StatusCodes.Status502BadGateway)] + [InlineData(StatusCodes.Status503ServiceUnavailable)] + [InlineData(StatusCodes.Status504GatewayTimeout)] + [InlineData(StatusCodes.Status505HttpVersionNotsupported)] + [InlineData(StatusCodes.Status506VariantAlsoNegotiates)] + [InlineData(StatusCodes.Status507InsufficientStorage)] + public void ResponseIsCacheable_NonSuccessStatusCodes_NotAllowed(int statusCode) + { + var httpContext = CreateDefaultContext(); + httpContext.Response.StatusCode = statusCode; + httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + Public = true + }; + + Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext)); + } + + [Fact] + public void ResponseIsCacheable_NoExpiryRequirements_IsAllowed() + { + var httpContext = CreateDefaultContext(); + httpContext.Response.StatusCode = StatusCodes.Status200OK; + var headers = httpContext.Response.GetTypedHeaders(); + headers.CacheControl = new CacheControlHeaderValue() + { + Public = true + }; + + var utcNow = DateTimeOffset.UtcNow; + headers.Date = utcNow; + httpContext.GetResponseCachingState().ResponseTime = DateTimeOffset.MaxValue; + + Assert.True(new CacheabilityValidator().ResponseIsCacheable(httpContext)); + } + + [Fact] + public void ResponseIsCacheable_PastExpiry_NotAllowed() + { + var httpContext = CreateDefaultContext(); + httpContext.Response.StatusCode = StatusCodes.Status200OK; + var headers = httpContext.Response.GetTypedHeaders(); + headers.CacheControl = new CacheControlHeaderValue() + { + Public = true + }; + var utcNow = DateTimeOffset.UtcNow; + headers.Expires = utcNow; + + headers.Date = utcNow; + httpContext.GetResponseCachingState().ResponseTime = DateTimeOffset.MaxValue; + + Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext)); + } + + [Fact] + public void ResponseIsCacheable_MaxAgeOverridesExpiry_ToAllowed() + { + var utcNow = DateTimeOffset.UtcNow; + var httpContext = CreateDefaultContext(); + httpContext.Response.StatusCode = StatusCodes.Status200OK; + var headers = httpContext.Response.GetTypedHeaders(); + headers.CacheControl = new CacheControlHeaderValue() + { + Public = true, + MaxAge = TimeSpan.FromSeconds(10) + }; + headers.Expires = utcNow; + headers.Date = utcNow; + httpContext.GetResponseCachingState().ResponseTime = utcNow + TimeSpan.FromSeconds(9); + + Assert.True(new CacheabilityValidator().ResponseIsCacheable(httpContext)); + } + + [Fact] + public void ResponseIsCacheable_MaxAgeOverridesExpiry_ToNotAllowed() + { + var utcNow = DateTimeOffset.UtcNow; + var httpContext = CreateDefaultContext(); + httpContext.Response.StatusCode = StatusCodes.Status200OK; + var headers = httpContext.Response.GetTypedHeaders(); + headers.CacheControl = new CacheControlHeaderValue() + { + Public = true, + MaxAge = TimeSpan.FromSeconds(10) + }; + headers.Expires = utcNow; + headers.Date = utcNow; + httpContext.GetResponseCachingState().ResponseTime = utcNow + TimeSpan.FromSeconds(11); + + Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext)); + } + + [Fact] + public void ResponseIsCacheable_SharedMaxAgeOverridesMaxAge_ToAllowed() + { + var utcNow = DateTimeOffset.UtcNow; + var httpContext = CreateDefaultContext(); + httpContext.Response.StatusCode = StatusCodes.Status200OK; + var headers = httpContext.Response.GetTypedHeaders(); + headers.CacheControl = new CacheControlHeaderValue() + { + Public = true, + MaxAge = TimeSpan.FromSeconds(10), + SharedMaxAge = TimeSpan.FromSeconds(15) + }; + headers.Date = utcNow; + httpContext.GetResponseCachingState().ResponseTime = utcNow + TimeSpan.FromSeconds(11); + + Assert.True(new CacheabilityValidator().ResponseIsCacheable(httpContext)); + } + + [Fact] + public void ResponseIsCacheable_SharedMaxAgeOverridesMaxAge_ToNotFresh() + { + var utcNow = DateTimeOffset.UtcNow; + var httpContext = CreateDefaultContext(); + httpContext.Response.StatusCode = StatusCodes.Status200OK; + var headers = httpContext.Response.GetTypedHeaders(); + headers.CacheControl = new CacheControlHeaderValue() + { + Public = true, + MaxAge = TimeSpan.FromSeconds(10), + SharedMaxAge = TimeSpan.FromSeconds(5) + }; + headers.Date = utcNow; + httpContext.GetResponseCachingState().ResponseTime = utcNow + TimeSpan.FromSeconds(6); + + Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext)); + } + + [Fact] + public void EntryIsFresh_NoExpiryRequirements_IsFresh() + { + var utcNow = DateTimeOffset.UtcNow; + var httpContext = CreateDefaultContext(); + httpContext.GetResponseCachingState().ResponseTime = DateTimeOffset.MaxValue; + var cachedHeaders = new ResponseHeaders(new HeaderDictionary()) + { + CacheControl = new CacheControlHeaderValue() + { + Public = true + } + }; + + Assert.True(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders)); + } + + [Fact] + public void EntryIsFresh_PastExpiry_IsNotFresh() + { + var httpContext = CreateDefaultContext(); + httpContext.GetResponseCachingState().ResponseTime = DateTimeOffset.MaxValue; + var cachedHeaders = new ResponseHeaders(new HeaderDictionary()) + { + CacheControl = new CacheControlHeaderValue() + { + Public = true + }, + Expires = DateTimeOffset.UtcNow + }; + + Assert.False(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders)); + } + + [Fact] + public void EntryIsFresh_MaxAgeOverridesExpiry_ToFresh() + { + var utcNow = DateTimeOffset.UtcNow; + var httpContext = CreateDefaultContext(); + var state = httpContext.GetResponseCachingState(); + state.CachedEntryAge = TimeSpan.FromSeconds(9); + state.ResponseTime = utcNow + state.CachedEntryAge; + var cachedHeaders = new ResponseHeaders(new HeaderDictionary()) + { + CacheControl = new CacheControlHeaderValue() + { + Public = true, + MaxAge = TimeSpan.FromSeconds(10) + }, + Expires = utcNow + }; + + Assert.True(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders)); + } + + [Fact] + public void EntryIsFresh_MaxAgeOverridesExpiry_ToNotFresh() + { + var utcNow = DateTimeOffset.UtcNow; + var httpContext = CreateDefaultContext(); + var state = httpContext.GetResponseCachingState(); + state.CachedEntryAge = TimeSpan.FromSeconds(11); + state.ResponseTime = utcNow + state.CachedEntryAge; + var cachedHeaders = new ResponseHeaders(new HeaderDictionary()) + { + CacheControl = new CacheControlHeaderValue() + { + Public = true, + MaxAge = TimeSpan.FromSeconds(10) + }, + Expires = utcNow + }; + + Assert.False(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders)); + } + + [Fact] + public void EntryIsFresh_SharedMaxAgeOverridesMaxAge_ToFresh() + { + var utcNow = DateTimeOffset.UtcNow; + var httpContext = CreateDefaultContext(); + var state = httpContext.GetResponseCachingState(); + state.CachedEntryAge = TimeSpan.FromSeconds(11); + state.ResponseTime = utcNow + state.CachedEntryAge; + var cachedHeaders = new ResponseHeaders(new HeaderDictionary()) + { + CacheControl = new CacheControlHeaderValue() + { + Public = true, + MaxAge = TimeSpan.FromSeconds(10), + SharedMaxAge = TimeSpan.FromSeconds(15) + }, + Expires = utcNow + }; + + Assert.True(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders)); + } + + [Fact] + public void EntryIsFresh_SharedMaxAgeOverridesMaxAge_ToNotFresh() + { + var utcNow = DateTimeOffset.UtcNow; + var httpContext = CreateDefaultContext(); + var state = httpContext.GetResponseCachingState(); + state.CachedEntryAge = TimeSpan.FromSeconds(6); + state.ResponseTime = utcNow + state.CachedEntryAge; + var cachedHeaders = new ResponseHeaders(new HeaderDictionary()) + { + CacheControl = new CacheControlHeaderValue() + { + Public = true, + MaxAge = TimeSpan.FromSeconds(10), + SharedMaxAge = TimeSpan.FromSeconds(5) + }, + Expires = utcNow + }; + + Assert.False(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders)); + } + + [Fact] + public void EntryIsFresh_MinFreshReducesFreshness_ToNotFresh() + { + var httpContext = CreateDefaultContext(); + httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + MinFresh = TimeSpan.FromSeconds(3) + }; + var cachedHeaders = new ResponseHeaders(new HeaderDictionary()) + { + CacheControl = new CacheControlHeaderValue() + { + MaxAge = TimeSpan.FromSeconds(10), + SharedMaxAge = TimeSpan.FromSeconds(5) + } + }; + httpContext.GetResponseCachingState().CachedEntryAge = TimeSpan.FromSeconds(3); + + Assert.False(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders)); + } + + [Fact] + public void EntryIsFresh_RequestMaxAgeRestrictAge_ToNotFresh() + { + var httpContext = CreateDefaultContext(); + httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + MaxAge = TimeSpan.FromSeconds(5) + }; + var cachedHeaders = new ResponseHeaders(new HeaderDictionary()) + { + CacheControl = new CacheControlHeaderValue() + { + MaxAge = TimeSpan.FromSeconds(10), + } + }; + httpContext.GetResponseCachingState().CachedEntryAge = TimeSpan.FromSeconds(6); + + Assert.False(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders)); + } + + [Fact] + public void EntryIsFresh_MaxStaleOverridesFreshness_ToFresh() + { + var httpContext = CreateDefaultContext(); + httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + MaxAge = TimeSpan.FromSeconds(5), + MaxStale = true, // This value must be set to true in order to specify MaxStaleLimit + MaxStaleLimit = TimeSpan.FromSeconds(10) + }; + var cachedHeaders = new ResponseHeaders(new HeaderDictionary()) + { + CacheControl = new CacheControlHeaderValue() + { + MaxAge = TimeSpan.FromSeconds(5), + } + }; + httpContext.GetResponseCachingState().CachedEntryAge = TimeSpan.FromSeconds(6); + + Assert.True(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders)); + } + + [Fact] + public void EntryIsFresh_MustRevalidateOverridesRequestMaxStale_ToNotFresh() + { + var httpContext = CreateDefaultContext(); + httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + MaxAge = TimeSpan.FromSeconds(5), + MaxStale = true, // This value must be set to true in order to specify MaxStaleLimit + MaxStaleLimit = TimeSpan.FromSeconds(10) + }; + var cachedHeaders = new ResponseHeaders(new HeaderDictionary()) + { + CacheControl = new CacheControlHeaderValue() + { + MaxAge = TimeSpan.FromSeconds(5), + MustRevalidate = true + } + }; + httpContext.GetResponseCachingState().CachedEntryAge = TimeSpan.FromSeconds(6); + + Assert.False(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders)); + } + + [Fact] + public void EntryIsFresh_IgnoresRequestVerificationWhenSpecified() + { + var httpContext = CreateDefaultContext(); + httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + MinFresh = TimeSpan.FromSeconds(1), + MaxAge = TimeSpan.FromSeconds(3) + }; + var cachedHeaders = new ResponseHeaders(new HeaderDictionary()) + { + CacheControl = new CacheControlHeaderValue() + { + MaxAge = TimeSpan.FromSeconds(10), + SharedMaxAge = TimeSpan.FromSeconds(5) + } + }; + httpContext.GetResponseCachingState().CachedEntryAge = TimeSpan.FromSeconds(3); + + Assert.True(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders)); + } + + private static HttpContext CreateDefaultContext() + { + var context = new DefaultHttpContext(); + context.AddResponseCachingState(); + return context; + } + } +} diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/DefaultResponseCacheEntrySerializerTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/DefaultResponseCacheEntrySerializerTests.cs deleted file mode 100644 index 2469af5ff8..0000000000 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/DefaultResponseCacheEntrySerializerTests.cs +++ /dev/null @@ -1,126 +0,0 @@ -// 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.Linq; -using Microsoft.AspNetCore.ResponseCaching.Internal; -using Xunit; -using Microsoft.AspNetCore.Http; -using System.Text; - -namespace Microsoft.AspNetCore.ResponseCaching.Tests -{ - public class DefaultResponseCacheEntrySerializerTests - { - [Fact] - public void Serialize_NullObject_Throws() - { - Assert.Throws(() => DefaultResponseCacheSerializer.Serialize(null)); - } - - [Fact] - public void Serialize_UnknownObject_Throws() - { - Assert.Throws(() => DefaultResponseCacheSerializer.Serialize(new object())); - } - - [Fact] - public void RoundTrip_CachedResponses_Succeeds() - { - var headers = new HeaderDictionary(); - headers["keyA"] = "valueA"; - headers["keyB"] = "valueB"; - var cachedEntry = new CachedResponse() - { - Created = DateTimeOffset.UtcNow, - StatusCode = StatusCodes.Status200OK, - Body = Encoding.ASCII.GetBytes("Hello world"), - Headers = headers - }; - - AssertCachedResponsesEqual(cachedEntry, (CachedResponse)DefaultResponseCacheSerializer.Deserialize(DefaultResponseCacheSerializer.Serialize(cachedEntry))); - } - - [Fact] - public void RoundTrip_Empty_CachedVaryBy_Succeeds() - { - var cachedVaryBy = new CachedVaryBy(); - - AssertCachedVarybyEqual(cachedVaryBy, (CachedVaryBy)DefaultResponseCacheSerializer.Deserialize(DefaultResponseCacheSerializer.Serialize(cachedVaryBy))); - } - - [Fact] - public void RoundTrip_HeadersOnly_CachedVaryBy_Succeeds() - { - var headers = new[] { "headerA", "headerB" }; - var cachedVaryBy = new CachedVaryBy() - { - Headers = headers - }; - - AssertCachedVarybyEqual(cachedVaryBy, (CachedVaryBy)DefaultResponseCacheSerializer.Deserialize(DefaultResponseCacheSerializer.Serialize(cachedVaryBy))); - } - - [Fact] - public void RoundTrip_ParamsOnly_CachedVaryBy_Succeeds() - { - var param = new[] { "paramA", "paramB" }; - var cachedVaryBy = new CachedVaryBy() - { - Params = param - }; - - AssertCachedVarybyEqual(cachedVaryBy, (CachedVaryBy)DefaultResponseCacheSerializer.Deserialize(DefaultResponseCacheSerializer.Serialize(cachedVaryBy))); - } - - [Fact] - public void RoundTrip_HeadersAndParams_CachedVaryBy_Succeeds() - { - var headers = new[] { "headerA", "headerB" }; - var param = new[] { "paramA", "paramB" }; - var cachedVaryBy = new CachedVaryBy() - { - Headers = headers, - Params = param - }; - - AssertCachedVarybyEqual(cachedVaryBy, (CachedVaryBy)DefaultResponseCacheSerializer.Deserialize(DefaultResponseCacheSerializer.Serialize(cachedVaryBy))); - } - - [Fact] - public void Deserialize_InvalidEntries_ReturnsNull() - { - var headers = new[] { "headerA", "headerB" }; - var cachedVaryBy = new CachedVaryBy() - { - Headers = headers - }; - var serializedEntry = DefaultResponseCacheSerializer.Serialize(cachedVaryBy); - Array.Reverse(serializedEntry); - - Assert.Null(DefaultResponseCacheSerializer.Deserialize(serializedEntry)); - } - - private static void AssertCachedResponsesEqual(CachedResponse expected, CachedResponse actual) - { - Assert.NotNull(actual); - Assert.NotNull(expected); - Assert.Equal(expected.Created, actual.Created); - Assert.Equal(expected.StatusCode, actual.StatusCode); - Assert.Equal(expected.Headers.Count, actual.Headers.Count); - foreach (var expectedHeader in expected.Headers) - { - Assert.Equal(expectedHeader.Value, actual.Headers[expectedHeader.Key]); - } - Assert.True(expected.Body.SequenceEqual(actual.Body)); - } - - private static void AssertCachedVarybyEqual(CachedVaryBy expected, CachedVaryBy actual) - { - Assert.NotNull(actual); - Assert.NotNull(expected); - Assert.Equal(expected.Headers, actual.Headers); - Assert.Equal(expected.Params, actual.Params); - } - } -} diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/HttpContextInternalExtensionTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/HttpContextInternalExtensionTests.cs new file mode 100644 index 0000000000..74e15534bf --- /dev/null +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/HttpContextInternalExtensionTests.cs @@ -0,0 +1,37 @@ +// 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.AspNetCore.ResponseCaching.Internal; +using Xunit; + +namespace Microsoft.AspNetCore.ResponseCaching.Tests +{ + public class HttpContextInternalExtensionTests + { + [Fact] + public void AddingSecondResponseCachingFeature_Throws() + { + var httpContext = new DefaultHttpContext(); + + // Should not throw + httpContext.AddResponseCachingFeature(); + + // Should throw + Assert.ThrowsAny(() => httpContext.AddResponseCachingFeature()); + } + + [Fact] + public void AddingSecondResponseCachingState_Throws() + { + var httpContext = new DefaultHttpContext(); + + // Should not throw + httpContext.AddResponseCachingState(); + + // Should throw + Assert.ThrowsAny(() => httpContext.AddResponseCachingState()); + } + } +} diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/KeyProviderTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/KeyProviderTests.cs new file mode 100644 index 0000000000..8f96278fc3 --- /dev/null +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/KeyProviderTests.cs @@ -0,0 +1,155 @@ +// 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.Http; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Options; +using Xunit; + +namespace Microsoft.AspNetCore.ResponseCaching.Tests +{ + public class DefaultKeyProviderTests + { + private static readonly char KeyDelimiter = '\x1e'; + + [Fact] + public void DefaultKeyProvider_CreateBaseKey_IncludesOnlyNormalizedMethodAndPath() + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.Method = "head"; + httpContext.Request.Path = "/path/subpath"; + httpContext.Request.Scheme = "https"; + httpContext.Request.Host = new HostString("example.com", 80); + httpContext.Request.PathBase = "/pathBase"; + httpContext.Request.QueryString = new QueryString("?query.Key=a&query.Value=b"); + var keyProvider = CreateTestKeyProvider(); + + Assert.Equal($"HEAD{KeyDelimiter}/PATH/SUBPATH", keyProvider.CreateBaseKey(httpContext)); + } + + [Fact] + public void DefaultKeyProvider_CreateBaseKey_CaseInsensitivePath_NormalizesPath() + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.Method = "GET"; + httpContext.Request.Path = "/Path"; + var keyProvider = CreateTestKeyProvider(new ResponseCachingOptions() + { + CaseSensitivePaths = false + }); + + Assert.Equal($"GET{KeyDelimiter}/PATH", keyProvider.CreateBaseKey(httpContext)); + } + + [Fact] + public void DefaultKeyProvider_CreateBaseKey_CaseSensitivePath_PreservesPathCase() + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.Method = "GET"; + httpContext.Request.Path = "/Path"; + var keyProvider = CreateTestKeyProvider(new ResponseCachingOptions() + { + CaseSensitivePaths = true + }); + + Assert.Equal($"GET{KeyDelimiter}/Path", keyProvider.CreateBaseKey(httpContext)); + } + + [Fact] + public void DefaultKeyProvider_CreateVaryKey_IncludesListedHeadersOnly() + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.Method = "GET"; + httpContext.Request.Path = "/"; + httpContext.Request.Headers["HeaderA"] = "ValueA"; + httpContext.Request.Headers["HeaderB"] = "ValueB"; + var keyProvider = CreateTestKeyProvider(); + + Assert.Equal($"GET{KeyDelimiter}/{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=null", + keyProvider.CreateVaryKey(httpContext, new VaryRules() + { + Headers = new string[] { "HeaderA", "HeaderC" } + })); + } + + [Fact] + public void DefaultKeyProvider_CreateVaryKey_IncludesListedParamsOnly() + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.Method = "GET"; + httpContext.Request.Path = "/"; + httpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB"); + var keyProvider = CreateTestKeyProvider(); + + Assert.Equal($"GET{KeyDelimiter}/{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=null", + keyProvider.CreateVaryKey(httpContext, new VaryRules() + { + Params = new string[] { "ParamA", "ParamC" } + })); + } + + [Fact] + public void DefaultKeyProvider_CreateVaryKey_IncludesParams_ParamNameCaseInsensitive_UseParamCasing() + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.Method = "GET"; + httpContext.Request.Path = "/"; + httpContext.Request.QueryString = new QueryString("?parama=ValueA¶mB=ValueB"); + var keyProvider = CreateTestKeyProvider(); + + Assert.Equal($"GET{KeyDelimiter}/{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=null", + keyProvider.CreateVaryKey(httpContext, new VaryRules() + { + Params = new string[] { "ParamA", "ParamC" } + })); + } + + [Fact] + public void DefaultKeyProvider_CreateVaryKey_IncludesAllQueryParamsGivenAsterisk() + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.Method = "GET"; + httpContext.Request.Path = "/"; + httpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB"); + var keyProvider = CreateTestKeyProvider(); + + // To support case insensitivity, all param keys are converted to upper case. + // Explicit params uses the casing specified in the setting. + Assert.Equal($"GET{KeyDelimiter}/{KeyDelimiter}Q{KeyDelimiter}PARAMA=ValueA{KeyDelimiter}PARAMB=ValueB", + keyProvider.CreateVaryKey(httpContext, new VaryRules() + { + Params = new string[] { "*" } + })); + } + + [Fact] + public void DefaultKeyProvider_CreateVaryKey_IncludesListedHeadersAndParams() + { + var httpContext = new DefaultHttpContext(); + httpContext.Request.Method = "GET"; + httpContext.Request.Path = "/"; + httpContext.Request.Headers["HeaderA"] = "ValueA"; + httpContext.Request.Headers["HeaderB"] = "ValueB"; + httpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB"); + var keyProvider = CreateTestKeyProvider(); + + Assert.Equal($"GET{KeyDelimiter}/{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=null{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=null", + keyProvider.CreateVaryKey(httpContext, new VaryRules() + { + Headers = new string[] { "HeaderA", "HeaderC" }, + Params = new string[] { "ParamA", "ParamC" } + })); + } + + private static IKeyProvider CreateTestKeyProvider() + { + return CreateTestKeyProvider(new ResponseCachingOptions()); + } + + private static IKeyProvider CreateTestKeyProvider(ResponseCachingOptions options) + { + return new KeyProvider(new DefaultObjectPoolProvider(), Options.Create(options)); + } + } +} diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs index 1227eb9b80..c4caca269b 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; @@ -12,6 +11,7 @@ using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Headers; using Microsoft.AspNetCore.ResponseCaching.Internal; using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Options; using Microsoft.Net.Http.Headers; using Xunit; @@ -19,708 +19,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { public class ResponseCachingContextTests { - private static readonly char KeyDelimiter = '\x1e'; - - [Theory] - [InlineData("GET")] - [InlineData("HEAD")] - public void RequestIsCacheable_CacheableMethods_Allowed(string method) - { - var httpContext = new DefaultHttpContext(); - httpContext.Request.Method = method; - var context = CreateTestContext(httpContext); - - Assert.True(context.RequestIsCacheable()); - } - - [Theory] - [InlineData("POST")] - [InlineData("OPTIONS")] - [InlineData("PUT")] - [InlineData("DELETE")] - [InlineData("TRACE")] - [InlineData("CONNECT")] - [InlineData("")] - [InlineData(null)] - public void RequestIsCacheable_UncacheableMethods_NotAllowed(string method) - { - var httpContext = new DefaultHttpContext(); - httpContext.Request.Method = method; - var context = CreateTestContext(httpContext); - - Assert.False(context.RequestIsCacheable()); - } - - [Fact] - public void RequestIsCacheable_AuthorizationHeaders_NotAllowed() - { - var httpContext = new DefaultHttpContext(); - httpContext.Request.Method = "GET"; - httpContext.Request.Headers[HeaderNames.Authorization] = "Basic plaintextUN:plaintextPW"; - var context = CreateTestContext(httpContext); - - Assert.False(context.RequestIsCacheable()); - } - - [Fact] - public void RequestIsCacheable_NoCache_NotAllowed() - { - var httpContext = new DefaultHttpContext(); - httpContext.Request.Method = "GET"; - httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() - { - NoCache = true - }; - var context = CreateTestContext(httpContext); - - Assert.False(context.RequestIsCacheable()); - } - - [Fact] - public void RequestIsCacheable_NoStore_Allowed() - { - var httpContext = new DefaultHttpContext(); - httpContext.Request.Method = "GET"; - httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() - { - NoStore = true - }; - var context = CreateTestContext(httpContext); - - Assert.True(context.RequestIsCacheable()); - } - - [Fact] - public void RequestIsCacheable_LegacyDirectives_NotAllowed() - { - var httpContext = new DefaultHttpContext(); - httpContext.Request.Method = "GET"; - httpContext.Request.Headers[HeaderNames.Pragma] = "no-cache"; - var context = CreateTestContext(httpContext); - - Assert.False(context.RequestIsCacheable()); - } - - [Fact] - public void RequestIsCacheable_LegacyDirectives_OverridenByCacheControl() - { - var httpContext = new DefaultHttpContext(); - httpContext.Request.Method = "GET"; - httpContext.Request.Headers[HeaderNames.Pragma] = "no-cache"; - httpContext.Request.Headers[HeaderNames.CacheControl] = "max-age=10"; - var context = CreateTestContext(httpContext); - - Assert.True(context.RequestIsCacheable()); - } - - private class AllowUnrecognizedHTTPMethodRequests : IResponseCachingCacheabilityValidator - { - public OverrideResult RequestIsCacheableOverride(HttpContext httpContext) => - httpContext.Request.Method == "UNRECOGNIZED" ? OverrideResult.Cache : OverrideResult.DoNotCache; - - public OverrideResult ResponseIsCacheableOverride(HttpContext httpContext) => OverrideResult.UseDefaultLogic; - } - - [Fact] - public void RequestIsCacheableOverride_OverridesDefaultBehavior_ToAllowed() - { - var httpContext = new DefaultHttpContext(); - httpContext.Request.Method = "UNRECOGNIZED"; - var responseCachingContext = CreateTestContext(httpContext, new AllowUnrecognizedHTTPMethodRequests()); - - Assert.True(responseCachingContext.RequestIsCacheable()); - } - - private class DisallowGetHTTPMethodRequests : IResponseCachingCacheabilityValidator - { - public OverrideResult RequestIsCacheableOverride(HttpContext httpContext) => - httpContext.Request.Method == "GET" ? OverrideResult.DoNotCache : OverrideResult.Cache; - - public OverrideResult ResponseIsCacheableOverride(HttpContext httpContext) => OverrideResult.UseDefaultLogic; - } - - [Fact] - public void RequestIsCacheableOverride_OverridesDefaultBehavior_ToNotAllowed() - { - var httpContext = new DefaultHttpContext(); - httpContext.Request.Method = "GET"; - var responseCachingContext = CreateTestContext(httpContext, new DisallowGetHTTPMethodRequests()); - - Assert.False(responseCachingContext.RequestIsCacheable()); - } - - [Fact] - public void RequestIsCacheableOverride_IgnoreFallsBackToDefaultBehavior() - { - var httpContext = new DefaultHttpContext(); - httpContext.Request.Method = "GET"; - var responseCachingContext = CreateTestContext(httpContext, new NoopCacheabilityValidator()); - - Assert.True(responseCachingContext.RequestIsCacheable()); - - httpContext.Request.Method = "UNRECOGNIZED"; - - Assert.False(responseCachingContext.RequestIsCacheable()); - } - - [Fact] - public void CreateCacheKey_Includes_UppercaseMethodAndPath() - { - var httpContext = new DefaultHttpContext(); - httpContext.Request.Method = "head"; - httpContext.Request.Path = "/path/subpath"; - httpContext.Request.Scheme = "https"; - httpContext.Request.Host = new HostString("example.com", 80); - httpContext.Request.PathBase = "/pathBase"; - httpContext.Request.QueryString = new QueryString("?query.Key=a&query.Value=b"); - var context = CreateTestContext(httpContext); - - Assert.Equal($"HEAD{KeyDelimiter}/PATH/SUBPATH", context.CreateCacheKey()); - } - - [Fact] - public void CreateCacheKey_Includes_ListedVaryByHeadersOnly() - { - var httpContext = new DefaultHttpContext(); - httpContext.Request.Method = "GET"; - httpContext.Request.Path = "/"; - httpContext.Request.Headers["HeaderA"] = "ValueA"; - httpContext.Request.Headers["HeaderB"] = "ValueB"; - var context = CreateTestContext(httpContext); - - Assert.Equal($"GET{KeyDelimiter}/{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=null", context.CreateCacheKey(new CachedVaryBy() - { - Headers = new string[] { "HeaderA", "HeaderC" } - })); - } - - [Fact] - public void CreateCacheKey_Includes_ListedVaryByParamsOnly() - { - var httpContext = new DefaultHttpContext(); - httpContext.Request.Method = "GET"; - httpContext.Request.Path = "/"; - httpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB"); - var context = CreateTestContext(httpContext); - - Assert.Equal($"GET{KeyDelimiter}/{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=null", context.CreateCacheKey(new CachedVaryBy() - { - Params = new string[] { "ParamA", "ParamC" } - })); - } - - [Fact] - public void CreateCacheKey_Includes_VaryByParams_ParamNameCaseInsensitive_UseVaryByCasing() - { - var httpContext = new DefaultHttpContext(); - httpContext.Request.Method = "GET"; - httpContext.Request.Path = "/"; - httpContext.Request.QueryString = new QueryString("?parama=ValueA¶mB=ValueB"); - var context = CreateTestContext(httpContext); - - Assert.Equal($"GET{KeyDelimiter}/{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=null", context.CreateCacheKey(new CachedVaryBy() - { - Params = new string[] { "ParamA", "ParamC" } - })); - } - - [Fact] - public void CreateCacheKey_Includes_AllQueryParamsGivenAsterisk() - { - var httpContext = new DefaultHttpContext(); - httpContext.Request.Method = "GET"; - httpContext.Request.Path = "/"; - httpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB"); - var context = CreateTestContext(httpContext); - - // To support case insensitivity, all param keys are converted to lower case. - // Explicit VaryBy uses the casing specified in the setting. - Assert.Equal($"GET{KeyDelimiter}/{KeyDelimiter}Q{KeyDelimiter}PARAMA=ValueA{KeyDelimiter}PARAMB=ValueB", context.CreateCacheKey(new CachedVaryBy() - { - Params = new string[] { "*" } - })); - } - - [Fact] - public void CreateCacheKey_Includes_ListedVaryByHeadersAndParams() - { - var httpContext = new DefaultHttpContext(); - httpContext.Request.Method = "GET"; - httpContext.Request.Path = "/"; - httpContext.Request.Headers["HeaderA"] = "ValueA"; - httpContext.Request.Headers["HeaderB"] = "ValueB"; - httpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB"); - var context = CreateTestContext(httpContext); - - Assert.Equal($"GET{KeyDelimiter}/{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=null{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=null", context.CreateCacheKey(new CachedVaryBy() - { - Headers = new string[] { "HeaderA", "HeaderC" }, - Params = new string[] { "ParamA", "ParamC" } - })); - } - - private class KeyModifier : IResponseCachingCacheKeyModifier - { - public string CreatKeyPrefix(HttpContext httpContext) => "CustomizedKeyPrefix"; - } - - [Fact] - public void CreateCacheKey_CacheKeyModifier_AddsPrefix() - { - var httpContext = new DefaultHttpContext(); - httpContext.Request.Method = "GET"; - httpContext.Request.Path = "/"; - httpContext.Request.Headers["HeaderA"] = "ValueA"; - httpContext.Request.Headers["HeaderB"] = "ValueB"; - var responseCachingContext = CreateTestContext(httpContext, new KeyModifier()); - - Assert.Equal($"CustomizedKeyPrefix{KeyDelimiter}GET{KeyDelimiter}/{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=null", responseCachingContext.CreateCacheKey(new CachedVaryBy() - { - Headers = new string[] { "HeaderA", "HeaderC" } - })); - } - - [Fact] - public void CreateCacheKey_CaseInsensitivePath_NormalizesPath() - { - var httpContext = new DefaultHttpContext(); - httpContext.Request.Method = "GET"; - httpContext.Request.Path = "/Path"; - var context = CreateTestContext(httpContext, new ResponseCachingOptions() - { - CaseSensitivePaths = false - }); - - Assert.Equal($"GET{KeyDelimiter}/PATH", context.CreateCacheKey()); - } - - [Fact] - public void CreateCacheKey_CaseSensitivePath_PreservesPathCase() - { - var httpContext = new DefaultHttpContext(); - httpContext.Request.Method = "GET"; - httpContext.Request.Path = "/Path"; - var context = CreateTestContext(httpContext, new ResponseCachingOptions() - { - CaseSensitivePaths = true - }); - - Assert.Equal($"GET{KeyDelimiter}/Path", context.CreateCacheKey()); - } - - [Fact] - public void ResponseIsCacheable_NoPublic_NotAllowed() - { - var httpContext = new DefaultHttpContext(); - var context = CreateTestContext(httpContext); - - Assert.False(context.ResponseIsCacheable()); - } - - [Fact] - public void ResponseIsCacheable_Public_Allowed() - { - var httpContext = new DefaultHttpContext(); - httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() - { - Public = true - }; - var context = CreateTestContext(httpContext); - - Assert.True(context.ResponseIsCacheable()); - } - - [Fact] - public void ResponseIsCacheable_NoCache_NotAllowed() - { - var httpContext = new DefaultHttpContext(); - httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() - { - Public = true, - NoCache = true - }; - var context = CreateTestContext(httpContext); - - Assert.False(context.ResponseIsCacheable()); - } - - [Fact] - public void ResponseIsCacheable_RequestNoStore_NotAllowed() - { - var httpContext = new DefaultHttpContext(); - httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() - { - NoStore = true - }; - httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() - { - Public = true - }; - var context = CreateTestContext(httpContext); - - Assert.False(context.ResponseIsCacheable()); - } - - [Fact] - public void ResponseIsCacheable_ResponseNoStore_NotAllowed() - { - var httpContext = new DefaultHttpContext(); - httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() - { - Public = true, - NoStore = true - }; - var context = CreateTestContext(httpContext); - - Assert.False(context.ResponseIsCacheable()); - } - - [Fact] - public void ResponseIsCacheable_VaryByStar_NotAllowed() - { - var httpContext = new DefaultHttpContext(); - httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() - { - Public = true - }; - httpContext.Response.Headers[HeaderNames.Vary] = "*"; - var context = CreateTestContext(httpContext); - - Assert.False(context.ResponseIsCacheable()); - } - - [Fact] - public void ResponseIsCacheable_Private_NotAllowed() - { - var httpContext = new DefaultHttpContext(); - httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() - { - Public = true, - Private = true - }; - var context = CreateTestContext(httpContext); - - Assert.False(context.ResponseIsCacheable()); - } - - [Theory] - [InlineData(StatusCodes.Status200OK)] - public void ResponseIsCacheable_SuccessStatusCodes_Allowed(int statusCode) - { - var httpContext = new DefaultHttpContext(); - httpContext.Response.StatusCode = statusCode; - httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() - { - Public = true - }; - var context = CreateTestContext(httpContext); - - Assert.True(context.ResponseIsCacheable()); - } - - [Theory] - [InlineData(StatusCodes.Status201Created)] - [InlineData(StatusCodes.Status202Accepted)] - [InlineData(StatusCodes.Status203NonAuthoritative)] - [InlineData(StatusCodes.Status204NoContent)] - [InlineData(StatusCodes.Status205ResetContent)] - [InlineData(StatusCodes.Status206PartialContent)] - [InlineData(StatusCodes.Status207MultiStatus)] - [InlineData(StatusCodes.Status300MultipleChoices)] - [InlineData(StatusCodes.Status301MovedPermanently)] - [InlineData(StatusCodes.Status302Found)] - [InlineData(StatusCodes.Status303SeeOther)] - [InlineData(StatusCodes.Status304NotModified)] - [InlineData(StatusCodes.Status305UseProxy)] - [InlineData(StatusCodes.Status306SwitchProxy)] - [InlineData(StatusCodes.Status307TemporaryRedirect)] - [InlineData(StatusCodes.Status308PermanentRedirect)] - [InlineData(StatusCodes.Status400BadRequest)] - [InlineData(StatusCodes.Status401Unauthorized)] - [InlineData(StatusCodes.Status402PaymentRequired)] - [InlineData(StatusCodes.Status403Forbidden)] - [InlineData(StatusCodes.Status404NotFound)] - [InlineData(StatusCodes.Status405MethodNotAllowed)] - [InlineData(StatusCodes.Status406NotAcceptable)] - [InlineData(StatusCodes.Status407ProxyAuthenticationRequired)] - [InlineData(StatusCodes.Status408RequestTimeout)] - [InlineData(StatusCodes.Status409Conflict)] - [InlineData(StatusCodes.Status410Gone)] - [InlineData(StatusCodes.Status411LengthRequired)] - [InlineData(StatusCodes.Status412PreconditionFailed)] - [InlineData(StatusCodes.Status413RequestEntityTooLarge)] - [InlineData(StatusCodes.Status414RequestUriTooLong)] - [InlineData(StatusCodes.Status415UnsupportedMediaType)] - [InlineData(StatusCodes.Status416RequestedRangeNotSatisfiable)] - [InlineData(StatusCodes.Status417ExpectationFailed)] - [InlineData(StatusCodes.Status418ImATeapot)] - [InlineData(StatusCodes.Status419AuthenticationTimeout)] - [InlineData(StatusCodes.Status422UnprocessableEntity)] - [InlineData(StatusCodes.Status423Locked)] - [InlineData(StatusCodes.Status424FailedDependency)] - [InlineData(StatusCodes.Status451UnavailableForLegalReasons)] - [InlineData(StatusCodes.Status500InternalServerError)] - [InlineData(StatusCodes.Status501NotImplemented)] - [InlineData(StatusCodes.Status502BadGateway)] - [InlineData(StatusCodes.Status503ServiceUnavailable)] - [InlineData(StatusCodes.Status504GatewayTimeout)] - [InlineData(StatusCodes.Status505HttpVersionNotsupported)] - [InlineData(StatusCodes.Status506VariantAlsoNegotiates)] - [InlineData(StatusCodes.Status507InsufficientStorage)] - public void ResponseIsCacheable_NonSuccessStatusCodes_NotAllowed(int statusCode) - { - var httpContext = new DefaultHttpContext(); - httpContext.Response.StatusCode = statusCode; - httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() - { - Public = true - }; - var context = CreateTestContext(httpContext); - - Assert.False(context.ResponseIsCacheable()); - } - - private class Allow500Response : IResponseCachingCacheabilityValidator - { - public OverrideResult RequestIsCacheableOverride(HttpContext httpContext) => OverrideResult.UseDefaultLogic; - - public OverrideResult ResponseIsCacheableOverride(HttpContext httpContext) => - httpContext.Response.StatusCode == StatusCodes.Status500InternalServerError ? OverrideResult.Cache : OverrideResult.DoNotCache; - } - - [Fact] - public void ResponseIsCacheableOverride_OverridesDefaultBehavior_ToAllowed() - { - var httpContext = new DefaultHttpContext(); - httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError; - httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() - { - Public = true - }; - var responseCachingContext = CreateTestContext(httpContext, new Allow500Response()); - - Assert.True(responseCachingContext.ResponseIsCacheable()); - } - - private class Disallow200Response : IResponseCachingCacheabilityValidator - { - public OverrideResult RequestIsCacheableOverride(HttpContext httpContext) => OverrideResult.UseDefaultLogic; - - public OverrideResult ResponseIsCacheableOverride(HttpContext httpContext) => - httpContext.Response.StatusCode == StatusCodes.Status200OK ? OverrideResult.DoNotCache : OverrideResult.Cache; - } - - [Fact] - public void ResponseIsCacheableOverride_OverridesDefaultBehavior_ToNotAllowed() - { - var httpContext = new DefaultHttpContext(); - httpContext.Response.StatusCode = StatusCodes.Status200OK; - httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() - { - Public = true - }; - var responseCachingContext = CreateTestContext(httpContext, new Disallow200Response()); - - Assert.False(responseCachingContext.ResponseIsCacheable()); - } - - [Fact] - public void ResponseIsCacheableOverride_IgnoreFallsBackToDefaultBehavior() - { - var httpContext = new DefaultHttpContext(); - httpContext.Response.StatusCode = StatusCodes.Status200OK; - httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() - { - Public = true - }; - var responseCachingContext = CreateTestContext(httpContext, new NoopCacheabilityValidator()); - - Assert.True(responseCachingContext.ResponseIsCacheable()); - - httpContext.Response.StatusCode = StatusCodes.Status500InternalServerError; - - Assert.False(responseCachingContext.ResponseIsCacheable()); - } - - [Fact] - public void EntryIsFresh_NoExpiryRequirements_IsFresh() - { - var httpContext = new DefaultHttpContext(); - var context = CreateTestContext(httpContext); - - Assert.True(context.EntryIsFresh(new ResponseHeaders(new HeaderDictionary()), TimeSpan.MaxValue, verifyAgainstRequest: false)); - } - - [Fact] - public void EntryIsFresh_PastExpiry_IsNotFresh() - { - var httpContext = new DefaultHttpContext(); - var utcNow = DateTimeOffset.UtcNow; - httpContext.Response.GetTypedHeaders().Expires = utcNow; - var context = CreateTestContext(httpContext); - context._responseTime = utcNow; - - Assert.False(context.EntryIsFresh(httpContext.Response.GetTypedHeaders(), TimeSpan.MaxValue, verifyAgainstRequest: false)); - } - - [Fact] - public void EntryIsFresh_MaxAgeOverridesExpiry_ToFresh() - { - var utcNow = DateTimeOffset.UtcNow; - var httpContext = new DefaultHttpContext(); - - var responseHeaders = httpContext.Response.GetTypedHeaders(); - responseHeaders.Expires = utcNow; - responseHeaders.CacheControl = new CacheControlHeaderValue() - { - MaxAge = TimeSpan.FromSeconds(10) - }; - - var context = CreateTestContext(httpContext); - context._responseTime = utcNow; - - Assert.True(context.EntryIsFresh(httpContext.Response.GetTypedHeaders(), TimeSpan.FromSeconds(10), verifyAgainstRequest: false)); - } - - [Fact] - public void EntryIsFresh_MaxAgeOverridesExpiry_ToNotFresh() - { - var utcNow = DateTimeOffset.UtcNow; - var httpContext = new DefaultHttpContext(); - - var responseHeaders = httpContext.Response.GetTypedHeaders(); - responseHeaders.Expires = utcNow; - responseHeaders.CacheControl = new CacheControlHeaderValue() - { - MaxAge = TimeSpan.FromSeconds(10) - }; - - var context = CreateTestContext(httpContext); - context._responseTime = utcNow; - - Assert.False(context.EntryIsFresh(httpContext.Response.GetTypedHeaders(), TimeSpan.FromSeconds(11), verifyAgainstRequest: false)); - } - - [Fact] - public void EntryIsFresh_SharedMaxAgeOverridesMaxAge_ToFresh() - { - var httpContext = new DefaultHttpContext(); - httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() - { - MaxAge = TimeSpan.FromSeconds(10), - SharedMaxAge = TimeSpan.FromSeconds(15) - }; - var context = CreateTestContext(httpContext); - - Assert.True(context.EntryIsFresh(httpContext.Response.GetTypedHeaders(), TimeSpan.FromSeconds(11), verifyAgainstRequest: false)); - } - - [Fact] - public void EntryIsFresh_SharedMaxAgeOverridesMaxAge_ToNotFresh() - { - var httpContext = new DefaultHttpContext(); - httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() - { - MaxAge = TimeSpan.FromSeconds(10), - SharedMaxAge = TimeSpan.FromSeconds(5) - }; - var context = CreateTestContext(httpContext); - - Assert.False(context.EntryIsFresh(httpContext.Response.GetTypedHeaders(), TimeSpan.FromSeconds(6), verifyAgainstRequest: false)); - } - - [Fact] - public void EntryIsFresh_MinFreshReducesFreshness_ToNotFresh() - { - var httpContext = new DefaultHttpContext(); - httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() - { - MinFresh = TimeSpan.FromSeconds(3) - }; - httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() - { - MaxAge = TimeSpan.FromSeconds(10), - SharedMaxAge = TimeSpan.FromSeconds(5) - }; - var context = CreateTestContext(httpContext); - - Assert.False(context.EntryIsFresh(httpContext.Response.GetTypedHeaders(), TimeSpan.FromSeconds(3), verifyAgainstRequest: true)); - } - - [Fact] - public void EntryIsFresh_RequestMaxAgeRestrictAge_ToNotFresh() - { - var httpContext = new DefaultHttpContext(); - httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() - { - MaxAge = TimeSpan.FromSeconds(5) - }; - httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() - { - MaxAge = TimeSpan.FromSeconds(10), - }; - var context = CreateTestContext(httpContext); - - Assert.False(context.EntryIsFresh(httpContext.Response.GetTypedHeaders(), TimeSpan.FromSeconds(6), verifyAgainstRequest: true)); - } - - [Fact] - public void EntryIsFresh_MaxStaleOverridesFreshness_ToFresh() - { - var httpContext = new DefaultHttpContext(); - httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() - { - MaxAge = TimeSpan.FromSeconds(5), - MaxStale = true, // This value must be set to true in order to specify MaxStaleLimit - MaxStaleLimit = TimeSpan.FromSeconds(10) - }; - httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() - { - MaxAge = TimeSpan.FromSeconds(5), - }; - var context = CreateTestContext(httpContext); - - Assert.True(context.EntryIsFresh(httpContext.Response.GetTypedHeaders(), TimeSpan.FromSeconds(6), verifyAgainstRequest: true)); - } - - [Fact] - public void EntryIsFresh_MustRevalidateOverridesRequestMaxStale_ToNotFresh() - { - var httpContext = new DefaultHttpContext(); - httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() - { - MaxAge = TimeSpan.FromSeconds(5), - MaxStale = true, // This value must be set to true in order to specify MaxStaleLimit - MaxStaleLimit = TimeSpan.FromSeconds(10) - }; - httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() - { - MaxAge = TimeSpan.FromSeconds(5), - MustRevalidate = true - }; - var context = CreateTestContext(httpContext); - - Assert.False(context.EntryIsFresh(httpContext.Response.GetTypedHeaders(), TimeSpan.FromSeconds(6), verifyAgainstRequest: true)); - } - - [Fact] - public void EntryIsFresh_IgnoresRequestVerificationWhenSpecified() - { - var httpContext = new DefaultHttpContext(); - httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() - { - MinFresh = TimeSpan.FromSeconds(1), - MaxAge = TimeSpan.FromSeconds(3) - }; - httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() - { - MaxAge = TimeSpan.FromSeconds(10), - SharedMaxAge = TimeSpan.FromSeconds(5) - }; - var context = CreateTestContext(httpContext); - - Assert.True(context.EntryIsFresh(httpContext.Response.GetTypedHeaders(), TimeSpan.FromSeconds(3), verifyAgainstRequest: false)); - } [Fact] public void ConditionalRequestSatisfied_NotConditionalRequest_Fails() @@ -861,8 +159,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests return CreateTestContext( httpContext, new ResponseCachingOptions(), - new NoopCacheKeyModifier(), - new NoopCacheabilityValidator()); + new CacheabilityValidator()); } private static ResponseCachingContext CreateTestContext(HttpContext httpContext, ResponseCachingOptions options) @@ -870,41 +167,30 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests return CreateTestContext( httpContext, options, - new NoopCacheKeyModifier(), - new NoopCacheabilityValidator()); + new CacheabilityValidator()); } - private static ResponseCachingContext CreateTestContext(HttpContext httpContext, IResponseCachingCacheKeyModifier cacheKeyModifier) + private static ResponseCachingContext CreateTestContext(HttpContext httpContext, ICacheabilityValidator cacheabilityValidator) { return CreateTestContext( httpContext, new ResponseCachingOptions(), - cacheKeyModifier, - new NoopCacheabilityValidator()); - } - - private static ResponseCachingContext CreateTestContext(HttpContext httpContext, IResponseCachingCacheabilityValidator cacheabilityValidator) - { - return CreateTestContext( - httpContext, - new ResponseCachingOptions(), - new NoopCacheKeyModifier(), cacheabilityValidator); } private static ResponseCachingContext CreateTestContext( HttpContext httpContext, ResponseCachingOptions options, - IResponseCachingCacheKeyModifier cacheKeyModifier, - IResponseCachingCacheabilityValidator cacheabilityValidator) + ICacheabilityValidator cacheabilityValidator) { + httpContext.AddResponseCachingState(); + return new ResponseCachingContext( httpContext, new TestResponseCache(), options, - new DefaultObjectPool(new StringBuilderPooledObjectPolicy()), cacheabilityValidator, - cacheKeyModifier); + new KeyProvider(new DefaultObjectPoolProvider(), Options.Create(options))); } private class TestResponseCache : IResponseCache diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs index 783dfd8f86..c28c31e8c1 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs @@ -49,7 +49,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public async void ServesCachedContent_IfVaryByHeader_Matches() + public async void ServesCachedContent_IfVaryHeader_Matches() { var builder = CreateBuilderWithResponseCaching(async (context) => { @@ -69,7 +69,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public async void ServesFreshContent_IfVaryByHeader_Mismatches() + public async void ServesFreshContent_IfVaryHeader_Mismatches() { var builder = CreateBuilderWithResponseCaching(async (context) => { @@ -90,11 +90,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public async void ServesCachedContent_IfVaryByParams_Matches() + public async void ServesCachedContent_IfVaryParams_Matches() { var builder = CreateBuilderWithResponseCaching(async (context) => { - context.GetResponseCachingFeature().VaryByParams = "param"; + context.GetResponseCachingFeature().VaryParams = "param"; await DefaultRequestDelegate(context); }); @@ -109,11 +109,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public async void ServesCachedContent_IfVaryByParamsExplicit_Matches_ParamNameCaseInsensitive() + public async void ServesCachedContent_IfVaryParamsExplicit_Matches_ParamNameCaseInsensitive() { var builder = CreateBuilderWithResponseCaching(async (context) => { - context.GetResponseCachingFeature().VaryByParams = new[] { "ParamA", "paramb" }; + context.GetResponseCachingFeature().VaryParams = new[] { "ParamA", "paramb" }; await DefaultRequestDelegate(context); }); @@ -128,11 +128,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public async void ServesCachedContent_IfVaryByParamsStar_Matches_ParamNameCaseInsensitive() + public async void ServesCachedContent_IfVaryParamsStar_Matches_ParamNameCaseInsensitive() { var builder = CreateBuilderWithResponseCaching(async (context) => { - context.GetResponseCachingFeature().VaryByParams = new[] { "*" }; + context.GetResponseCachingFeature().VaryParams = new[] { "*" }; await DefaultRequestDelegate(context); }); @@ -147,11 +147,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public async void ServesCachedContent_IfVaryByParamsExplicit_Matches_OrderInsensitive() + public async void ServesCachedContent_IfVaryParamsExplicit_Matches_OrderInsensitive() { var builder = CreateBuilderWithResponseCaching(async (context) => { - context.GetResponseCachingFeature().VaryByParams = new[] { "ParamB", "ParamA" }; + context.GetResponseCachingFeature().VaryParams = new[] { "ParamB", "ParamA" }; await DefaultRequestDelegate(context); }); @@ -166,11 +166,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public async void ServesCachedContent_IfVaryByParamsStar_Matches_OrderInsensitive() + public async void ServesCachedContent_IfVaryParamsStar_Matches_OrderInsensitive() { var builder = CreateBuilderWithResponseCaching(async (context) => { - context.GetResponseCachingFeature().VaryByParams = new[] { "*" }; + context.GetResponseCachingFeature().VaryParams = new[] { "*" }; await DefaultRequestDelegate(context); }); @@ -185,11 +185,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public async void ServesFreshContent_IfVaryByParams_Mismatches() + public async void ServesFreshContent_IfVaryParams_Mismatches() { var builder = CreateBuilderWithResponseCaching(async (context) => { - context.GetResponseCachingFeature().VaryByParams = "param"; + context.GetResponseCachingFeature().VaryParams = "param"; await DefaultRequestDelegate(context); }); @@ -204,11 +204,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public async void ServesFreshContent_IfVaryByParamsExplicit_Mismatch_ParamValueCaseSensitive() + public async void ServesFreshContent_IfVaryParamsExplicit_Mismatch_ParamValueCaseSensitive() { var builder = CreateBuilderWithResponseCaching(async (context) => { - context.GetResponseCachingFeature().VaryByParams = new[] { "ParamA", "ParamB" }; + context.GetResponseCachingFeature().VaryParams = new[] { "ParamA", "ParamB" }; await DefaultRequestDelegate(context); }); @@ -223,11 +223,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public async void ServesFreshContent_IfVaryByParamsStar_Mismatch_ParamValueCaseSensitive() + public async void ServesFreshContent_IfVaryParamsStar_Mismatch_ParamValueCaseSensitive() { var builder = CreateBuilderWithResponseCaching(async (context) => { - context.GetResponseCachingFeature().VaryByParams = new[] { "*" }; + context.GetResponseCachingFeature().VaryParams = new[] { "*" }; await DefaultRequestDelegate(context); }); @@ -281,7 +281,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public async void ServesCachedContent_WithoutSetCookie() + public async void ServesFreshContent_IfSetCookie_IsSpecified() { var builder = CreateBuilderWithResponseCaching(async (context) => { @@ -295,20 +295,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var initialResponse = await client.GetAsync(""); var subsequentResponse = await client.GetAsync(""); - initialResponse.EnsureSuccessStatusCode(); - subsequentResponse.EnsureSuccessStatusCode(); - - foreach (var header in initialResponse.Headers) - { - if (!string.Equals(HeaderNames.SetCookie, header.Key, StringComparison.OrdinalIgnoreCase)) - { - Assert.Equal(initialResponse.Headers.GetValues(header.Key), subsequentResponse.Headers.GetValues(header.Key)); - } - } - Assert.True(initialResponse.Headers.Contains(HeaderNames.SetCookie)); - Assert.True(subsequentResponse.Headers.Contains(HeaderNames.Age)); - Assert.False(subsequentResponse.Headers.Contains(HeaderNames.SetCookie)); - Assert.Equal(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync()); + await AssertResponseNotCachedAsync(initialResponse, subsequentResponse); } } From 6a04fe5fb7838c3ae32c948ae5bef5cc1795dacf Mon Sep 17 00:00:00 2001 From: John Luo Date: Wed, 7 Sep 2016 16:57:49 -0700 Subject: [PATCH 025/188] Store body and header separately, preliminary sharding - Add fast id --- .../CacheEntrySerializer.cs | 121 ++++--- .../CachedResponse.cs | 2 + .../CachedResponseBody.cs} | 4 +- .../CacheEntry/CachedVaryRules.cs | 12 + .../Interfaces/IKeyProvider.cs | 6 +- .../Internal/FastGuid.cs | 80 +++++ .../Internal/MemoryResponseCache.cs | 4 +- .../Internal/ResponseCachingState.cs | 2 + .../KeyProvider.cs | 10 +- .../ResponseCachingContext.cs | 101 ++++-- .../ResponseCachingOptions.cs | 5 + .../project.json | 1 + .../CacheEntrySerializerTests.cs | 114 ++++--- .../KeyProviderTests.cs | 59 ++-- .../ResponseCachingContextTests.cs | 320 +++++++++++++++++- .../ResponseCachingTests.cs | 23 ++ 16 files changed, 727 insertions(+), 137 deletions(-) rename src/Microsoft.AspNetCore.ResponseCaching/{Internal => CacheEntry}/CacheEntrySerializer.cs (66%) rename src/Microsoft.AspNetCore.ResponseCaching/{Internal => CacheEntry}/CachedResponse.cs (91%) rename src/Microsoft.AspNetCore.ResponseCaching/{Internal/CachedVaryRules.cs => CacheEntry/CachedResponseBody.cs} (75%) create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedVaryRules.cs create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Internal/FastGuid.cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntrySerializer.cs b/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CacheEntrySerializer.cs similarity index 66% rename from src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntrySerializer.cs rename to src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CacheEntrySerializer.cs index 0f4f3f5a37..48d28e319f 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntrySerializer.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CacheEntrySerializer.cs @@ -13,6 +13,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal public static object Deserialize(byte[] serializedEntry) { + if (serializedEntry == null) + { + return null; + } + using (var memory = new MemoryStream(serializedEntry)) { using (var reader = new BinaryReader(memory)) @@ -37,7 +42,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal // Serialization Format // Format version (int) - // Type (char: 'R' for CachedResponse, 'V' for CachedVaryRules) + // Type (char: 'B' for CachedResponseBody, 'R' for CachedResponse, 'V' for CachedVaryRules) // Type-dependent data (see CachedResponse and CachedVaryRules) public static object Read(BinaryReader reader) { @@ -53,15 +58,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal var type = reader.ReadChar(); - if (type == 'R') + if (type == 'B') { - var cachedResponse = ReadCachedResponse(reader); - return cachedResponse; + return ReadCachedResponseBody(reader); + } + else if (type == 'R') + { + return ReadCachedResponse(reader); } else if (type == 'V') { - var cachedVaryRules = ReadCachedVaryRules(reader); - return cachedVaryRules; + return ReadCachedVaryRules(reader); } // Unable to read as CachedResponse or CachedVaryRules @@ -69,16 +76,30 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal } // Serialization Format + // Body length (int) + // Body (byte[]) + private static CachedResponseBody ReadCachedResponseBody(BinaryReader reader) + { + var bodyLength = reader.ReadInt32(); + var body = reader.ReadBytes(bodyLength); + + return new CachedResponseBody() { Body = body }; + } + + // Serialization Format + // BodyKeyPrefix (string) // Creation time - UtcTicks (long) // Status code (int) // Header count (int) // Header(s) // Key (string) // Value (string) - // Body length (int) - // Body (byte[]) + // ContainsBody (bool) + // Body length (int) + // Body (byte[]) private static CachedResponse ReadCachedResponse(BinaryReader reader) { + var bodyKeyPrefix = reader.ReadString(); var created = new DateTimeOffset(reader.ReadInt64(), TimeSpan.Zero); var statusCode = reader.ReadInt32(); var headerCount = reader.ReadInt32(); @@ -89,25 +110,28 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal var value = reader.ReadString(); headers[key] = value; } - var bodyLength = reader.ReadInt32(); - var body = reader.ReadBytes(bodyLength); - return new CachedResponse { Created = created, StatusCode = statusCode, Headers = headers, Body = body }; + var containsBody = reader.ReadBoolean(); + int bodyLength; + byte[] body = null; + if (containsBody) + { + bodyLength = reader.ReadInt32(); + body = reader.ReadBytes(bodyLength); + } + + return new CachedResponse { BodyKeyPrefix = bodyKeyPrefix, Created = created, StatusCode = statusCode, Headers = headers, Body = body }; } // Serialization Format - // ContainsVaryRules (bool) - // If containing vary rules: - // Headers count - // Headers if count > 0 (comma separated string) - // Params count - // Params if count > 0 (comma separated string) + // Guid (long) + // Headers count + // Header(s) (comma separated string) + // Params count + // Param(s) (comma separated string) private static CachedVaryRules ReadCachedVaryRules(BinaryReader reader) { - if (!reader.ReadBoolean()) - { - return new CachedVaryRules(); - } + var varyKeyPrefix = reader.ReadString(); var headerCount = reader.ReadInt32(); var headers = new string[headerCount]; @@ -122,7 +146,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal param[index] = reader.ReadString(); } - return new CachedVaryRules { VaryRules = new VaryRules() { Headers = headers, Params = param } }; + return new CachedVaryRules { VaryKeyPrefix = varyKeyPrefix, VaryRules = new VaryRules() { Headers = headers, Params = param } }; } // See serialization format above @@ -140,7 +164,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal writer.Write(FormatVersion); - if (entry is CachedResponse) + if (entry is CachedResponseBody) + { + writer.Write('B'); + WriteCachedResponseBody(writer, entry as CachedResponseBody); + } + else if (entry is CachedResponse) { writer.Write('R'); WriteCachedResponse(writer, entry as CachedResponse); @@ -156,9 +185,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal } } + // See serialization format above + private static void WriteCachedResponseBody(BinaryWriter writer, CachedResponseBody entry) + { + writer.Write(entry.Body.Length); + writer.Write(entry.Body); + } + // See serialization format above private static void WriteCachedResponse(BinaryWriter writer, CachedResponse entry) { + writer.Write(entry.BodyKeyPrefix); writer.Write(entry.Created.UtcTicks); writer.Write(entry.StatusCode); writer.Write(entry.Headers.Count); @@ -168,31 +205,33 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal writer.Write(header.Value); } - writer.Write(entry.Body.Length); - writer.Write(entry.Body); - } - - // See serialization format above - private static void WriteCachedVaryRules(BinaryWriter writer, CachedVaryRules varyRules) - { - if (varyRules.VaryRules == null) + if (entry.Body == null) { writer.Write(false); } else { writer.Write(true); - writer.Write(varyRules.VaryRules.Headers.Count); - foreach (var header in varyRules.VaryRules.Headers) - { - writer.Write(header); - } + writer.Write(entry.Body.Length); + writer.Write(entry.Body); + } + } - writer.Write(varyRules.VaryRules.Params.Count); - foreach (var param in varyRules.VaryRules.Params) - { - writer.Write(param); - } + // See serialization format above + private static void WriteCachedVaryRules(BinaryWriter writer, CachedVaryRules varyRules) + { + writer.Write(varyRules.VaryKeyPrefix); + + writer.Write(varyRules.VaryRules.Headers.Count); + foreach (var header in varyRules.VaryRules.Headers) + { + writer.Write(header); + } + + writer.Write(varyRules.VaryRules.Params.Count); + foreach (var param in varyRules.VaryRules.Params) + { + writer.Write(param); } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/CachedResponse.cs b/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedResponse.cs similarity index 91% rename from src/Microsoft.AspNetCore.ResponseCaching/Internal/CachedResponse.cs rename to src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedResponse.cs index 4743d370b5..d4413bf4d1 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/CachedResponse.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedResponse.cs @@ -8,6 +8,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal { internal class CachedResponse { + internal string BodyKeyPrefix { get; set; } + internal DateTimeOffset Created { get; set; } internal int StatusCode { get; set; } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/CachedVaryRules.cs b/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedResponseBody.cs similarity index 75% rename from src/Microsoft.AspNetCore.ResponseCaching/Internal/CachedVaryRules.cs rename to src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedResponseBody.cs index bd74a08c8c..643fac449c 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/CachedVaryRules.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedResponseBody.cs @@ -3,8 +3,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal { - internal class CachedVaryRules + internal class CachedResponseBody { - internal VaryRules VaryRules; + internal byte[] Body { get; set; } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedVaryRules.cs b/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedVaryRules.cs new file mode 100644 index 0000000000..de3355e761 --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedVaryRules.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. + +namespace Microsoft.AspNetCore.ResponseCaching.Internal +{ + internal class CachedVaryRules + { + internal string VaryKeyPrefix { get; set; } + + internal VaryRules VaryRules { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IKeyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IKeyProvider.cs index 888498f908..972b9917fe 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IKeyProvider.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IKeyProvider.cs @@ -8,18 +8,18 @@ namespace Microsoft.AspNetCore.ResponseCaching public interface IKeyProvider { /// - /// Create a key using the HTTP request. + /// Create a base key using the HTTP request. /// /// The . /// The created base key. string CreateBaseKey(HttpContext httpContext); /// - /// Create a key using the HTTP context and vary rules. + /// Create a vary key using the HTTP context and vary rules. /// /// The . /// The . - /// The created base key. + /// The created vary key. string CreateVaryKey(HttpContext httpContext, VaryRules varyRules); } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/FastGuid.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/FastGuid.cs new file mode 100644 index 0000000000..075403f35c --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/FastGuid.cs @@ -0,0 +1,80 @@ +// 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; + +namespace Microsoft.AspNetCore.ResponseCaching.Internal +{ + internal class FastGuid + { + // Base32 encoding - in ascii sort order for easy text based sorting + private static readonly string _encode32Chars = "0123456789ABCDEFGHIJKLMNOPQRSTUV"; + // Global ID + private static long NextId; + + // Instance components + private string _idString; + internal long IdValue { get; private set; } + + internal string IdString + { + get + { + if (_idString == null) + { + _idString = GenerateGuidString(this); + } + return _idString; + } + } + + // Static constructor to initialize global components + static FastGuid() + { + var guidBytes = Guid.NewGuid().ToByteArray(); + + // Use the first 4 bytes from the Guid to initialize global ID + NextId = + guidBytes[0] << 32 | + guidBytes[1] << 40 | + guidBytes[2] << 48 | + guidBytes[3] << 56; + } + + internal FastGuid(long id) + { + IdValue = id; + } + + internal static FastGuid NewGuid() + { + return new FastGuid(Interlocked.Increment(ref NextId)); + } + + private static unsafe string GenerateGuidString(FastGuid guid) + { + + // stackalloc to allocate array on stack rather than heap + char* charBuffer = stackalloc char[13]; + + // ID + charBuffer[0] = _encode32Chars[(int)(guid.IdValue >> 60) & 31]; + charBuffer[1] = _encode32Chars[(int)(guid.IdValue >> 55) & 31]; + charBuffer[2] = _encode32Chars[(int)(guid.IdValue >> 50) & 31]; + charBuffer[3] = _encode32Chars[(int)(guid.IdValue >> 45) & 31]; + charBuffer[4] = _encode32Chars[(int)(guid.IdValue >> 40) & 31]; + charBuffer[5] = _encode32Chars[(int)(guid.IdValue >> 35) & 31]; + charBuffer[6] = _encode32Chars[(int)(guid.IdValue >> 30) & 31]; + charBuffer[7] = _encode32Chars[(int)(guid.IdValue >> 25) & 31]; + charBuffer[8] = _encode32Chars[(int)(guid.IdValue >> 20) & 31]; + charBuffer[9] = _encode32Chars[(int)(guid.IdValue >> 15) & 31]; + charBuffer[10] = _encode32Chars[(int)(guid.IdValue >> 10) & 31]; + charBuffer[11] = _encode32Chars[(int)(guid.IdValue >> 5) & 31]; + charBuffer[12] = _encode32Chars[(int)guid.IdValue & 31]; + + // string ctor overload that takes char* + return new string(charBuffer, 0, 13); + } + } +} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs index 117d112db9..d5296c93e2 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs @@ -33,8 +33,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal public void Set(string key, object entry, TimeSpan validFor) { _cache.Set( - key, - entry, + key, + entry, new MemoryCacheEntryOptions() { AbsoluteExpirationRelativeToNow = validFor diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingState.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingState.cs index 3ff862a502..6ea80b95b0 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingState.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingState.cs @@ -38,6 +38,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal internal CachedResponse CachedResponse { get; set; } + internal CachedVaryRules CachedVaryRules { get; set; } + public RequestHeaders RequestHeaders { get diff --git a/src/Microsoft.AspNetCore.ResponseCaching/KeyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/KeyProvider.cs index 6eac525b33..027d2e3b77 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/KeyProvider.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/KeyProvider.cs @@ -6,7 +6,6 @@ using System.Linq; using System.Text; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.ResponseCaching.Internal; using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; @@ -70,10 +69,9 @@ namespace Microsoft.AspNetCore.ResponseCaching { throw new ArgumentNullException(nameof(httpContext)); } - if (varyRules == null) + if (varyRules == null || (StringValues.IsNullOrEmpty(varyRules.Headers) && StringValues.IsNullOrEmpty(varyRules.Params))) { - // TODO: replace this with a GUID - return httpContext.GetResponseCachingState()?.BaseKey ?? CreateBaseKey(httpContext); + return httpContext.GetResponseCachingState().CachedVaryRules.VaryKeyPrefix; } var request = httpContext.Request; @@ -81,8 +79,8 @@ namespace Microsoft.AspNetCore.ResponseCaching try { - // TODO: replace this with a GUID - builder.Append(httpContext.GetResponseCachingState()?.BaseKey ?? CreateBaseKey(httpContext)); + // Prepend with the Guid of the CachedVaryRules + builder.Append(httpContext.GetResponseCachingState().CachedVaryRules.VaryKeyPrefix); // Vary by headers if (varyRules?.Headers.Count > 0) diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs index cd34857e84..4b9fe410ba 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs @@ -68,17 +68,18 @@ namespace Microsoft.AspNetCore.ResponseCaching if (cacheEntry is CachedVaryRules) { // Request contains vary rules, recompute key and try again + State.CachedVaryRules = cacheEntry as CachedVaryRules; var varyKey = _keyProvider.CreateVaryKey(_httpContext, ((CachedVaryRules)cacheEntry).VaryRules); cacheEntry = _cache.Get(varyKey); } if (cacheEntry is CachedResponse) { - var cachedResponse = cacheEntry as CachedResponse; - var cachedResponseHeaders = new ResponseHeaders(cachedResponse.Headers); + State.CachedResponse = cacheEntry as CachedResponse; + var cachedResponseHeaders = new ResponseHeaders(State.CachedResponse.Headers); State.ResponseTime = _options.SystemClock.UtcNow; - var cachedEntryAge = State.ResponseTime - cachedResponse.Created; + var cachedEntryAge = State.ResponseTime - State.CachedResponse.Created; State.CachedEntryAge = cachedEntryAge > TimeSpan.Zero ? cachedEntryAge : TimeSpan.Zero; if (_cacheabilityValidator.CachedEntryIsFresh(_httpContext, cachedResponseHeaders)) @@ -94,16 +95,22 @@ namespace Microsoft.AspNetCore.ResponseCaching { var response = _httpContext.Response; // Copy the cached status code and response headers - response.StatusCode = cachedResponse.StatusCode; - foreach (var header in cachedResponse.Headers) + response.StatusCode = State.CachedResponse.StatusCode; + foreach (var header in State.CachedResponse.Headers) { response.Headers.Add(header); } response.Headers[HeaderNames.Age] = State.CachedEntryAge.TotalSeconds.ToString("F0", CultureInfo.InvariantCulture); + var body = State.CachedResponse.Body ?? + ((CachedResponseBody)_cache.Get(State.CachedResponse.BodyKeyPrefix))?.Body; - var body = cachedResponse.Body; + // If the body is not found, something went wrong. + if (body == null) + { + return false; + } // Copy the cached response body if (body.Length > 0) @@ -181,19 +188,31 @@ namespace Microsoft.AspNetCore.ResponseCaching // Check if any vary rules exist if (!StringValues.IsNullOrEmpty(varyHeaderValue) || !StringValues.IsNullOrEmpty(varyParamsValue)) { - var cachedVaryRules = new CachedVaryRules - { - VaryRules = new VaryRules() - { - // TODO: Vary Encoding - Headers = varyHeaderValue, - Params = varyParamsValue - } - }; + // Normalize order and casing of vary by rules + var normalizedVaryHeaderValue = GetNormalizedStringValues(varyHeaderValue); + var normalizedVaryParamsValue = GetNormalizedStringValues(varyParamsValue); - // TODO: Overwrite? - _cache.Set(State.BaseKey, cachedVaryRules, State.CachedResponseValidFor); - State.VaryKey = _keyProvider.CreateVaryKey(_httpContext, cachedVaryRules.VaryRules); + // Update vary rules if they are different + if (State.CachedVaryRules == null || + !StringValues.Equals(State.CachedVaryRules.VaryRules.Params, normalizedVaryParamsValue) || + !StringValues.Equals(State.CachedVaryRules.VaryRules.Headers, normalizedVaryHeaderValue)) + { + var cachedVaryRules = new CachedVaryRules + { + VaryKeyPrefix = FastGuid.NewGuid().IdString, + VaryRules = new VaryRules() + { + // TODO: Vary Encoding + Headers = normalizedVaryHeaderValue, + Params = normalizedVaryParamsValue + } + }; + + State.CachedVaryRules = cachedVaryRules; + _cache.Set(State.BaseKey, cachedVaryRules, State.CachedResponseValidFor); + } + + State.VaryKey = _keyProvider.CreateVaryKey(_httpContext, State.CachedVaryRules.VaryRules); } // Ensure date header is set @@ -202,9 +221,10 @@ namespace Microsoft.AspNetCore.ResponseCaching State.ResponseHeaders.Date = State.ResponseTime; } - // Store the response to cache + // Store the response on the state State.CachedResponse = new CachedResponse { + BodyKeyPrefix = FastGuid.NewGuid().IdString, Created = State.ResponseHeaders.Date.Value, StatusCode = _httpContext.Response.StatusCode }; @@ -227,9 +247,24 @@ namespace Microsoft.AspNetCore.ResponseCaching { if (State.ShouldCacheResponse && ResponseCacheStream.BufferingEnabled) { - State.CachedResponse.Body = ResponseCacheStream.BufferedStream.ToArray(); + if (ResponseCacheStream.BufferedStream.Length >= _options.MinimumSplitBodySize) + { + // Store response and response body separately + _cache.Set(State.VaryKey ?? State.BaseKey, State.CachedResponse, State.CachedResponseValidFor); - _cache.Set(State.VaryKey ?? State.BaseKey, State.CachedResponse, State.CachedResponseValidFor); + var cachedResponseBody = new CachedResponseBody() + { + Body = ResponseCacheStream.BufferedStream.ToArray() + }; + + _cache.Set(State.CachedResponse.BodyKeyPrefix, cachedResponseBody, State.CachedResponseValidFor); + } + else + { + // Store response and response body together + State.CachedResponse.Body = ResponseCacheStream.BufferedStream.ToArray(); + _cache.Set(State.VaryKey ?? State.BaseKey, State.CachedResponse, State.CachedResponseValidFor); + } } } @@ -275,5 +310,29 @@ namespace Microsoft.AspNetCore.ResponseCaching // TODO: Move this temporary interface with endpoint to HttpAbstractions _httpContext.RemoveResponseCachingFeature(); } + + // Normalize order and casing + internal static StringValues GetNormalizedStringValues(StringValues stringVales) + { + if (stringVales.Count == 1) + { + return new StringValues(stringVales.ToString().ToUpperInvariant()); + } + else + { + var originalArray = stringVales.ToArray(); + var newArray = new string[originalArray.Length]; + + for (int i = 0; i < originalArray.Length; i++) + { + newArray[i] = originalArray[i].ToUpperInvariant(); + } + + // Since the casing has already been normalized, use Ordinal comparison + Array.Sort(newArray, StringComparer.Ordinal); + + return new StringValues(newArray); + } + } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingOptions.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingOptions.cs index fefe65c4e2..7630237ec5 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingOptions.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingOptions.cs @@ -18,6 +18,11 @@ namespace Microsoft.AspNetCore.Builder /// public bool CaseSensitivePaths { get; set; } = false; + /// + /// The smallest size in bytes for which the headers and body of the response will be stored separately. The default is set to 70 KB. + /// + public long MinimumSplitBodySize { get; set; } = 70 * 1024; + /// /// For testing purposes only. /// diff --git a/src/Microsoft.AspNetCore.ResponseCaching/project.json b/src/Microsoft.AspNetCore.ResponseCaching/project.json index 70fa887b15..d2521465b9 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/project.json +++ b/src/Microsoft.AspNetCore.ResponseCaching/project.json @@ -2,6 +2,7 @@ "version": "0.1.0-*", "buildOptions": { "warningsAsErrors": true, + "allowUnsafe": true, "keyFile": "../../tools/Key.snk", "nowarn": [ "CS1591" diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheEntrySerializerTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheEntrySerializerTests.cs index e85dabe9f6..bd2ad03bb1 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheEntrySerializerTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheEntrySerializerTests.cs @@ -25,78 +25,109 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public void RoundTrip_CachedResponses_Succeeds() + public void Deserialize_NullObject_ReturnsNull() + { + Assert.Null(CacheEntrySerializer.Deserialize(null)); + } + + [Fact] + public void RoundTrip_CachedResponseBody_Succeeds() + { + var cachedResponseBody = new CachedResponseBody() + { + Body = Encoding.ASCII.GetBytes("Hello world"), + }; + + AssertCachedResponseBodyEqual(cachedResponseBody, (CachedResponseBody)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedResponseBody))); + } + + [Fact] + public void RoundTrip_CachedResponseWithoutBody_Succeeds() { var headers = new HeaderDictionary(); headers["keyA"] = "valueA"; headers["keyB"] = "valueB"; - var cachedEntry = new CachedResponse() + var cachedResponse = new CachedResponse() { + BodyKeyPrefix = FastGuid.NewGuid().IdString, + Created = DateTimeOffset.UtcNow, + StatusCode = StatusCodes.Status200OK, + Headers = headers + }; + + AssertCachedResponseEqual(cachedResponse, (CachedResponse)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedResponse))); + } + + [Fact] + public void RoundTrip_CachedResponseWithBody_Succeeds() + { + var headers = new HeaderDictionary(); + headers["keyA"] = "valueA"; + headers["keyB"] = "valueB"; + var cachedResponse = new CachedResponse() + { + BodyKeyPrefix = FastGuid.NewGuid().IdString, Created = DateTimeOffset.UtcNow, StatusCode = StatusCodes.Status200OK, Body = Encoding.ASCII.GetBytes("Hello world"), Headers = headers }; - AssertCachedResponsesEqual(cachedEntry, (CachedResponse)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedEntry))); + AssertCachedResponseEqual(cachedResponse, (CachedResponse)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedResponse))); } [Fact] - public void RoundTrip_Empty_CachedVaryRules_Succeeds() + public void RoundTrip_CachedVaryRule_EmptyRules_Succeeds() { - var cachedVaryRules = new CachedVaryRules(); - - AssertCachedVaryRulesEqual(cachedVaryRules, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRules))); - } - - [Fact] - public void RoundTrip_CachedVaryRules_EmptyRules_Succeeds() - { - var cachedVaryRules = new CachedVaryRules() + var cachedVaryRule = new CachedVaryRules() { + VaryKeyPrefix = FastGuid.NewGuid().IdString, VaryRules = new VaryRules() }; - AssertCachedVaryRulesEqual(cachedVaryRules, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRules))); + AssertCachedVaryRuleEqual(cachedVaryRule, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRule))); } [Fact] - public void RoundTrip_HeadersOnly_CachedVaryRules_Succeeds() + public void RoundTrip_CachedVaryRule_HeadersOnly_Succeeds() { var headers = new[] { "headerA", "headerB" }; - var cachedVaryRules = new CachedVaryRules() + var cachedVaryRule = new CachedVaryRules() { + VaryKeyPrefix = FastGuid.NewGuid().IdString, VaryRules = new VaryRules() { Headers = headers } }; - AssertCachedVaryRulesEqual(cachedVaryRules, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRules))); + AssertCachedVaryRuleEqual(cachedVaryRule, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRule))); } [Fact] - public void RoundTrip_ParamsOnly_CachedVaryRules_Succeeds() + public void RoundTrip_CachedVaryRule_ParamsOnly_Succeeds() { var param = new[] { "paramA", "paramB" }; - var cachedVaryRules = new CachedVaryRules() + var cachedVaryRule = new CachedVaryRules() { + VaryKeyPrefix = FastGuid.NewGuid().IdString, VaryRules = new VaryRules() { Params = param } }; - AssertCachedVaryRulesEqual(cachedVaryRules, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRules))); + AssertCachedVaryRuleEqual(cachedVaryRule, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRule))); } [Fact] - public void RoundTrip_HeadersAndParams_CachedVaryRules_Succeeds() + public void RoundTrip_CachedVaryRule_HeadersAndParams_Succeeds() { var headers = new[] { "headerA", "headerB" }; var param = new[] { "paramA", "paramB" }; - var cachedVaryRules = new CachedVaryRules() + var cachedVaryRule = new CachedVaryRules() { + VaryKeyPrefix = FastGuid.NewGuid().IdString, VaryRules = new VaryRules() { Headers = headers, @@ -104,30 +135,37 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } }; - AssertCachedVaryRulesEqual(cachedVaryRules, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRules))); + AssertCachedVaryRuleEqual(cachedVaryRule, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRule))); } [Fact] public void Deserialize_InvalidEntries_ReturnsNull() { var headers = new[] { "headerA", "headerB" }; - var cachedVaryRules = new CachedVaryRules() + var cachedVaryRule = new CachedVaryRules() { + VaryKeyPrefix = FastGuid.NewGuid().IdString, VaryRules = new VaryRules() { Headers = headers } }; - var serializedEntry = CacheEntrySerializer.Serialize(cachedVaryRules); + var serializedEntry = CacheEntrySerializer.Serialize(cachedVaryRule); Array.Reverse(serializedEntry); Assert.Null(CacheEntrySerializer.Deserialize(serializedEntry)); } - private static void AssertCachedResponsesEqual(CachedResponse expected, CachedResponse actual) + private static void AssertCachedResponseBodyEqual(CachedResponseBody expected, CachedResponseBody actual) + { + Assert.True(expected.Body.SequenceEqual(actual.Body)); + } + + private static void AssertCachedResponseEqual(CachedResponse expected, CachedResponse actual) { Assert.NotNull(actual); Assert.NotNull(expected); + Assert.Equal(expected.BodyKeyPrefix, actual.BodyKeyPrefix); Assert.Equal(expected.Created, actual.Created); Assert.Equal(expected.StatusCode, actual.StatusCode); Assert.Equal(expected.Headers.Count, actual.Headers.Count); @@ -135,23 +173,23 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { Assert.Equal(expectedHeader.Value, actual.Headers[expectedHeader.Key]); } - Assert.True(expected.Body.SequenceEqual(actual.Body)); - } - - private static void AssertCachedVaryRulesEqual(CachedVaryRules expected, CachedVaryRules actual) - { - Assert.NotNull(actual); - Assert.NotNull(expected); - if (expected.VaryRules == null) + if (expected.Body == null) { - Assert.Null(actual.VaryRules); + Assert.Null(actual.Body); } else { - Assert.NotNull(actual.VaryRules); - Assert.Equal(expected.VaryRules.Headers, actual.VaryRules.Headers); - Assert.Equal(expected.VaryRules.Params, actual.VaryRules.Params); + Assert.True(expected.Body.SequenceEqual(actual.Body)); } } + + private static void AssertCachedVaryRuleEqual(CachedVaryRules expected, CachedVaryRules actual) + { + Assert.NotNull(actual); + Assert.NotNull(expected); + Assert.Equal(expected.VaryKeyPrefix, actual.VaryKeyPrefix); + Assert.Equal(expected.VaryRules.Headers, actual.VaryRules.Headers); + Assert.Equal(expected.VaryRules.Params, actual.VaryRules.Params); + } } } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/KeyProviderTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/KeyProviderTests.cs index 8f96278fc3..17922f5afa 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/KeyProviderTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/KeyProviderTests.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.ResponseCaching.Internal; using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.Options; using Xunit; @@ -12,11 +13,15 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests public class DefaultKeyProviderTests { private static readonly char KeyDelimiter = '\x1e'; + private static readonly CachedVaryRules TestVaryRules = new CachedVaryRules() + { + VaryKeyPrefix = FastGuid.NewGuid().IdString + }; [Fact] public void DefaultKeyProvider_CreateBaseKey_IncludesOnlyNormalizedMethodAndPath() { - var httpContext = new DefaultHttpContext(); + var httpContext = CreateDefaultContext(); httpContext.Request.Method = "head"; httpContext.Request.Path = "/path/subpath"; httpContext.Request.Scheme = "https"; @@ -31,7 +36,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public void DefaultKeyProvider_CreateBaseKey_CaseInsensitivePath_NormalizesPath() { - var httpContext = new DefaultHttpContext(); + var httpContext = CreateDefaultContext(); httpContext.Request.Method = "GET"; httpContext.Request.Path = "/Path"; var keyProvider = CreateTestKeyProvider(new ResponseCachingOptions() @@ -45,7 +50,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public void DefaultKeyProvider_CreateBaseKey_CaseSensitivePath_PreservesPathCase() { - var httpContext = new DefaultHttpContext(); + var httpContext = CreateDefaultContext(); httpContext.Request.Method = "GET"; httpContext.Request.Path = "/Path"; var keyProvider = CreateTestKeyProvider(new ResponseCachingOptions() @@ -56,17 +61,25 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Assert.Equal($"GET{KeyDelimiter}/Path", keyProvider.CreateBaseKey(httpContext)); } + [Fact] + public void DefaultKeyProvider_CreateVaryKey_ReturnsCachedVaryGuid_IfVaryRulesIsNullOrEmpty() + { + var httpContext = CreateDefaultContext(); + var keyProvider = CreateTestKeyProvider(); + + Assert.Equal($"{TestVaryRules.VaryKeyPrefix}", keyProvider.CreateVaryKey(httpContext, null)); + Assert.Equal($"{TestVaryRules.VaryKeyPrefix}", keyProvider.CreateVaryKey(httpContext, new VaryRules())); + } + [Fact] public void DefaultKeyProvider_CreateVaryKey_IncludesListedHeadersOnly() { - var httpContext = new DefaultHttpContext(); - httpContext.Request.Method = "GET"; - httpContext.Request.Path = "/"; + var httpContext = CreateDefaultContext(); httpContext.Request.Headers["HeaderA"] = "ValueA"; httpContext.Request.Headers["HeaderB"] = "ValueB"; var keyProvider = CreateTestKeyProvider(); - Assert.Equal($"GET{KeyDelimiter}/{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=null", + Assert.Equal($"{TestVaryRules.VaryKeyPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=null", keyProvider.CreateVaryKey(httpContext, new VaryRules() { Headers = new string[] { "HeaderA", "HeaderC" } @@ -76,13 +89,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public void DefaultKeyProvider_CreateVaryKey_IncludesListedParamsOnly() { - var httpContext = new DefaultHttpContext(); - httpContext.Request.Method = "GET"; - httpContext.Request.Path = "/"; + var httpContext = CreateDefaultContext(); httpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB"); var keyProvider = CreateTestKeyProvider(); - Assert.Equal($"GET{KeyDelimiter}/{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=null", + Assert.Equal($"{TestVaryRules.VaryKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=null", keyProvider.CreateVaryKey(httpContext, new VaryRules() { Params = new string[] { "ParamA", "ParamC" } @@ -92,13 +103,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public void DefaultKeyProvider_CreateVaryKey_IncludesParams_ParamNameCaseInsensitive_UseParamCasing() { - var httpContext = new DefaultHttpContext(); - httpContext.Request.Method = "GET"; - httpContext.Request.Path = "/"; + var httpContext = CreateDefaultContext(); httpContext.Request.QueryString = new QueryString("?parama=ValueA¶mB=ValueB"); var keyProvider = CreateTestKeyProvider(); - Assert.Equal($"GET{KeyDelimiter}/{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=null", + Assert.Equal($"{TestVaryRules.VaryKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=null", keyProvider.CreateVaryKey(httpContext, new VaryRules() { Params = new string[] { "ParamA", "ParamC" } @@ -108,15 +117,13 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public void DefaultKeyProvider_CreateVaryKey_IncludesAllQueryParamsGivenAsterisk() { - var httpContext = new DefaultHttpContext(); - httpContext.Request.Method = "GET"; - httpContext.Request.Path = "/"; + var httpContext = CreateDefaultContext(); httpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB"); var keyProvider = CreateTestKeyProvider(); // To support case insensitivity, all param keys are converted to upper case. // Explicit params uses the casing specified in the setting. - Assert.Equal($"GET{KeyDelimiter}/{KeyDelimiter}Q{KeyDelimiter}PARAMA=ValueA{KeyDelimiter}PARAMB=ValueB", + Assert.Equal($"{TestVaryRules.VaryKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}PARAMA=ValueA{KeyDelimiter}PARAMB=ValueB", keyProvider.CreateVaryKey(httpContext, new VaryRules() { Params = new string[] { "*" } @@ -126,15 +133,13 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public void DefaultKeyProvider_CreateVaryKey_IncludesListedHeadersAndParams() { - var httpContext = new DefaultHttpContext(); - httpContext.Request.Method = "GET"; - httpContext.Request.Path = "/"; + var httpContext = CreateDefaultContext(); httpContext.Request.Headers["HeaderA"] = "ValueA"; httpContext.Request.Headers["HeaderB"] = "ValueB"; httpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB"); var keyProvider = CreateTestKeyProvider(); - Assert.Equal($"GET{KeyDelimiter}/{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=null{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=null", + Assert.Equal($"{TestVaryRules.VaryKeyPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=null{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=null", keyProvider.CreateVaryKey(httpContext, new VaryRules() { Headers = new string[] { "HeaderA", "HeaderC" }, @@ -142,6 +147,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests })); } + private static HttpContext CreateDefaultContext() + { + var context = new DefaultHttpContext(); + context.AddResponseCachingState(); + context.GetResponseCachingState().CachedVaryRules = TestVaryRules; + return context; + } + private static IKeyProvider CreateTestKeyProvider() { return CreateTestKeyProvider(new ResponseCachingOptions()); diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs index c4caca269b..ec11698d0d 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs @@ -14,6 +14,7 @@ using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.Options; using Microsoft.Net.Http.Headers; using Xunit; +using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.ResponseCaching.Tests { @@ -154,10 +155,312 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Assert.False(context.ConditionalRequestSatisfied(cachedHeaders)); } + [Fact] + public void FinalizeCachingHeaders_DoNotUpdateShouldCacheResponse_IfResponseIsNotCacheable() + { + var httpContext = new DefaultHttpContext(); + var context = CreateTestContext(httpContext); + var state = httpContext.GetResponseCachingState(); + + Assert.False(state.ShouldCacheResponse); + + context.ShimResponseStream(); + context.FinalizeCachingHeaders(); + + Assert.False(state.ShouldCacheResponse); + } + + [Fact] + public void FinalizeCachingHeaders_UpdateShouldCacheResponse_IfResponseIsCacheable() + { + var httpContext = new DefaultHttpContext(); + httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + Public = true + }; + var context = CreateTestContext(httpContext); + var state = httpContext.GetResponseCachingState(); + + Assert.False(state.ShouldCacheResponse); + + context.FinalizeCachingHeaders(); + + Assert.True(state.ShouldCacheResponse); + } + + [Fact] + public void FinalizeCachingHeaders_DefaultResponseValidity_Is10Seconds() + { + var httpContext = new DefaultHttpContext(); + httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + Public = true + }; + var context = CreateTestContext(httpContext); + + context.FinalizeCachingHeaders(); + + Assert.Equal(TimeSpan.FromSeconds(10), httpContext.GetResponseCachingState().CachedResponseValidFor); + } + + [Fact] + public void FinalizeCachingHeaders_ResponseValidity_UseExpiryIfAvailable() + { + var httpContext = new DefaultHttpContext(); + httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + Public = true + }; + var context = CreateTestContext(httpContext); + + var state = httpContext.GetResponseCachingState(); + var utcNow = DateTimeOffset.MinValue; + state.ResponseTime = utcNow; + state.ResponseHeaders.Expires = utcNow + TimeSpan.FromSeconds(11); + + context.FinalizeCachingHeaders(); + + Assert.Equal(TimeSpan.FromSeconds(11), state.CachedResponseValidFor); + } + + [Fact] + public void FinalizeCachingHeaders_ResponseValidity_UseMaxAgeIfAvailable() + { + var httpContext = new DefaultHttpContext(); + httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + Public = true, + MaxAge = TimeSpan.FromSeconds(12) + }; + var context = CreateTestContext(httpContext); + + var state = httpContext.GetResponseCachingState(); + state.ResponseTime = DateTimeOffset.UtcNow; + state.ResponseHeaders.Expires = state.ResponseTime + TimeSpan.FromSeconds(11); + + context.FinalizeCachingHeaders(); + + Assert.Equal(TimeSpan.FromSeconds(12), state.CachedResponseValidFor); + } + + [Fact] + public void FinalizeCachingHeaders_ResponseValidity_UseSharedMaxAgeIfAvailable() + { + var httpContext = new DefaultHttpContext(); + httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + Public = true, + MaxAge = TimeSpan.FromSeconds(12), + SharedMaxAge = TimeSpan.FromSeconds(13) + }; + var context = CreateTestContext(httpContext); + + var state = httpContext.GetResponseCachingState(); + state.ResponseTime = DateTimeOffset.UtcNow; + state.ResponseHeaders.Expires = state.ResponseTime + TimeSpan.FromSeconds(11); + + context.FinalizeCachingHeaders(); + + Assert.Equal(TimeSpan.FromSeconds(13), state.CachedResponseValidFor); + } + + [Fact] + public void FinalizeCachingHeaders_UpdateCachedVaryRules_IfNotEquivalentToPrevious() + { + var httpContext = new DefaultHttpContext(); + var cache = new TestResponseCache(); + var context = CreateTestContext(httpContext, cache, new ResponseCachingOptions()); + var state = httpContext.GetResponseCachingState(); + + httpContext.Response.Headers[HeaderNames.Vary] = new StringValues(new[] { "headerA", "HEADERB", "HEADERc" }); + httpContext.AddResponseCachingFeature(); + httpContext.GetResponseCachingFeature().VaryParams = new StringValues(new[] { "paramB", "PARAMAA" }); + httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + Public = true, + }; + var cachedVaryRules = new CachedVaryRules() + { + VaryRules = new VaryRules() + { + Headers = new StringValues(new[] { "HeaderA", "HeaderB" }), + Params = new StringValues(new[] { "ParamA", "ParamB" }) + } + }; + state.CachedVaryRules = cachedVaryRules; + + context.FinalizeCachingHeaders(); + + Assert.Equal(1, cache.StoredItems); + Assert.NotSame(cachedVaryRules, state.CachedVaryRules); + } + + [Fact] + public void FinalizeCachingHeaders_DoNotUpdateCachedVaryRules_IfEquivalentToPrevious() + { + var httpContext = new DefaultHttpContext(); + var cache = new TestResponseCache(); + var context = CreateTestContext(httpContext, cache, new ResponseCachingOptions()); + var state = httpContext.GetResponseCachingState(); + + httpContext.Response.Headers[HeaderNames.Vary] = new StringValues(new[] { "headerA", "HEADERB" }); + httpContext.AddResponseCachingFeature(); + httpContext.GetResponseCachingFeature().VaryParams = new StringValues(new[] { "paramB", "PARAMA" }); + httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + Public = true, + }; + var cachedVaryRules = new CachedVaryRules() + { + VaryKeyPrefix = FastGuid.NewGuid().IdString, + VaryRules = new VaryRules() + { + Headers = new StringValues(new[] { "HEADERA", "HEADERB" }), + Params = new StringValues(new[] { "PARAMA", "PARAMB" }) + } + }; + state.CachedVaryRules = cachedVaryRules; + + context.FinalizeCachingHeaders(); + + Assert.Equal(0, cache.StoredItems); + Assert.Same(cachedVaryRules, state.CachedVaryRules); + } + + [Fact] + public void FinalizeCachingHeaders_DoNotAddDate_IfSpecified() + { + var httpContext = new DefaultHttpContext(); + httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + Public = true + }; + var context = CreateTestContext(httpContext); + var state = httpContext.GetResponseCachingState(); + var utcNow = DateTimeOffset.MinValue; + state.ResponseTime = utcNow; + + Assert.Null(state.ResponseHeaders.Date); + + context.FinalizeCachingHeaders(); + + Assert.Equal(utcNow, state.ResponseHeaders.Date); + } + + [Fact] + public void FinalizeCachingHeaders_AddsDate_IfNoneSpecified() + { + var httpContext = new DefaultHttpContext(); + httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + Public = true + }; + var context = CreateTestContext(httpContext); + var state = httpContext.GetResponseCachingState(); + var utcNow = DateTimeOffset.MinValue; + state.ResponseHeaders.Date = utcNow; + state.ResponseTime = utcNow + TimeSpan.FromSeconds(10); + + Assert.Equal(utcNow, state.ResponseHeaders.Date); + + context.FinalizeCachingHeaders(); + + Assert.Equal(utcNow, state.ResponseHeaders.Date); + } + + [Fact] + public void FinalizeCachingHeaders_StoresCachedResponse_InState() + { + var httpContext = new DefaultHttpContext(); + httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + Public = true + }; + var context = CreateTestContext(httpContext); + var state = httpContext.GetResponseCachingState(); + + Assert.Null(state.CachedResponse); + + context.FinalizeCachingHeaders(); + + Assert.NotNull(state.CachedResponse); + } + + [Fact] + public async Task FinalizeCachingBody_StoreResponseBodySeparately_IfLargerThanLimit() + { + var httpContext = new DefaultHttpContext(); + var cache = new TestResponseCache(); + var context = CreateTestContext(httpContext, cache, new ResponseCachingOptions()); + + context.ShimResponseStream(); + await httpContext.Response.WriteAsync(new string('0', 70 * 1024)); + + var state = httpContext.GetResponseCachingState(); + state.ShouldCacheResponse = true; + state.CachedResponse = new CachedResponse() + { + BodyKeyPrefix = FastGuid.NewGuid().IdString + }; + state.BaseKey = "BaseKey"; + state.CachedResponseValidFor = TimeSpan.FromSeconds(10); + + context.FinalizeCachingBody(); + + Assert.Equal(2, cache.StoredItems); + } + + [Fact] + public async Task FinalizeCachingBody_StoreResponseBodyInCachedResponse_IfSmallerThanLimit() + { + var httpContext = new DefaultHttpContext(); + var cache = new TestResponseCache(); + var context = CreateTestContext(httpContext, cache, new ResponseCachingOptions()); + + context.ShimResponseStream(); + await httpContext.Response.WriteAsync(new string('0', 70 * 1024 - 1)); + + var state = httpContext.GetResponseCachingState(); + state.ShouldCacheResponse = true; + state.CachedResponse = new CachedResponse() + { + BodyKeyPrefix = FastGuid.NewGuid().IdString + }; + state.BaseKey = "BaseKey"; + state.CachedResponseValidFor = TimeSpan.FromSeconds(10); + + context.FinalizeCachingBody(); + + Assert.Equal(1, cache.StoredItems); + } + + [Fact] + public void NormalizeStringValues_NormalizesCasingToUpper() + { + var uppercaseStrings = new StringValues(new[] { "STRINGA", "STRINGB" }); + var lowercaseStrings = new StringValues(new[] { "stringA", "stringB" }); + + var normalizedStrings = ResponseCachingContext.GetNormalizedStringValues(lowercaseStrings); + + Assert.Equal(uppercaseStrings, normalizedStrings); + } + + [Fact] + public void NormalizeStringValues_NormalizesOrder() + { + var orderedStrings = new StringValues(new[] { "STRINGA", "STRINGB" }); + var reverseOrderStrings = new StringValues(new[] { "STRINGB", "STRINGA" }); + + var normalizedStrings = ResponseCachingContext.GetNormalizedStringValues(reverseOrderStrings); + + Assert.Equal(orderedStrings, normalizedStrings); + } + private static ResponseCachingContext CreateTestContext(HttpContext httpContext) { return CreateTestContext( httpContext, + new TestResponseCache(), new ResponseCachingOptions(), new CacheabilityValidator()); } @@ -166,6 +469,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { return CreateTestContext( httpContext, + new TestResponseCache(), options, new CacheabilityValidator()); } @@ -174,12 +478,23 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { return CreateTestContext( httpContext, + new TestResponseCache(), new ResponseCachingOptions(), cacheabilityValidator); } + private static ResponseCachingContext CreateTestContext(HttpContext httpContext, IResponseCache responseCache, ResponseCachingOptions options) + { + return CreateTestContext( + httpContext, + responseCache, + options, + new CacheabilityValidator()); + } + private static ResponseCachingContext CreateTestContext( HttpContext httpContext, + IResponseCache responseCache, ResponseCachingOptions options, ICacheabilityValidator cacheabilityValidator) { @@ -187,7 +502,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests return new ResponseCachingContext( httpContext, - new TestResponseCache(), + responseCache, options, cacheabilityValidator, new KeyProvider(new DefaultObjectPoolProvider(), Options.Create(options))); @@ -195,6 +510,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests private class TestResponseCache : IResponseCache { + public int StoredItems { get; private set; } + public object Get(string key) { return null; @@ -206,6 +523,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests public void Set(string key, object entry, TimeSpan validFor) { + StoredItems++; } } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs index c28c31e8c1..d37d76e30e 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs @@ -497,6 +497,29 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } } + [Fact] + public async void ServesCachedContent_WithoutReplacingCachedVaryBy_OnCacheMiss() + { + var builder = CreateBuilderWithResponseCaching(async (context) => + { + context.Response.Headers[HeaderNames.Vary] = HeaderNames.From; + await DefaultRequestDelegate(context); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + client.DefaultRequestHeaders.From = "user@example.com"; + var initialResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.From = "user2@example.com"; + var otherResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.From = "user@example.com"; + var subsequentResponse = await client.GetAsync(""); + + await AssertResponseCachedAsync(initialResponse, subsequentResponse); + } + } + private static async Task AssertResponseCachedAsync(HttpResponseMessage initialResponse, HttpResponseMessage subsequentResponse) { initialResponse.EnsureSuccessStatusCode(); From 65b89668bb0e49d4b168de6a634739468a1d95f9 Mon Sep 17 00:00:00 2001 From: John Luo Date: Thu, 8 Sep 2016 17:16:32 -0700 Subject: [PATCH 026/188] Allow lookup of multiple keys - Do not cache if content-length mismatches with the length of the response body --- .../Interfaces/IKeyProvider.cs | 24 +- .../Internal/ResponseCachingState.cs | 4 +- .../KeyProvider.cs | 15 +- .../ResponseCachingContext.cs | 159 ++++---- .../KeyProviderTests.cs | 38 +- .../ResponseCachingContextTests.cs | 378 ++++++++++++++---- 6 files changed, 435 insertions(+), 183 deletions(-) diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IKeyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IKeyProvider.cs index 972b9917fe..2013b202b5 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IKeyProvider.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IKeyProvider.cs @@ -1,6 +1,7 @@ // 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.AspNetCore.Http; namespace Microsoft.AspNetCore.ResponseCaching @@ -8,18 +9,33 @@ namespace Microsoft.AspNetCore.ResponseCaching public interface IKeyProvider { /// - /// Create a base key using the HTTP request. + /// Create a base key using the HTTP context for storing items. /// /// The . /// The created base key. - string CreateBaseKey(HttpContext httpContext); + string CreateStorageBaseKey(HttpContext httpContext); /// - /// Create a vary key using the HTTP context and vary rules. + /// Create one or more base keys using the HTTP context for looking up items. + /// + /// The . + /// An ordered containing the base keys to try when looking up items. + IEnumerable CreateLookupBaseKey(HttpContext httpContext); + + /// + /// Create a vary key using the HTTP context and vary rules for storing items. /// /// The . /// The . /// The created vary key. - string CreateVaryKey(HttpContext httpContext, VaryRules varyRules); + string CreateStorageVaryKey(HttpContext httpContext, VaryRules varyRules); + + /// + /// Create one or more vary keys using the HTTP context and vary rules for looking up items. + /// + /// The . + /// The . + /// An ordered containing the vary keys to try when looking up items. + IEnumerable CreateLookupVaryKey(HttpContext httpContext, VaryRules varyRules); } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingState.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingState.cs index 6ea80b95b0..422f7c9746 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingState.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingState.cs @@ -26,9 +26,9 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal public bool ShouldCacheResponse { get; internal set; } - public string BaseKey { get; internal set; } + public string StorageBaseKey { get; internal set; } - public string VaryKey { get; internal set; } + public string StorageVaryKey { get; internal set; } public DateTimeOffset ResponseTime { get; internal set; } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/KeyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/KeyProvider.cs index 027d2e3b77..4de7a40b36 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/KeyProvider.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/KeyProvider.cs @@ -2,6 +2,7 @@ // 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.Linq; using System.Text; using Microsoft.AspNetCore.Builder; @@ -35,9 +36,19 @@ namespace Microsoft.AspNetCore.ResponseCaching _options = options.Value; } + public virtual IEnumerable CreateLookupBaseKey(HttpContext httpContext) + { + return new string[] { CreateStorageBaseKey(httpContext) }; + } + + public virtual IEnumerable CreateLookupVaryKey(HttpContext httpContext, VaryRules varyRules) + { + return new string[] { CreateStorageVaryKey(httpContext, varyRules) }; + } + // GET/PATH // TODO: Method invariant retrieval? E.g. HEAD after GET to the same resource. - public virtual string CreateBaseKey(HttpContext httpContext) + public virtual string CreateStorageBaseKey(HttpContext httpContext) { if (httpContext == null) { @@ -63,7 +74,7 @@ namespace Microsoft.AspNetCore.ResponseCaching } // BaseKeyHHeaderName=HeaderValueQQueryName=QueryValue - public virtual string CreateVaryKey(HttpContext httpContext, VaryRules varyRules) + public virtual string CreateStorageVaryKey(HttpContext httpContext, VaryRules varyRules) { if (httpContext == null) { diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs index 4b9fe410ba..55dc1824ca 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs @@ -59,84 +59,101 @@ namespace Microsoft.AspNetCore.ResponseCaching private IHttpSendFileFeature OriginalSendFileFeature { get; set; } - internal async Task TryServeFromCacheAsync() + internal async Task TryServeCachedResponseAsync(CachedResponse cachedResponse) { - State.BaseKey = _keyProvider.CreateBaseKey(_httpContext); - var cacheEntry = _cache.Get(State.BaseKey); - var responseServed = false; + State.CachedResponse = cachedResponse; + var cachedResponseHeaders = new ResponseHeaders(State.CachedResponse.Headers); - if (cacheEntry is CachedVaryRules) + State.ResponseTime = _options.SystemClock.UtcNow; + var cachedEntryAge = State.ResponseTime - State.CachedResponse.Created; + State.CachedEntryAge = cachedEntryAge > TimeSpan.Zero ? cachedEntryAge : TimeSpan.Zero; + + if (_cacheabilityValidator.CachedEntryIsFresh(_httpContext, cachedResponseHeaders)) { - // Request contains vary rules, recompute key and try again - State.CachedVaryRules = cacheEntry as CachedVaryRules; - var varyKey = _keyProvider.CreateVaryKey(_httpContext, ((CachedVaryRules)cacheEntry).VaryRules); - cacheEntry = _cache.Get(varyKey); - } - - if (cacheEntry is CachedResponse) - { - State.CachedResponse = cacheEntry as CachedResponse; - var cachedResponseHeaders = new ResponseHeaders(State.CachedResponse.Headers); - - State.ResponseTime = _options.SystemClock.UtcNow; - var cachedEntryAge = State.ResponseTime - State.CachedResponse.Created; - State.CachedEntryAge = cachedEntryAge > TimeSpan.Zero ? cachedEntryAge : TimeSpan.Zero; - - if (_cacheabilityValidator.CachedEntryIsFresh(_httpContext, cachedResponseHeaders)) + // Check conditional request rules + if (ConditionalRequestSatisfied(cachedResponseHeaders)) { - responseServed = true; - - // Check conditional request rules - if (ConditionalRequestSatisfied(cachedResponseHeaders)) - { - _httpContext.Response.StatusCode = StatusCodes.Status304NotModified; - } - else - { - var response = _httpContext.Response; - // Copy the cached status code and response headers - response.StatusCode = State.CachedResponse.StatusCode; - foreach (var header in State.CachedResponse.Headers) - { - response.Headers.Add(header); - } - - response.Headers[HeaderNames.Age] = State.CachedEntryAge.TotalSeconds.ToString("F0", CultureInfo.InvariantCulture); - - var body = State.CachedResponse.Body ?? - ((CachedResponseBody)_cache.Get(State.CachedResponse.BodyKeyPrefix))?.Body; - - // If the body is not found, something went wrong. - if (body == null) - { - return false; - } - - // Copy the cached response body - if (body.Length > 0) - { - // Add a content-length if required - if (response.ContentLength == null && StringValues.IsNullOrEmpty(response.Headers[HeaderNames.TransferEncoding])) - { - response.ContentLength = body.Length; - } - await response.Body.WriteAsync(body, 0, body.Length); - } - } + _httpContext.Response.StatusCode = StatusCodes.Status304NotModified; } else { - // TODO: Validate with endpoint instead + var response = _httpContext.Response; + // Copy the cached status code and response headers + response.StatusCode = State.CachedResponse.StatusCode; + foreach (var header in State.CachedResponse.Headers) + { + response.Headers.Add(header); + } + + response.Headers[HeaderNames.Age] = State.CachedEntryAge.TotalSeconds.ToString("F0", CultureInfo.InvariantCulture); + + var body = State.CachedResponse.Body ?? + ((CachedResponseBody)_cache.Get(State.CachedResponse.BodyKeyPrefix))?.Body; + + // If the body is not found, something went wrong. + if (body == null) + { + return false; + } + + // Copy the cached response body + if (body.Length > 0) + { + // Add a content-length if required + if (response.ContentLength == null && StringValues.IsNullOrEmpty(response.Headers[HeaderNames.TransferEncoding])) + { + response.ContentLength = body.Length; + } + await response.Body.WriteAsync(body, 0, body.Length); + } + } + + return true; + } + else + { + // TODO: Validate with endpoint instead + } + + return false; + } + + internal async Task TryServeFromCacheAsync() + { + foreach (var baseKey in _keyProvider.CreateLookupBaseKey(_httpContext)) + { + var cacheEntry = _cache.Get(baseKey); + + if (cacheEntry is CachedVaryRules) + { + // Request contains vary rules, recompute key(s) and try again + State.CachedVaryRules = cacheEntry as CachedVaryRules; + + foreach (var varyKey in _keyProvider.CreateLookupVaryKey(_httpContext, State.CachedVaryRules.VaryRules)) + { + cacheEntry = _cache.Get(varyKey); + + if (cacheEntry is CachedResponse && await TryServeCachedResponseAsync(cacheEntry as CachedResponse)) + { + return true; + } + } + } + + if (cacheEntry is CachedResponse && await TryServeCachedResponseAsync(cacheEntry as CachedResponse)) + { + return true; } } - if (!responseServed && State.RequestCacheControl.OnlyIfCached) + + if (State.RequestCacheControl.OnlyIfCached) { _httpContext.Response.StatusCode = StatusCodes.Status504GatewayTimeout; - responseServed = true; + return true; } - return responseServed; + return false; } internal bool ConditionalRequestSatisfied(ResponseHeaders cachedResponseHeaders) @@ -174,6 +191,7 @@ namespace Microsoft.AspNetCore.ResponseCaching if (_cacheabilityValidator.ResponseIsCacheable(_httpContext)) { State.ShouldCacheResponse = true; + State.StorageBaseKey = _keyProvider.CreateStorageBaseKey(_httpContext); // Create the cache entry now var response = _httpContext.Response; @@ -209,10 +227,10 @@ namespace Microsoft.AspNetCore.ResponseCaching }; State.CachedVaryRules = cachedVaryRules; - _cache.Set(State.BaseKey, cachedVaryRules, State.CachedResponseValidFor); + _cache.Set(State.StorageBaseKey, cachedVaryRules, State.CachedResponseValidFor); } - State.VaryKey = _keyProvider.CreateVaryKey(_httpContext, State.CachedVaryRules.VaryRules); + State.StorageVaryKey = _keyProvider.CreateStorageVaryKey(_httpContext, State.CachedVaryRules.VaryRules); } // Ensure date header is set @@ -245,12 +263,15 @@ namespace Microsoft.AspNetCore.ResponseCaching internal void FinalizeCachingBody() { - if (State.ShouldCacheResponse && ResponseCacheStream.BufferingEnabled) + if (State.ShouldCacheResponse && + ResponseCacheStream.BufferingEnabled && + (State.ResponseHeaders.ContentLength == null || + State.ResponseHeaders.ContentLength == ResponseCacheStream.BufferedStream.Length)) { if (ResponseCacheStream.BufferedStream.Length >= _options.MinimumSplitBodySize) { // Store response and response body separately - _cache.Set(State.VaryKey ?? State.BaseKey, State.CachedResponse, State.CachedResponseValidFor); + _cache.Set(State.StorageVaryKey ?? State.StorageBaseKey, State.CachedResponse, State.CachedResponseValidFor); var cachedResponseBody = new CachedResponseBody() { @@ -263,7 +284,7 @@ namespace Microsoft.AspNetCore.ResponseCaching { // Store response and response body together State.CachedResponse.Body = ResponseCacheStream.BufferedStream.ToArray(); - _cache.Set(State.VaryKey ?? State.BaseKey, State.CachedResponse, State.CachedResponseValidFor); + _cache.Set(State.StorageVaryKey ?? State.StorageBaseKey, State.CachedResponse, State.CachedResponseValidFor); } } } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/KeyProviderTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/KeyProviderTests.cs index 17922f5afa..98e1450b9b 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/KeyProviderTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/KeyProviderTests.cs @@ -19,7 +19,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests }; [Fact] - public void DefaultKeyProvider_CreateBaseKey_IncludesOnlyNormalizedMethodAndPath() + public void DefaultKeyProvider_CreateStorageBaseKey_IncludesOnlyNormalizedMethodAndPath() { var httpContext = CreateDefaultContext(); httpContext.Request.Method = "head"; @@ -30,11 +30,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests httpContext.Request.QueryString = new QueryString("?query.Key=a&query.Value=b"); var keyProvider = CreateTestKeyProvider(); - Assert.Equal($"HEAD{KeyDelimiter}/PATH/SUBPATH", keyProvider.CreateBaseKey(httpContext)); + Assert.Equal($"HEAD{KeyDelimiter}/PATH/SUBPATH", keyProvider.CreateStorageBaseKey(httpContext)); } [Fact] - public void DefaultKeyProvider_CreateBaseKey_CaseInsensitivePath_NormalizesPath() + public void DefaultKeyProvider_CreateStorageBaseKey_CaseInsensitivePath_NormalizesPath() { var httpContext = CreateDefaultContext(); httpContext.Request.Method = "GET"; @@ -44,11 +44,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests CaseSensitivePaths = false }); - Assert.Equal($"GET{KeyDelimiter}/PATH", keyProvider.CreateBaseKey(httpContext)); + Assert.Equal($"GET{KeyDelimiter}/PATH", keyProvider.CreateStorageBaseKey(httpContext)); } [Fact] - public void DefaultKeyProvider_CreateBaseKey_CaseSensitivePath_PreservesPathCase() + public void DefaultKeyProvider_CreateStorageBaseKey_CaseSensitivePath_PreservesPathCase() { var httpContext = CreateDefaultContext(); httpContext.Request.Method = "GET"; @@ -58,21 +58,21 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests CaseSensitivePaths = true }); - Assert.Equal($"GET{KeyDelimiter}/Path", keyProvider.CreateBaseKey(httpContext)); + Assert.Equal($"GET{KeyDelimiter}/Path", keyProvider.CreateStorageBaseKey(httpContext)); } [Fact] - public void DefaultKeyProvider_CreateVaryKey_ReturnsCachedVaryGuid_IfVaryRulesIsNullOrEmpty() + public void DefaultKeyProvider_CreateStorageVaryKey_ReturnsCachedVaryGuid_IfVaryRulesIsNullOrEmpty() { var httpContext = CreateDefaultContext(); var keyProvider = CreateTestKeyProvider(); - Assert.Equal($"{TestVaryRules.VaryKeyPrefix}", keyProvider.CreateVaryKey(httpContext, null)); - Assert.Equal($"{TestVaryRules.VaryKeyPrefix}", keyProvider.CreateVaryKey(httpContext, new VaryRules())); + Assert.Equal($"{TestVaryRules.VaryKeyPrefix}", keyProvider.CreateStorageVaryKey(httpContext, null)); + Assert.Equal($"{TestVaryRules.VaryKeyPrefix}", keyProvider.CreateStorageVaryKey(httpContext, new VaryRules())); } [Fact] - public void DefaultKeyProvider_CreateVaryKey_IncludesListedHeadersOnly() + public void DefaultKeyProvider_CreateStorageVaryKey_IncludesListedHeadersOnly() { var httpContext = CreateDefaultContext(); httpContext.Request.Headers["HeaderA"] = "ValueA"; @@ -80,42 +80,42 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var keyProvider = CreateTestKeyProvider(); Assert.Equal($"{TestVaryRules.VaryKeyPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=null", - keyProvider.CreateVaryKey(httpContext, new VaryRules() + keyProvider.CreateStorageVaryKey(httpContext, new VaryRules() { Headers = new string[] { "HeaderA", "HeaderC" } })); } [Fact] - public void DefaultKeyProvider_CreateVaryKey_IncludesListedParamsOnly() + public void DefaultKeyProvider_CreateStorageVaryKey_IncludesListedParamsOnly() { var httpContext = CreateDefaultContext(); httpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB"); var keyProvider = CreateTestKeyProvider(); Assert.Equal($"{TestVaryRules.VaryKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=null", - keyProvider.CreateVaryKey(httpContext, new VaryRules() + keyProvider.CreateStorageVaryKey(httpContext, new VaryRules() { Params = new string[] { "ParamA", "ParamC" } })); } [Fact] - public void DefaultKeyProvider_CreateVaryKey_IncludesParams_ParamNameCaseInsensitive_UseParamCasing() + public void DefaultKeyProvider_CreateStorageVaryKey_IncludesParams_ParamNameCaseInsensitive_UseParamCasing() { var httpContext = CreateDefaultContext(); httpContext.Request.QueryString = new QueryString("?parama=ValueA¶mB=ValueB"); var keyProvider = CreateTestKeyProvider(); Assert.Equal($"{TestVaryRules.VaryKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=null", - keyProvider.CreateVaryKey(httpContext, new VaryRules() + keyProvider.CreateStorageVaryKey(httpContext, new VaryRules() { Params = new string[] { "ParamA", "ParamC" } })); } [Fact] - public void DefaultKeyProvider_CreateVaryKey_IncludesAllQueryParamsGivenAsterisk() + public void DefaultKeyProvider_CreateStorageVaryKey_IncludesAllQueryParamsGivenAsterisk() { var httpContext = CreateDefaultContext(); httpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB"); @@ -124,14 +124,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests // To support case insensitivity, all param keys are converted to upper case. // Explicit params uses the casing specified in the setting. Assert.Equal($"{TestVaryRules.VaryKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}PARAMA=ValueA{KeyDelimiter}PARAMB=ValueB", - keyProvider.CreateVaryKey(httpContext, new VaryRules() + keyProvider.CreateStorageVaryKey(httpContext, new VaryRules() { Params = new string[] { "*" } })); } [Fact] - public void DefaultKeyProvider_CreateVaryKey_IncludesListedHeadersAndParams() + public void DefaultKeyProvider_CreateStorageVaryKey_IncludesListedHeadersAndParams() { var httpContext = CreateDefaultContext(); httpContext.Request.Headers["HeaderA"] = "ValueA"; @@ -140,7 +140,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var keyProvider = CreateTestKeyProvider(); Assert.Equal($"{TestVaryRules.VaryKeyPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=null{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=null", - keyProvider.CreateVaryKey(httpContext, new VaryRules() + keyProvider.CreateStorageVaryKey(httpContext, new VaryRules() { Headers = new string[] { "HeaderA", "HeaderC" }, Params = new string[] { "ParamA", "ParamC" } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs index ec11698d0d..c900f3161e 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs @@ -10,16 +10,100 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Headers; using Microsoft.AspNetCore.ResponseCaching.Internal; +using Microsoft.Extensions.Internal; using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; using Xunit; -using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.ResponseCaching.Tests { public class ResponseCachingContextTests { + [Fact] + public async Task TryServeFromCacheAsync_OnlyIfCached_Serves504() + { + var cache = new TestResponseCache(); + var httpContext = new DefaultHttpContext(); + var context = CreateTestContext(httpContext, responseCache: cache, keyProvider: new TestKeyProvider()); + httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + OnlyIfCached = true + }; + + Assert.True(await context.TryServeFromCacheAsync()); + Assert.Equal(StatusCodes.Status504GatewayTimeout, httpContext.Response.StatusCode); + } + + [Fact] + public async Task TryServeFromCacheAsync_CachedResponseNotFound_Fails() + { + var cache = new TestResponseCache(); + var httpContext = new DefaultHttpContext(); + var context = CreateTestContext(httpContext, responseCache: cache, keyProvider: new TestKeyProvider(new[] { "BaseKey", "BaseKey2" })); + + Assert.False(await context.TryServeFromCacheAsync()); + Assert.Equal(2, cache.GetCount); + } + + [Fact] + public async Task TryServeFromCacheAsync_CachedResponseFound_Succeeds() + { + var cache = new TestResponseCache(); + var httpContext = new DefaultHttpContext(); + var context = CreateTestContext(httpContext, responseCache: cache, keyProvider: new TestKeyProvider(new[] { "BaseKey", "BaseKey2" })); + + cache.Set( + "BaseKey2", + new CachedResponse() + { + Body = new byte[0] + }, + TimeSpan.Zero); + + Assert.True(await context.TryServeFromCacheAsync()); + Assert.Equal(2, cache.GetCount); + } + + [Fact] + public async Task TryServeFromCacheAsync_VaryRuleFound_CachedResponseNotFound_Fails() + { + var cache = new TestResponseCache(); + var httpContext = new DefaultHttpContext(); + var context = CreateTestContext(httpContext, responseCache: cache, keyProvider: new TestKeyProvider(new[] { "BaseKey", "BaseKey2" })); + + cache.Set( + "BaseKey2", + new CachedVaryRules(), + TimeSpan.Zero); + + Assert.False(await context.TryServeFromCacheAsync()); + Assert.Equal(2, cache.GetCount); + } + + [Fact] + public async Task TryServeFromCacheAsync_VaryRuleFound_CachedResponseFound_Succeeds() + { + var cache = new TestResponseCache(); + var httpContext = new DefaultHttpContext(); + var context = CreateTestContext(httpContext, responseCache: cache, keyProvider: new TestKeyProvider(new[] { "BaseKey", "BaseKey2" }, new[] { "VaryKey", "VaryKey2" })); + + cache.Set( + "BaseKey2", + new CachedVaryRules(), + TimeSpan.Zero); + cache.Set( + "BaseKey2VaryKey2", + new CachedResponse() + { + Body = new byte[0] + }, + TimeSpan.Zero); + + Assert.True(await context.TryServeFromCacheAsync()); + Assert.Equal(6, cache.GetCount); + } [Fact] public void ConditionalRequestSatisfied_NotConditionalRequest_Fails() @@ -159,7 +243,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests public void FinalizeCachingHeaders_DoNotUpdateShouldCacheResponse_IfResponseIsNotCacheable() { var httpContext = new DefaultHttpContext(); - var context = CreateTestContext(httpContext); + var context = CreateTestContext(httpContext, cacheabilityValidator: new CacheabilityValidator()); var state = httpContext.GetResponseCachingState(); Assert.False(state.ShouldCacheResponse); @@ -178,7 +262,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { Public = true }; - var context = CreateTestContext(httpContext); + var context = CreateTestContext(httpContext, cacheabilityValidator: new CacheabilityValidator()); var state = httpContext.GetResponseCachingState(); Assert.False(state.ShouldCacheResponse); @@ -192,10 +276,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests public void FinalizeCachingHeaders_DefaultResponseValidity_Is10Seconds() { var httpContext = new DefaultHttpContext(); - httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() - { - Public = true - }; var context = CreateTestContext(httpContext); context.FinalizeCachingHeaders(); @@ -207,10 +287,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests public void FinalizeCachingHeaders_ResponseValidity_UseExpiryIfAvailable() { var httpContext = new DefaultHttpContext(); - httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() - { - Public = true - }; var context = CreateTestContext(httpContext); var state = httpContext.GetResponseCachingState(); @@ -229,7 +305,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var httpContext = new DefaultHttpContext(); httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() { - Public = true, MaxAge = TimeSpan.FromSeconds(12) }; var context = CreateTestContext(httpContext); @@ -249,7 +324,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var httpContext = new DefaultHttpContext(); httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() { - Public = true, MaxAge = TimeSpan.FromSeconds(12), SharedMaxAge = TimeSpan.FromSeconds(13) }; @@ -269,16 +343,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var httpContext = new DefaultHttpContext(); var cache = new TestResponseCache(); - var context = CreateTestContext(httpContext, cache, new ResponseCachingOptions()); + var context = CreateTestContext(httpContext, cache); var state = httpContext.GetResponseCachingState(); httpContext.Response.Headers[HeaderNames.Vary] = new StringValues(new[] { "headerA", "HEADERB", "HEADERc" }); httpContext.AddResponseCachingFeature(); httpContext.GetResponseCachingFeature().VaryParams = new StringValues(new[] { "paramB", "PARAMAA" }); - httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() - { - Public = true, - }; var cachedVaryRules = new CachedVaryRules() { VaryRules = new VaryRules() @@ -291,7 +361,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.FinalizeCachingHeaders(); - Assert.Equal(1, cache.StoredItems); + Assert.Equal(1, cache.SetCount); Assert.NotSame(cachedVaryRules, state.CachedVaryRules); } @@ -300,16 +370,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var httpContext = new DefaultHttpContext(); var cache = new TestResponseCache(); - var context = CreateTestContext(httpContext, cache, new ResponseCachingOptions()); + var context = CreateTestContext(httpContext, cache); var state = httpContext.GetResponseCachingState(); httpContext.Response.Headers[HeaderNames.Vary] = new StringValues(new[] { "headerA", "HEADERB" }); httpContext.AddResponseCachingFeature(); httpContext.GetResponseCachingFeature().VaryParams = new StringValues(new[] { "paramB", "PARAMA" }); - httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() - { - Public = true, - }; var cachedVaryRules = new CachedVaryRules() { VaryKeyPrefix = FastGuid.NewGuid().IdString, @@ -323,7 +389,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.FinalizeCachingHeaders(); - Assert.Equal(0, cache.StoredItems); + Assert.Equal(0, cache.SetCount); Assert.Same(cachedVaryRules, state.CachedVaryRules); } @@ -331,10 +397,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests public void FinalizeCachingHeaders_DoNotAddDate_IfSpecified() { var httpContext = new DefaultHttpContext(); - httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() - { - Public = true - }; var context = CreateTestContext(httpContext); var state = httpContext.GetResponseCachingState(); var utcNow = DateTimeOffset.MinValue; @@ -351,10 +413,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests public void FinalizeCachingHeaders_AddsDate_IfNoneSpecified() { var httpContext = new DefaultHttpContext(); - httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() - { - Public = true - }; var context = CreateTestContext(httpContext); var state = httpContext.GetResponseCachingState(); var utcNow = DateTimeOffset.MinValue; @@ -372,10 +430,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests public void FinalizeCachingHeaders_StoresCachedResponse_InState() { var httpContext = new DefaultHttpContext(); - httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() - { - Public = true - }; var context = CreateTestContext(httpContext); var state = httpContext.GetResponseCachingState(); @@ -391,7 +445,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var httpContext = new DefaultHttpContext(); var cache = new TestResponseCache(); - var context = CreateTestContext(httpContext, cache, new ResponseCachingOptions()); + var context = CreateTestContext(httpContext, cache); context.ShimResponseStream(); await httpContext.Response.WriteAsync(new string('0', 70 * 1024)); @@ -402,12 +456,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { BodyKeyPrefix = FastGuid.NewGuid().IdString }; - state.BaseKey = "BaseKey"; + state.StorageBaseKey = "BaseKey"; state.CachedResponseValidFor = TimeSpan.FromSeconds(10); context.FinalizeCachingBody(); - Assert.Equal(2, cache.StoredItems); + Assert.Equal(2, cache.SetCount); } [Fact] @@ -415,7 +469,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var httpContext = new DefaultHttpContext(); var cache = new TestResponseCache(); - var context = CreateTestContext(httpContext, cache, new ResponseCachingOptions()); + var context = CreateTestContext(httpContext, cache); context.ShimResponseStream(); await httpContext.Response.WriteAsync(new string('0', 70 * 1024 - 1)); @@ -426,12 +480,113 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { BodyKeyPrefix = FastGuid.NewGuid().IdString }; - state.BaseKey = "BaseKey"; + state.StorageBaseKey = "BaseKey"; state.CachedResponseValidFor = TimeSpan.FromSeconds(10); context.FinalizeCachingBody(); - Assert.Equal(1, cache.StoredItems); + Assert.Equal(1, cache.SetCount); + } + + [Fact] + public async Task FinalizeCachingBody_StoreResponseBodySeparately_LimitIsConfigurable() + { + var httpContext = new DefaultHttpContext(); + var cache = new TestResponseCache(); + var context = CreateTestContext(httpContext, cache, new ResponseCachingOptions() + { + MinimumSplitBodySize = 2048 + }); + + context.ShimResponseStream(); + await httpContext.Response.WriteAsync(new string('0', 1024)); + + var state = httpContext.GetResponseCachingState(); + state.ShouldCacheResponse = true; + state.CachedResponse = new CachedResponse() + { + BodyKeyPrefix = FastGuid.NewGuid().IdString + }; + state.StorageBaseKey = "BaseKey"; + state.CachedResponseValidFor = TimeSpan.FromSeconds(10); + + context.FinalizeCachingBody(); + + Assert.Equal(1, cache.SetCount); + } + + [Fact] + public async Task FinalizeCachingBody_Cache_IfContentLengthMatches() + { + var httpContext = new DefaultHttpContext(); + var cache = new TestResponseCache(); + var context = CreateTestContext(httpContext, cache); + + context.ShimResponseStream(); + httpContext.Response.ContentLength = 10; + await httpContext.Response.WriteAsync(new string('0', 10)); + + var state = httpContext.GetResponseCachingState(); + state.ShouldCacheResponse = true; + state.CachedResponse = new CachedResponse() + { + BodyKeyPrefix = FastGuid.NewGuid().IdString + }; + state.StorageBaseKey = "BaseKey"; + state.CachedResponseValidFor = TimeSpan.FromSeconds(10); + + context.FinalizeCachingBody(); + + Assert.Equal(1, cache.SetCount); + } + + [Fact] + public async Task FinalizeCachingBody_DoNotCache_IfContentLengthMismatches() + { + var httpContext = new DefaultHttpContext(); + var cache = new TestResponseCache(); + var context = CreateTestContext(httpContext, cache); + + context.ShimResponseStream(); + httpContext.Response.ContentLength = 9; + await httpContext.Response.WriteAsync(new string('0', 10)); + + var state = httpContext.GetResponseCachingState(); + state.ShouldCacheResponse = true; + state.CachedResponse = new CachedResponse() + { + BodyKeyPrefix = FastGuid.NewGuid().IdString + }; + state.StorageBaseKey = "BaseKey"; + state.CachedResponseValidFor = TimeSpan.FromSeconds(10); + + context.FinalizeCachingBody(); + + Assert.Equal(0, cache.SetCount); + } + + [Fact] + public async Task FinalizeCachingBody_Cache_IfContentLengthAbsent() + { + var httpContext = new DefaultHttpContext(); + var cache = new TestResponseCache(); + var context = CreateTestContext(httpContext, cache); + + context.ShimResponseStream(); + await httpContext.Response.WriteAsync(new string('0', 10)); + + var state = httpContext.GetResponseCachingState(); + state.ShouldCacheResponse = true; + state.CachedResponse = new CachedResponse() + { + BodyKeyPrefix = FastGuid.NewGuid().IdString + }; + state.StorageBaseKey = "BaseKey"; + state.CachedResponseValidFor = TimeSpan.FromSeconds(10); + + context.FinalizeCachingBody(); + + Assert.Equal(1, cache.SetCount); } [Fact] @@ -456,48 +611,30 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Assert.Equal(orderedStrings, normalizedStrings); } - private static ResponseCachingContext CreateTestContext(HttpContext httpContext) - { - return CreateTestContext( - httpContext, - new TestResponseCache(), - new ResponseCachingOptions(), - new CacheabilityValidator()); - } - - private static ResponseCachingContext CreateTestContext(HttpContext httpContext, ResponseCachingOptions options) - { - return CreateTestContext( - httpContext, - new TestResponseCache(), - options, - new CacheabilityValidator()); - } - - private static ResponseCachingContext CreateTestContext(HttpContext httpContext, ICacheabilityValidator cacheabilityValidator) - { - return CreateTestContext( - httpContext, - new TestResponseCache(), - new ResponseCachingOptions(), - cacheabilityValidator); - } - - private static ResponseCachingContext CreateTestContext(HttpContext httpContext, IResponseCache responseCache, ResponseCachingOptions options) - { - return CreateTestContext( - httpContext, - responseCache, - options, - new CacheabilityValidator()); - } - private static ResponseCachingContext CreateTestContext( HttpContext httpContext, - IResponseCache responseCache, - ResponseCachingOptions options, - ICacheabilityValidator cacheabilityValidator) + IResponseCache responseCache = null, + ResponseCachingOptions options = null, + IKeyProvider keyProvider = null, + ICacheabilityValidator cacheabilityValidator = null) { + if (responseCache == null) + { + responseCache = new TestResponseCache(); + } + if (options == null) + { + options = new ResponseCachingOptions(); + } + if (keyProvider == null) + { + keyProvider = new KeyProvider(new DefaultObjectPoolProvider(), Options.Create(options)); + } + if (cacheabilityValidator == null) + { + cacheabilityValidator = new TestCacheabilityValidator(); + } + httpContext.AddResponseCachingState(); return new ResponseCachingContext( @@ -505,16 +642,82 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests responseCache, options, cacheabilityValidator, - new KeyProvider(new DefaultObjectPoolProvider(), Options.Create(options))); + keyProvider); + } + + private class TestCacheabilityValidator : ICacheabilityValidator + { + public bool CachedEntryIsFresh(HttpContext httpContext, ResponseHeaders cachedResponseHeaders) => true; + + public bool RequestIsCacheable(HttpContext httpContext) => true; + + public bool ResponseIsCacheable(HttpContext httpContext) => true; + } + + private class TestKeyProvider : IKeyProvider + { + private readonly StringValues _baseKey; + private readonly StringValues _varyKey; + + public TestKeyProvider(StringValues? lookupBaseKey = null, StringValues? lookupVaryKey = null) + { + if (lookupBaseKey.HasValue) + { + _baseKey = lookupBaseKey.Value; + } + if (lookupVaryKey.HasValue) + { + _varyKey = lookupVaryKey.Value; + } + } + + public IEnumerable CreateLookupBaseKey(HttpContext httpContext) => _baseKey; + + + public IEnumerable CreateLookupVaryKey(HttpContext httpContext, VaryRules varyRules) + { + foreach (var baseKey in _baseKey) + { + foreach (var varyKey in _varyKey) + { + yield return baseKey + varyKey; + } + } + } + + public string CreateBodyKey(HttpContext httpContext) + { + throw new NotImplementedException(); + } + + public string CreateStorageBaseKey(HttpContext httpContext) + { + throw new NotImplementedException(); + } + + public string CreateStorageVaryKey(HttpContext httpContext, VaryRules varyRules) + { + throw new NotImplementedException(); + } } private class TestResponseCache : IResponseCache { - public int StoredItems { get; private set; } + private readonly IDictionary _storage = new Dictionary(); + public int GetCount { get; private set; } + public int SetCount { get; private set; } public object Get(string key) { - return null; + GetCount++; + try + { + return _storage[key]; + } + catch + { + return null; + } } public void Remove(string key) @@ -523,7 +726,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests public void Set(string key, object entry, TimeSpan validFor) { - StoredItems++; + SetCount++; + _storage[key] = entry; } } @@ -531,7 +735,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellation) { - return Task.FromResult(0); + return TaskCache.CompletedTask; } } } From e236e640559d1a7e4f2820a75ff85f9af0bb0103 Mon Sep 17 00:00:00 2001 From: John Luo Date: Mon, 12 Sep 2016 17:24:39 -0700 Subject: [PATCH 027/188] Fallback to empty cache control when none is explicitly cached --- .../CacheabilityValidator.cs | 14 ++++++++------ .../CacheabilityValidatorTests.cs | 10 ++++++++++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/Microsoft.AspNetCore.ResponseCaching/CacheabilityValidator.cs b/src/Microsoft.AspNetCore.ResponseCaching/CacheabilityValidator.cs index 0e46bd470d..2669c32041 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/CacheabilityValidator.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/CacheabilityValidator.cs @@ -4,7 +4,6 @@ using System; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Headers; -using Microsoft.AspNetCore.ResponseCaching.Internal; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; @@ -12,6 +11,8 @@ namespace Microsoft.AspNetCore.ResponseCaching { public class CacheabilityValidator : ICacheabilityValidator { + private static readonly CacheControlHeaderValue EmptyCacheControl = new CacheControlHeaderValue(); + public virtual bool RequestIsCacheable(HttpContext httpContext) { var state = httpContext.GetResponseCachingState(); @@ -157,6 +158,7 @@ namespace Microsoft.AspNetCore.ResponseCaching { var state = httpContext.GetResponseCachingState(); var age = state.CachedEntryAge; + var cachedControlHeaders = cachedResponseHeaders.CacheControl ?? EmptyCacheControl; // Add min-fresh requirements if (state.RequestCacheControl.MinFresh != null) @@ -165,18 +167,18 @@ namespace Microsoft.AspNetCore.ResponseCaching } // Validate shared max age, this overrides any max age settings for shared caches - if (age > cachedResponseHeaders.CacheControl.SharedMaxAge) + if (age > cachedControlHeaders.SharedMaxAge) { // shared max age implies must revalidate return false; } - else if (cachedResponseHeaders.CacheControl.SharedMaxAge == null) + else if (cachedControlHeaders.SharedMaxAge == null) { // Validate max age - if (age > cachedResponseHeaders.CacheControl.MaxAge || age > state.RequestCacheControl.MaxAge) + if (age > cachedControlHeaders.MaxAge || age > state.RequestCacheControl.MaxAge) { // Must revalidate - if (cachedResponseHeaders.CacheControl.MustRevalidate) + if (cachedControlHeaders.MustRevalidate) { return false; } @@ -190,7 +192,7 @@ namespace Microsoft.AspNetCore.ResponseCaching return false; } - else if (cachedResponseHeaders.CacheControl.MaxAge == null && state.RequestCacheControl.MaxAge == null) + else if (cachedControlHeaders.MaxAge == null && state.RequestCacheControl.MaxAge == null) { // Validate expiration if (state.ResponseTime > cachedResponseHeaders.Expires) diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheabilityValidatorTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheabilityValidatorTests.cs index 5ead7ab80e..e3cd724195 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheabilityValidatorTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheabilityValidatorTests.cs @@ -386,6 +386,16 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext)); } + [Fact] + public void EntryIsFresh_NoCachedCacheControl_FallsbackToEmptyCacheControl() + { + var utcNow = DateTimeOffset.UtcNow; + var httpContext = CreateDefaultContext(); + httpContext.GetResponseCachingState().ResponseTime = DateTimeOffset.MaxValue; + + Assert.True(new CacheabilityValidator().CachedEntryIsFresh(httpContext, new ResponseHeaders(new HeaderDictionary()))); + } + [Fact] public void EntryIsFresh_NoExpiryRequirements_IsFresh() { From ccfa090e6e7d359e6fcbddb7b6a98a619b69dfcf Mon Sep 17 00:00:00 2001 From: John Luo Date: Mon, 12 Sep 2016 17:36:47 -0700 Subject: [PATCH 028/188] API review renames and updates --- .../CacheEntry/CacheEntrySerializer.cs | 10 +- .../CacheEntry/CachedResponse.cs | 14 +- .../CacheEntry/CachedResponseBody.cs | 6 +- .../CacheEntry/CachedVaryRules.cs | 16 +- .../{KeyProvider.cs => CacheKeyProvider.cs} | 71 +- .../CacheabilityValidator.cs | 61 +- .../ResponseCachingHttpContextExtensions.cs | 6 - ...ponseCachingServiceCollectionExtensions.cs | 2 +- .../Interfaces/ICacheKeyProvider.cs | 38 + .../Interfaces/ICacheabilityValidator.cs | 16 +- .../Interfaces/IKeyProvider.cs | 41 - ...ns.cs => InternalHttpContextExtensions.cs} | 21 +- .../Internal/ResponseCachingState.cs | 91 --- .../ResponseCachingContext.cs | 364 ++------- .../ResponseCachingMiddleware.cs | 378 +++++++-- .../VaryRules.cs | 13 - .../CacheEntrySerializerTests.cs | 29 +- .../CacheabilityValidatorTests.cs | 331 ++++---- .../HttpContextInternalExtensionTests.cs | 12 - .../KeyProviderTests.cs | 182 +++-- .../ResponseCachingContextTests.cs | 742 ------------------ .../ResponseCachingMiddlewareTests.cs | 578 ++++++++++++++ .../ResponseCachingTests.cs | 138 +--- .../TestUtils.cs | 211 +++++ 24 files changed, 1602 insertions(+), 1769 deletions(-) rename src/Microsoft.AspNetCore.ResponseCaching/{KeyProvider.cs => CacheKeyProvider.cs} (63%) create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Interfaces/ICacheKeyProvider.cs delete mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IKeyProvider.cs rename src/Microsoft.AspNetCore.ResponseCaching/Internal/{HttpContextInternalExtensions.cs => InternalHttpContextExtensions.cs} (51%) delete mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingState.cs delete mode 100644 src/Microsoft.AspNetCore.ResponseCaching/VaryRules.cs delete mode 100644 test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs create mode 100644 test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs create mode 100644 test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CacheEntrySerializer.cs b/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CacheEntrySerializer.cs index 48d28e319f..caa4061773 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CacheEntrySerializer.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CacheEntrySerializer.cs @@ -146,7 +146,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal param[index] = reader.ReadString(); } - return new CachedVaryRules { VaryKeyPrefix = varyKeyPrefix, VaryRules = new VaryRules() { Headers = headers, Params = param } }; + return new CachedVaryRules { VaryKeyPrefix = varyKeyPrefix, Headers = headers, Params = param }; } // See serialization format above @@ -222,14 +222,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal { writer.Write(varyRules.VaryKeyPrefix); - writer.Write(varyRules.VaryRules.Headers.Count); - foreach (var header in varyRules.VaryRules.Headers) + writer.Write(varyRules.Headers.Count); + foreach (var header in varyRules.Headers) { writer.Write(header); } - writer.Write(varyRules.VaryRules.Params.Count); - foreach (var param in varyRules.VaryRules.Params) + writer.Write(varyRules.Params.Count); + foreach (var param in varyRules.Params) { writer.Write(param); } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedResponse.cs b/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedResponse.cs index d4413bf4d1..552b29d96a 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedResponse.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedResponse.cs @@ -4,18 +4,18 @@ using System; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.ResponseCaching.Internal +namespace Microsoft.AspNetCore.ResponseCaching { - internal class CachedResponse + public class CachedResponse { - internal string BodyKeyPrefix { get; set; } + public string BodyKeyPrefix { get; internal set; } - internal DateTimeOffset Created { get; set; } + public DateTimeOffset Created { get; internal set; } - internal int StatusCode { get; set; } + public int StatusCode { get; internal set; } - internal IHeaderDictionary Headers { get; set; } = new HeaderDictionary(); + public IHeaderDictionary Headers { get; internal set; } = new HeaderDictionary(); - internal byte[] Body { get; set; } + public byte[] Body { get; internal set; } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedResponseBody.cs b/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedResponseBody.cs index 643fac449c..d714e3f131 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedResponseBody.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedResponseBody.cs @@ -1,10 +1,10 @@ // 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.Internal +namespace Microsoft.AspNetCore.ResponseCaching { - internal class CachedResponseBody + public class CachedResponseBody { - internal byte[] Body { get; set; } + public byte[] Body { get; internal set; } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedVaryRules.cs b/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedVaryRules.cs index de3355e761..f3a8a9a75a 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedVaryRules.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedVaryRules.cs @@ -1,12 +1,16 @@ // 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.Internal -{ - internal class CachedVaryRules - { - internal string VaryKeyPrefix { get; set; } +using Microsoft.Extensions.Primitives; - internal VaryRules VaryRules { get; set; } +namespace Microsoft.AspNetCore.ResponseCaching +{ + public class CachedVaryRules + { + public string VaryKeyPrefix { get; internal set; } + + public StringValues Headers { get; internal set; } + + public StringValues Params { get; internal set; } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/KeyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/CacheKeyProvider.cs similarity index 63% rename from src/Microsoft.AspNetCore.ResponseCaching/KeyProvider.cs rename to src/Microsoft.AspNetCore.ResponseCaching/CacheKeyProvider.cs index 4de7a40b36..4e42f92083 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/KeyProvider.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/CacheKeyProvider.cs @@ -6,14 +6,13 @@ using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.ResponseCaching { - public class KeyProvider : IKeyProvider + public class CacheKeyProvider : ICacheKeyProvider { // Use the record separator for delimiting components of the cache key to avoid possible collisions private static readonly char KeyDelimiter = '\x1e'; @@ -21,7 +20,7 @@ namespace Microsoft.AspNetCore.ResponseCaching private readonly ObjectPool _builderPool; private readonly ResponseCachingOptions _options; - public KeyProvider(ObjectPoolProvider poolProvider, IOptions options) + public CacheKeyProvider(ObjectPoolProvider poolProvider, IOptions options) { if (poolProvider == null) { @@ -36,26 +35,25 @@ namespace Microsoft.AspNetCore.ResponseCaching _options = options.Value; } - public virtual IEnumerable CreateLookupBaseKey(HttpContext httpContext) + public virtual IEnumerable CreateLookupBaseKeys(ResponseCachingContext context) { - return new string[] { CreateStorageBaseKey(httpContext) }; + return new string[] { CreateStorageBaseKey(context) }; } - public virtual IEnumerable CreateLookupVaryKey(HttpContext httpContext, VaryRules varyRules) + public virtual IEnumerable CreateLookupVaryKeys(ResponseCachingContext context) { - return new string[] { CreateStorageVaryKey(httpContext, varyRules) }; + return new string[] { CreateStorageVaryKey(context) }; } // GET/PATH - // TODO: Method invariant retrieval? E.g. HEAD after GET to the same resource. - public virtual string CreateStorageBaseKey(HttpContext httpContext) + public virtual string CreateStorageBaseKey(ResponseCachingContext context) { - if (httpContext == null) + if (context == null) { - throw new ArgumentNullException(nameof(httpContext)); + throw new ArgumentNullException(nameof(context)); } - var request = httpContext.Request; + var request = context.HttpContext.Request; var builder = _builderPool.Get(); try @@ -74,24 +72,31 @@ namespace Microsoft.AspNetCore.ResponseCaching } // BaseKeyHHeaderName=HeaderValueQQueryName=QueryValue - public virtual string CreateStorageVaryKey(HttpContext httpContext, VaryRules varyRules) + public virtual string CreateStorageVaryKey(ResponseCachingContext context) { - if (httpContext == null) + if (context == null) { - throw new ArgumentNullException(nameof(httpContext)); - } - if (varyRules == null || (StringValues.IsNullOrEmpty(varyRules.Headers) && StringValues.IsNullOrEmpty(varyRules.Params))) - { - return httpContext.GetResponseCachingState().CachedVaryRules.VaryKeyPrefix; + throw new ArgumentNullException(nameof(context)); } - var request = httpContext.Request; + var varyRules = context.CachedVaryRules; + if (varyRules == null) + { + throw new InvalidOperationException($"{nameof(CachedVaryRules)} must not be null on the {nameof(ResponseCachingContext)}"); + } + + if ((StringValues.IsNullOrEmpty(varyRules.Headers) && StringValues.IsNullOrEmpty(varyRules.Params))) + { + return varyRules.VaryKeyPrefix; + } + + var request = context.HttpContext.Request; var builder = _builderPool.Get(); try { // Prepend with the Guid of the CachedVaryRules - builder.Append(httpContext.GetResponseCachingState().CachedVaryRules.VaryKeyPrefix); + builder.Append(varyRules.VaryKeyPrefix); // Vary by headers if (varyRules?.Headers.Count > 0) @@ -102,18 +107,11 @@ namespace Microsoft.AspNetCore.ResponseCaching foreach (var header in varyRules.Headers) { - var value = httpContext.Request.Headers[header]; - - // TODO: How to handle null/empty string? - if (StringValues.IsNullOrEmpty(value)) - { - value = "null"; - } - builder.Append(KeyDelimiter) .Append(header) .Append("=") - .Append(value); + // TODO: Perf - iterate the string values instead? + .Append(context.HttpContext.Request.Headers[header]); } } @@ -127,7 +125,7 @@ namespace Microsoft.AspNetCore.ResponseCaching if (varyRules.Params.Count == 1 && string.Equals(varyRules.Params[0], "*", StringComparison.Ordinal)) { // Vary by all available query params - foreach (var query in httpContext.Request.Query.OrderBy(q => q.Key, StringComparer.OrdinalIgnoreCase)) + foreach (var query in context.HttpContext.Request.Query.OrderBy(q => q.Key, StringComparer.OrdinalIgnoreCase)) { builder.Append(KeyDelimiter) .Append(query.Key.ToUpperInvariant()) @@ -139,18 +137,11 @@ namespace Microsoft.AspNetCore.ResponseCaching { foreach (var param in varyRules.Params) { - var value = httpContext.Request.Query[param]; - - // TODO: How to handle null/empty string? - if (StringValues.IsNullOrEmpty(value)) - { - value = "null"; - } - builder.Append(KeyDelimiter) .Append(param) .Append("=") - .Append(value); + // TODO: Perf - iterate the string values instead? + .Append(context.HttpContext.Request.Query[param]); } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/CacheabilityValidator.cs b/src/Microsoft.AspNetCore.ResponseCaching/CacheabilityValidator.cs index 2669c32041..fa86e766e7 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/CacheabilityValidator.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/CacheabilityValidator.cs @@ -13,13 +13,11 @@ namespace Microsoft.AspNetCore.ResponseCaching { private static readonly CacheControlHeaderValue EmptyCacheControl = new CacheControlHeaderValue(); - public virtual bool RequestIsCacheable(HttpContext httpContext) + public virtual bool IsRequestCacheable(ResponseCachingContext context) { - var state = httpContext.GetResponseCachingState(); - // Verify the method // TODO: RFC lists POST as a cacheable method when explicit freshness information is provided, but this is not widely implemented. Will revisit. - var request = httpContext.Request; + var request = context.HttpContext.Request; if (!string.Equals("GET", request.Method, StringComparison.OrdinalIgnoreCase) && !string.Equals("HEAD", request.Method, StringComparison.OrdinalIgnoreCase)) { @@ -37,7 +35,7 @@ namespace Microsoft.AspNetCore.ResponseCaching // TODO: no-cache requests can be retrieved upon validation with origin if (!StringValues.IsNullOrEmpty(request.Headers[HeaderNames.CacheControl])) { - if (state.RequestCacheControl.NoCache) + if (context.RequestCacheControlHeaderValue.NoCache) { return false; } @@ -59,31 +57,29 @@ namespace Microsoft.AspNetCore.ResponseCaching return true; } - public virtual bool ResponseIsCacheable(HttpContext httpContext) + public virtual bool IsResponseCacheable(ResponseCachingContext context) { - var state = httpContext.GetResponseCachingState(); - // Only cache pages explicitly marked with public // TODO: Consider caching responses that are not marked as public but otherwise cacheable? - if (!state.ResponseCacheControl.Public) + if (!context.ResponseCacheControlHeaderValue.Public) { return false; } // Check no-store - if (state.RequestCacheControl.NoStore || state.ResponseCacheControl.NoStore) + if (context.RequestCacheControlHeaderValue.NoStore || context.ResponseCacheControlHeaderValue.NoStore) { return false; } // Check no-cache // TODO: Handle no-cache with headers - if (state.ResponseCacheControl.NoCache) + if (context.ResponseCacheControlHeaderValue.NoCache) { return false; } - var response = httpContext.Response; + var response = context.HttpContext.Response; // Do not cache responses with Set-Cookie headers if (!StringValues.IsNullOrEmpty(response.Headers[HeaderNames.SetCookie])) @@ -101,7 +97,7 @@ namespace Microsoft.AspNetCore.ResponseCaching // TODO: public MAY override the cacheability checks for private and status codes // Check private - if (state.ResponseCacheControl.Private) + if (context.ResponseCacheControlHeaderValue.Private) { return false; } @@ -115,35 +111,35 @@ namespace Microsoft.AspNetCore.ResponseCaching // Check response freshness // TODO: apparent age vs corrected age value - if (state.ResponseHeaders.Date == null) + if (context.TypedResponseHeaders.Date == null) { - if (state.ResponseCacheControl.SharedMaxAge == null && - state.ResponseCacheControl.MaxAge == null && - state.ResponseTime > state.ResponseHeaders.Expires) + if (context.ResponseCacheControlHeaderValue.SharedMaxAge == null && + context.ResponseCacheControlHeaderValue.MaxAge == null && + context.ResponseTime > context.TypedResponseHeaders.Expires) { return false; } } else { - var age = state.ResponseTime - state.ResponseHeaders.Date.Value; + var age = context.ResponseTime - context.TypedResponseHeaders.Date.Value; // Validate shared max age - if (age > state.ResponseCacheControl.SharedMaxAge) + if (age > context.ResponseCacheControlHeaderValue.SharedMaxAge) { return false; } - else if (state.ResponseCacheControl.SharedMaxAge == null) + else if (context.ResponseCacheControlHeaderValue.SharedMaxAge == null) { // Validate max age - if (age > state.ResponseCacheControl.MaxAge) + if (age > context.ResponseCacheControlHeaderValue.MaxAge) { return false; } - else if (state.ResponseCacheControl.MaxAge == null) + else if (context.ResponseCacheControlHeaderValue.MaxAge == null) { // Validate expiration - if (state.ResponseTime > state.ResponseHeaders.Expires) + if (context.ResponseTime > context.TypedResponseHeaders.Expires) { return false; } @@ -154,16 +150,15 @@ namespace Microsoft.AspNetCore.ResponseCaching return true; } - public virtual bool CachedEntryIsFresh(HttpContext httpContext, ResponseHeaders cachedResponseHeaders) + public virtual bool IsCachedEntryFresh(ResponseCachingContext context) { - var state = httpContext.GetResponseCachingState(); - var age = state.CachedEntryAge; - var cachedControlHeaders = cachedResponseHeaders.CacheControl ?? EmptyCacheControl; + var age = context.CachedEntryAge; + var cachedControlHeaders = context.CachedResponseHeaders.CacheControl ?? EmptyCacheControl; // Add min-fresh requirements - if (state.RequestCacheControl.MinFresh != null) + if (context.RequestCacheControlHeaderValue.MinFresh != null) { - age += state.RequestCacheControl.MinFresh.Value; + age += context.RequestCacheControlHeaderValue.MinFresh.Value; } // Validate shared max age, this overrides any max age settings for shared caches @@ -175,7 +170,7 @@ namespace Microsoft.AspNetCore.ResponseCaching else if (cachedControlHeaders.SharedMaxAge == null) { // Validate max age - if (age > cachedControlHeaders.MaxAge || age > state.RequestCacheControl.MaxAge) + if (age > cachedControlHeaders.MaxAge || age > context.RequestCacheControlHeaderValue.MaxAge) { // Must revalidate if (cachedControlHeaders.MustRevalidate) @@ -184,7 +179,7 @@ namespace Microsoft.AspNetCore.ResponseCaching } // Request allows stale values - if (age < state.RequestCacheControl.MaxStaleLimit) + if (age < context.RequestCacheControlHeaderValue.MaxStaleLimit) { // TODO: Add warning header indicating the response is stale return true; @@ -192,10 +187,10 @@ namespace Microsoft.AspNetCore.ResponseCaching return false; } - else if (cachedControlHeaders.MaxAge == null && state.RequestCacheControl.MaxAge == null) + else if (cachedControlHeaders.MaxAge == null && context.RequestCacheControlHeaderValue.MaxAge == null) { // Validate expiration - if (state.ResponseTime > cachedResponseHeaders.Expires) + if (context.ResponseTime > context.CachedResponseHeaders.Expires) { return false; } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCachingHttpContextExtensions.cs b/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCachingHttpContextExtensions.cs index ae6481949c..50b5f73f52 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCachingHttpContextExtensions.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCachingHttpContextExtensions.cs @@ -2,18 +2,12 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.ResponseCaching.Internal; namespace Microsoft.AspNetCore.ResponseCaching { // TODO: Temporary interface for endpoints to specify options for response caching public static class ResponseCachingHttpContextExtensions { - public static ResponseCachingState GetResponseCachingState(this HttpContext httpContext) - { - return httpContext.Features.Get(); - } - public static ResponseCachingFeature GetResponseCachingFeature(this HttpContext httpContext) { return httpContext.Features.Get(); diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCachingServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCachingServiceCollectionExtensions.cs index 4877d34bdc..962ddf281f 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCachingServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCachingServiceCollectionExtensions.cs @@ -40,7 +40,7 @@ namespace Microsoft.Extensions.DependencyInjection private static IServiceCollection AddResponseCachingServices(this IServiceCollection services) { - services.TryAdd(ServiceDescriptor.Singleton()); + services.TryAdd(ServiceDescriptor.Singleton()); services.TryAdd(ServiceDescriptor.Singleton()); return services; diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/ICacheKeyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/ICacheKeyProvider.cs new file mode 100644 index 0000000000..570041e290 --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/ICacheKeyProvider.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.Collections.Generic; + +namespace Microsoft.AspNetCore.ResponseCaching +{ + public interface ICacheKeyProvider + { + /// + /// Create a base key for storing items. + /// + /// The . + /// The created base key. + string CreateStorageBaseKey(ResponseCachingContext context); + + /// + /// Create one or more base keys for looking up items. + /// + /// The . + /// An ordered containing the base keys to try when looking up items. + IEnumerable CreateLookupBaseKeys(ResponseCachingContext context); + + /// + /// Create a vary key for storing items. + /// + /// The . + /// The created vary key. + string CreateStorageVaryKey(ResponseCachingContext context); + + /// + /// Create one or more vary keys for looking up items. + /// + /// The . + /// An ordered containing the vary keys to try when looking up items. + IEnumerable CreateLookupVaryKeys(ResponseCachingContext context); + } +} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/ICacheabilityValidator.cs b/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/ICacheabilityValidator.cs index 01ded6818a..a260ce87d1 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/ICacheabilityValidator.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/ICacheabilityValidator.cs @@ -1,9 +1,6 @@ // 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.AspNetCore.Http.Headers; - namespace Microsoft.AspNetCore.ResponseCaching { public interface ICacheabilityValidator @@ -11,23 +8,22 @@ namespace Microsoft.AspNetCore.ResponseCaching /// /// Determine the cacheability of an HTTP request. /// - /// The . + /// The . /// true if the request is cacheable; otherwise false. - bool RequestIsCacheable(HttpContext httpContext); + bool IsRequestCacheable(ResponseCachingContext context); /// /// Determine the cacheability of an HTTP response. /// - /// The . + /// The . /// true if the response is cacheable; otherwise false. - bool ResponseIsCacheable(HttpContext httpContext); + bool IsResponseCacheable(ResponseCachingContext context); /// /// Determine the freshness of the cached entry. /// - /// The . - /// The of the cached entry. + /// The . /// true if the cached entry is fresh; otherwise false. - bool CachedEntryIsFresh(HttpContext httpContext, ResponseHeaders cachedResponseHeaders); + bool IsCachedEntryFresh(ResponseCachingContext context); } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IKeyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IKeyProvider.cs deleted file mode 100644 index 2013b202b5..0000000000 --- a/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IKeyProvider.cs +++ /dev/null @@ -1,41 +0,0 @@ -// 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.AspNetCore.Http; - -namespace Microsoft.AspNetCore.ResponseCaching -{ - public interface IKeyProvider - { - /// - /// Create a base key using the HTTP context for storing items. - /// - /// The . - /// The created base key. - string CreateStorageBaseKey(HttpContext httpContext); - - /// - /// Create one or more base keys using the HTTP context for looking up items. - /// - /// The . - /// An ordered containing the base keys to try when looking up items. - IEnumerable CreateLookupBaseKey(HttpContext httpContext); - - /// - /// Create a vary key using the HTTP context and vary rules for storing items. - /// - /// The . - /// The . - /// The created vary key. - string CreateStorageVaryKey(HttpContext httpContext, VaryRules varyRules); - - /// - /// Create one or more vary keys using the HTTP context and vary rules for looking up items. - /// - /// The . - /// The . - /// An ordered containing the vary keys to try when looking up items. - IEnumerable CreateLookupVaryKey(HttpContext httpContext, VaryRules varyRules); - } -} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/HttpContextInternalExtensions.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/InternalHttpContextExtensions.cs similarity index 51% rename from src/Microsoft.AspNetCore.ResponseCaching/Internal/HttpContextInternalExtensions.cs rename to src/Microsoft.AspNetCore.ResponseCaching/Internal/InternalHttpContextExtensions.cs index 3c148ef340..640edba4a1 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/HttpContextInternalExtensions.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/InternalHttpContextExtensions.cs @@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.ResponseCaching.Internal { - internal static class HttpContextInternalExtensions + internal static class InternalHttpContextExtensions { internal static void AddResponseCachingFeature(this HttpContext httpContext) { @@ -21,24 +21,5 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal { httpContext.Features.Set(null); } - - internal static void AddResponseCachingState(this HttpContext httpContext) - { - if (httpContext.GetResponseCachingState() != null) - { - throw new InvalidOperationException($"Another instance of {nameof(ResponseCachingState)} already exists. Only one instance of {nameof(ResponseCachingMiddleware)} can be configured for an application."); - } - httpContext.Features.Set(new ResponseCachingState(httpContext)); - } - - internal static void RemoveResponseCachingState(this HttpContext httpContext) - { - httpContext.Features.Set(null); - } - - internal static ResponseCachingState GetResponseCachingState(this HttpContext httpContext) - { - return httpContext.Features.Get(); - } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingState.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingState.cs deleted file mode 100644 index 422f7c9746..0000000000 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingState.cs +++ /dev/null @@ -1,91 +0,0 @@ -// 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.AspNetCore.Http.Headers; -using Microsoft.Net.Http.Headers; - -namespace Microsoft.AspNetCore.ResponseCaching.Internal -{ - public class ResponseCachingState - { - private static readonly CacheControlHeaderValue EmptyCacheControl = new CacheControlHeaderValue(); - - private readonly HttpContext _httpContext; - - private RequestHeaders _requestHeaders; - private ResponseHeaders _responseHeaders; - private CacheControlHeaderValue _requestCacheControl; - private CacheControlHeaderValue _responseCacheControl; - - internal ResponseCachingState(HttpContext httpContext) - { - _httpContext = httpContext; - } - - public bool ShouldCacheResponse { get; internal set; } - - public string StorageBaseKey { get; internal set; } - - public string StorageVaryKey { get; internal set; } - - public DateTimeOffset ResponseTime { get; internal set; } - - public TimeSpan CachedEntryAge { get; internal set; } - - public TimeSpan CachedResponseValidFor { get; internal set; } - - internal CachedResponse CachedResponse { get; set; } - - internal CachedVaryRules CachedVaryRules { get; set; } - - public RequestHeaders RequestHeaders - { - get - { - if (_requestHeaders == null) - { - _requestHeaders = _httpContext.Request.GetTypedHeaders(); - } - return _requestHeaders; - } - } - - public ResponseHeaders ResponseHeaders - { - get - { - if (_responseHeaders == null) - { - _responseHeaders = _httpContext.Response.GetTypedHeaders(); - } - return _responseHeaders; - } - } - - public CacheControlHeaderValue RequestCacheControl - { - get - { - if (_requestCacheControl == null) - { - _requestCacheControl = RequestHeaders.CacheControl ?? EmptyCacheControl; - } - return _requestCacheControl; - } - } - - public CacheControlHeaderValue ResponseCacheControl - { - get - { - if (_responseCacheControl == null) - { - _responseCacheControl = ResponseHeaders.CacheControl ?? EmptyCacheControl; - } - return _responseCacheControl; - } - } - } -} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs index 55dc1824ca..bea436517e 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs @@ -3,356 +3,102 @@ using System; using System.IO; -using System.Globalization; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Headers; using Microsoft.AspNetCore.ResponseCaching.Internal; -using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.ResponseCaching { - internal class ResponseCachingContext + public class ResponseCachingContext { - private readonly HttpContext _httpContext; - private readonly IResponseCache _cache; - private readonly ResponseCachingOptions _options; - private readonly ICacheabilityValidator _cacheabilityValidator; - private readonly IKeyProvider _keyProvider; + private static readonly CacheControlHeaderValue EmptyCacheControl = new CacheControlHeaderValue(); - private ResponseCachingState _state; + private RequestHeaders _requestHeaders; + private ResponseHeaders _responseHeaders; + private CacheControlHeaderValue _requestCacheControl; + private CacheControlHeaderValue _responseCacheControl; internal ResponseCachingContext( - HttpContext httpContext, - IResponseCache cache, - ResponseCachingOptions options, - ICacheabilityValidator cacheabilityValidator, - IKeyProvider keyProvider) + HttpContext httpContext) { - _httpContext = httpContext; - _cache = cache; - _options = options; - _cacheabilityValidator = cacheabilityValidator; - _keyProvider = keyProvider; + HttpContext = httpContext; } - internal ResponseCachingState State - { - get - { - if (_state == null) - { - _state = _httpContext.GetResponseCachingState(); - } - return _state; - } - } + public HttpContext HttpContext { get; } + + public bool ShouldCacheResponse { get; internal set; } + + public string StorageBaseKey { get; internal set; } + + public string StorageVaryKey { get; internal set; } + + public DateTimeOffset ResponseTime { get; internal set; } + + public TimeSpan CachedEntryAge { get; internal set; } + + public TimeSpan CachedResponseValidFor { get; internal set; } + + public CachedResponse CachedResponse { get; internal set; } + + public CachedVaryRules CachedVaryRules { get; internal set; } internal bool ResponseStarted { get; set; } - private Stream OriginalResponseStream { get; set; } + internal Stream OriginalResponseStream { get; set; } - private ResponseCacheStream ResponseCacheStream { get; set; } + internal ResponseCacheStream ResponseCacheStream { get; set; } - private IHttpSendFileFeature OriginalSendFileFeature { get; set; } + internal IHttpSendFileFeature OriginalSendFileFeature { get; set; } - internal async Task TryServeCachedResponseAsync(CachedResponse cachedResponse) + internal ResponseHeaders CachedResponseHeaders { get; set; } + + internal RequestHeaders TypedRequestHeaders { - State.CachedResponse = cachedResponse; - var cachedResponseHeaders = new ResponseHeaders(State.CachedResponse.Headers); - - State.ResponseTime = _options.SystemClock.UtcNow; - var cachedEntryAge = State.ResponseTime - State.CachedResponse.Created; - State.CachedEntryAge = cachedEntryAge > TimeSpan.Zero ? cachedEntryAge : TimeSpan.Zero; - - if (_cacheabilityValidator.CachedEntryIsFresh(_httpContext, cachedResponseHeaders)) + get { - // Check conditional request rules - if (ConditionalRequestSatisfied(cachedResponseHeaders)) + if (_requestHeaders == null) { - _httpContext.Response.StatusCode = StatusCodes.Status304NotModified; + _requestHeaders = HttpContext.Request.GetTypedHeaders(); } - else - { - var response = _httpContext.Response; - // Copy the cached status code and response headers - response.StatusCode = State.CachedResponse.StatusCode; - foreach (var header in State.CachedResponse.Headers) - { - response.Headers.Add(header); - } - - response.Headers[HeaderNames.Age] = State.CachedEntryAge.TotalSeconds.ToString("F0", CultureInfo.InvariantCulture); - - var body = State.CachedResponse.Body ?? - ((CachedResponseBody)_cache.Get(State.CachedResponse.BodyKeyPrefix))?.Body; - - // If the body is not found, something went wrong. - if (body == null) - { - return false; - } - - // Copy the cached response body - if (body.Length > 0) - { - // Add a content-length if required - if (response.ContentLength == null && StringValues.IsNullOrEmpty(response.Headers[HeaderNames.TransferEncoding])) - { - response.ContentLength = body.Length; - } - await response.Body.WriteAsync(body, 0, body.Length); - } - } - - return true; - } - else - { - // TODO: Validate with endpoint instead - } - - return false; - } - - internal async Task TryServeFromCacheAsync() - { - foreach (var baseKey in _keyProvider.CreateLookupBaseKey(_httpContext)) - { - var cacheEntry = _cache.Get(baseKey); - - if (cacheEntry is CachedVaryRules) - { - // Request contains vary rules, recompute key(s) and try again - State.CachedVaryRules = cacheEntry as CachedVaryRules; - - foreach (var varyKey in _keyProvider.CreateLookupVaryKey(_httpContext, State.CachedVaryRules.VaryRules)) - { - cacheEntry = _cache.Get(varyKey); - - if (cacheEntry is CachedResponse && await TryServeCachedResponseAsync(cacheEntry as CachedResponse)) - { - return true; - } - } - } - - if (cacheEntry is CachedResponse && await TryServeCachedResponseAsync(cacheEntry as CachedResponse)) - { - return true; - } - } - - - if (State.RequestCacheControl.OnlyIfCached) - { - _httpContext.Response.StatusCode = StatusCodes.Status504GatewayTimeout; - return true; - } - - return false; - } - - internal bool ConditionalRequestSatisfied(ResponseHeaders cachedResponseHeaders) - { - var ifNoneMatchHeader = State.RequestHeaders.IfNoneMatch; - - if (ifNoneMatchHeader != null) - { - if (ifNoneMatchHeader.Count == 1 && ifNoneMatchHeader[0].Equals(EntityTagHeaderValue.Any)) - { - return true; - } - - if (cachedResponseHeaders.ETag != null) - { - foreach (var tag in ifNoneMatchHeader) - { - if (cachedResponseHeaders.ETag.Compare(tag, useStrongComparison: true)) - { - return true; - } - } - } - } - else if ((cachedResponseHeaders.LastModified ?? cachedResponseHeaders.Date) <= State.RequestHeaders.IfUnmodifiedSince) - { - return true; - } - - return false; - } - - internal void FinalizeCachingHeaders() - { - if (_cacheabilityValidator.ResponseIsCacheable(_httpContext)) - { - State.ShouldCacheResponse = true; - State.StorageBaseKey = _keyProvider.CreateStorageBaseKey(_httpContext); - - // Create the cache entry now - var response = _httpContext.Response; - var varyHeaderValue = response.Headers[HeaderNames.Vary]; - var varyParamsValue = _httpContext.GetResponseCachingFeature()?.VaryParams ?? StringValues.Empty; - State.CachedResponseValidFor = State.ResponseCacheControl.SharedMaxAge - ?? State.ResponseCacheControl.MaxAge - ?? (State.ResponseHeaders.Expires - State.ResponseTime) - // TODO: Heuristics for expiration? - ?? TimeSpan.FromSeconds(10); - - // Check if any vary rules exist - if (!StringValues.IsNullOrEmpty(varyHeaderValue) || !StringValues.IsNullOrEmpty(varyParamsValue)) - { - // Normalize order and casing of vary by rules - var normalizedVaryHeaderValue = GetNormalizedStringValues(varyHeaderValue); - var normalizedVaryParamsValue = GetNormalizedStringValues(varyParamsValue); - - // Update vary rules if they are different - if (State.CachedVaryRules == null || - !StringValues.Equals(State.CachedVaryRules.VaryRules.Params, normalizedVaryParamsValue) || - !StringValues.Equals(State.CachedVaryRules.VaryRules.Headers, normalizedVaryHeaderValue)) - { - var cachedVaryRules = new CachedVaryRules - { - VaryKeyPrefix = FastGuid.NewGuid().IdString, - VaryRules = new VaryRules() - { - // TODO: Vary Encoding - Headers = normalizedVaryHeaderValue, - Params = normalizedVaryParamsValue - } - }; - - State.CachedVaryRules = cachedVaryRules; - _cache.Set(State.StorageBaseKey, cachedVaryRules, State.CachedResponseValidFor); - } - - State.StorageVaryKey = _keyProvider.CreateStorageVaryKey(_httpContext, State.CachedVaryRules.VaryRules); - } - - // Ensure date header is set - if (State.ResponseHeaders.Date == null) - { - State.ResponseHeaders.Date = State.ResponseTime; - } - - // Store the response on the state - State.CachedResponse = new CachedResponse - { - BodyKeyPrefix = FastGuid.NewGuid().IdString, - Created = State.ResponseHeaders.Date.Value, - StatusCode = _httpContext.Response.StatusCode - }; - - foreach (var header in State.ResponseHeaders.Headers) - { - if (!string.Equals(header.Key, HeaderNames.Age, StringComparison.OrdinalIgnoreCase)) - { - State.CachedResponse.Headers.Add(header); - } - } - } - else - { - ResponseCacheStream.DisableBuffering(); + return _requestHeaders; } } - internal void FinalizeCachingBody() + internal ResponseHeaders TypedResponseHeaders { - if (State.ShouldCacheResponse && - ResponseCacheStream.BufferingEnabled && - (State.ResponseHeaders.ContentLength == null || - State.ResponseHeaders.ContentLength == ResponseCacheStream.BufferedStream.Length)) + get { - if (ResponseCacheStream.BufferedStream.Length >= _options.MinimumSplitBodySize) + if (_responseHeaders == null) { - // Store response and response body separately - _cache.Set(State.StorageVaryKey ?? State.StorageBaseKey, State.CachedResponse, State.CachedResponseValidFor); - - var cachedResponseBody = new CachedResponseBody() - { - Body = ResponseCacheStream.BufferedStream.ToArray() - }; - - _cache.Set(State.CachedResponse.BodyKeyPrefix, cachedResponseBody, State.CachedResponseValidFor); + _responseHeaders = HttpContext.Response.GetTypedHeaders(); } - else + return _responseHeaders; + } + } + + internal CacheControlHeaderValue RequestCacheControlHeaderValue + { + get + { + if (_requestCacheControl == null) { - // Store response and response body together - State.CachedResponse.Body = ResponseCacheStream.BufferedStream.ToArray(); - _cache.Set(State.StorageVaryKey ?? State.StorageBaseKey, State.CachedResponse, State.CachedResponseValidFor); + _requestCacheControl = TypedRequestHeaders.CacheControl ?? EmptyCacheControl; } + return _requestCacheControl; } } - internal void OnResponseStarting() + internal CacheControlHeaderValue ResponseCacheControlHeaderValue { - if (!ResponseStarted) + get { - ResponseStarted = true; - State.ResponseTime = _options.SystemClock.UtcNow; - - FinalizeCachingHeaders(); - } - } - - internal void ShimResponseStream() - { - // TODO: Consider caching large responses on disk and serving them from there. - - // Shim response stream - OriginalResponseStream = _httpContext.Response.Body; - ResponseCacheStream = new ResponseCacheStream(OriginalResponseStream, _options.MaximumCachedBodySize); - _httpContext.Response.Body = ResponseCacheStream; - - // Shim IHttpSendFileFeature - OriginalSendFileFeature = _httpContext.Features.Get(); - if (OriginalSendFileFeature != null) - { - _httpContext.Features.Set(new SendFileFeatureWrapper(OriginalSendFileFeature, ResponseCacheStream)); - } - - // TODO: Move this temporary interface with endpoint to HttpAbstractions - _httpContext.AddResponseCachingFeature(); - } - - internal void UnshimResponseStream() - { - // Unshim response stream - _httpContext.Response.Body = OriginalResponseStream; - - // Unshim IHttpSendFileFeature - _httpContext.Features.Set(OriginalSendFileFeature); - - // TODO: Move this temporary interface with endpoint to HttpAbstractions - _httpContext.RemoveResponseCachingFeature(); - } - - // Normalize order and casing - internal static StringValues GetNormalizedStringValues(StringValues stringVales) - { - if (stringVales.Count == 1) - { - return new StringValues(stringVales.ToString().ToUpperInvariant()); - } - else - { - var originalArray = stringVales.ToArray(); - var newArray = new string[originalArray.Length]; - - for (int i = 0; i < originalArray.Length; i++) + if (_responseCacheControl == null) { - newArray[i] = originalArray[i].ToUpperInvariant(); + _responseCacheControl = TypedResponseHeaders.CacheControl ?? EmptyCacheControl; } - - // Since the casing has already been normalized, use Ordinal comparison - Array.Sort(newArray, StringComparer.Ordinal); - - return new StringValues(newArray); + return _responseCacheControl; } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs index f5b62fd2ae..8e0d6c910f 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs @@ -2,35 +2,37 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Globalization; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Http.Headers; using Microsoft.AspNetCore.ResponseCaching.Internal; using Microsoft.Extensions.Internal; using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.ResponseCaching { public class ResponseCachingMiddleware { - private static readonly Func OnStartingCallback = state => - { - ((ResponseCachingContext)state).OnResponseStarting(); - return TaskCache.CompletedTask; - }; + private static readonly TimeSpan DefaultExpirationTimeSpan = TimeSpan.FromSeconds(10); private readonly RequestDelegate _next; private readonly IResponseCache _cache; private readonly ResponseCachingOptions _options; private readonly ICacheabilityValidator _cacheabilityValidator; - private readonly IKeyProvider _keyProvider; + private readonly ICacheKeyProvider _cacheKeyProvider; + private readonly Func _onStartingCallback; public ResponseCachingMiddleware( RequestDelegate next, IResponseCache cache, IOptions options, ICacheabilityValidator cacheabilityValidator, - IKeyProvider keyProvider) + ICacheKeyProvider cacheKeyProvider) { if (next == null) { @@ -48,70 +50,352 @@ namespace Microsoft.AspNetCore.ResponseCaching { throw new ArgumentNullException(nameof(cacheabilityValidator)); } - if (keyProvider == null) + if (cacheKeyProvider == null) { - throw new ArgumentNullException(nameof(keyProvider)); + throw new ArgumentNullException(nameof(cacheKeyProvider)); } _next = next; _cache = cache; _options = options.Value; _cacheabilityValidator = cacheabilityValidator; - _keyProvider = keyProvider; + _cacheKeyProvider = cacheKeyProvider; + _onStartingCallback = state => + { + OnResponseStarting((ResponseCachingContext)state); + return TaskCache.CompletedTask; + }; } - public async Task Invoke(HttpContext context) + public async Task Invoke(HttpContext httpContext) { - context.AddResponseCachingState(); + var context = new ResponseCachingContext(httpContext); - try + // Should we attempt any caching logic? + if (_cacheabilityValidator.IsRequestCacheable(context)) { - var cachingContext = new ResponseCachingContext( - context, - _cache, - _options, - _cacheabilityValidator, - _keyProvider); - - // Should we attempt any caching logic? - if (_cacheabilityValidator.RequestIsCacheable(context)) + // Can this request be served from cache? + if (await TryServeFromCacheAsync(context)) { - // Can this request be served from cache? - if (await cachingContext.TryServeFromCacheAsync()) - { - return; - } + return; + } - // Hook up to listen to the response stream - cachingContext.ShimResponseStream(); + // Hook up to listen to the response stream + ShimResponseStream(context); - try - { - // Subscribe to OnStarting event - context.Response.OnStarting(OnStartingCallback, cachingContext); + try + { + // Subscribe to OnStarting event + httpContext.Response.OnStarting(_onStartingCallback, context); - await _next(context); + await _next(httpContext); - // If there was no response body, check the response headers now. We can cache things like redirects. - cachingContext.OnResponseStarting(); + // If there was no response body, check the response headers now. We can cache things like redirects. + OnResponseStarting(context); - // Finalize the cache entry - cachingContext.FinalizeCachingBody(); - } - finally - { - cachingContext.UnshimResponseStream(); - } + // Finalize the cache entry + FinalizeCachingBody(context); + } + finally + { + UnshimResponseStream(context); + } + } + else + { + // TODO: Invalidate resources for successful unsafe methods? Required by RFC + await _next(httpContext); + } + } + + internal async Task TryServeCachedResponseAsync(ResponseCachingContext context, CachedResponse cachedResponse) + { + context.CachedResponse = cachedResponse; + context.CachedResponseHeaders = new ResponseHeaders(cachedResponse.Headers); + context.ResponseTime = _options.SystemClock.UtcNow; + var cachedEntryAge = context.ResponseTime - context.CachedResponse.Created; + context.CachedEntryAge = cachedEntryAge > TimeSpan.Zero ? cachedEntryAge : TimeSpan.Zero; + + if (_cacheabilityValidator.IsCachedEntryFresh(context)) + { + // Check conditional request rules + if (ConditionalRequestSatisfied(context)) + { + context.HttpContext.Response.StatusCode = StatusCodes.Status304NotModified; } else { - // TODO: Invalidate resources for successful unsafe methods? Required by RFC - await _next(context); + var response = context.HttpContext.Response; + // Copy the cached status code and response headers + response.StatusCode = context.CachedResponse.StatusCode; + foreach (var header in context.CachedResponse.Headers) + { + response.Headers.Add(header); + } + + response.Headers[HeaderNames.Age] = context.CachedEntryAge.TotalSeconds.ToString("F0", CultureInfo.InvariantCulture); + + var body = context.CachedResponse.Body ?? + ((CachedResponseBody)_cache.Get(context.CachedResponse.BodyKeyPrefix))?.Body; + + // If the body is not found, something went wrong. + if (body == null) + { + return false; + } + + // Copy the cached response body + if (body.Length > 0) + { + // Add a content-length if required + if (response.ContentLength == null && StringValues.IsNullOrEmpty(response.Headers[HeaderNames.TransferEncoding])) + { + response.ContentLength = body.Length; + } + await response.Body.WriteAsync(body, 0, body.Length); + } + } + + return true; + } + else + { + // TODO: Validate with endpoint instead + } + + return false; + } + + internal async Task TryServeFromCacheAsync(ResponseCachingContext context) + { + foreach (var baseKey in _cacheKeyProvider.CreateLookupBaseKeys(context)) + { + var cacheEntry = _cache.Get(baseKey); + + if (cacheEntry is CachedVaryRules) + { + // Request contains vary rules, recompute key(s) and try again + context.CachedVaryRules = (CachedVaryRules)cacheEntry; + + foreach (var varyKey in _cacheKeyProvider.CreateLookupVaryKeys(context)) + { + cacheEntry = _cache.Get(varyKey); + + if (cacheEntry is CachedResponse && await TryServeCachedResponseAsync(context, (CachedResponse)cacheEntry)) + { + return true; + } + } + } + + if (cacheEntry is CachedResponse && await TryServeCachedResponseAsync(context, (CachedResponse)cacheEntry)) + { + return true; } } - finally + + + if (context.RequestCacheControlHeaderValue.OnlyIfCached) { - context.RemoveResponseCachingState(); + context.HttpContext.Response.StatusCode = StatusCodes.Status504GatewayTimeout; + return true; + } + + return false; + } + + internal void FinalizeCachingHeaders(ResponseCachingContext context) + { + if (_cacheabilityValidator.IsResponseCacheable(context)) + { + context.ShouldCacheResponse = true; + context.StorageBaseKey = _cacheKeyProvider.CreateStorageBaseKey(context); + + // Create the cache entry now + var response = context.HttpContext.Response; + var varyHeaderValue = response.Headers[HeaderNames.Vary]; + var varyParamsValue = context.HttpContext.GetResponseCachingFeature()?.VaryParams ?? StringValues.Empty; + context.CachedResponseValidFor = context.ResponseCacheControlHeaderValue.SharedMaxAge ?? + context.ResponseCacheControlHeaderValue.MaxAge ?? + (context.TypedResponseHeaders.Expires - context.ResponseTime) ?? + DefaultExpirationTimeSpan; + + // Check if any vary rules exist + if (!StringValues.IsNullOrEmpty(varyHeaderValue) || !StringValues.IsNullOrEmpty(varyParamsValue)) + { + // Normalize order and casing of vary by rules + var normalizedVaryHeaderValue = GetNormalizedStringValues(varyHeaderValue); + var normalizedVaryParamsValue = GetNormalizedStringValues(varyParamsValue); + + // Update vary rules if they are different + if (context.CachedVaryRules == null || + !StringValues.Equals(context.CachedVaryRules.Params, normalizedVaryParamsValue) || + !StringValues.Equals(context.CachedVaryRules.Headers, normalizedVaryHeaderValue)) + { + context.CachedVaryRules = new CachedVaryRules + { + VaryKeyPrefix = FastGuid.NewGuid().IdString, + Headers = normalizedVaryHeaderValue, + Params = normalizedVaryParamsValue + }; + + _cache.Set(context.StorageBaseKey, context.CachedVaryRules, context.CachedResponseValidFor); + } + + context.StorageVaryKey = _cacheKeyProvider.CreateStorageVaryKey(context); + } + + // Ensure date header is set + if (context.TypedResponseHeaders.Date == null) + { + context.TypedResponseHeaders.Date = context.ResponseTime; + } + + // Store the response on the state + context.CachedResponse = new CachedResponse + { + BodyKeyPrefix = FastGuid.NewGuid().IdString, + Created = context.TypedResponseHeaders.Date.Value, + StatusCode = context.HttpContext.Response.StatusCode + }; + + foreach (var header in context.TypedResponseHeaders.Headers) + { + if (!string.Equals(header.Key, HeaderNames.Age, StringComparison.OrdinalIgnoreCase)) + { + context.CachedResponse.Headers.Add(header); + } + } + } + else + { + context.ResponseCacheStream.DisableBuffering(); + } + } + + internal void FinalizeCachingBody(ResponseCachingContext context) + { + if (context.ShouldCacheResponse && + context.ResponseCacheStream.BufferingEnabled && + (context.TypedResponseHeaders.ContentLength == null || + context.TypedResponseHeaders.ContentLength == context.ResponseCacheStream.BufferedStream.Length)) + { + if (context.ResponseCacheStream.BufferedStream.Length >= _options.MinimumSplitBodySize) + { + // Store response and response body separately + _cache.Set(context.StorageVaryKey ?? context.StorageBaseKey, context.CachedResponse, context.CachedResponseValidFor); + + var cachedResponseBody = new CachedResponseBody() + { + Body = context.ResponseCacheStream.BufferedStream.ToArray() + }; + + _cache.Set(context.CachedResponse.BodyKeyPrefix, cachedResponseBody, context.CachedResponseValidFor); + } + else + { + // Store response and response body together + context.CachedResponse.Body = context.ResponseCacheStream.BufferedStream.ToArray(); + _cache.Set(context.StorageVaryKey ?? context.StorageBaseKey, context.CachedResponse, context.CachedResponseValidFor); + } + } + } + + internal void OnResponseStarting(ResponseCachingContext context) + { + if (!context.ResponseStarted) + { + context.ResponseStarted = true; + context.ResponseTime = _options.SystemClock.UtcNow; + + FinalizeCachingHeaders(context); + } + } + + internal void ShimResponseStream(ResponseCachingContext context) + { + // TODO: Consider caching large responses on disk and serving them from there. + + // Shim response stream + context.OriginalResponseStream = context.HttpContext.Response.Body; + context.ResponseCacheStream = new ResponseCacheStream(context.OriginalResponseStream, _options.MaximumCachedBodySize); + context.HttpContext.Response.Body = context.ResponseCacheStream; + + // Shim IHttpSendFileFeature + context.OriginalSendFileFeature = context.HttpContext.Features.Get(); + if (context.OriginalSendFileFeature != null) + { + context.HttpContext.Features.Set(new SendFileFeatureWrapper(context.OriginalSendFileFeature, context.ResponseCacheStream)); + } + + // TODO: Move this temporary interface with endpoint to HttpAbstractions + context.HttpContext.AddResponseCachingFeature(); + } + + internal static void UnshimResponseStream(ResponseCachingContext context) + { + // Unshim response stream + context.HttpContext.Response.Body = context.OriginalResponseStream; + + // Unshim IHttpSendFileFeature + context.HttpContext.Features.Set(context.OriginalSendFileFeature); + + // TODO: Move this temporary interface with endpoint to HttpAbstractions + context.HttpContext.RemoveResponseCachingFeature(); + } + + internal static bool ConditionalRequestSatisfied(ResponseCachingContext context) + { + var cachedResponseHeaders = context.CachedResponseHeaders; + var ifNoneMatchHeader = context.TypedRequestHeaders.IfNoneMatch; + + if (ifNoneMatchHeader != null) + { + if (ifNoneMatchHeader.Count == 1 && ifNoneMatchHeader[0].Equals(EntityTagHeaderValue.Any)) + { + return true; + } + + if (cachedResponseHeaders.ETag != null) + { + foreach (var tag in ifNoneMatchHeader) + { + if (cachedResponseHeaders.ETag.Compare(tag, useStrongComparison: true)) + { + return true; + } + } + } + } + else if ((cachedResponseHeaders.LastModified ?? cachedResponseHeaders.Date) <= context.TypedRequestHeaders.IfUnmodifiedSince) + { + return true; + } + + return false; + } + + // Normalize order and casing + internal static StringValues GetNormalizedStringValues(StringValues stringValues) + { + if (stringValues.Count == 1) + { + return new StringValues(stringValues.ToString().ToUpperInvariant()); + } + else + { + var originalArray = stringValues.ToArray(); + var newArray = new string[originalArray.Length]; + + for (int i = 0; i < originalArray.Length; i++) + { + newArray[i] = originalArray[i].ToUpperInvariant(); + } + + // Since the casing has already been normalized, use Ordinal comparison + Array.Sort(newArray, StringComparer.Ordinal); + + return new StringValues(newArray); } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/VaryRules.cs b/src/Microsoft.AspNetCore.ResponseCaching/VaryRules.cs deleted file mode 100644 index 46d9419528..0000000000 --- a/src/Microsoft.AspNetCore.ResponseCaching/VaryRules.cs +++ /dev/null @@ -1,13 +0,0 @@ -// 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 -{ - public class VaryRules - { - internal StringValues Headers { get; set; } - internal StringValues Params { get; set; } - } -} diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheEntrySerializerTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheEntrySerializerTests.cs index bd2ad03bb1..a2b1269855 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheEntrySerializerTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheEntrySerializerTests.cs @@ -81,8 +81,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var cachedVaryRule = new CachedVaryRules() { - VaryKeyPrefix = FastGuid.NewGuid().IdString, - VaryRules = new VaryRules() + VaryKeyPrefix = FastGuid.NewGuid().IdString }; AssertCachedVaryRuleEqual(cachedVaryRule, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRule))); @@ -95,10 +94,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var cachedVaryRule = new CachedVaryRules() { VaryKeyPrefix = FastGuid.NewGuid().IdString, - VaryRules = new VaryRules() - { - Headers = headers - } + Headers = headers }; AssertCachedVaryRuleEqual(cachedVaryRule, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRule))); @@ -111,10 +107,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var cachedVaryRule = new CachedVaryRules() { VaryKeyPrefix = FastGuid.NewGuid().IdString, - VaryRules = new VaryRules() - { - Params = param - } + Params = param }; AssertCachedVaryRuleEqual(cachedVaryRule, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRule))); @@ -128,11 +121,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var cachedVaryRule = new CachedVaryRules() { VaryKeyPrefix = FastGuid.NewGuid().IdString, - VaryRules = new VaryRules() - { - Headers = headers, - Params = param - } + Headers = headers, + Params = param }; AssertCachedVaryRuleEqual(cachedVaryRule, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRule))); @@ -145,10 +135,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var cachedVaryRule = new CachedVaryRules() { VaryKeyPrefix = FastGuid.NewGuid().IdString, - VaryRules = new VaryRules() - { - Headers = headers - } + Headers = headers }; var serializedEntry = CacheEntrySerializer.Serialize(cachedVaryRule); Array.Reverse(serializedEntry); @@ -188,8 +175,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Assert.NotNull(actual); Assert.NotNull(expected); Assert.Equal(expected.VaryKeyPrefix, actual.VaryKeyPrefix); - Assert.Equal(expected.VaryRules.Headers, actual.VaryRules.Headers); - Assert.Equal(expected.VaryRules.Params, actual.VaryRules.Params); + Assert.Equal(expected.Headers, actual.Headers); + Assert.Equal(expected.Params, actual.Params); } } } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheabilityValidatorTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheabilityValidatorTests.cs index e3cd724195..88be66b80a 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheabilityValidatorTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheabilityValidatorTests.cs @@ -4,7 +4,6 @@ using System; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Headers; -using Microsoft.AspNetCore.ResponseCaching.Internal; using Microsoft.Net.Http.Headers; using Xunit; @@ -17,10 +16,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [InlineData("HEAD")] public void RequestIsCacheable_CacheableMethods_Allowed(string method) { - var httpContext = CreateDefaultContext(); - httpContext.Request.Method = method; + var context = TestUtils.CreateTestContext(); + context.HttpContext.Request.Method = method; - Assert.True(new CacheabilityValidator().RequestIsCacheable(httpContext)); + Assert.True(new CacheabilityValidator().IsRequestCacheable(context)); } [Theory] @@ -34,182 +33,182 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [InlineData(null)] public void RequestIsCacheable_UncacheableMethods_NotAllowed(string method) { - var httpContext = CreateDefaultContext(); - httpContext.Request.Method = method; + var context = TestUtils.CreateTestContext(); + context.HttpContext.Request.Method = method; - Assert.False(new CacheabilityValidator().RequestIsCacheable(httpContext)); + Assert.False(new CacheabilityValidator().IsRequestCacheable(context)); } [Fact] public void RequestIsCacheable_AuthorizationHeaders_NotAllowed() { - var httpContext = CreateDefaultContext(); - httpContext.Request.Method = "GET"; - httpContext.Request.Headers[HeaderNames.Authorization] = "Basic plaintextUN:plaintextPW"; + var context = TestUtils.CreateTestContext(); + context.HttpContext.Request.Method = "GET"; + context.HttpContext.Request.Headers[HeaderNames.Authorization] = "Basic plaintextUN:plaintextPW"; - Assert.False(new CacheabilityValidator().RequestIsCacheable(httpContext)); + Assert.False(new CacheabilityValidator().IsRequestCacheable(context)); } [Fact] public void RequestIsCacheable_NoCache_NotAllowed() { - var httpContext = CreateDefaultContext(); - httpContext.Request.Method = "GET"; - httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + var context = TestUtils.CreateTestContext(); + context.HttpContext.Request.Method = "GET"; + context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() { NoCache = true }; - Assert.False(new CacheabilityValidator().RequestIsCacheable(httpContext)); + Assert.False(new CacheabilityValidator().IsRequestCacheable(context)); } [Fact] public void RequestIsCacheable_NoStore_Allowed() { - var httpContext = CreateDefaultContext(); - httpContext.Request.Method = "GET"; - httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + var context = TestUtils.CreateTestContext(); + context.HttpContext.Request.Method = "GET"; + context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() { NoStore = true }; - Assert.True(new CacheabilityValidator().RequestIsCacheable(httpContext)); + Assert.True(new CacheabilityValidator().IsRequestCacheable(context)); } [Fact] public void RequestIsCacheable_LegacyDirectives_NotAllowed() { - var httpContext = CreateDefaultContext(); - httpContext.Request.Method = "GET"; - httpContext.Request.Headers[HeaderNames.Pragma] = "no-cache"; + var context = TestUtils.CreateTestContext(); + context.HttpContext.Request.Method = "GET"; + context.HttpContext.Request.Headers[HeaderNames.Pragma] = "no-cache"; - Assert.False(new CacheabilityValidator().RequestIsCacheable(httpContext)); + Assert.False(new CacheabilityValidator().IsRequestCacheable(context)); } [Fact] public void RequestIsCacheable_LegacyDirectives_OverridenByCacheControl() { - var httpContext = CreateDefaultContext(); - httpContext.Request.Method = "GET"; - httpContext.Request.Headers[HeaderNames.Pragma] = "no-cache"; - httpContext.Request.Headers[HeaderNames.CacheControl] = "max-age=10"; + var context = TestUtils.CreateTestContext(); + context.HttpContext.Request.Method = "GET"; + context.HttpContext.Request.Headers[HeaderNames.Pragma] = "no-cache"; + context.HttpContext.Request.Headers[HeaderNames.CacheControl] = "max-age=10"; - Assert.True(new CacheabilityValidator().RequestIsCacheable(httpContext)); + Assert.True(new CacheabilityValidator().IsRequestCacheable(context)); } [Fact] public void ResponseIsCacheable_NoPublic_NotAllowed() { - var httpContext = CreateDefaultContext(); + var context = TestUtils.CreateTestContext(); - Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext)); + Assert.False(new CacheabilityValidator().IsResponseCacheable(context)); } [Fact] public void ResponseIsCacheable_Public_Allowed() { - var httpContext = CreateDefaultContext(); - httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + var context = TestUtils.CreateTestContext(); + context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() { Public = true }; - Assert.True(new CacheabilityValidator().ResponseIsCacheable(httpContext)); + Assert.True(new CacheabilityValidator().IsResponseCacheable(context)); } [Fact] public void ResponseIsCacheable_NoCache_NotAllowed() { - var httpContext = CreateDefaultContext(); - httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + var context = TestUtils.CreateTestContext(); + context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() { Public = true, NoCache = true }; - Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext)); + Assert.False(new CacheabilityValidator().IsResponseCacheable(context)); } [Fact] public void ResponseIsCacheable_RequestNoStore_NotAllowed() { - var httpContext = CreateDefaultContext(); - httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + var context = TestUtils.CreateTestContext(); + context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() { NoStore = true }; - httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() { Public = true }; - Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext)); + Assert.False(new CacheabilityValidator().IsResponseCacheable(context)); } [Fact] public void ResponseIsCacheable_ResponseNoStore_NotAllowed() { - var httpContext = CreateDefaultContext(); - httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + var context = TestUtils.CreateTestContext(); + context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() { Public = true, NoStore = true }; - Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext)); + Assert.False(new CacheabilityValidator().IsResponseCacheable(context)); } [Fact] public void ResponseIsCacheable_SetCookieHeader_NotAllowed() { - var httpContext = CreateDefaultContext(); - httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + var context = TestUtils.CreateTestContext(); + context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() { Public = true }; - httpContext.Response.Headers[HeaderNames.SetCookie] = "cookieName=cookieValue"; + context.HttpContext.Response.Headers[HeaderNames.SetCookie] = "cookieName=cookieValue"; - Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext)); + Assert.False(new CacheabilityValidator().IsResponseCacheable(context)); } [Fact] public void ResponseIsCacheable_VaryHeaderByStar_NotAllowed() { - var httpContext = CreateDefaultContext(); - httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + var context = TestUtils.CreateTestContext(); + context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() { Public = true }; - httpContext.Response.Headers[HeaderNames.Vary] = "*"; + context.HttpContext.Response.Headers[HeaderNames.Vary] = "*"; - Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext)); + Assert.False(new CacheabilityValidator().IsResponseCacheable(context)); } [Fact] public void ResponseIsCacheable_Private_NotAllowed() { - var httpContext = CreateDefaultContext(); - httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + var context = TestUtils.CreateTestContext(); + context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() { Public = true, Private = true }; - Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext)); + Assert.False(new CacheabilityValidator().IsResponseCacheable(context)); } [Theory] [InlineData(StatusCodes.Status200OK)] public void ResponseIsCacheable_SuccessStatusCodes_Allowed(int statusCode) { - var httpContext = CreateDefaultContext(); - httpContext.Response.StatusCode = statusCode; - httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + var context = TestUtils.CreateTestContext(); + context.HttpContext.Response.StatusCode = statusCode; + context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() { Public = true }; - Assert.True(new CacheabilityValidator().ResponseIsCacheable(httpContext)); + Assert.True(new CacheabilityValidator().IsResponseCacheable(context)); } [Theory] @@ -263,146 +262,141 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [InlineData(StatusCodes.Status507InsufficientStorage)] public void ResponseIsCacheable_NonSuccessStatusCodes_NotAllowed(int statusCode) { - var httpContext = CreateDefaultContext(); - httpContext.Response.StatusCode = statusCode; - httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + var context = TestUtils.CreateTestContext(); + context.HttpContext.Response.StatusCode = statusCode; + context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() { Public = true }; - Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext)); + Assert.False(new CacheabilityValidator().IsResponseCacheable(context)); } [Fact] public void ResponseIsCacheable_NoExpiryRequirements_IsAllowed() { - var httpContext = CreateDefaultContext(); - httpContext.Response.StatusCode = StatusCodes.Status200OK; - var headers = httpContext.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() + var context = TestUtils.CreateTestContext(); + context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; + context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() { Public = true }; var utcNow = DateTimeOffset.UtcNow; - headers.Date = utcNow; - httpContext.GetResponseCachingState().ResponseTime = DateTimeOffset.MaxValue; + context.TypedResponseHeaders.Date = utcNow; + context.ResponseTime = DateTimeOffset.MaxValue; - Assert.True(new CacheabilityValidator().ResponseIsCacheable(httpContext)); + Assert.True(new CacheabilityValidator().IsResponseCacheable(context)); } [Fact] public void ResponseIsCacheable_PastExpiry_NotAllowed() { - var httpContext = CreateDefaultContext(); - httpContext.Response.StatusCode = StatusCodes.Status200OK; - var headers = httpContext.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() + var context = TestUtils.CreateTestContext(); + context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; + context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() { Public = true }; var utcNow = DateTimeOffset.UtcNow; - headers.Expires = utcNow; + context.TypedResponseHeaders.Expires = utcNow; - headers.Date = utcNow; - httpContext.GetResponseCachingState().ResponseTime = DateTimeOffset.MaxValue; + context.TypedResponseHeaders.Date = utcNow; + context.ResponseTime = DateTimeOffset.MaxValue; - Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext)); + Assert.False(new CacheabilityValidator().IsResponseCacheable(context)); } [Fact] public void ResponseIsCacheable_MaxAgeOverridesExpiry_ToAllowed() { var utcNow = DateTimeOffset.UtcNow; - var httpContext = CreateDefaultContext(); - httpContext.Response.StatusCode = StatusCodes.Status200OK; - var headers = httpContext.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() + var context = TestUtils.CreateTestContext(); + context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; + context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() { Public = true, MaxAge = TimeSpan.FromSeconds(10) }; - headers.Expires = utcNow; - headers.Date = utcNow; - httpContext.GetResponseCachingState().ResponseTime = utcNow + TimeSpan.FromSeconds(9); + context.TypedResponseHeaders.Expires = utcNow; + context.TypedResponseHeaders.Date = utcNow; + context.ResponseTime = utcNow + TimeSpan.FromSeconds(9); - Assert.True(new CacheabilityValidator().ResponseIsCacheable(httpContext)); + Assert.True(new CacheabilityValidator().IsResponseCacheable(context)); } [Fact] public void ResponseIsCacheable_MaxAgeOverridesExpiry_ToNotAllowed() { var utcNow = DateTimeOffset.UtcNow; - var httpContext = CreateDefaultContext(); - httpContext.Response.StatusCode = StatusCodes.Status200OK; - var headers = httpContext.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() + var context = TestUtils.CreateTestContext(); + context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; + context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() { Public = true, MaxAge = TimeSpan.FromSeconds(10) }; - headers.Expires = utcNow; - headers.Date = utcNow; - httpContext.GetResponseCachingState().ResponseTime = utcNow + TimeSpan.FromSeconds(11); + context.TypedResponseHeaders.Expires = utcNow; + context.TypedResponseHeaders.Date = utcNow; + context.ResponseTime = utcNow + TimeSpan.FromSeconds(11); - Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext)); + Assert.False(new CacheabilityValidator().IsResponseCacheable(context)); } [Fact] public void ResponseIsCacheable_SharedMaxAgeOverridesMaxAge_ToAllowed() { var utcNow = DateTimeOffset.UtcNow; - var httpContext = CreateDefaultContext(); - httpContext.Response.StatusCode = StatusCodes.Status200OK; - var headers = httpContext.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() + var context = TestUtils.CreateTestContext(); + context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; + context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() { Public = true, MaxAge = TimeSpan.FromSeconds(10), SharedMaxAge = TimeSpan.FromSeconds(15) }; - headers.Date = utcNow; - httpContext.GetResponseCachingState().ResponseTime = utcNow + TimeSpan.FromSeconds(11); + context.TypedResponseHeaders.Date = utcNow; + context.ResponseTime = utcNow + TimeSpan.FromSeconds(11); - Assert.True(new CacheabilityValidator().ResponseIsCacheable(httpContext)); + Assert.True(new CacheabilityValidator().IsResponseCacheable(context)); } [Fact] public void ResponseIsCacheable_SharedMaxAgeOverridesMaxAge_ToNotFresh() { var utcNow = DateTimeOffset.UtcNow; - var httpContext = CreateDefaultContext(); - httpContext.Response.StatusCode = StatusCodes.Status200OK; - var headers = httpContext.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() + var context = TestUtils.CreateTestContext(); + context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; + context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() { Public = true, MaxAge = TimeSpan.FromSeconds(10), SharedMaxAge = TimeSpan.FromSeconds(5) }; - headers.Date = utcNow; - httpContext.GetResponseCachingState().ResponseTime = utcNow + TimeSpan.FromSeconds(6); + context.TypedResponseHeaders.Date = utcNow; + context.ResponseTime = utcNow + TimeSpan.FromSeconds(6); - Assert.False(new CacheabilityValidator().ResponseIsCacheable(httpContext)); + Assert.False(new CacheabilityValidator().IsResponseCacheable(context)); } [Fact] public void EntryIsFresh_NoCachedCacheControl_FallsbackToEmptyCacheControl() { var utcNow = DateTimeOffset.UtcNow; - var httpContext = CreateDefaultContext(); - httpContext.GetResponseCachingState().ResponseTime = DateTimeOffset.MaxValue; + var context = TestUtils.CreateTestContext(); + context.ResponseTime = DateTimeOffset.MaxValue; + context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); - Assert.True(new CacheabilityValidator().CachedEntryIsFresh(httpContext, new ResponseHeaders(new HeaderDictionary()))); + Assert.True(new CacheabilityValidator().IsCachedEntryFresh(context)); } [Fact] public void EntryIsFresh_NoExpiryRequirements_IsFresh() { var utcNow = DateTimeOffset.UtcNow; - var httpContext = CreateDefaultContext(); - httpContext.GetResponseCachingState().ResponseTime = DateTimeOffset.MaxValue; - var cachedHeaders = new ResponseHeaders(new HeaderDictionary()) + var context = TestUtils.CreateTestContext(); + context.ResponseTime = DateTimeOffset.MaxValue; + context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) { CacheControl = new CacheControlHeaderValue() { @@ -410,15 +404,15 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } }; - Assert.True(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders)); + Assert.True(new CacheabilityValidator().IsCachedEntryFresh(context)); } [Fact] public void EntryIsFresh_PastExpiry_IsNotFresh() { - var httpContext = CreateDefaultContext(); - httpContext.GetResponseCachingState().ResponseTime = DateTimeOffset.MaxValue; - var cachedHeaders = new ResponseHeaders(new HeaderDictionary()) + var context = TestUtils.CreateTestContext(); + context.ResponseTime = DateTimeOffset.MaxValue; + context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) { CacheControl = new CacheControlHeaderValue() { @@ -427,18 +421,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Expires = DateTimeOffset.UtcNow }; - Assert.False(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders)); + Assert.False(new CacheabilityValidator().IsCachedEntryFresh(context)); } [Fact] public void EntryIsFresh_MaxAgeOverridesExpiry_ToFresh() { var utcNow = DateTimeOffset.UtcNow; - var httpContext = CreateDefaultContext(); - var state = httpContext.GetResponseCachingState(); - state.CachedEntryAge = TimeSpan.FromSeconds(9); - state.ResponseTime = utcNow + state.CachedEntryAge; - var cachedHeaders = new ResponseHeaders(new HeaderDictionary()) + var context = TestUtils.CreateTestContext(); + context.CachedEntryAge = TimeSpan.FromSeconds(9); + context.ResponseTime = utcNow + context.CachedEntryAge; + context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) { CacheControl = new CacheControlHeaderValue() { @@ -448,18 +441,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Expires = utcNow }; - Assert.True(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders)); + Assert.True(new CacheabilityValidator().IsCachedEntryFresh(context)); } [Fact] public void EntryIsFresh_MaxAgeOverridesExpiry_ToNotFresh() { var utcNow = DateTimeOffset.UtcNow; - var httpContext = CreateDefaultContext(); - var state = httpContext.GetResponseCachingState(); - state.CachedEntryAge = TimeSpan.FromSeconds(11); - state.ResponseTime = utcNow + state.CachedEntryAge; - var cachedHeaders = new ResponseHeaders(new HeaderDictionary()) + var context = TestUtils.CreateTestContext(); + context.CachedEntryAge = TimeSpan.FromSeconds(11); + context.ResponseTime = utcNow + context.CachedEntryAge; + context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) { CacheControl = new CacheControlHeaderValue() { @@ -469,18 +461,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Expires = utcNow }; - Assert.False(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders)); + Assert.False(new CacheabilityValidator().IsCachedEntryFresh(context)); } [Fact] public void EntryIsFresh_SharedMaxAgeOverridesMaxAge_ToFresh() { var utcNow = DateTimeOffset.UtcNow; - var httpContext = CreateDefaultContext(); - var state = httpContext.GetResponseCachingState(); - state.CachedEntryAge = TimeSpan.FromSeconds(11); - state.ResponseTime = utcNow + state.CachedEntryAge; - var cachedHeaders = new ResponseHeaders(new HeaderDictionary()) + var context = TestUtils.CreateTestContext(); + context.CachedEntryAge = TimeSpan.FromSeconds(11); + context.ResponseTime = utcNow + context.CachedEntryAge; + context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) { CacheControl = new CacheControlHeaderValue() { @@ -491,18 +482,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Expires = utcNow }; - Assert.True(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders)); + Assert.True(new CacheabilityValidator().IsCachedEntryFresh(context)); } [Fact] public void EntryIsFresh_SharedMaxAgeOverridesMaxAge_ToNotFresh() { var utcNow = DateTimeOffset.UtcNow; - var httpContext = CreateDefaultContext(); - var state = httpContext.GetResponseCachingState(); - state.CachedEntryAge = TimeSpan.FromSeconds(6); - state.ResponseTime = utcNow + state.CachedEntryAge; - var cachedHeaders = new ResponseHeaders(new HeaderDictionary()) + var context = TestUtils.CreateTestContext(); + context.CachedEntryAge = TimeSpan.FromSeconds(6); + context.ResponseTime = utcNow + context.CachedEntryAge; + context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) { CacheControl = new CacheControlHeaderValue() { @@ -513,18 +503,18 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Expires = utcNow }; - Assert.False(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders)); + Assert.False(new CacheabilityValidator().IsCachedEntryFresh(context)); } [Fact] public void EntryIsFresh_MinFreshReducesFreshness_ToNotFresh() { - var httpContext = CreateDefaultContext(); - httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + var context = TestUtils.CreateTestContext(); + context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() { MinFresh = TimeSpan.FromSeconds(3) }; - var cachedHeaders = new ResponseHeaders(new HeaderDictionary()) + context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) { CacheControl = new CacheControlHeaderValue() { @@ -532,64 +522,64 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests SharedMaxAge = TimeSpan.FromSeconds(5) } }; - httpContext.GetResponseCachingState().CachedEntryAge = TimeSpan.FromSeconds(3); + context.CachedEntryAge = TimeSpan.FromSeconds(3); - Assert.False(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders)); + Assert.False(new CacheabilityValidator().IsCachedEntryFresh(context)); } [Fact] public void EntryIsFresh_RequestMaxAgeRestrictAge_ToNotFresh() { - var httpContext = CreateDefaultContext(); - httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + var context = TestUtils.CreateTestContext(); + context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() { MaxAge = TimeSpan.FromSeconds(5) }; - var cachedHeaders = new ResponseHeaders(new HeaderDictionary()) + context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) { CacheControl = new CacheControlHeaderValue() { MaxAge = TimeSpan.FromSeconds(10), } }; - httpContext.GetResponseCachingState().CachedEntryAge = TimeSpan.FromSeconds(6); + context.CachedEntryAge = TimeSpan.FromSeconds(6); - Assert.False(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders)); + Assert.False(new CacheabilityValidator().IsCachedEntryFresh(context)); } [Fact] public void EntryIsFresh_MaxStaleOverridesFreshness_ToFresh() { - var httpContext = CreateDefaultContext(); - httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + var context = TestUtils.CreateTestContext(); + context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() { MaxAge = TimeSpan.FromSeconds(5), MaxStale = true, // This value must be set to true in order to specify MaxStaleLimit MaxStaleLimit = TimeSpan.FromSeconds(10) }; - var cachedHeaders = new ResponseHeaders(new HeaderDictionary()) + context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) { CacheControl = new CacheControlHeaderValue() { MaxAge = TimeSpan.FromSeconds(5), } }; - httpContext.GetResponseCachingState().CachedEntryAge = TimeSpan.FromSeconds(6); + context.CachedEntryAge = TimeSpan.FromSeconds(6); - Assert.True(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders)); + Assert.True(new CacheabilityValidator().IsCachedEntryFresh(context)); } [Fact] public void EntryIsFresh_MustRevalidateOverridesRequestMaxStale_ToNotFresh() { - var httpContext = CreateDefaultContext(); - httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + var context = TestUtils.CreateTestContext(); + context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() { MaxAge = TimeSpan.FromSeconds(5), MaxStale = true, // This value must be set to true in order to specify MaxStaleLimit MaxStaleLimit = TimeSpan.FromSeconds(10) }; - var cachedHeaders = new ResponseHeaders(new HeaderDictionary()) + context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) { CacheControl = new CacheControlHeaderValue() { @@ -597,21 +587,21 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests MustRevalidate = true } }; - httpContext.GetResponseCachingState().CachedEntryAge = TimeSpan.FromSeconds(6); + context.CachedEntryAge = TimeSpan.FromSeconds(6); - Assert.False(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders)); + Assert.False(new CacheabilityValidator().IsCachedEntryFresh(context)); } [Fact] public void EntryIsFresh_IgnoresRequestVerificationWhenSpecified() { - var httpContext = CreateDefaultContext(); - httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + var context = TestUtils.CreateTestContext(); + context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() { MinFresh = TimeSpan.FromSeconds(1), MaxAge = TimeSpan.FromSeconds(3) }; - var cachedHeaders = new ResponseHeaders(new HeaderDictionary()) + context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) { CacheControl = new CacheControlHeaderValue() { @@ -619,16 +609,9 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests SharedMaxAge = TimeSpan.FromSeconds(5) } }; - httpContext.GetResponseCachingState().CachedEntryAge = TimeSpan.FromSeconds(3); + context.CachedEntryAge = TimeSpan.FromSeconds(3); - Assert.True(new CacheabilityValidator().CachedEntryIsFresh(httpContext, cachedHeaders)); - } - - private static HttpContext CreateDefaultContext() - { - var context = new DefaultHttpContext(); - context.AddResponseCachingState(); - return context; + Assert.True(new CacheabilityValidator().IsCachedEntryFresh(context)); } } } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/HttpContextInternalExtensionTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/HttpContextInternalExtensionTests.cs index 74e15534bf..5bf1cbf5b7 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/HttpContextInternalExtensionTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/HttpContextInternalExtensionTests.cs @@ -21,17 +21,5 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests // Should throw Assert.ThrowsAny(() => httpContext.AddResponseCachingFeature()); } - - [Fact] - public void AddingSecondResponseCachingState_Throws() - { - var httpContext = new DefaultHttpContext(); - - // Should not throw - httpContext.AddResponseCachingState(); - - // Should throw - Assert.ThrowsAny(() => httpContext.AddResponseCachingState()); - } } } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/KeyProviderTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/KeyProviderTests.cs index 98e1450b9b..1239b2c901 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/KeyProviderTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/KeyProviderTests.cs @@ -1,11 +1,10 @@ // 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.Http; using Microsoft.AspNetCore.ResponseCaching.Internal; -using Microsoft.Extensions.ObjectPool; -using Microsoft.Extensions.Options; using Xunit; namespace Microsoft.AspNetCore.ResponseCaching.Tests @@ -13,156 +12,155 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests public class DefaultKeyProviderTests { private static readonly char KeyDelimiter = '\x1e'; - private static readonly CachedVaryRules TestVaryRules = new CachedVaryRules() - { - VaryKeyPrefix = FastGuid.NewGuid().IdString - }; [Fact] public void DefaultKeyProvider_CreateStorageBaseKey_IncludesOnlyNormalizedMethodAndPath() { - var httpContext = CreateDefaultContext(); - httpContext.Request.Method = "head"; - httpContext.Request.Path = "/path/subpath"; - httpContext.Request.Scheme = "https"; - httpContext.Request.Host = new HostString("example.com", 80); - httpContext.Request.PathBase = "/pathBase"; - httpContext.Request.QueryString = new QueryString("?query.Key=a&query.Value=b"); - var keyProvider = CreateTestKeyProvider(); + var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); + var context = TestUtils.CreateTestContext(); + context.HttpContext.Request.Method = "head"; + context.HttpContext.Request.Path = "/path/subpath"; + context.HttpContext.Request.Scheme = "https"; + context.HttpContext.Request.Host = new HostString("example.com", 80); + context.HttpContext.Request.PathBase = "/pathBase"; + context.HttpContext.Request.QueryString = new QueryString("?query.Key=a&query.Value=b"); - Assert.Equal($"HEAD{KeyDelimiter}/PATH/SUBPATH", keyProvider.CreateStorageBaseKey(httpContext)); + Assert.Equal($"HEAD{KeyDelimiter}/PATH/SUBPATH", cacheKeyProvider.CreateStorageBaseKey(context)); } [Fact] public void DefaultKeyProvider_CreateStorageBaseKey_CaseInsensitivePath_NormalizesPath() { - var httpContext = CreateDefaultContext(); - httpContext.Request.Method = "GET"; - httpContext.Request.Path = "/Path"; - var keyProvider = CreateTestKeyProvider(new ResponseCachingOptions() + var cacheKeyProvider = TestUtils.CreateTestKeyProvider(new ResponseCachingOptions() { CaseSensitivePaths = false }); + var context = TestUtils.CreateTestContext(); + context.HttpContext.Request.Method = "GET"; + context.HttpContext.Request.Path = "/Path"; - Assert.Equal($"GET{KeyDelimiter}/PATH", keyProvider.CreateStorageBaseKey(httpContext)); + Assert.Equal($"GET{KeyDelimiter}/PATH", cacheKeyProvider.CreateStorageBaseKey(context)); } [Fact] public void DefaultKeyProvider_CreateStorageBaseKey_CaseSensitivePath_PreservesPathCase() { - var httpContext = CreateDefaultContext(); - httpContext.Request.Method = "GET"; - httpContext.Request.Path = "/Path"; - var keyProvider = CreateTestKeyProvider(new ResponseCachingOptions() + var cacheKeyProvider = TestUtils.CreateTestKeyProvider(new ResponseCachingOptions() { CaseSensitivePaths = true }); + var context = TestUtils.CreateTestContext(); + context.HttpContext.Request.Method = "GET"; + context.HttpContext.Request.Path = "/Path"; - Assert.Equal($"GET{KeyDelimiter}/Path", keyProvider.CreateStorageBaseKey(httpContext)); + Assert.Equal($"GET{KeyDelimiter}/Path", cacheKeyProvider.CreateStorageBaseKey(context)); } [Fact] - public void DefaultKeyProvider_CreateStorageVaryKey_ReturnsCachedVaryGuid_IfVaryRulesIsNullOrEmpty() + public void DefaultKeyProvider_CreateStorageVaryKey_Throws_IfVaryRulesIsNull() { - var httpContext = CreateDefaultContext(); - var keyProvider = CreateTestKeyProvider(); + var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); + var context = TestUtils.CreateTestContext(); - Assert.Equal($"{TestVaryRules.VaryKeyPrefix}", keyProvider.CreateStorageVaryKey(httpContext, null)); - Assert.Equal($"{TestVaryRules.VaryKeyPrefix}", keyProvider.CreateStorageVaryKey(httpContext, new VaryRules())); + Assert.Throws(() => cacheKeyProvider.CreateStorageVaryKey(context)); + } + + [Fact] + public void DefaultKeyProvider_CreateStorageVaryKey_ReturnsCachedVaryGuid_IfVaryRulesIsEmpty() + { + var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); + var context = TestUtils.CreateTestContext(); + context.CachedVaryRules = new CachedVaryRules() + { + VaryKeyPrefix = FastGuid.NewGuid().IdString + }; + + Assert.Equal($"{context.CachedVaryRules.VaryKeyPrefix}", cacheKeyProvider.CreateStorageVaryKey(context)); } [Fact] public void DefaultKeyProvider_CreateStorageVaryKey_IncludesListedHeadersOnly() { - var httpContext = CreateDefaultContext(); - httpContext.Request.Headers["HeaderA"] = "ValueA"; - httpContext.Request.Headers["HeaderB"] = "ValueB"; - var keyProvider = CreateTestKeyProvider(); + var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); + var context = TestUtils.CreateTestContext(); + context.HttpContext.Request.Headers["HeaderA"] = "ValueA"; + context.HttpContext.Request.Headers["HeaderB"] = "ValueB"; + context.CachedVaryRules = new CachedVaryRules() + { + Headers = new string[] { "HeaderA", "HeaderC" } + }; - Assert.Equal($"{TestVaryRules.VaryKeyPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=null", - keyProvider.CreateStorageVaryKey(httpContext, new VaryRules() - { - Headers = new string[] { "HeaderA", "HeaderC" } - })); + Assert.Equal($"{context.CachedVaryRules.VaryKeyPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=", + cacheKeyProvider.CreateStorageVaryKey(context)); } [Fact] public void DefaultKeyProvider_CreateStorageVaryKey_IncludesListedParamsOnly() { - var httpContext = CreateDefaultContext(); - httpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB"); - var keyProvider = CreateTestKeyProvider(); + var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); + var context = TestUtils.CreateTestContext(); + context.HttpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB"); + context.CachedVaryRules = new CachedVaryRules() + { + VaryKeyPrefix = FastGuid.NewGuid().IdString, + Params = new string[] { "ParamA", "ParamC" } + }; - Assert.Equal($"{TestVaryRules.VaryKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=null", - keyProvider.CreateStorageVaryKey(httpContext, new VaryRules() - { - Params = new string[] { "ParamA", "ParamC" } - })); + Assert.Equal($"{context.CachedVaryRules.VaryKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=", + cacheKeyProvider.CreateStorageVaryKey(context)); } [Fact] public void DefaultKeyProvider_CreateStorageVaryKey_IncludesParams_ParamNameCaseInsensitive_UseParamCasing() { - var httpContext = CreateDefaultContext(); - httpContext.Request.QueryString = new QueryString("?parama=ValueA¶mB=ValueB"); - var keyProvider = CreateTestKeyProvider(); + var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); + var context = TestUtils.CreateTestContext(); + context.HttpContext.Request.QueryString = new QueryString("?parama=ValueA¶mB=ValueB"); + context.CachedVaryRules = new CachedVaryRules() + { + VaryKeyPrefix = FastGuid.NewGuid().IdString, + Params = new string[] { "ParamA", "ParamC" } + }; - Assert.Equal($"{TestVaryRules.VaryKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=null", - keyProvider.CreateStorageVaryKey(httpContext, new VaryRules() - { - Params = new string[] { "ParamA", "ParamC" } - })); + Assert.Equal($"{context.CachedVaryRules.VaryKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=", + cacheKeyProvider.CreateStorageVaryKey(context)); } [Fact] public void DefaultKeyProvider_CreateStorageVaryKey_IncludesAllQueryParamsGivenAsterisk() { - var httpContext = CreateDefaultContext(); - httpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB"); - var keyProvider = CreateTestKeyProvider(); + var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); + var context = TestUtils.CreateTestContext(); + context.HttpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB"); + context.CachedVaryRules = new CachedVaryRules() + { + VaryKeyPrefix = FastGuid.NewGuid().IdString, + Params = new string[] { "*" } + }; // To support case insensitivity, all param keys are converted to upper case. // Explicit params uses the casing specified in the setting. - Assert.Equal($"{TestVaryRules.VaryKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}PARAMA=ValueA{KeyDelimiter}PARAMB=ValueB", - keyProvider.CreateStorageVaryKey(httpContext, new VaryRules() - { - Params = new string[] { "*" } - })); + Assert.Equal($"{context.CachedVaryRules.VaryKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}PARAMA=ValueA{KeyDelimiter}PARAMB=ValueB", + cacheKeyProvider.CreateStorageVaryKey(context)); } [Fact] public void DefaultKeyProvider_CreateStorageVaryKey_IncludesListedHeadersAndParams() { - var httpContext = CreateDefaultContext(); - httpContext.Request.Headers["HeaderA"] = "ValueA"; - httpContext.Request.Headers["HeaderB"] = "ValueB"; - httpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB"); - var keyProvider = CreateTestKeyProvider(); + var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); + var context = TestUtils.CreateTestContext(); + context.HttpContext.Request.Headers["HeaderA"] = "ValueA"; + context.HttpContext.Request.Headers["HeaderB"] = "ValueB"; + context.HttpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB"); + context.CachedVaryRules = new CachedVaryRules() + { + VaryKeyPrefix = FastGuid.NewGuid().IdString, + Headers = new string[] { "HeaderA", "HeaderC" }, + Params = new string[] { "ParamA", "ParamC" } + }; - Assert.Equal($"{TestVaryRules.VaryKeyPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=null{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=null", - keyProvider.CreateStorageVaryKey(httpContext, new VaryRules() - { - Headers = new string[] { "HeaderA", "HeaderC" }, - Params = new string[] { "ParamA", "ParamC" } - })); - } - - private static HttpContext CreateDefaultContext() - { - var context = new DefaultHttpContext(); - context.AddResponseCachingState(); - context.GetResponseCachingState().CachedVaryRules = TestVaryRules; - return context; - } - - private static IKeyProvider CreateTestKeyProvider() - { - return CreateTestKeyProvider(new ResponseCachingOptions()); - } - - private static IKeyProvider CreateTestKeyProvider(ResponseCachingOptions options) - { - return new KeyProvider(new DefaultObjectPoolProvider(), Options.Create(options)); + Assert.Equal($"{context.CachedVaryRules.VaryKeyPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC={KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=", + cacheKeyProvider.CreateStorageVaryKey(context)); } } } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs deleted file mode 100644 index c900f3161e..0000000000 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingContextTests.cs +++ /dev/null @@ -1,742 +0,0 @@ -// 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.Threading; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; -using Microsoft.AspNetCore.Http.Headers; -using Microsoft.AspNetCore.ResponseCaching.Internal; -using Microsoft.Extensions.Internal; -using Microsoft.Extensions.ObjectPool; -using Microsoft.Extensions.Options; -using Microsoft.Extensions.Primitives; -using Microsoft.Net.Http.Headers; -using Xunit; - -namespace Microsoft.AspNetCore.ResponseCaching.Tests -{ - public class ResponseCachingContextTests - { - [Fact] - public async Task TryServeFromCacheAsync_OnlyIfCached_Serves504() - { - var cache = new TestResponseCache(); - var httpContext = new DefaultHttpContext(); - var context = CreateTestContext(httpContext, responseCache: cache, keyProvider: new TestKeyProvider()); - httpContext.Request.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() - { - OnlyIfCached = true - }; - - Assert.True(await context.TryServeFromCacheAsync()); - Assert.Equal(StatusCodes.Status504GatewayTimeout, httpContext.Response.StatusCode); - } - - [Fact] - public async Task TryServeFromCacheAsync_CachedResponseNotFound_Fails() - { - var cache = new TestResponseCache(); - var httpContext = new DefaultHttpContext(); - var context = CreateTestContext(httpContext, responseCache: cache, keyProvider: new TestKeyProvider(new[] { "BaseKey", "BaseKey2" })); - - Assert.False(await context.TryServeFromCacheAsync()); - Assert.Equal(2, cache.GetCount); - } - - [Fact] - public async Task TryServeFromCacheAsync_CachedResponseFound_Succeeds() - { - var cache = new TestResponseCache(); - var httpContext = new DefaultHttpContext(); - var context = CreateTestContext(httpContext, responseCache: cache, keyProvider: new TestKeyProvider(new[] { "BaseKey", "BaseKey2" })); - - cache.Set( - "BaseKey2", - new CachedResponse() - { - Body = new byte[0] - }, - TimeSpan.Zero); - - Assert.True(await context.TryServeFromCacheAsync()); - Assert.Equal(2, cache.GetCount); - } - - [Fact] - public async Task TryServeFromCacheAsync_VaryRuleFound_CachedResponseNotFound_Fails() - { - var cache = new TestResponseCache(); - var httpContext = new DefaultHttpContext(); - var context = CreateTestContext(httpContext, responseCache: cache, keyProvider: new TestKeyProvider(new[] { "BaseKey", "BaseKey2" })); - - cache.Set( - "BaseKey2", - new CachedVaryRules(), - TimeSpan.Zero); - - Assert.False(await context.TryServeFromCacheAsync()); - Assert.Equal(2, cache.GetCount); - } - - [Fact] - public async Task TryServeFromCacheAsync_VaryRuleFound_CachedResponseFound_Succeeds() - { - var cache = new TestResponseCache(); - var httpContext = new DefaultHttpContext(); - var context = CreateTestContext(httpContext, responseCache: cache, keyProvider: new TestKeyProvider(new[] { "BaseKey", "BaseKey2" }, new[] { "VaryKey", "VaryKey2" })); - - cache.Set( - "BaseKey2", - new CachedVaryRules(), - TimeSpan.Zero); - cache.Set( - "BaseKey2VaryKey2", - new CachedResponse() - { - Body = new byte[0] - }, - TimeSpan.Zero); - - Assert.True(await context.TryServeFromCacheAsync()); - Assert.Equal(6, cache.GetCount); - } - - [Fact] - public void ConditionalRequestSatisfied_NotConditionalRequest_Fails() - { - var context = CreateTestContext(new DefaultHttpContext()); - var cachedHeaders = new ResponseHeaders(new HeaderDictionary()); - - Assert.False(context.ConditionalRequestSatisfied(cachedHeaders)); - } - - [Fact] - public void ConditionalRequestSatisfied_IfUnmodifiedSince_FallsbackToDateHeader() - { - var utcNow = DateTimeOffset.UtcNow; - var cachedHeaders = new ResponseHeaders(new HeaderDictionary()); - var httpContext = new DefaultHttpContext(); - var context = CreateTestContext(httpContext); - - httpContext.Request.GetTypedHeaders().IfUnmodifiedSince = utcNow; - - // Verify modifications in the past succeeds - cachedHeaders.Date = utcNow - TimeSpan.FromSeconds(10); - Assert.True(context.ConditionalRequestSatisfied(cachedHeaders)); - - // Verify modifications at present succeeds - cachedHeaders.Date = utcNow; - Assert.True(context.ConditionalRequestSatisfied(cachedHeaders)); - - // Verify modifications in the future fails - cachedHeaders.Date = utcNow + TimeSpan.FromSeconds(10); - Assert.False(context.ConditionalRequestSatisfied(cachedHeaders)); - } - - [Fact] - public void ConditionalRequestSatisfied_IfUnmodifiedSince_LastModifiedOverridesDateHeader() - { - var utcNow = DateTimeOffset.UtcNow; - var cachedHeaders = new ResponseHeaders(new HeaderDictionary()); - var httpContext = new DefaultHttpContext(); - var context = CreateTestContext(httpContext); - - httpContext.Request.GetTypedHeaders().IfUnmodifiedSince = utcNow; - - // Verify modifications in the past succeeds - cachedHeaders.Date = utcNow + TimeSpan.FromSeconds(10); - cachedHeaders.LastModified = utcNow - TimeSpan.FromSeconds(10); - Assert.True(context.ConditionalRequestSatisfied(cachedHeaders)); - - // Verify modifications at present - cachedHeaders.Date = utcNow + TimeSpan.FromSeconds(10); - cachedHeaders.LastModified = utcNow; - Assert.True(context.ConditionalRequestSatisfied(cachedHeaders)); - - // Verify modifications in the future fails - cachedHeaders.Date = utcNow - TimeSpan.FromSeconds(10); - cachedHeaders.LastModified = utcNow + TimeSpan.FromSeconds(10); - Assert.False(context.ConditionalRequestSatisfied(cachedHeaders)); - } - - [Fact] - public void ConditionalRequestSatisfied_IfNoneMatch_Overrides_IfUnmodifiedSince_ToPass() - { - var utcNow = DateTimeOffset.UtcNow; - var cachedHeaders = new ResponseHeaders(new HeaderDictionary()); - var httpContext = new DefaultHttpContext(); - var requestHeaders = httpContext.Request.GetTypedHeaders(); - var context = CreateTestContext(httpContext); - - // This would fail the IfUnmodifiedSince checks - requestHeaders.IfUnmodifiedSince = utcNow; - cachedHeaders.LastModified = utcNow + TimeSpan.FromSeconds(10); - - requestHeaders.IfNoneMatch = new List(new[] { EntityTagHeaderValue.Any }); - Assert.True(context.ConditionalRequestSatisfied(cachedHeaders)); - } - - [Fact] - public void ConditionalRequestSatisfied_IfNoneMatch_Overrides_IfUnmodifiedSince_ToFail() - { - var utcNow = DateTimeOffset.UtcNow; - var cachedHeaders = new ResponseHeaders(new HeaderDictionary()); - var httpContext = new DefaultHttpContext(); - var requestHeaders = httpContext.Request.GetTypedHeaders(); - var context = CreateTestContext(httpContext); - - // This would pass the IfUnmodifiedSince checks - requestHeaders.IfUnmodifiedSince = utcNow; - cachedHeaders.LastModified = utcNow - TimeSpan.FromSeconds(10); - - requestHeaders.IfNoneMatch = new List(new[] { new EntityTagHeaderValue("\"E1\"") }); - Assert.False(context.ConditionalRequestSatisfied(cachedHeaders)); - } - - [Fact] - public void ConditionalRequestSatisfied_IfNoneMatch_AnyWithoutETagInResponse_Passes() - { - var cachedHeaders = new ResponseHeaders(new HeaderDictionary()); - var httpContext = new DefaultHttpContext(); - var context = CreateTestContext(httpContext); - - httpContext.Request.GetTypedHeaders().IfNoneMatch = new List(new[] { new EntityTagHeaderValue("\"E1\"") }); - - Assert.False(context.ConditionalRequestSatisfied(cachedHeaders)); - } - - [Fact] - public void ConditionalRequestSatisfied_IfNoneMatch_ExplicitWithMatch_Passes() - { - var cachedHeaders = new ResponseHeaders(new HeaderDictionary()) - { - ETag = new EntityTagHeaderValue("\"E1\"") - }; - var httpContext = new DefaultHttpContext(); - var context = CreateTestContext(httpContext); - - httpContext.Request.GetTypedHeaders().IfNoneMatch = new List(new[] { new EntityTagHeaderValue("\"E1\"") }); - - Assert.True(context.ConditionalRequestSatisfied(cachedHeaders)); - } - - [Fact] - public void ConditionalRequestSatisfied_IfNoneMatch_ExplicitWithoutMatch_Fails() - { - var cachedHeaders = new ResponseHeaders(new HeaderDictionary()) - { - ETag = new EntityTagHeaderValue("\"E2\"") - }; - var httpContext = new DefaultHttpContext(); - var context = CreateTestContext(httpContext); - - httpContext.Request.GetTypedHeaders().IfNoneMatch = new List(new[] { new EntityTagHeaderValue("\"E1\"") }); - - Assert.False(context.ConditionalRequestSatisfied(cachedHeaders)); - } - - [Fact] - public void FinalizeCachingHeaders_DoNotUpdateShouldCacheResponse_IfResponseIsNotCacheable() - { - var httpContext = new DefaultHttpContext(); - var context = CreateTestContext(httpContext, cacheabilityValidator: new CacheabilityValidator()); - var state = httpContext.GetResponseCachingState(); - - Assert.False(state.ShouldCacheResponse); - - context.ShimResponseStream(); - context.FinalizeCachingHeaders(); - - Assert.False(state.ShouldCacheResponse); - } - - [Fact] - public void FinalizeCachingHeaders_UpdateShouldCacheResponse_IfResponseIsCacheable() - { - var httpContext = new DefaultHttpContext(); - httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() - { - Public = true - }; - var context = CreateTestContext(httpContext, cacheabilityValidator: new CacheabilityValidator()); - var state = httpContext.GetResponseCachingState(); - - Assert.False(state.ShouldCacheResponse); - - context.FinalizeCachingHeaders(); - - Assert.True(state.ShouldCacheResponse); - } - - [Fact] - public void FinalizeCachingHeaders_DefaultResponseValidity_Is10Seconds() - { - var httpContext = new DefaultHttpContext(); - var context = CreateTestContext(httpContext); - - context.FinalizeCachingHeaders(); - - Assert.Equal(TimeSpan.FromSeconds(10), httpContext.GetResponseCachingState().CachedResponseValidFor); - } - - [Fact] - public void FinalizeCachingHeaders_ResponseValidity_UseExpiryIfAvailable() - { - var httpContext = new DefaultHttpContext(); - var context = CreateTestContext(httpContext); - - var state = httpContext.GetResponseCachingState(); - var utcNow = DateTimeOffset.MinValue; - state.ResponseTime = utcNow; - state.ResponseHeaders.Expires = utcNow + TimeSpan.FromSeconds(11); - - context.FinalizeCachingHeaders(); - - Assert.Equal(TimeSpan.FromSeconds(11), state.CachedResponseValidFor); - } - - [Fact] - public void FinalizeCachingHeaders_ResponseValidity_UseMaxAgeIfAvailable() - { - var httpContext = new DefaultHttpContext(); - httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() - { - MaxAge = TimeSpan.FromSeconds(12) - }; - var context = CreateTestContext(httpContext); - - var state = httpContext.GetResponseCachingState(); - state.ResponseTime = DateTimeOffset.UtcNow; - state.ResponseHeaders.Expires = state.ResponseTime + TimeSpan.FromSeconds(11); - - context.FinalizeCachingHeaders(); - - Assert.Equal(TimeSpan.FromSeconds(12), state.CachedResponseValidFor); - } - - [Fact] - public void FinalizeCachingHeaders_ResponseValidity_UseSharedMaxAgeIfAvailable() - { - var httpContext = new DefaultHttpContext(); - httpContext.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() - { - MaxAge = TimeSpan.FromSeconds(12), - SharedMaxAge = TimeSpan.FromSeconds(13) - }; - var context = CreateTestContext(httpContext); - - var state = httpContext.GetResponseCachingState(); - state.ResponseTime = DateTimeOffset.UtcNow; - state.ResponseHeaders.Expires = state.ResponseTime + TimeSpan.FromSeconds(11); - - context.FinalizeCachingHeaders(); - - Assert.Equal(TimeSpan.FromSeconds(13), state.CachedResponseValidFor); - } - - [Fact] - public void FinalizeCachingHeaders_UpdateCachedVaryRules_IfNotEquivalentToPrevious() - { - var httpContext = new DefaultHttpContext(); - var cache = new TestResponseCache(); - var context = CreateTestContext(httpContext, cache); - var state = httpContext.GetResponseCachingState(); - - httpContext.Response.Headers[HeaderNames.Vary] = new StringValues(new[] { "headerA", "HEADERB", "HEADERc" }); - httpContext.AddResponseCachingFeature(); - httpContext.GetResponseCachingFeature().VaryParams = new StringValues(new[] { "paramB", "PARAMAA" }); - var cachedVaryRules = new CachedVaryRules() - { - VaryRules = new VaryRules() - { - Headers = new StringValues(new[] { "HeaderA", "HeaderB" }), - Params = new StringValues(new[] { "ParamA", "ParamB" }) - } - }; - state.CachedVaryRules = cachedVaryRules; - - context.FinalizeCachingHeaders(); - - Assert.Equal(1, cache.SetCount); - Assert.NotSame(cachedVaryRules, state.CachedVaryRules); - } - - [Fact] - public void FinalizeCachingHeaders_DoNotUpdateCachedVaryRules_IfEquivalentToPrevious() - { - var httpContext = new DefaultHttpContext(); - var cache = new TestResponseCache(); - var context = CreateTestContext(httpContext, cache); - var state = httpContext.GetResponseCachingState(); - - httpContext.Response.Headers[HeaderNames.Vary] = new StringValues(new[] { "headerA", "HEADERB" }); - httpContext.AddResponseCachingFeature(); - httpContext.GetResponseCachingFeature().VaryParams = new StringValues(new[] { "paramB", "PARAMA" }); - var cachedVaryRules = new CachedVaryRules() - { - VaryKeyPrefix = FastGuid.NewGuid().IdString, - VaryRules = new VaryRules() - { - Headers = new StringValues(new[] { "HEADERA", "HEADERB" }), - Params = new StringValues(new[] { "PARAMA", "PARAMB" }) - } - }; - state.CachedVaryRules = cachedVaryRules; - - context.FinalizeCachingHeaders(); - - Assert.Equal(0, cache.SetCount); - Assert.Same(cachedVaryRules, state.CachedVaryRules); - } - - [Fact] - public void FinalizeCachingHeaders_DoNotAddDate_IfSpecified() - { - var httpContext = new DefaultHttpContext(); - var context = CreateTestContext(httpContext); - var state = httpContext.GetResponseCachingState(); - var utcNow = DateTimeOffset.MinValue; - state.ResponseTime = utcNow; - - Assert.Null(state.ResponseHeaders.Date); - - context.FinalizeCachingHeaders(); - - Assert.Equal(utcNow, state.ResponseHeaders.Date); - } - - [Fact] - public void FinalizeCachingHeaders_AddsDate_IfNoneSpecified() - { - var httpContext = new DefaultHttpContext(); - var context = CreateTestContext(httpContext); - var state = httpContext.GetResponseCachingState(); - var utcNow = DateTimeOffset.MinValue; - state.ResponseHeaders.Date = utcNow; - state.ResponseTime = utcNow + TimeSpan.FromSeconds(10); - - Assert.Equal(utcNow, state.ResponseHeaders.Date); - - context.FinalizeCachingHeaders(); - - Assert.Equal(utcNow, state.ResponseHeaders.Date); - } - - [Fact] - public void FinalizeCachingHeaders_StoresCachedResponse_InState() - { - var httpContext = new DefaultHttpContext(); - var context = CreateTestContext(httpContext); - var state = httpContext.GetResponseCachingState(); - - Assert.Null(state.CachedResponse); - - context.FinalizeCachingHeaders(); - - Assert.NotNull(state.CachedResponse); - } - - [Fact] - public async Task FinalizeCachingBody_StoreResponseBodySeparately_IfLargerThanLimit() - { - var httpContext = new DefaultHttpContext(); - var cache = new TestResponseCache(); - var context = CreateTestContext(httpContext, cache); - - context.ShimResponseStream(); - await httpContext.Response.WriteAsync(new string('0', 70 * 1024)); - - var state = httpContext.GetResponseCachingState(); - state.ShouldCacheResponse = true; - state.CachedResponse = new CachedResponse() - { - BodyKeyPrefix = FastGuid.NewGuid().IdString - }; - state.StorageBaseKey = "BaseKey"; - state.CachedResponseValidFor = TimeSpan.FromSeconds(10); - - context.FinalizeCachingBody(); - - Assert.Equal(2, cache.SetCount); - } - - [Fact] - public async Task FinalizeCachingBody_StoreResponseBodyInCachedResponse_IfSmallerThanLimit() - { - var httpContext = new DefaultHttpContext(); - var cache = new TestResponseCache(); - var context = CreateTestContext(httpContext, cache); - - context.ShimResponseStream(); - await httpContext.Response.WriteAsync(new string('0', 70 * 1024 - 1)); - - var state = httpContext.GetResponseCachingState(); - state.ShouldCacheResponse = true; - state.CachedResponse = new CachedResponse() - { - BodyKeyPrefix = FastGuid.NewGuid().IdString - }; - state.StorageBaseKey = "BaseKey"; - state.CachedResponseValidFor = TimeSpan.FromSeconds(10); - - context.FinalizeCachingBody(); - - Assert.Equal(1, cache.SetCount); - } - - [Fact] - public async Task FinalizeCachingBody_StoreResponseBodySeparately_LimitIsConfigurable() - { - var httpContext = new DefaultHttpContext(); - var cache = new TestResponseCache(); - var context = CreateTestContext(httpContext, cache, new ResponseCachingOptions() - { - MinimumSplitBodySize = 2048 - }); - - context.ShimResponseStream(); - await httpContext.Response.WriteAsync(new string('0', 1024)); - - var state = httpContext.GetResponseCachingState(); - state.ShouldCacheResponse = true; - state.CachedResponse = new CachedResponse() - { - BodyKeyPrefix = FastGuid.NewGuid().IdString - }; - state.StorageBaseKey = "BaseKey"; - state.CachedResponseValidFor = TimeSpan.FromSeconds(10); - - context.FinalizeCachingBody(); - - Assert.Equal(1, cache.SetCount); - } - - [Fact] - public async Task FinalizeCachingBody_Cache_IfContentLengthMatches() - { - var httpContext = new DefaultHttpContext(); - var cache = new TestResponseCache(); - var context = CreateTestContext(httpContext, cache); - - context.ShimResponseStream(); - httpContext.Response.ContentLength = 10; - await httpContext.Response.WriteAsync(new string('0', 10)); - - var state = httpContext.GetResponseCachingState(); - state.ShouldCacheResponse = true; - state.CachedResponse = new CachedResponse() - { - BodyKeyPrefix = FastGuid.NewGuid().IdString - }; - state.StorageBaseKey = "BaseKey"; - state.CachedResponseValidFor = TimeSpan.FromSeconds(10); - - context.FinalizeCachingBody(); - - Assert.Equal(1, cache.SetCount); - } - - [Fact] - public async Task FinalizeCachingBody_DoNotCache_IfContentLengthMismatches() - { - var httpContext = new DefaultHttpContext(); - var cache = new TestResponseCache(); - var context = CreateTestContext(httpContext, cache); - - context.ShimResponseStream(); - httpContext.Response.ContentLength = 9; - await httpContext.Response.WriteAsync(new string('0', 10)); - - var state = httpContext.GetResponseCachingState(); - state.ShouldCacheResponse = true; - state.CachedResponse = new CachedResponse() - { - BodyKeyPrefix = FastGuid.NewGuid().IdString - }; - state.StorageBaseKey = "BaseKey"; - state.CachedResponseValidFor = TimeSpan.FromSeconds(10); - - context.FinalizeCachingBody(); - - Assert.Equal(0, cache.SetCount); - } - - [Fact] - public async Task FinalizeCachingBody_Cache_IfContentLengthAbsent() - { - var httpContext = new DefaultHttpContext(); - var cache = new TestResponseCache(); - var context = CreateTestContext(httpContext, cache); - - context.ShimResponseStream(); - await httpContext.Response.WriteAsync(new string('0', 10)); - - var state = httpContext.GetResponseCachingState(); - state.ShouldCacheResponse = true; - state.CachedResponse = new CachedResponse() - { - BodyKeyPrefix = FastGuid.NewGuid().IdString - }; - state.StorageBaseKey = "BaseKey"; - state.CachedResponseValidFor = TimeSpan.FromSeconds(10); - - context.FinalizeCachingBody(); - - Assert.Equal(1, cache.SetCount); - } - - [Fact] - public void NormalizeStringValues_NormalizesCasingToUpper() - { - var uppercaseStrings = new StringValues(new[] { "STRINGA", "STRINGB" }); - var lowercaseStrings = new StringValues(new[] { "stringA", "stringB" }); - - var normalizedStrings = ResponseCachingContext.GetNormalizedStringValues(lowercaseStrings); - - Assert.Equal(uppercaseStrings, normalizedStrings); - } - - [Fact] - public void NormalizeStringValues_NormalizesOrder() - { - var orderedStrings = new StringValues(new[] { "STRINGA", "STRINGB" }); - var reverseOrderStrings = new StringValues(new[] { "STRINGB", "STRINGA" }); - - var normalizedStrings = ResponseCachingContext.GetNormalizedStringValues(reverseOrderStrings); - - Assert.Equal(orderedStrings, normalizedStrings); - } - - private static ResponseCachingContext CreateTestContext( - HttpContext httpContext, - IResponseCache responseCache = null, - ResponseCachingOptions options = null, - IKeyProvider keyProvider = null, - ICacheabilityValidator cacheabilityValidator = null) - { - if (responseCache == null) - { - responseCache = new TestResponseCache(); - } - if (options == null) - { - options = new ResponseCachingOptions(); - } - if (keyProvider == null) - { - keyProvider = new KeyProvider(new DefaultObjectPoolProvider(), Options.Create(options)); - } - if (cacheabilityValidator == null) - { - cacheabilityValidator = new TestCacheabilityValidator(); - } - - httpContext.AddResponseCachingState(); - - return new ResponseCachingContext( - httpContext, - responseCache, - options, - cacheabilityValidator, - keyProvider); - } - - private class TestCacheabilityValidator : ICacheabilityValidator - { - public bool CachedEntryIsFresh(HttpContext httpContext, ResponseHeaders cachedResponseHeaders) => true; - - public bool RequestIsCacheable(HttpContext httpContext) => true; - - public bool ResponseIsCacheable(HttpContext httpContext) => true; - } - - private class TestKeyProvider : IKeyProvider - { - private readonly StringValues _baseKey; - private readonly StringValues _varyKey; - - public TestKeyProvider(StringValues? lookupBaseKey = null, StringValues? lookupVaryKey = null) - { - if (lookupBaseKey.HasValue) - { - _baseKey = lookupBaseKey.Value; - } - if (lookupVaryKey.HasValue) - { - _varyKey = lookupVaryKey.Value; - } - } - - public IEnumerable CreateLookupBaseKey(HttpContext httpContext) => _baseKey; - - - public IEnumerable CreateLookupVaryKey(HttpContext httpContext, VaryRules varyRules) - { - foreach (var baseKey in _baseKey) - { - foreach (var varyKey in _varyKey) - { - yield return baseKey + varyKey; - } - } - } - - public string CreateBodyKey(HttpContext httpContext) - { - throw new NotImplementedException(); - } - - public string CreateStorageBaseKey(HttpContext httpContext) - { - throw new NotImplementedException(); - } - - public string CreateStorageVaryKey(HttpContext httpContext, VaryRules varyRules) - { - throw new NotImplementedException(); - } - } - - private class TestResponseCache : IResponseCache - { - private readonly IDictionary _storage = new Dictionary(); - public int GetCount { get; private set; } - public int SetCount { get; private set; } - - public object Get(string key) - { - GetCount++; - try - { - return _storage[key]; - } - catch - { - return null; - } - } - - public void Remove(string key) - { - } - - public void Set(string key, object entry, TimeSpan validFor) - { - SetCount++; - _storage[key] = entry; - } - } - - private class TestHttpSendFileFeature : IHttpSendFileFeature - { - public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellation) - { - return TaskCache.CompletedTask; - } - } - } -} diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs new file mode 100644 index 0000000000..b480724d4b --- /dev/null +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs @@ -0,0 +1,578 @@ +// 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.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Headers; +using Microsoft.AspNetCore.ResponseCaching.Internal; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; +using Xunit; + +namespace Microsoft.AspNetCore.ResponseCaching.Tests +{ + public class ResponseCachingMiddlewareTests + { + [Fact] + public async Task TryServeFromCacheAsync_OnlyIfCached_Serves504() + { + var cache = new TestResponseCache(); + var middleware = TestUtils.CreateTestMiddleware(responseCache: cache, cacheKeyProvider: new TestKeyProvider()); + var context = TestUtils.CreateTestContext(); + context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() + { + OnlyIfCached = true + }; + + Assert.True(await middleware.TryServeFromCacheAsync(context)); + Assert.Equal(StatusCodes.Status504GatewayTimeout, context.HttpContext.Response.StatusCode); + } + + [Fact] + public async Task TryServeFromCacheAsync_CachedResponseNotFound_Fails() + { + var cache = new TestResponseCache(); + var middleware = TestUtils.CreateTestMiddleware(responseCache: cache, cacheKeyProvider: new TestKeyProvider(new[] { "BaseKey", "BaseKey2" })); + var context = TestUtils.CreateTestContext(); + + Assert.False(await middleware.TryServeFromCacheAsync(context)); + Assert.Equal(2, cache.GetCount); + } + + [Fact] + public async Task TryServeFromCacheAsync_CachedResponseFound_Succeeds() + { + var cache = new TestResponseCache(); + var middleware = TestUtils.CreateTestMiddleware(responseCache: cache, cacheKeyProvider: new TestKeyProvider(new[] { "BaseKey", "BaseKey2" })); + var context = TestUtils.CreateTestContext(); + + cache.Set( + "BaseKey2", + new CachedResponse() + { + Body = new byte[0] + }, + TimeSpan.Zero); + + Assert.True(await middleware.TryServeFromCacheAsync(context)); + Assert.Equal(2, cache.GetCount); + } + + [Fact] + public async Task TryServeFromCacheAsync_VaryRuleFound_CachedResponseNotFound_Fails() + { + var cache = new TestResponseCache(); + var middleware = TestUtils.CreateTestMiddleware(responseCache: cache, cacheKeyProvider: new TestKeyProvider(new[] { "BaseKey", "BaseKey2" })); + var context = TestUtils.CreateTestContext(); + + cache.Set( + "BaseKey2", + new CachedVaryRules(), + TimeSpan.Zero); + + Assert.False(await middleware.TryServeFromCacheAsync(context)); + Assert.Equal(2, cache.GetCount); + } + + [Fact] + public async Task TryServeFromCacheAsync_VaryRuleFound_CachedResponseFound_Succeeds() + { + var cache = new TestResponseCache(); + var middleware = TestUtils.CreateTestMiddleware(responseCache: cache, cacheKeyProvider: new TestKeyProvider(new[] { "BaseKey", "BaseKey2" }, new[] { "VaryKey", "VaryKey2" })); + var context = TestUtils.CreateTestContext(); + + cache.Set( + "BaseKey2", + new CachedVaryRules(), + TimeSpan.Zero); + cache.Set( + "BaseKey2VaryKey2", + new CachedResponse() + { + Body = new byte[0] + }, + TimeSpan.Zero); + + Assert.True(await middleware.TryServeFromCacheAsync(context)); + Assert.Equal(6, cache.GetCount); + } + + [Fact] + public void ConditionalRequestSatisfied_NotConditionalRequest_Fails() + { + var context = TestUtils.CreateTestContext(); + context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); + + Assert.False(ResponseCachingMiddleware.ConditionalRequestSatisfied(context)); + } + + [Fact] + public void ConditionalRequestSatisfied_IfUnmodifiedSince_FallsbackToDateHeader() + { + var utcNow = DateTimeOffset.UtcNow; + var context = TestUtils.CreateTestContext(); + context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); + + context.TypedRequestHeaders.IfUnmodifiedSince = utcNow; + + // Verify modifications in the past succeeds + context.CachedResponseHeaders.Date = utcNow - TimeSpan.FromSeconds(10); + Assert.True(ResponseCachingMiddleware.ConditionalRequestSatisfied(context)); + + // Verify modifications at present succeeds + context.CachedResponseHeaders.Date = utcNow; + Assert.True(ResponseCachingMiddleware.ConditionalRequestSatisfied(context)); + + // Verify modifications in the future fails + context.CachedResponseHeaders.Date = utcNow + TimeSpan.FromSeconds(10); + Assert.False(ResponseCachingMiddleware.ConditionalRequestSatisfied(context)); + } + + [Fact] + public void ConditionalRequestSatisfied_IfUnmodifiedSince_LastModifiedOverridesDateHeader() + { + var utcNow = DateTimeOffset.UtcNow; + var context = TestUtils.CreateTestContext(); + context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); + + context.TypedRequestHeaders.IfUnmodifiedSince = utcNow; + + // Verify modifications in the past succeeds + context.CachedResponseHeaders.Date = utcNow + TimeSpan.FromSeconds(10); + context.CachedResponseHeaders.LastModified = utcNow - TimeSpan.FromSeconds(10); + Assert.True(ResponseCachingMiddleware.ConditionalRequestSatisfied(context)); + + // Verify modifications at present + context.CachedResponseHeaders.Date = utcNow + TimeSpan.FromSeconds(10); + context.CachedResponseHeaders.LastModified = utcNow; + Assert.True(ResponseCachingMiddleware.ConditionalRequestSatisfied(context)); + + // Verify modifications in the future fails + context.CachedResponseHeaders.Date = utcNow - TimeSpan.FromSeconds(10); + context.CachedResponseHeaders.LastModified = utcNow + TimeSpan.FromSeconds(10); + Assert.False(ResponseCachingMiddleware.ConditionalRequestSatisfied(context)); + } + + [Fact] + public void ConditionalRequestSatisfied_IfNoneMatch_Overrides_IfUnmodifiedSince_ToPass() + { + var utcNow = DateTimeOffset.UtcNow; + var context = TestUtils.CreateTestContext(); + context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); + + // This would fail the IfUnmodifiedSince checks + context.TypedRequestHeaders.IfUnmodifiedSince = utcNow; + context.CachedResponseHeaders.LastModified = utcNow + TimeSpan.FromSeconds(10); + + context.TypedRequestHeaders.IfNoneMatch = new List(new[] { EntityTagHeaderValue.Any }); + Assert.True(ResponseCachingMiddleware.ConditionalRequestSatisfied(context)); + } + + [Fact] + public void ConditionalRequestSatisfied_IfNoneMatch_Overrides_IfUnmodifiedSince_ToFail() + { + var utcNow = DateTimeOffset.UtcNow; + var context = TestUtils.CreateTestContext(); + context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); + + // This would pass the IfUnmodifiedSince checks + context.TypedRequestHeaders.IfUnmodifiedSince = utcNow; + context.CachedResponseHeaders.LastModified = utcNow - TimeSpan.FromSeconds(10); + + context.TypedRequestHeaders.IfNoneMatch = new List(new[] { new EntityTagHeaderValue("\"E1\"") }); + Assert.False(ResponseCachingMiddleware.ConditionalRequestSatisfied(context)); + } + + [Fact] + public void ConditionalRequestSatisfied_IfNoneMatch_AnyWithoutETagInResponse_Passes() + { + var context = TestUtils.CreateTestContext(); + context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); + + context.TypedRequestHeaders.IfNoneMatch = new List(new[] { new EntityTagHeaderValue("\"E1\"") }); + + Assert.False(ResponseCachingMiddleware.ConditionalRequestSatisfied(context)); + } + + [Fact] + public void ConditionalRequestSatisfied_IfNoneMatch_ExplicitWithMatch_Passes() + { + var context = TestUtils.CreateTestContext(); + context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) + { + ETag = new EntityTagHeaderValue("\"E1\"") + }; + + context.TypedRequestHeaders.IfNoneMatch = new List(new[] { new EntityTagHeaderValue("\"E1\"") }); + + Assert.True(ResponseCachingMiddleware.ConditionalRequestSatisfied(context)); + } + + [Fact] + public void ConditionalRequestSatisfied_IfNoneMatch_ExplicitWithoutMatch_Fails() + { + var context = TestUtils.CreateTestContext(); + context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) + { + ETag = new EntityTagHeaderValue("\"E2\"") + }; + + context.TypedRequestHeaders.IfNoneMatch = new List(new[] { new EntityTagHeaderValue("\"E1\"") }); + + Assert.False(ResponseCachingMiddleware.ConditionalRequestSatisfied(context)); + } + + [Fact] + public void FinalizeCachingHeaders_DoNotUpdateShouldCacheResponse_IfResponseIsNotCacheable() + { + var middleware = TestUtils.CreateTestMiddleware(cacheabilityValidator: new CacheabilityValidator()); + var context = TestUtils.CreateTestContext(); + + Assert.False(context.ShouldCacheResponse); + + middleware.ShimResponseStream(context); + middleware.FinalizeCachingHeaders(context); + + Assert.False(context.ShouldCacheResponse); + } + + [Fact] + public void FinalizeCachingHeaders_UpdateShouldCacheResponse_IfResponseIsCacheable() + { + var middleware = TestUtils.CreateTestMiddleware(cacheabilityValidator: new CacheabilityValidator()); + var context = TestUtils.CreateTestContext(); + context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + { + Public = true + }; + + Assert.False(context.ShouldCacheResponse); + + middleware.FinalizeCachingHeaders(context); + + Assert.True(context.ShouldCacheResponse); + } + + [Fact] + public void FinalizeCachingHeaders_DefaultResponseValidity_Is10Seconds() + { + var middleware = TestUtils.CreateTestMiddleware(); + var context = TestUtils.CreateTestContext(); + + middleware.FinalizeCachingHeaders(context); + + Assert.Equal(TimeSpan.FromSeconds(10), context.CachedResponseValidFor); + } + + [Fact] + public void FinalizeCachingHeaders_ResponseValidity_UseExpiryIfAvailable() + { + var utcNow = DateTimeOffset.MinValue; + var middleware = TestUtils.CreateTestMiddleware(); + var context = TestUtils.CreateTestContext(); + + context.ResponseTime = utcNow; + context.TypedResponseHeaders.Expires = utcNow + TimeSpan.FromSeconds(11); + + middleware.FinalizeCachingHeaders(context); + + Assert.Equal(TimeSpan.FromSeconds(11), context.CachedResponseValidFor); + } + + [Fact] + public void FinalizeCachingHeaders_ResponseValidity_UseMaxAgeIfAvailable() + { + var middleware = TestUtils.CreateTestMiddleware(); + var context = TestUtils.CreateTestContext(); + context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + { + MaxAge = TimeSpan.FromSeconds(12) + }; + + context.ResponseTime = DateTimeOffset.UtcNow; + context.TypedResponseHeaders.Expires = context.ResponseTime + TimeSpan.FromSeconds(11); + + middleware.FinalizeCachingHeaders(context); + + Assert.Equal(TimeSpan.FromSeconds(12), context.CachedResponseValidFor); + } + + [Fact] + public void FinalizeCachingHeaders_ResponseValidity_UseSharedMaxAgeIfAvailable() + { + var middleware = TestUtils.CreateTestMiddleware(); + var context = TestUtils.CreateTestContext(); + context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + { + MaxAge = TimeSpan.FromSeconds(12), + SharedMaxAge = TimeSpan.FromSeconds(13) + }; + + context.ResponseTime = DateTimeOffset.UtcNow; + context.TypedResponseHeaders.Expires = context.ResponseTime + TimeSpan.FromSeconds(11); + + middleware.FinalizeCachingHeaders(context); + + Assert.Equal(TimeSpan.FromSeconds(13), context.CachedResponseValidFor); + } + + [Fact] + public void FinalizeCachingHeaders_UpdateCachedVaryRules_IfNotEquivalentToPrevious() + { + var cache = new TestResponseCache(); + var middleware = TestUtils.CreateTestMiddleware(cache); + var context = TestUtils.CreateTestContext(); + + context.HttpContext.Response.Headers[HeaderNames.Vary] = new StringValues(new[] { "headerA", "HEADERB", "HEADERc" }); + context.HttpContext.AddResponseCachingFeature(); + context.HttpContext.GetResponseCachingFeature().VaryParams = new StringValues(new[] { "paramB", "PARAMAA" }); + var cachedVaryRules = new CachedVaryRules() + { + Headers = new StringValues(new[] { "HeaderA", "HeaderB" }), + Params = new StringValues(new[] { "ParamA", "ParamB" }) + }; + context.CachedVaryRules = cachedVaryRules; + + middleware.FinalizeCachingHeaders(context); + + Assert.Equal(1, cache.SetCount); + Assert.NotSame(cachedVaryRules, context.CachedVaryRules); + } + + [Fact] + public void FinalizeCachingHeaders_DoNotUpdateCachedVaryRules_IfEquivalentToPrevious() + { + var cache = new TestResponseCache(); + var middleware = TestUtils.CreateTestMiddleware(cache); + var context = TestUtils.CreateTestContext(); + + context.HttpContext.Response.Headers[HeaderNames.Vary] = new StringValues(new[] { "headerA", "HEADERB" }); + context.HttpContext.AddResponseCachingFeature(); + context.HttpContext.GetResponseCachingFeature().VaryParams = new StringValues(new[] { "paramB", "PARAMA" }); + var cachedVaryRules = new CachedVaryRules() + { + VaryKeyPrefix = FastGuid.NewGuid().IdString, + Headers = new StringValues(new[] { "HEADERA", "HEADERB" }), + Params = new StringValues(new[] { "PARAMA", "PARAMB" }) + }; + context.CachedVaryRules = cachedVaryRules; + + middleware.FinalizeCachingHeaders(context); + + Assert.Equal(0, cache.SetCount); + Assert.Same(cachedVaryRules, context.CachedVaryRules); + } + + [Fact] + public void FinalizeCachingHeaders_DoNotAddDate_IfSpecified() + { + var utcNow = DateTimeOffset.MinValue; + var middleware = TestUtils.CreateTestMiddleware(); + var context = TestUtils.CreateTestContext(); + context.ResponseTime = utcNow; + + Assert.Null(context.TypedResponseHeaders.Date); + + middleware.FinalizeCachingHeaders(context); + + Assert.Equal(utcNow, context.TypedResponseHeaders.Date); + } + + [Fact] + public void FinalizeCachingHeaders_AddsDate_IfNoneSpecified() + { + var utcNow = DateTimeOffset.MinValue; + var middleware = TestUtils.CreateTestMiddleware(); + var context = TestUtils.CreateTestContext(); + context.TypedResponseHeaders.Date = utcNow; + context.ResponseTime = utcNow + TimeSpan.FromSeconds(10); + + Assert.Equal(utcNow, context.TypedResponseHeaders.Date); + + middleware.FinalizeCachingHeaders(context); + + Assert.Equal(utcNow, context.TypedResponseHeaders.Date); + } + + [Fact] + public void FinalizeCachingHeaders_StoresCachedResponse_InState() + { + var middleware = TestUtils.CreateTestMiddleware(); + var context = TestUtils.CreateTestContext(); + + Assert.Null(context.CachedResponse); + + middleware.FinalizeCachingHeaders(context); + + Assert.NotNull(context.CachedResponse); + } + + [Fact] + public async Task FinalizeCachingBody_StoreResponseBodySeparately_IfLargerThanLimit() + { + var cache = new TestResponseCache(); + var middleware = TestUtils.CreateTestMiddleware(cache); + var context = TestUtils.CreateTestContext(); + + middleware.ShimResponseStream(context); + await context.HttpContext.Response.WriteAsync(new string('0', 70 * 1024)); + + context.ShouldCacheResponse = true; + context.CachedResponse = new CachedResponse() + { + BodyKeyPrefix = FastGuid.NewGuid().IdString + }; + context.StorageBaseKey = "BaseKey"; + context.CachedResponseValidFor = TimeSpan.FromSeconds(10); + + middleware.FinalizeCachingBody(context); + + Assert.Equal(2, cache.SetCount); + } + + [Fact] + public async Task FinalizeCachingBody_StoreResponseBodyInCachedResponse_IfSmallerThanLimit() + { + var cache = new TestResponseCache(); + var middleware = TestUtils.CreateTestMiddleware(cache); + var context = TestUtils.CreateTestContext(); + + middleware.ShimResponseStream(context); + await context.HttpContext.Response.WriteAsync(new string('0', 70 * 1024 - 1)); + + context.ShouldCacheResponse = true; + context.CachedResponse = new CachedResponse() + { + BodyKeyPrefix = FastGuid.NewGuid().IdString + }; + context.StorageBaseKey = "BaseKey"; + context.CachedResponseValidFor = TimeSpan.FromSeconds(10); + + middleware.FinalizeCachingBody(context); + + Assert.Equal(1, cache.SetCount); + } + + [Fact] + public async Task FinalizeCachingBody_StoreResponseBodySeparately_LimitIsConfigurable() + { + var cache = new TestResponseCache(); + var middleware = TestUtils.CreateTestMiddleware(cache, new ResponseCachingOptions() + { + MinimumSplitBodySize = 2048 + }); + var context = TestUtils.CreateTestContext(); + + middleware.ShimResponseStream(context); + await context.HttpContext.Response.WriteAsync(new string('0', 1024)); + + context.ShouldCacheResponse = true; + context.CachedResponse = new CachedResponse() + { + BodyKeyPrefix = FastGuid.NewGuid().IdString + }; + context.StorageBaseKey = "BaseKey"; + context.CachedResponseValidFor = TimeSpan.FromSeconds(10); + + middleware.FinalizeCachingBody(context); + + Assert.Equal(1, cache.SetCount); + } + + [Fact] + public async Task FinalizeCachingBody_Cache_IfContentLengthMatches() + { + var cache = new TestResponseCache(); + var middleware = TestUtils.CreateTestMiddleware(cache); + var context = TestUtils.CreateTestContext(); + + middleware.ShimResponseStream(context); + context.HttpContext.Response.ContentLength = 10; + await context.HttpContext.Response.WriteAsync(new string('0', 10)); + + context.ShouldCacheResponse = true; + context.CachedResponse = new CachedResponse() + { + BodyKeyPrefix = FastGuid.NewGuid().IdString + }; + context.StorageBaseKey = "BaseKey"; + context.CachedResponseValidFor = TimeSpan.FromSeconds(10); + + middleware.FinalizeCachingBody(context); + + Assert.Equal(1, cache.SetCount); + } + + [Fact] + public async Task FinalizeCachingBody_DoNotCache_IfContentLengthMismatches() + { + var cache = new TestResponseCache(); + var middleware = TestUtils.CreateTestMiddleware(cache); + var context = TestUtils.CreateTestContext(); + + middleware.ShimResponseStream(context); + context.HttpContext.Response.ContentLength = 9; + await context.HttpContext.Response.WriteAsync(new string('0', 10)); + + context.ShouldCacheResponse = true; + context.CachedResponse = new CachedResponse() + { + BodyKeyPrefix = FastGuid.NewGuid().IdString + }; + context.StorageBaseKey = "BaseKey"; + context.CachedResponseValidFor = TimeSpan.FromSeconds(10); + + middleware.FinalizeCachingBody(context); + + Assert.Equal(0, cache.SetCount); + } + + [Fact] + public async Task FinalizeCachingBody_Cache_IfContentLengthAbsent() + { + var cache = new TestResponseCache(); + var middleware = TestUtils.CreateTestMiddleware(cache); + var context = TestUtils.CreateTestContext(); + + middleware.ShimResponseStream(context); + await context.HttpContext.Response.WriteAsync(new string('0', 10)); + + context.ShouldCacheResponse = true; + context.CachedResponse = new CachedResponse() + { + BodyKeyPrefix = FastGuid.NewGuid().IdString + }; + context.StorageBaseKey = "BaseKey"; + context.CachedResponseValidFor = TimeSpan.FromSeconds(10); + + middleware.FinalizeCachingBody(context); + + Assert.Equal(1, cache.SetCount); + } + + [Fact] + public void NormalizeStringValues_NormalizesCasingToUpper() + { + var uppercaseStrings = new StringValues(new[] { "STRINGA", "STRINGB" }); + var lowercaseStrings = new StringValues(new[] { "stringA", "stringB" }); + + var normalizedStrings = ResponseCachingMiddleware.GetNormalizedStringValues(lowercaseStrings); + + Assert.Equal(uppercaseStrings, normalizedStrings); + } + + [Fact] + public void NormalizeStringValues_NormalizesOrder() + { + var orderedStrings = new StringValues(new[] { "STRINGA", "STRINGB" }); + var reverseOrderStrings = new StringValues(new[] { "STRINGB", "STRINGA" }); + + var normalizedStrings = ResponseCachingMiddleware.GetNormalizedStringValues(reverseOrderStrings); + + Assert.Equal(orderedStrings, normalizedStrings); + } + } +} diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs index d37d76e30e..e910ad5478 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs @@ -6,11 +6,9 @@ using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Net.Http.Headers; using Xunit; @@ -21,7 +19,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfAvailable() { - var builder = CreateBuilderWithResponseCaching(); + var builder = TestUtils.CreateBuilderWithResponseCaching(); using (var server = new TestServer(builder)) { @@ -36,7 +34,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfNotAvailable() { - var builder = CreateBuilderWithResponseCaching(); + var builder = TestUtils.CreateBuilderWithResponseCaching(); using (var server = new TestServer(builder)) { @@ -51,10 +49,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfVaryHeader_Matches() { - var builder = CreateBuilderWithResponseCaching(async (context) => + var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) => { context.Response.Headers[HeaderNames.Vary] = HeaderNames.From; - await DefaultRequestDelegate(context); + await TestUtils.DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -71,10 +69,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfVaryHeader_Mismatches() { - var builder = CreateBuilderWithResponseCaching(async (context) => + var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) => { context.Response.Headers[HeaderNames.Vary] = HeaderNames.From; - await DefaultRequestDelegate(context); + await TestUtils.DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -92,10 +90,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfVaryParams_Matches() { - var builder = CreateBuilderWithResponseCaching(async (context) => + var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) => { context.GetResponseCachingFeature().VaryParams = "param"; - await DefaultRequestDelegate(context); + await TestUtils.DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -111,10 +109,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfVaryParamsExplicit_Matches_ParamNameCaseInsensitive() { - var builder = CreateBuilderWithResponseCaching(async (context) => + var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) => { context.GetResponseCachingFeature().VaryParams = new[] { "ParamA", "paramb" }; - await DefaultRequestDelegate(context); + await TestUtils.DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -130,10 +128,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfVaryParamsStar_Matches_ParamNameCaseInsensitive() { - var builder = CreateBuilderWithResponseCaching(async (context) => + var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) => { context.GetResponseCachingFeature().VaryParams = new[] { "*" }; - await DefaultRequestDelegate(context); + await TestUtils.DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -149,10 +147,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfVaryParamsExplicit_Matches_OrderInsensitive() { - var builder = CreateBuilderWithResponseCaching(async (context) => + var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) => { context.GetResponseCachingFeature().VaryParams = new[] { "ParamB", "ParamA" }; - await DefaultRequestDelegate(context); + await TestUtils.DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -168,10 +166,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfVaryParamsStar_Matches_OrderInsensitive() { - var builder = CreateBuilderWithResponseCaching(async (context) => + var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) => { context.GetResponseCachingFeature().VaryParams = new[] { "*" }; - await DefaultRequestDelegate(context); + await TestUtils.DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -187,10 +185,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfVaryParams_Mismatches() { - var builder = CreateBuilderWithResponseCaching(async (context) => + var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) => { context.GetResponseCachingFeature().VaryParams = "param"; - await DefaultRequestDelegate(context); + await TestUtils.DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -206,10 +204,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfVaryParamsExplicit_Mismatch_ParamValueCaseSensitive() { - var builder = CreateBuilderWithResponseCaching(async (context) => + var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) => { context.GetResponseCachingFeature().VaryParams = new[] { "ParamA", "ParamB" }; - await DefaultRequestDelegate(context); + await TestUtils.DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -225,10 +223,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfVaryParamsStar_Mismatch_ParamValueCaseSensitive() { - var builder = CreateBuilderWithResponseCaching(async (context) => + var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) => { context.GetResponseCachingFeature().VaryParams = new[] { "*" }; - await DefaultRequestDelegate(context); + await TestUtils.DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -244,7 +242,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfRequestRequirements_NotMet() { - var builder = CreateBuilderWithResponseCaching(); + var builder = TestUtils.CreateBuilderWithResponseCaching(); using (var server = new TestServer(builder)) { @@ -263,7 +261,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void Serves504_IfOnlyIfCachedHeader_IsSpecified() { - var builder = CreateBuilderWithResponseCaching(); + var builder = TestUtils.CreateBuilderWithResponseCaching(); using (var server = new TestServer(builder)) { @@ -283,10 +281,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfSetCookie_IsSpecified() { - var builder = CreateBuilderWithResponseCaching(async (context) => + var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) => { var headers = context.Response.Headers[HeaderNames.SetCookie] = "cookieName=cookieValue"; - await DefaultRequestDelegate(context); + await TestUtils.DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -302,7 +300,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfIHttpSendFileFeature_NotUsed() { - var builder = CreateBuilderWithResponseCaching(app => + var builder = TestUtils.CreateBuilderWithResponseCaching(app => { app.Use(async (context, next) => { @@ -324,7 +322,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfIHttpSendFileFeature_Used() { - var builder = CreateBuilderWithResponseCaching( + var builder = TestUtils.CreateBuilderWithResponseCaching( app => { app.Use(async (context, next) => @@ -333,10 +331,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests await next.Invoke(); }); }, - async (context) => + requestDelegate: async (context) => { await context.Features.Get().SendFileAsync("dummy", 0, 0, CancellationToken.None); - await DefaultRequestDelegate(context); + await TestUtils.DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -352,7 +350,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfSubsequentRequest_ContainsNoStore() { - var builder = CreateBuilderWithResponseCaching(); + var builder = TestUtils.CreateBuilderWithResponseCaching(); using (var server = new TestServer(builder)) { @@ -371,7 +369,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfInitialRequestContains_NoStore() { - var builder = CreateBuilderWithResponseCaching(); + var builder = TestUtils.CreateBuilderWithResponseCaching(); using (var server = new TestServer(builder)) { @@ -390,7 +388,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void Serves304_IfIfModifiedSince_Satisfied() { - var builder = CreateBuilderWithResponseCaching(); + var builder = TestUtils.CreateBuilderWithResponseCaching(); using (var server = new TestServer(builder)) { @@ -407,7 +405,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfIfModifiedSince_NotSatisfied() { - var builder = CreateBuilderWithResponseCaching(); + var builder = TestUtils.CreateBuilderWithResponseCaching(); using (var server = new TestServer(builder)) { @@ -423,10 +421,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void Serves304_IfIfNoneMatch_Satisfied() { - var builder = CreateBuilderWithResponseCaching(async (context) => + var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) => { var headers = context.Response.GetTypedHeaders().ETag = new EntityTagHeaderValue("\"E1\""); - await DefaultRequestDelegate(context); + await TestUtils.DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -444,10 +442,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfIfNoneMatch_NotSatisfied() { - var builder = CreateBuilderWithResponseCaching(async (context) => + var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) => { var headers = context.Response.GetTypedHeaders().ETag = new EntityTagHeaderValue("\"E1\""); - await DefaultRequestDelegate(context); + await TestUtils.DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -464,7 +462,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfBodySize_IsCacheable() { - var builder = CreateBuilderWithResponseCaching(new ResponseCachingOptions() + var builder = TestUtils.CreateBuilderWithResponseCaching(options: new ResponseCachingOptions() { MaximumCachedBodySize = 100 }); @@ -482,7 +480,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfBodySize_IsNotCacheable() { - var builder = CreateBuilderWithResponseCaching(new ResponseCachingOptions() + var builder = TestUtils.CreateBuilderWithResponseCaching(options: new ResponseCachingOptions() { MaximumCachedBodySize = 1 }); @@ -500,10 +498,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_WithoutReplacingCachedVaryBy_OnCacheMiss() { - var builder = CreateBuilderWithResponseCaching(async (context) => + var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) => { context.Response.Headers[HeaderNames.Vary] = HeaderNames.From; - await DefaultRequestDelegate(context); + await TestUtils.DefaultRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -541,57 +539,5 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Assert.False(subsequentResponse.Headers.Contains(HeaderNames.Age)); Assert.NotEqual(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync()); } - - private static RequestDelegate DefaultRequestDelegate = async (context) => - { - var uniqueId = Guid.NewGuid().ToString(); - var headers = context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }; - headers.Date = DateTimeOffset.UtcNow; - headers.Headers["X-Value"] = uniqueId; - await context.Response.WriteAsync(uniqueId); - }; - - private static IWebHostBuilder CreateBuilderWithResponseCaching() => - CreateBuilderWithResponseCaching(app => { }, new ResponseCachingOptions(), DefaultRequestDelegate); - - private static IWebHostBuilder CreateBuilderWithResponseCaching(ResponseCachingOptions options) => - CreateBuilderWithResponseCaching(app => { }, options, DefaultRequestDelegate); - - private static IWebHostBuilder CreateBuilderWithResponseCaching(RequestDelegate requestDelegate) => - CreateBuilderWithResponseCaching(app => { }, new ResponseCachingOptions(), requestDelegate); - - private static IWebHostBuilder CreateBuilderWithResponseCaching(Action configureDelegate) => - CreateBuilderWithResponseCaching(configureDelegate, new ResponseCachingOptions(), DefaultRequestDelegate); - - private static IWebHostBuilder CreateBuilderWithResponseCaching(Action configureDelegate, RequestDelegate requestDelegate) => - CreateBuilderWithResponseCaching(configureDelegate, new ResponseCachingOptions(), requestDelegate); - - private static IWebHostBuilder CreateBuilderWithResponseCaching(Action configureDelegate, ResponseCachingOptions options, RequestDelegate requestDelegate) - { - return new WebHostBuilder() - .ConfigureServices(services => - { - services.AddDistributedResponseCache(); - }) - .Configure(app => - { - configureDelegate(app); - app.UseResponseCaching(options); - app.Run(requestDelegate); - }); - } - - private class DummySendFileFeature : IHttpSendFileFeature - { - public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellation) - { - return Task.FromResult(0); - } - } } } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs new file mode 100644 index 0000000000..23c80cddf4 --- /dev/null +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs @@ -0,0 +1,211 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Internal; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.ResponseCaching.Tests +{ + internal class TestUtils + { + internal static RequestDelegate DefaultRequestDelegate = async (context) => + { + var uniqueId = Guid.NewGuid().ToString(); + var headers = context.Response.GetTypedHeaders(); + headers.CacheControl = new CacheControlHeaderValue() + { + Public = true, + MaxAge = TimeSpan.FromSeconds(10) + }; + headers.Date = DateTimeOffset.UtcNow; + headers.Headers["X-Value"] = uniqueId; + await context.Response.WriteAsync(uniqueId); + }; + + internal static ICacheKeyProvider CreateTestKeyProvider() + { + return CreateTestKeyProvider(new ResponseCachingOptions()); + } + + internal static ICacheKeyProvider CreateTestKeyProvider(ResponseCachingOptions options) + { + return new CacheKeyProvider(new DefaultObjectPoolProvider(), Options.Create(options)); + } + + internal static IWebHostBuilder CreateBuilderWithResponseCaching( + Action configureDelegate = null, + ResponseCachingOptions options = null, + RequestDelegate requestDelegate = null) + { + if (configureDelegate == null) + { + configureDelegate = app => { }; + } + if (options == null) + { + options = new ResponseCachingOptions(); + } + if (requestDelegate == null) + { + requestDelegate = DefaultRequestDelegate; + } + + return new WebHostBuilder() + .ConfigureServices(services => + { + services.AddDistributedResponseCache(); + }) + .Configure(app => + { + configureDelegate(app); + app.UseResponseCaching(options); + app.Run(requestDelegate); + }); + } + + internal static ResponseCachingMiddleware CreateTestMiddleware( + IResponseCache responseCache = null, + ResponseCachingOptions options = null, + ICacheKeyProvider cacheKeyProvider = null, + ICacheabilityValidator cacheabilityValidator = null) + { + if (responseCache == null) + { + responseCache = new TestResponseCache(); + } + if (options == null) + { + options = new ResponseCachingOptions(); + } + if (cacheKeyProvider == null) + { + cacheKeyProvider = new CacheKeyProvider(new DefaultObjectPoolProvider(), Options.Create(options)); + } + if (cacheabilityValidator == null) + { + cacheabilityValidator = new TestCacheabilityValidator(); + } + + return new ResponseCachingMiddleware( + httpContext => TaskCache.CompletedTask, + responseCache, + Options.Create(options), + cacheabilityValidator, + cacheKeyProvider); + } + + internal static ResponseCachingContext CreateTestContext() + { + return new ResponseCachingContext(new DefaultHttpContext()); + } + } + + internal class DummySendFileFeature : IHttpSendFileFeature + { + public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellation) + { + return TaskCache.CompletedTask; + } + } + + internal class TestCacheabilityValidator : ICacheabilityValidator + { + public bool IsCachedEntryFresh(ResponseCachingContext context) => true; + + public bool IsRequestCacheable(ResponseCachingContext context) => true; + + public bool IsResponseCacheable(ResponseCachingContext context) => true; + } + + internal class TestKeyProvider : ICacheKeyProvider + { + private readonly StringValues _baseKey; + private readonly StringValues _varyKey; + + public TestKeyProvider(StringValues? lookupBaseKey = null, StringValues? lookupVaryKey = null) + { + if (lookupBaseKey.HasValue) + { + _baseKey = lookupBaseKey.Value; + } + if (lookupVaryKey.HasValue) + { + _varyKey = lookupVaryKey.Value; + } + } + + public IEnumerable CreateLookupBaseKeys(ResponseCachingContext context) => _baseKey; + + + public IEnumerable CreateLookupVaryKeys(ResponseCachingContext context) + { + foreach (var baseKey in _baseKey) + { + foreach (var varyKey in _varyKey) + { + yield return baseKey + varyKey; + } + } + } + + public string CreateStorageBaseKey(ResponseCachingContext context) + { + throw new NotImplementedException(); + } + + public string CreateStorageVaryKey(ResponseCachingContext context) + { + throw new NotImplementedException(); + } + } + + internal class TestResponseCache : IResponseCache + { + private readonly IDictionary _storage = new Dictionary(); + public int GetCount { get; private set; } + public int SetCount { get; private set; } + + public object Get(string key) + { + GetCount++; + try + { + return _storage[key]; + } + catch + { + return null; + } + } + + public void Remove(string key) + { + } + + public void Set(string key, object entry, TimeSpan validFor) + { + SetCount++; + _storage[key] = entry; + } + } + + internal class TestHttpSendFileFeature : IHttpSendFileFeature + { + public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellation) + { + return TaskCache.CompletedTask; + } + } +} From 6c13371fa04f0db7cbce2709cbc88e31cbfa731a Mon Sep 17 00:00:00 2001 From: John Luo Date: Wed, 14 Sep 2016 15:20:48 -0700 Subject: [PATCH 029/188] API review renames and updates continued - Consolidate base key to be singular --- samples/ResponseCachingSample/Startup.cs | 2 +- .../CacheEntry/CacheEntrySerializer.cs | 28 +-- .../CacheEntry/CachedResponse.cs | 10 +- .../CacheEntry/CachedResponseBody.cs | 2 +- ...achedVaryRules.cs => CachedVaryByRules.cs} | 8 +- ...tensions.cs => ResponseCacheExtensions.cs} | 10 +- ... => ResponseCacheHttpContextExtensions.cs} | 6 +- ...sponseCacheServiceCollectionExtensions.cs} | 16 +- .../Interfaces/ICacheKeyProvider.cs | 38 --- .../Interfaces/ICacheabilityValidator.cs | 29 --- .../Interfaces/IResponseCacheKeyProvider.cs | 31 +++ .../IResponseCachePolicyProvider.cs | 29 +++ ...esponseCache.cs => IResponseCacheStore.cs} | 2 +- ...he.cs => DistributedResponseCacheStore.cs} | 4 +- .../Internal/InternalHttpContextExtensions.cs | 12 +- ...seCache.cs => MemoryResponseCacheStore.cs} | 4 +- ...hingContext.cs => ResponseCacheContext.cs} | 8 +- ...hingFeature.cs => ResponseCacheFeature.cs} | 4 +- ...rovider.cs => ResponseCacheKeyProvider.cs} | 45 ++-- ...ddleware.cs => ResponseCacheMiddleware.cs} | 128 +++++----- ...hingOptions.cs => ResponseCacheOptions.cs} | 4 +- ...ator.cs => ResponseCachePolicyProvider.cs} | 9 +- .../CacheEntrySerializerTests.cs | 42 ++-- .../HttpContextInternalExtensionTests.cs | 6 +- ...ts.cs => ResponseCacheKeyProviderTests.cs} | 82 +++--- ...sts.cs => ResponseCacheMiddlewareTests.cs} | 237 +++++++++--------- ...cs => ResponseCachePolicyProviderTests.cs} | 142 +++++------ ...eCachingTests.cs => ResponseCacheTests.cs} | 100 ++++---- .../TestUtils.cs | 103 ++++---- 29 files changed, 555 insertions(+), 586 deletions(-) rename src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/{CachedVaryRules.cs => CachedVaryByRules.cs} (56%) rename src/Microsoft.AspNetCore.ResponseCaching/Extensions/{ResponseCachingExtensions.cs => ResponseCacheExtensions.cs} (63%) rename src/Microsoft.AspNetCore.ResponseCaching/Extensions/{ResponseCachingHttpContextExtensions.cs => ResponseCacheHttpContextExtensions.cs} (61%) rename src/Microsoft.AspNetCore.ResponseCaching/Extensions/{ResponseCachingServiceCollectionExtensions.cs => ResponseCacheServiceCollectionExtensions.cs} (67%) delete mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Interfaces/ICacheKeyProvider.cs delete mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Interfaces/ICacheabilityValidator.cs create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCacheKeyProvider.cs create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCachePolicyProvider.cs rename src/Microsoft.AspNetCore.ResponseCaching/Interfaces/{IResponseCache.cs => IResponseCacheStore.cs} (90%) rename src/Microsoft.AspNetCore.ResponseCaching/Internal/{DistributedResponseCache.cs => DistributedResponseCacheStore.cs} (91%) rename src/Microsoft.AspNetCore.ResponseCaching/Internal/{MemoryResponseCache.cs => MemoryResponseCacheStore.cs} (88%) rename src/Microsoft.AspNetCore.ResponseCaching/{ResponseCachingContext.cs => ResponseCacheContext.cs} (93%) rename src/Microsoft.AspNetCore.ResponseCaching/{ResponseCachingFeature.cs => ResponseCacheFeature.cs} (79%) rename src/Microsoft.AspNetCore.ResponseCaching/{CacheKeyProvider.cs => ResponseCacheKeyProvider.cs} (71%) rename src/Microsoft.AspNetCore.ResponseCaching/{ResponseCachingMiddleware.cs => ResponseCacheMiddleware.cs} (74%) rename src/Microsoft.AspNetCore.ResponseCaching/{ResponseCachingOptions.cs => ResponseCacheOptions.cs} (91%) rename src/Microsoft.AspNetCore.ResponseCaching/{CacheabilityValidator.cs => ResponseCachePolicyProvider.cs} (95%) rename test/Microsoft.AspNetCore.ResponseCaching.Tests/{KeyProviderTests.cs => ResponseCacheKeyProviderTests.cs} (57%) rename test/Microsoft.AspNetCore.ResponseCaching.Tests/{ResponseCachingMiddlewareTests.cs => ResponseCacheMiddlewareTests.cs} (66%) rename test/Microsoft.AspNetCore.ResponseCaching.Tests/{CacheabilityValidatorTests.cs => ResponseCachePolicyProviderTests.cs} (77%) rename test/Microsoft.AspNetCore.ResponseCaching.Tests/{ResponseCachingTests.cs => ResponseCacheTests.cs} (89%) diff --git a/samples/ResponseCachingSample/Startup.cs b/samples/ResponseCachingSample/Startup.cs index 510f0d0e0e..1cad8d6c1f 100644 --- a/samples/ResponseCachingSample/Startup.cs +++ b/samples/ResponseCachingSample/Startup.cs @@ -20,7 +20,7 @@ namespace ResponseCachingSample public void Configure(IApplicationBuilder app) { - app.UseResponseCaching(); + app.UseResponseCache(); app.Run(async (context) => { // These settings should be configured by context.Response.Cache.* diff --git a/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CacheEntrySerializer.cs b/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CacheEntrySerializer.cs index caa4061773..81e89f7304 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CacheEntrySerializer.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CacheEntrySerializer.cs @@ -42,8 +42,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal // Serialization Format // Format version (int) - // Type (char: 'B' for CachedResponseBody, 'R' for CachedResponse, 'V' for CachedVaryRules) - // Type-dependent data (see CachedResponse and CachedVaryRules) + // Type (char: 'B' for CachedResponseBody, 'R' for CachedResponse, 'V' for CachedVaryByRules) + // Type-dependent data (see CachedResponse and CachedVaryByRules) public static object Read(BinaryReader reader) { if (reader == null) @@ -68,10 +68,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal } else if (type == 'V') { - return ReadCachedVaryRules(reader); + return ReadCachedVaryByRules(reader); } - // Unable to read as CachedResponse or CachedVaryRules + // Unable to read as CachedResponse or CachedVaryByRules return null; } @@ -129,7 +129,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal // Header(s) (comma separated string) // Params count // Param(s) (comma separated string) - private static CachedVaryRules ReadCachedVaryRules(BinaryReader reader) + private static CachedVaryByRules ReadCachedVaryByRules(BinaryReader reader) { var varyKeyPrefix = reader.ReadString(); @@ -146,7 +146,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal param[index] = reader.ReadString(); } - return new CachedVaryRules { VaryKeyPrefix = varyKeyPrefix, Headers = headers, Params = param }; + return new CachedVaryByRules { VaryByKeyPrefix = varyKeyPrefix, Headers = headers, Params = param }; } // See serialization format above @@ -174,10 +174,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal writer.Write('R'); WriteCachedResponse(writer, entry as CachedResponse); } - else if (entry is CachedVaryRules) + else if (entry is CachedVaryByRules) { writer.Write('V'); - WriteCachedVaryRules(writer, entry as CachedVaryRules); + WriteCachedVaryByRules(writer, entry as CachedVaryByRules); } else { @@ -218,18 +218,18 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal } // See serialization format above - private static void WriteCachedVaryRules(BinaryWriter writer, CachedVaryRules varyRules) + private static void WriteCachedVaryByRules(BinaryWriter writer, CachedVaryByRules varyByRules) { - writer.Write(varyRules.VaryKeyPrefix); + writer.Write(varyByRules.VaryByKeyPrefix); - writer.Write(varyRules.Headers.Count); - foreach (var header in varyRules.Headers) + writer.Write(varyByRules.Headers.Count); + foreach (var header in varyByRules.Headers) { writer.Write(header); } - writer.Write(varyRules.Params.Count); - foreach (var param in varyRules.Params) + writer.Write(varyByRules.Params.Count); + foreach (var param in varyByRules.Params) { writer.Write(param); } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedResponse.cs b/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedResponse.cs index 552b29d96a..7e32ec2959 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedResponse.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedResponse.cs @@ -8,14 +8,14 @@ namespace Microsoft.AspNetCore.ResponseCaching { public class CachedResponse { - public string BodyKeyPrefix { get; internal set; } + public string BodyKeyPrefix { get; set; } - public DateTimeOffset Created { get; internal set; } + public DateTimeOffset Created { get; set; } - public int StatusCode { get; internal set; } + public int StatusCode { get; set; } - public IHeaderDictionary Headers { get; internal set; } = new HeaderDictionary(); + public IHeaderDictionary Headers { get; set; } = new HeaderDictionary(); - public byte[] Body { get; internal set; } + public byte[] Body { get; set; } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedResponseBody.cs b/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedResponseBody.cs index d714e3f131..a5ce8d6aca 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedResponseBody.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedResponseBody.cs @@ -5,6 +5,6 @@ namespace Microsoft.AspNetCore.ResponseCaching { public class CachedResponseBody { - public byte[] Body { get; internal set; } + public byte[] Body { get; set; } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedVaryRules.cs b/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedVaryByRules.cs similarity index 56% rename from src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedVaryRules.cs rename to src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedVaryByRules.cs index f3a8a9a75a..e301edbf4c 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedVaryRules.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedVaryByRules.cs @@ -5,12 +5,12 @@ using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.ResponseCaching { - public class CachedVaryRules + public class CachedVaryByRules { - public string VaryKeyPrefix { get; internal set; } + public string VaryByKeyPrefix { get; set; } - public StringValues Headers { get; internal set; } + public StringValues Headers { get; set; } - public StringValues Params { get; internal set; } + public StringValues Params { get; set; } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCachingExtensions.cs b/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCacheExtensions.cs similarity index 63% rename from src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCachingExtensions.cs rename to src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCacheExtensions.cs index 037863e4cf..8b96c3e8d8 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCachingExtensions.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCacheExtensions.cs @@ -7,19 +7,19 @@ using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Builder { - public static class ResponseCachingExtensions + public static class ResponseCacheExtensions { - public static IApplicationBuilder UseResponseCaching(this IApplicationBuilder app) + public static IApplicationBuilder UseResponseCache(this IApplicationBuilder app) { if (app == null) { throw new ArgumentNullException(nameof(app)); } - return app.UseMiddleware(); + return app.UseMiddleware(); } - public static IApplicationBuilder UseResponseCaching(this IApplicationBuilder app, ResponseCachingOptions options) + public static IApplicationBuilder UseResponseCache(this IApplicationBuilder app, ResponseCacheOptions options) { if (app == null) { @@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.Builder throw new ArgumentNullException(nameof(options)); } - return app.UseMiddleware(Options.Create(options)); + return app.UseMiddleware(Options.Create(options)); } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCachingHttpContextExtensions.cs b/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCacheHttpContextExtensions.cs similarity index 61% rename from src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCachingHttpContextExtensions.cs rename to src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCacheHttpContextExtensions.cs index 50b5f73f52..7c1915b14d 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCachingHttpContextExtensions.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCacheHttpContextExtensions.cs @@ -6,11 +6,11 @@ using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.ResponseCaching { // TODO: Temporary interface for endpoints to specify options for response caching - public static class ResponseCachingHttpContextExtensions + public static class ResponseCacheHttpContextExtensions { - public static ResponseCachingFeature GetResponseCachingFeature(this HttpContext httpContext) + public static ResponseCacheFeature GetResponseCacheFeature(this HttpContext httpContext) { - return httpContext.Features.Get(); + return httpContext.Features.Get(); } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCachingServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCacheServiceCollectionExtensions.cs similarity index 67% rename from src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCachingServiceCollectionExtensions.cs rename to src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCacheServiceCollectionExtensions.cs index 962ddf281f..51af53847e 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCachingServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCacheServiceCollectionExtensions.cs @@ -8,7 +8,7 @@ using Microsoft.Extensions.DependencyInjection.Extensions; namespace Microsoft.Extensions.DependencyInjection { - public static class ResponseCachingServiceCollectionExtensions + public static class ResponseCacheServiceCollectionExtensions { public static IServiceCollection AddMemoryResponseCache(this IServiceCollection services) { @@ -18,8 +18,8 @@ namespace Microsoft.Extensions.DependencyInjection } services.AddMemoryCache(); - services.AddResponseCachingServices(); - services.TryAdd(ServiceDescriptor.Singleton()); + services.AddResponseCacheServices(); + services.TryAdd(ServiceDescriptor.Singleton()); return services; } @@ -32,16 +32,16 @@ namespace Microsoft.Extensions.DependencyInjection } services.AddDistributedMemoryCache(); - services.AddResponseCachingServices(); - services.TryAdd(ServiceDescriptor.Singleton()); + services.AddResponseCacheServices(); + services.TryAdd(ServiceDescriptor.Singleton()); return services; } - private static IServiceCollection AddResponseCachingServices(this IServiceCollection services) + private static IServiceCollection AddResponseCacheServices(this IServiceCollection services) { - services.TryAdd(ServiceDescriptor.Singleton()); - services.TryAdd(ServiceDescriptor.Singleton()); + services.TryAdd(ServiceDescriptor.Singleton()); + services.TryAdd(ServiceDescriptor.Singleton()); return services; } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/ICacheKeyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/ICacheKeyProvider.cs deleted file mode 100644 index 570041e290..0000000000 --- a/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/ICacheKeyProvider.cs +++ /dev/null @@ -1,38 +0,0 @@ -// 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.AspNetCore.ResponseCaching -{ - public interface ICacheKeyProvider - { - /// - /// Create a base key for storing items. - /// - /// The . - /// The created base key. - string CreateStorageBaseKey(ResponseCachingContext context); - - /// - /// Create one or more base keys for looking up items. - /// - /// The . - /// An ordered containing the base keys to try when looking up items. - IEnumerable CreateLookupBaseKeys(ResponseCachingContext context); - - /// - /// Create a vary key for storing items. - /// - /// The . - /// The created vary key. - string CreateStorageVaryKey(ResponseCachingContext context); - - /// - /// Create one or more vary keys for looking up items. - /// - /// The . - /// An ordered containing the vary keys to try when looking up items. - IEnumerable CreateLookupVaryKeys(ResponseCachingContext context); - } -} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/ICacheabilityValidator.cs b/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/ICacheabilityValidator.cs deleted file mode 100644 index a260ce87d1..0000000000 --- a/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/ICacheabilityValidator.cs +++ /dev/null @@ -1,29 +0,0 @@ -// 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 ICacheabilityValidator - { - /// - /// Determine the cacheability of an HTTP request. - /// - /// The . - /// true if the request is cacheable; otherwise false. - bool IsRequestCacheable(ResponseCachingContext context); - - /// - /// Determine the cacheability of an HTTP response. - /// - /// The . - /// true if the response is cacheable; otherwise false. - bool IsResponseCacheable(ResponseCachingContext context); - - /// - /// Determine the freshness of the cached entry. - /// - /// The . - /// true if the cached entry is fresh; otherwise false. - bool IsCachedEntryFresh(ResponseCachingContext context); - } -} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCacheKeyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCacheKeyProvider.cs new file mode 100644 index 0000000000..89ae2ffa04 --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCacheKeyProvider.cs @@ -0,0 +1,31 @@ +// 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.AspNetCore.ResponseCaching +{ + public interface IResponseCacheKeyProvider + { + /// + /// Create a base key for a response cache entry. + /// + /// The . + /// The created base key. + string CreateBaseKey(ResponseCacheContext context); + + /// + /// Create a vary key for storing cached responses. + /// + /// The . + /// The created vary key. + string CreateStorageVaryByKey(ResponseCacheContext context); + + /// + /// Create one or more vary keys for looking up cached responses. + /// + /// The . + /// An ordered containing the vary keys to try when looking up items. + IEnumerable CreateLookupVaryByKeys(ResponseCacheContext context); + } +} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCachePolicyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCachePolicyProvider.cs new file mode 100644 index 0000000000..91ab9c5e14 --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCachePolicyProvider.cs @@ -0,0 +1,29 @@ +// 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 IResponseCachePolicyProvider + { + /// + /// Determine wehther the response cache middleware should be executed for the incoming HTTP request. + /// + /// The . + /// true if the request is cacheable; otherwise false. + bool IsRequestCacheable(ResponseCacheContext context); + + /// + /// Determine whether the response received by the middleware be cached for future requests. + /// + /// The . + /// true if the response is cacheable; otherwise false. + bool IsResponseCacheable(ResponseCacheContext context); + + /// + /// Determine whether the response retrieved from the response cache is fresh and be served. + /// + /// The . + /// true if the cached entry is fresh; otherwise false. + bool IsCachedEntryFresh(ResponseCacheContext context); + } +} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCache.cs b/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCacheStore.cs similarity index 90% rename from src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCache.cs rename to src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCacheStore.cs index dd120e6ec6..90998a71d4 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCache.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCacheStore.cs @@ -5,7 +5,7 @@ using System; namespace Microsoft.AspNetCore.ResponseCaching { - public interface IResponseCache + public interface IResponseCacheStore { object Get(string key); void Set(string key, object entry, TimeSpan validFor); diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/DistributedResponseCache.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/DistributedResponseCacheStore.cs similarity index 91% rename from src/Microsoft.AspNetCore.ResponseCaching/Internal/DistributedResponseCache.cs rename to src/Microsoft.AspNetCore.ResponseCaching/Internal/DistributedResponseCacheStore.cs index 7de5d8b946..aba8990b42 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/DistributedResponseCache.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/DistributedResponseCacheStore.cs @@ -6,11 +6,11 @@ using Microsoft.Extensions.Caching.Distributed; namespace Microsoft.AspNetCore.ResponseCaching.Internal { - internal class DistributedResponseCache : IResponseCache + public class DistributedResponseCacheStore : IResponseCacheStore { private readonly IDistributedCache _cache; - public DistributedResponseCache(IDistributedCache cache) + public DistributedResponseCacheStore(IDistributedCache cache) { if (cache == null) { diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/InternalHttpContextExtensions.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/InternalHttpContextExtensions.cs index 640edba4a1..e76aa83a68 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/InternalHttpContextExtensions.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/InternalHttpContextExtensions.cs @@ -8,18 +8,18 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal { internal static class InternalHttpContextExtensions { - internal static void AddResponseCachingFeature(this HttpContext httpContext) + internal static void AddResponseCacheFeature(this HttpContext httpContext) { - if (httpContext.GetResponseCachingFeature() != null) + if (httpContext.GetResponseCacheFeature() != null) { - throw new InvalidOperationException($"Another instance of {nameof(ResponseCachingFeature)} already exists. Only one instance of {nameof(ResponseCachingMiddleware)} can be configured for an application."); + throw new InvalidOperationException($"Another instance of {nameof(ResponseCacheFeature)} already exists. Only one instance of {nameof(ResponseCacheMiddleware)} can be configured for an application."); } - httpContext.Features.Set(new ResponseCachingFeature()); + httpContext.Features.Set(new ResponseCacheFeature()); } - internal static void RemoveResponseCachingFeature(this HttpContext httpContext) + internal static void RemoveResponseCacheFeature(this HttpContext httpContext) { - httpContext.Features.Set(null); + httpContext.Features.Set(null); } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCacheStore.cs similarity index 88% rename from src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs rename to src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCacheStore.cs index d5296c93e2..b92693137b 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCacheStore.cs @@ -6,11 +6,11 @@ using Microsoft.Extensions.Caching.Memory; namespace Microsoft.AspNetCore.ResponseCaching.Internal { - internal class MemoryResponseCache : IResponseCache + public class MemoryResponseCacheStore : IResponseCacheStore { private readonly IMemoryCache _cache; - public MemoryResponseCache(IMemoryCache cache) + public MemoryResponseCacheStore(IMemoryCache cache) { if (cache == null) { diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheContext.cs similarity index 93% rename from src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs rename to src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheContext.cs index bea436517e..70f96d7537 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingContext.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheContext.cs @@ -11,7 +11,7 @@ using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.ResponseCaching { - public class ResponseCachingContext + public class ResponseCacheContext { private static readonly CacheControlHeaderValue EmptyCacheControl = new CacheControlHeaderValue(); @@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.ResponseCaching private CacheControlHeaderValue _requestCacheControl; private CacheControlHeaderValue _responseCacheControl; - internal ResponseCachingContext( + internal ResponseCacheContext( HttpContext httpContext) { HttpContext = httpContext; @@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.ResponseCaching public bool ShouldCacheResponse { get; internal set; } - public string StorageBaseKey { get; internal set; } + public string BaseKey { get; internal set; } public string StorageVaryKey { get; internal set; } @@ -42,7 +42,7 @@ namespace Microsoft.AspNetCore.ResponseCaching public CachedResponse CachedResponse { get; internal set; } - public CachedVaryRules CachedVaryRules { get; internal set; } + public CachedVaryByRules CachedVaryByRules { get; internal set; } internal bool ResponseStarted { get; set; } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingFeature.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheFeature.cs similarity index 79% rename from src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingFeature.cs rename to src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheFeature.cs index e02c8e28ec..1233aff724 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingFeature.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheFeature.cs @@ -6,8 +6,8 @@ using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.ResponseCaching { // TODO: Temporary interface for endpoints to specify options for response caching - public class ResponseCachingFeature + public class ResponseCacheFeature { - public StringValues VaryParams { get; set; } + public StringValues VaryByParams { get; set; } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/CacheKeyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheKeyProvider.cs similarity index 71% rename from src/Microsoft.AspNetCore.ResponseCaching/CacheKeyProvider.cs rename to src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheKeyProvider.cs index 4e42f92083..318842f47f 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/CacheKeyProvider.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheKeyProvider.cs @@ -12,15 +12,15 @@ using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.ResponseCaching { - public class CacheKeyProvider : ICacheKeyProvider + public class ResponseCacheKeyProvider : IResponseCacheKeyProvider { // Use the record separator for delimiting components of the cache key to avoid possible collisions private static readonly char KeyDelimiter = '\x1e'; private readonly ObjectPool _builderPool; - private readonly ResponseCachingOptions _options; + private readonly ResponseCacheOptions _options; - public CacheKeyProvider(ObjectPoolProvider poolProvider, IOptions options) + public ResponseCacheKeyProvider(ObjectPoolProvider poolProvider, IOptions options) { if (poolProvider == null) { @@ -35,18 +35,13 @@ namespace Microsoft.AspNetCore.ResponseCaching _options = options.Value; } - public virtual IEnumerable CreateLookupBaseKeys(ResponseCachingContext context) + public virtual IEnumerable CreateLookupVaryByKeys(ResponseCacheContext context) { - return new string[] { CreateStorageBaseKey(context) }; - } - - public virtual IEnumerable CreateLookupVaryKeys(ResponseCachingContext context) - { - return new string[] { CreateStorageVaryKey(context) }; + return new string[] { CreateStorageVaryByKey(context) }; } // GET/PATH - public virtual string CreateStorageBaseKey(ResponseCachingContext context) + public virtual string CreateBaseKey(ResponseCacheContext context) { if (context == null) { @@ -61,7 +56,7 @@ namespace Microsoft.AspNetCore.ResponseCaching builder .Append(request.Method.ToUpperInvariant()) .Append(KeyDelimiter) - .Append(_options.CaseSensitivePaths ? request.Path.Value : request.Path.Value.ToUpperInvariant()); + .Append(_options.UseCaseSensitivePaths ? request.Path.Value : request.Path.Value.ToUpperInvariant()); return builder.ToString();; } @@ -72,22 +67,22 @@ namespace Microsoft.AspNetCore.ResponseCaching } // BaseKeyHHeaderName=HeaderValueQQueryName=QueryValue - public virtual string CreateStorageVaryKey(ResponseCachingContext context) + public virtual string CreateStorageVaryByKey(ResponseCacheContext context) { if (context == null) { throw new ArgumentNullException(nameof(context)); } - var varyRules = context.CachedVaryRules; - if (varyRules == null) + var varyByRules = context.CachedVaryByRules; + if (varyByRules == null) { - throw new InvalidOperationException($"{nameof(CachedVaryRules)} must not be null on the {nameof(ResponseCachingContext)}"); + throw new InvalidOperationException($"{nameof(CachedVaryByRules)} must not be null on the {nameof(ResponseCacheContext)}"); } - if ((StringValues.IsNullOrEmpty(varyRules.Headers) && StringValues.IsNullOrEmpty(varyRules.Params))) + if ((StringValues.IsNullOrEmpty(varyByRules.Headers) && StringValues.IsNullOrEmpty(varyByRules.Params))) { - return varyRules.VaryKeyPrefix; + return varyByRules.VaryByKeyPrefix; } var request = context.HttpContext.Request; @@ -95,17 +90,17 @@ namespace Microsoft.AspNetCore.ResponseCaching try { - // Prepend with the Guid of the CachedVaryRules - builder.Append(varyRules.VaryKeyPrefix); + // Prepend with the Guid of the CachedVaryByRules + builder.Append(varyByRules.VaryByKeyPrefix); // Vary by headers - if (varyRules?.Headers.Count > 0) + if (varyByRules?.Headers.Count > 0) { // Append a group separator for the header segment of the cache key builder.Append(KeyDelimiter) .Append('H'); - foreach (var header in varyRules.Headers) + foreach (var header in varyByRules.Headers) { builder.Append(KeyDelimiter) .Append(header) @@ -116,13 +111,13 @@ namespace Microsoft.AspNetCore.ResponseCaching } // Vary by query params - if (varyRules?.Params.Count > 0) + if (varyByRules?.Params.Count > 0) { // Append a group separator for the query parameter segment of the cache key builder.Append(KeyDelimiter) .Append('Q'); - if (varyRules.Params.Count == 1 && string.Equals(varyRules.Params[0], "*", StringComparison.Ordinal)) + if (varyByRules.Params.Count == 1 && string.Equals(varyByRules.Params[0], "*", StringComparison.Ordinal)) { // Vary by all available query params foreach (var query in context.HttpContext.Request.Query.OrderBy(q => q.Key, StringComparer.OrdinalIgnoreCase)) @@ -135,7 +130,7 @@ namespace Microsoft.AspNetCore.ResponseCaching } else { - foreach (var param in varyRules.Params) + foreach (var param in varyByRules.Params) { builder.Append(KeyDelimiter) .Append(param) diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs similarity index 74% rename from src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs rename to src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs index 8e0d6c910f..5c395ad306 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs @@ -16,63 +16,63 @@ using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.ResponseCaching { - public class ResponseCachingMiddleware + public class ResponseCacheMiddleware { private static readonly TimeSpan DefaultExpirationTimeSpan = TimeSpan.FromSeconds(10); private readonly RequestDelegate _next; - private readonly IResponseCache _cache; - private readonly ResponseCachingOptions _options; - private readonly ICacheabilityValidator _cacheabilityValidator; - private readonly ICacheKeyProvider _cacheKeyProvider; + private readonly IResponseCacheStore _store; + private readonly ResponseCacheOptions _options; + private readonly IResponseCachePolicyProvider _policyProvider; + private readonly IResponseCacheKeyProvider _keyProvider; private readonly Func _onStartingCallback; - public ResponseCachingMiddleware( + public ResponseCacheMiddleware( RequestDelegate next, - IResponseCache cache, - IOptions options, - ICacheabilityValidator cacheabilityValidator, - ICacheKeyProvider cacheKeyProvider) + IResponseCacheStore store, + IOptions options, + IResponseCachePolicyProvider policyProvider, + IResponseCacheKeyProvider keyProvider) { if (next == null) { throw new ArgumentNullException(nameof(next)); } - if (cache == null) + if (store == null) { - throw new ArgumentNullException(nameof(cache)); + throw new ArgumentNullException(nameof(store)); } if (options == null) { throw new ArgumentNullException(nameof(options)); } - if (cacheabilityValidator == null) + if (policyProvider == null) { - throw new ArgumentNullException(nameof(cacheabilityValidator)); + throw new ArgumentNullException(nameof(policyProvider)); } - if (cacheKeyProvider == null) + if (keyProvider == null) { - throw new ArgumentNullException(nameof(cacheKeyProvider)); + throw new ArgumentNullException(nameof(keyProvider)); } _next = next; - _cache = cache; + _store = store; _options = options.Value; - _cacheabilityValidator = cacheabilityValidator; - _cacheKeyProvider = cacheKeyProvider; + _policyProvider = policyProvider; + _keyProvider = keyProvider; _onStartingCallback = state => { - OnResponseStarting((ResponseCachingContext)state); + OnResponseStarting((ResponseCacheContext)state); return TaskCache.CompletedTask; }; } public async Task Invoke(HttpContext httpContext) { - var context = new ResponseCachingContext(httpContext); + var context = new ResponseCacheContext(httpContext); // Should we attempt any caching logic? - if (_cacheabilityValidator.IsRequestCacheable(context)) + if (_policyProvider.IsRequestCacheable(context)) { // Can this request be served from cache? if (await TryServeFromCacheAsync(context)) @@ -94,7 +94,7 @@ namespace Microsoft.AspNetCore.ResponseCaching OnResponseStarting(context); // Finalize the cache entry - FinalizeCachingBody(context); + FinalizeCacheBody(context); } finally { @@ -108,7 +108,7 @@ namespace Microsoft.AspNetCore.ResponseCaching } } - internal async Task TryServeCachedResponseAsync(ResponseCachingContext context, CachedResponse cachedResponse) + internal async Task TryServeCachedResponseAsync(ResponseCacheContext context, CachedResponse cachedResponse) { context.CachedResponse = cachedResponse; context.CachedResponseHeaders = new ResponseHeaders(cachedResponse.Headers); @@ -116,7 +116,7 @@ namespace Microsoft.AspNetCore.ResponseCaching var cachedEntryAge = context.ResponseTime - context.CachedResponse.Created; context.CachedEntryAge = cachedEntryAge > TimeSpan.Zero ? cachedEntryAge : TimeSpan.Zero; - if (_cacheabilityValidator.IsCachedEntryFresh(context)) + if (_policyProvider.IsCachedEntryFresh(context)) { // Check conditional request rules if (ConditionalRequestSatisfied(context)) @@ -136,7 +136,7 @@ namespace Microsoft.AspNetCore.ResponseCaching response.Headers[HeaderNames.Age] = context.CachedEntryAge.TotalSeconds.ToString("F0", CultureInfo.InvariantCulture); var body = context.CachedResponse.Body ?? - ((CachedResponseBody)_cache.Get(context.CachedResponse.BodyKeyPrefix))?.Body; + ((CachedResponseBody)_store.Get(context.CachedResponse.BodyKeyPrefix))?.Body; // If the body is not found, something went wrong. if (body == null) @@ -166,32 +166,30 @@ namespace Microsoft.AspNetCore.ResponseCaching return false; } - internal async Task TryServeFromCacheAsync(ResponseCachingContext context) + internal async Task TryServeFromCacheAsync(ResponseCacheContext context) { - foreach (var baseKey in _cacheKeyProvider.CreateLookupBaseKeys(context)) + context.BaseKey = _keyProvider.CreateBaseKey(context); + var cacheEntry = _store.Get(context.BaseKey); + + if (cacheEntry is CachedVaryByRules) { - var cacheEntry = _cache.Get(baseKey); + // Request contains vary rules, recompute key(s) and try again + context.CachedVaryByRules = (CachedVaryByRules)cacheEntry; - if (cacheEntry is CachedVaryRules) + foreach (var varyKey in _keyProvider.CreateLookupVaryByKeys(context)) { - // Request contains vary rules, recompute key(s) and try again - context.CachedVaryRules = (CachedVaryRules)cacheEntry; + cacheEntry = _store.Get(varyKey); - foreach (var varyKey in _cacheKeyProvider.CreateLookupVaryKeys(context)) + if (cacheEntry is CachedResponse && await TryServeCachedResponseAsync(context, (CachedResponse)cacheEntry)) { - cacheEntry = _cache.Get(varyKey); - - if (cacheEntry is CachedResponse && await TryServeCachedResponseAsync(context, (CachedResponse)cacheEntry)) - { - return true; - } + return true; } } + } - if (cacheEntry is CachedResponse && await TryServeCachedResponseAsync(context, (CachedResponse)cacheEntry)) - { - return true; - } + if (cacheEntry is CachedResponse && await TryServeCachedResponseAsync(context, (CachedResponse)cacheEntry)) + { + return true; } @@ -204,17 +202,17 @@ namespace Microsoft.AspNetCore.ResponseCaching return false; } - internal void FinalizeCachingHeaders(ResponseCachingContext context) + internal void FinalizeCacheHeaders(ResponseCacheContext context) { - if (_cacheabilityValidator.IsResponseCacheable(context)) + if (_policyProvider.IsResponseCacheable(context)) { context.ShouldCacheResponse = true; - context.StorageBaseKey = _cacheKeyProvider.CreateStorageBaseKey(context); + context.BaseKey = _keyProvider.CreateBaseKey(context); // Create the cache entry now var response = context.HttpContext.Response; var varyHeaderValue = response.Headers[HeaderNames.Vary]; - var varyParamsValue = context.HttpContext.GetResponseCachingFeature()?.VaryParams ?? StringValues.Empty; + var varyParamsValue = context.HttpContext.GetResponseCacheFeature()?.VaryByParams ?? StringValues.Empty; context.CachedResponseValidFor = context.ResponseCacheControlHeaderValue.SharedMaxAge ?? context.ResponseCacheControlHeaderValue.MaxAge ?? (context.TypedResponseHeaders.Expires - context.ResponseTime) ?? @@ -228,21 +226,21 @@ namespace Microsoft.AspNetCore.ResponseCaching var normalizedVaryParamsValue = GetNormalizedStringValues(varyParamsValue); // Update vary rules if they are different - if (context.CachedVaryRules == null || - !StringValues.Equals(context.CachedVaryRules.Params, normalizedVaryParamsValue) || - !StringValues.Equals(context.CachedVaryRules.Headers, normalizedVaryHeaderValue)) + if (context.CachedVaryByRules == null || + !StringValues.Equals(context.CachedVaryByRules.Params, normalizedVaryParamsValue) || + !StringValues.Equals(context.CachedVaryByRules.Headers, normalizedVaryHeaderValue)) { - context.CachedVaryRules = new CachedVaryRules + context.CachedVaryByRules = new CachedVaryByRules { - VaryKeyPrefix = FastGuid.NewGuid().IdString, + VaryByKeyPrefix = FastGuid.NewGuid().IdString, Headers = normalizedVaryHeaderValue, Params = normalizedVaryParamsValue }; - _cache.Set(context.StorageBaseKey, context.CachedVaryRules, context.CachedResponseValidFor); + _store.Set(context.BaseKey, context.CachedVaryByRules, context.CachedResponseValidFor); } - context.StorageVaryKey = _cacheKeyProvider.CreateStorageVaryKey(context); + context.StorageVaryKey = _keyProvider.CreateStorageVaryByKey(context); } // Ensure date header is set @@ -273,7 +271,7 @@ namespace Microsoft.AspNetCore.ResponseCaching } } - internal void FinalizeCachingBody(ResponseCachingContext context) + internal void FinalizeCacheBody(ResponseCacheContext context) { if (context.ShouldCacheResponse && context.ResponseCacheStream.BufferingEnabled && @@ -283,36 +281,36 @@ namespace Microsoft.AspNetCore.ResponseCaching if (context.ResponseCacheStream.BufferedStream.Length >= _options.MinimumSplitBodySize) { // Store response and response body separately - _cache.Set(context.StorageVaryKey ?? context.StorageBaseKey, context.CachedResponse, context.CachedResponseValidFor); + _store.Set(context.StorageVaryKey ?? context.BaseKey, context.CachedResponse, context.CachedResponseValidFor); var cachedResponseBody = new CachedResponseBody() { Body = context.ResponseCacheStream.BufferedStream.ToArray() }; - _cache.Set(context.CachedResponse.BodyKeyPrefix, cachedResponseBody, context.CachedResponseValidFor); + _store.Set(context.CachedResponse.BodyKeyPrefix, cachedResponseBody, context.CachedResponseValidFor); } else { // Store response and response body together context.CachedResponse.Body = context.ResponseCacheStream.BufferedStream.ToArray(); - _cache.Set(context.StorageVaryKey ?? context.StorageBaseKey, context.CachedResponse, context.CachedResponseValidFor); + _store.Set(context.StorageVaryKey ?? context.BaseKey, context.CachedResponse, context.CachedResponseValidFor); } } } - internal void OnResponseStarting(ResponseCachingContext context) + internal void OnResponseStarting(ResponseCacheContext context) { if (!context.ResponseStarted) { context.ResponseStarted = true; context.ResponseTime = _options.SystemClock.UtcNow; - FinalizeCachingHeaders(context); + FinalizeCacheHeaders(context); } } - internal void ShimResponseStream(ResponseCachingContext context) + internal void ShimResponseStream(ResponseCacheContext context) { // TODO: Consider caching large responses on disk and serving them from there. @@ -329,10 +327,10 @@ namespace Microsoft.AspNetCore.ResponseCaching } // TODO: Move this temporary interface with endpoint to HttpAbstractions - context.HttpContext.AddResponseCachingFeature(); + context.HttpContext.AddResponseCacheFeature(); } - internal static void UnshimResponseStream(ResponseCachingContext context) + internal static void UnshimResponseStream(ResponseCacheContext context) { // Unshim response stream context.HttpContext.Response.Body = context.OriginalResponseStream; @@ -341,10 +339,10 @@ namespace Microsoft.AspNetCore.ResponseCaching context.HttpContext.Features.Set(context.OriginalSendFileFeature); // TODO: Move this temporary interface with endpoint to HttpAbstractions - context.HttpContext.RemoveResponseCachingFeature(); + context.HttpContext.RemoveResponseCacheFeature(); } - internal static bool ConditionalRequestSatisfied(ResponseCachingContext context) + internal static bool ConditionalRequestSatisfied(ResponseCacheContext context) { var cachedResponseHeaders = context.CachedResponseHeaders; var ifNoneMatchHeader = context.TypedRequestHeaders.IfNoneMatch; diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingOptions.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheOptions.cs similarity index 91% rename from src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingOptions.cs rename to src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheOptions.cs index 7630237ec5..db9bae7575 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingOptions.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheOptions.cs @@ -6,7 +6,7 @@ using Microsoft.AspNetCore.ResponseCaching.Internal; namespace Microsoft.AspNetCore.Builder { - public class ResponseCachingOptions + public class ResponseCacheOptions { /// /// The largest cacheable size for the response body in bytes. The default is set to 1 MB. @@ -16,7 +16,7 @@ namespace Microsoft.AspNetCore.Builder /// /// true if request paths are case-sensitive; otherwise false. The default is to treat paths as case-insensitive. /// - public bool CaseSensitivePaths { get; set; } = false; + public bool UseCaseSensitivePaths { get; set; } = false; /// /// The smallest size in bytes for which the headers and body of the response will be stored separately. The default is set to 70 KB. diff --git a/src/Microsoft.AspNetCore.ResponseCaching/CacheabilityValidator.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachePolicyProvider.cs similarity index 95% rename from src/Microsoft.AspNetCore.ResponseCaching/CacheabilityValidator.cs rename to src/Microsoft.AspNetCore.ResponseCaching/ResponseCachePolicyProvider.cs index fa86e766e7..f92527be20 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/CacheabilityValidator.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachePolicyProvider.cs @@ -3,17 +3,16 @@ using System; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Headers; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.ResponseCaching { - public class CacheabilityValidator : ICacheabilityValidator + public class ResponseCachePolicyProvider : IResponseCachePolicyProvider { private static readonly CacheControlHeaderValue EmptyCacheControl = new CacheControlHeaderValue(); - public virtual bool IsRequestCacheable(ResponseCachingContext context) + public virtual bool IsRequestCacheable(ResponseCacheContext context) { // Verify the method // TODO: RFC lists POST as a cacheable method when explicit freshness information is provided, but this is not widely implemented. Will revisit. @@ -57,7 +56,7 @@ namespace Microsoft.AspNetCore.ResponseCaching return true; } - public virtual bool IsResponseCacheable(ResponseCachingContext context) + public virtual bool IsResponseCacheable(ResponseCacheContext context) { // Only cache pages explicitly marked with public // TODO: Consider caching responses that are not marked as public but otherwise cacheable? @@ -150,7 +149,7 @@ namespace Microsoft.AspNetCore.ResponseCaching return true; } - public virtual bool IsCachedEntryFresh(ResponseCachingContext context) + public virtual bool IsCachedEntryFresh(ResponseCacheContext context) { var age = context.CachedEntryAge; var cachedControlHeaders = context.CachedResponseHeaders.CacheControl ?? EmptyCacheControl; diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheEntrySerializerTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheEntrySerializerTests.cs index a2b1269855..a88caa995c 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheEntrySerializerTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheEntrySerializerTests.cs @@ -77,67 +77,67 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public void RoundTrip_CachedVaryRule_EmptyRules_Succeeds() + public void RoundTrip_CachedVaryByRule_EmptyRules_Succeeds() { - var cachedVaryRule = new CachedVaryRules() + var cachedVaryByRule = new CachedVaryByRules() { - VaryKeyPrefix = FastGuid.NewGuid().IdString + VaryByKeyPrefix = FastGuid.NewGuid().IdString }; - AssertCachedVaryRuleEqual(cachedVaryRule, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRule))); + AssertCachedVaryByRuleEqual(cachedVaryByRule, (CachedVaryByRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryByRule))); } [Fact] - public void RoundTrip_CachedVaryRule_HeadersOnly_Succeeds() + public void RoundTrip_CachedVaryByRule_HeadersOnly_Succeeds() { var headers = new[] { "headerA", "headerB" }; - var cachedVaryRule = new CachedVaryRules() + var cachedVaryByRule = new CachedVaryByRules() { - VaryKeyPrefix = FastGuid.NewGuid().IdString, + VaryByKeyPrefix = FastGuid.NewGuid().IdString, Headers = headers }; - AssertCachedVaryRuleEqual(cachedVaryRule, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRule))); + AssertCachedVaryByRuleEqual(cachedVaryByRule, (CachedVaryByRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryByRule))); } [Fact] - public void RoundTrip_CachedVaryRule_ParamsOnly_Succeeds() + public void RoundTrip_CachedVaryByRule_ParamsOnly_Succeeds() { var param = new[] { "paramA", "paramB" }; - var cachedVaryRule = new CachedVaryRules() + var cachedVaryByRule = new CachedVaryByRules() { - VaryKeyPrefix = FastGuid.NewGuid().IdString, + VaryByKeyPrefix = FastGuid.NewGuid().IdString, Params = param }; - AssertCachedVaryRuleEqual(cachedVaryRule, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRule))); + AssertCachedVaryByRuleEqual(cachedVaryByRule, (CachedVaryByRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryByRule))); } [Fact] - public void RoundTrip_CachedVaryRule_HeadersAndParams_Succeeds() + public void RoundTrip_CachedVaryByRule_HeadersAndParams_Succeeds() { var headers = new[] { "headerA", "headerB" }; var param = new[] { "paramA", "paramB" }; - var cachedVaryRule = new CachedVaryRules() + var cachedVaryByRule = new CachedVaryByRules() { - VaryKeyPrefix = FastGuid.NewGuid().IdString, + VaryByKeyPrefix = FastGuid.NewGuid().IdString, Headers = headers, Params = param }; - AssertCachedVaryRuleEqual(cachedVaryRule, (CachedVaryRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryRule))); + AssertCachedVaryByRuleEqual(cachedVaryByRule, (CachedVaryByRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryByRule))); } [Fact] public void Deserialize_InvalidEntries_ReturnsNull() { var headers = new[] { "headerA", "headerB" }; - var cachedVaryRule = new CachedVaryRules() + var cachedVaryByRule = new CachedVaryByRules() { - VaryKeyPrefix = FastGuid.NewGuid().IdString, + VaryByKeyPrefix = FastGuid.NewGuid().IdString, Headers = headers }; - var serializedEntry = CacheEntrySerializer.Serialize(cachedVaryRule); + var serializedEntry = CacheEntrySerializer.Serialize(cachedVaryByRule); Array.Reverse(serializedEntry); Assert.Null(CacheEntrySerializer.Deserialize(serializedEntry)); @@ -170,11 +170,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } } - private static void AssertCachedVaryRuleEqual(CachedVaryRules expected, CachedVaryRules actual) + private static void AssertCachedVaryByRuleEqual(CachedVaryByRules expected, CachedVaryByRules actual) { Assert.NotNull(actual); Assert.NotNull(expected); - Assert.Equal(expected.VaryKeyPrefix, actual.VaryKeyPrefix); + Assert.Equal(expected.VaryByKeyPrefix, actual.VaryByKeyPrefix); Assert.Equal(expected.Headers, actual.Headers); Assert.Equal(expected.Params, actual.Params); } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/HttpContextInternalExtensionTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/HttpContextInternalExtensionTests.cs index 5bf1cbf5b7..8efd416d16 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/HttpContextInternalExtensionTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/HttpContextInternalExtensionTests.cs @@ -11,15 +11,15 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests public class HttpContextInternalExtensionTests { [Fact] - public void AddingSecondResponseCachingFeature_Throws() + public void AddingSecondResponseCacheFeature_Throws() { var httpContext = new DefaultHttpContext(); // Should not throw - httpContext.AddResponseCachingFeature(); + httpContext.AddResponseCacheFeature(); // Should throw - Assert.ThrowsAny(() => httpContext.AddResponseCachingFeature()); + Assert.ThrowsAny(() => httpContext.AddResponseCacheFeature()); } } } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/KeyProviderTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheKeyProviderTests.cs similarity index 57% rename from test/Microsoft.AspNetCore.ResponseCaching.Tests/KeyProviderTests.cs rename to test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheKeyProviderTests.cs index 1239b2c901..621c9fb5fd 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/KeyProviderTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheKeyProviderTests.cs @@ -9,12 +9,12 @@ using Xunit; namespace Microsoft.AspNetCore.ResponseCaching.Tests { - public class DefaultKeyProviderTests + public class ResponseCacheKeyProviderTests { private static readonly char KeyDelimiter = '\x1e'; [Fact] - public void DefaultKeyProvider_CreateStorageBaseKey_IncludesOnlyNormalizedMethodAndPath() + public void ResponseCacheKeyProvider_CreateStorageBaseKey_IncludesOnlyNormalizedMethodAndPath() { var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); var context = TestUtils.CreateTestContext(); @@ -25,142 +25,142 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.HttpContext.Request.PathBase = "/pathBase"; context.HttpContext.Request.QueryString = new QueryString("?query.Key=a&query.Value=b"); - Assert.Equal($"HEAD{KeyDelimiter}/PATH/SUBPATH", cacheKeyProvider.CreateStorageBaseKey(context)); + Assert.Equal($"HEAD{KeyDelimiter}/PATH/SUBPATH", cacheKeyProvider.CreateBaseKey(context)); } [Fact] - public void DefaultKeyProvider_CreateStorageBaseKey_CaseInsensitivePath_NormalizesPath() + public void ResponseCacheKeyProvider_CreateStorageBaseKey_CaseInsensitivePath_NormalizesPath() { - var cacheKeyProvider = TestUtils.CreateTestKeyProvider(new ResponseCachingOptions() + var cacheKeyProvider = TestUtils.CreateTestKeyProvider(new ResponseCacheOptions() { - CaseSensitivePaths = false + UseCaseSensitivePaths = false }); var context = TestUtils.CreateTestContext(); context.HttpContext.Request.Method = "GET"; context.HttpContext.Request.Path = "/Path"; - Assert.Equal($"GET{KeyDelimiter}/PATH", cacheKeyProvider.CreateStorageBaseKey(context)); + Assert.Equal($"GET{KeyDelimiter}/PATH", cacheKeyProvider.CreateBaseKey(context)); } [Fact] - public void DefaultKeyProvider_CreateStorageBaseKey_CaseSensitivePath_PreservesPathCase() + public void ResponseCacheKeyProvider_CreateStorageBaseKey_CaseSensitivePath_PreservesPathCase() { - var cacheKeyProvider = TestUtils.CreateTestKeyProvider(new ResponseCachingOptions() + var cacheKeyProvider = TestUtils.CreateTestKeyProvider(new ResponseCacheOptions() { - CaseSensitivePaths = true + UseCaseSensitivePaths = true }); var context = TestUtils.CreateTestContext(); context.HttpContext.Request.Method = "GET"; context.HttpContext.Request.Path = "/Path"; - Assert.Equal($"GET{KeyDelimiter}/Path", cacheKeyProvider.CreateStorageBaseKey(context)); + Assert.Equal($"GET{KeyDelimiter}/Path", cacheKeyProvider.CreateBaseKey(context)); } [Fact] - public void DefaultKeyProvider_CreateStorageVaryKey_Throws_IfVaryRulesIsNull() + public void ResponseCacheKeyProvider_CreateStorageVaryByKey_Throws_IfVaryByRulesIsNull() { var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); var context = TestUtils.CreateTestContext(); - Assert.Throws(() => cacheKeyProvider.CreateStorageVaryKey(context)); + Assert.Throws(() => cacheKeyProvider.CreateStorageVaryByKey(context)); } [Fact] - public void DefaultKeyProvider_CreateStorageVaryKey_ReturnsCachedVaryGuid_IfVaryRulesIsEmpty() + public void ResponseCacheKeyProvider_CreateStorageVaryKey_ReturnsCachedVaryByGuid_IfVaryByRulesIsEmpty() { var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); var context = TestUtils.CreateTestContext(); - context.CachedVaryRules = new CachedVaryRules() + context.CachedVaryByRules = new CachedVaryByRules() { - VaryKeyPrefix = FastGuid.NewGuid().IdString + VaryByKeyPrefix = FastGuid.NewGuid().IdString }; - Assert.Equal($"{context.CachedVaryRules.VaryKeyPrefix}", cacheKeyProvider.CreateStorageVaryKey(context)); + Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}", cacheKeyProvider.CreateStorageVaryByKey(context)); } [Fact] - public void DefaultKeyProvider_CreateStorageVaryKey_IncludesListedHeadersOnly() + public void ResponseCacheKeyProvider_CreateStorageVaryKey_IncludesListedHeadersOnly() { var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); var context = TestUtils.CreateTestContext(); context.HttpContext.Request.Headers["HeaderA"] = "ValueA"; context.HttpContext.Request.Headers["HeaderB"] = "ValueB"; - context.CachedVaryRules = new CachedVaryRules() + context.CachedVaryByRules = new CachedVaryByRules() { Headers = new string[] { "HeaderA", "HeaderC" } }; - Assert.Equal($"{context.CachedVaryRules.VaryKeyPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=", - cacheKeyProvider.CreateStorageVaryKey(context)); + Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=", + cacheKeyProvider.CreateStorageVaryByKey(context)); } [Fact] - public void DefaultKeyProvider_CreateStorageVaryKey_IncludesListedParamsOnly() + public void ResponseCacheKeyProvider_CreateStorageVaryKey_IncludesListedParamsOnly() { var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); var context = TestUtils.CreateTestContext(); context.HttpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB"); - context.CachedVaryRules = new CachedVaryRules() + context.CachedVaryByRules = new CachedVaryByRules() { - VaryKeyPrefix = FastGuid.NewGuid().IdString, + VaryByKeyPrefix = FastGuid.NewGuid().IdString, Params = new string[] { "ParamA", "ParamC" } }; - Assert.Equal($"{context.CachedVaryRules.VaryKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=", - cacheKeyProvider.CreateStorageVaryKey(context)); + Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=", + cacheKeyProvider.CreateStorageVaryByKey(context)); } [Fact] - public void DefaultKeyProvider_CreateStorageVaryKey_IncludesParams_ParamNameCaseInsensitive_UseParamCasing() + public void ResponseCacheKeyProvider_CreateStorageVaryKey_IncludesParams_ParamNameCaseInsensitive_UseParamCasing() { var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); var context = TestUtils.CreateTestContext(); context.HttpContext.Request.QueryString = new QueryString("?parama=ValueA¶mB=ValueB"); - context.CachedVaryRules = new CachedVaryRules() + context.CachedVaryByRules = new CachedVaryByRules() { - VaryKeyPrefix = FastGuid.NewGuid().IdString, + VaryByKeyPrefix = FastGuid.NewGuid().IdString, Params = new string[] { "ParamA", "ParamC" } }; - Assert.Equal($"{context.CachedVaryRules.VaryKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=", - cacheKeyProvider.CreateStorageVaryKey(context)); + Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=", + cacheKeyProvider.CreateStorageVaryByKey(context)); } [Fact] - public void DefaultKeyProvider_CreateStorageVaryKey_IncludesAllQueryParamsGivenAsterisk() + public void ResponseCacheKeyProvider_CreateStorageVaryKey_IncludesAllQueryParamsGivenAsterisk() { var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); var context = TestUtils.CreateTestContext(); context.HttpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB"); - context.CachedVaryRules = new CachedVaryRules() + context.CachedVaryByRules = new CachedVaryByRules() { - VaryKeyPrefix = FastGuid.NewGuid().IdString, + VaryByKeyPrefix = FastGuid.NewGuid().IdString, Params = new string[] { "*" } }; // To support case insensitivity, all param keys are converted to upper case. // Explicit params uses the casing specified in the setting. - Assert.Equal($"{context.CachedVaryRules.VaryKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}PARAMA=ValueA{KeyDelimiter}PARAMB=ValueB", - cacheKeyProvider.CreateStorageVaryKey(context)); + Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}PARAMA=ValueA{KeyDelimiter}PARAMB=ValueB", + cacheKeyProvider.CreateStorageVaryByKey(context)); } [Fact] - public void DefaultKeyProvider_CreateStorageVaryKey_IncludesListedHeadersAndParams() + public void ResponseCacheKeyProvider_CreateStorageVaryKey_IncludesListedHeadersAndParams() { var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); var context = TestUtils.CreateTestContext(); context.HttpContext.Request.Headers["HeaderA"] = "ValueA"; context.HttpContext.Request.Headers["HeaderB"] = "ValueB"; context.HttpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB"); - context.CachedVaryRules = new CachedVaryRules() + context.CachedVaryByRules = new CachedVaryByRules() { - VaryKeyPrefix = FastGuid.NewGuid().IdString, + VaryByKeyPrefix = FastGuid.NewGuid().IdString, Headers = new string[] { "HeaderA", "HeaderC" }, Params = new string[] { "ParamA", "ParamC" } }; - Assert.Equal($"{context.CachedVaryRules.VaryKeyPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC={KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=", - cacheKeyProvider.CreateStorageVaryKey(context)); + Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC={KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=", + cacheKeyProvider.CreateStorageVaryByKey(context)); } } } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheMiddlewareTests.cs similarity index 66% rename from test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs rename to test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheMiddlewareTests.cs index b480724d4b..a329104ed1 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheMiddlewareTests.cs @@ -14,13 +14,13 @@ using Xunit; namespace Microsoft.AspNetCore.ResponseCaching.Tests { - public class ResponseCachingMiddlewareTests + public class ResponseCacheMiddlewareTests { [Fact] public async Task TryServeFromCacheAsync_OnlyIfCached_Serves504() { - var cache = new TestResponseCache(); - var middleware = TestUtils.CreateTestMiddleware(responseCache: cache, cacheKeyProvider: new TestKeyProvider()); + var store = new TestResponseCacheStore(); + var middleware = TestUtils.CreateTestMiddleware(store: store, keyProvider: new TestResponseCacheKeyProvider()); var context = TestUtils.CreateTestContext(); context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() { @@ -34,23 +34,23 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async Task TryServeFromCacheAsync_CachedResponseNotFound_Fails() { - var cache = new TestResponseCache(); - var middleware = TestUtils.CreateTestMiddleware(responseCache: cache, cacheKeyProvider: new TestKeyProvider(new[] { "BaseKey", "BaseKey2" })); + var store = new TestResponseCacheStore(); + var middleware = TestUtils.CreateTestMiddleware(store: store, keyProvider: new TestResponseCacheKeyProvider("BaseKey")); var context = TestUtils.CreateTestContext(); Assert.False(await middleware.TryServeFromCacheAsync(context)); - Assert.Equal(2, cache.GetCount); + Assert.Equal(1, store.GetCount); } [Fact] public async Task TryServeFromCacheAsync_CachedResponseFound_Succeeds() { - var cache = new TestResponseCache(); - var middleware = TestUtils.CreateTestMiddleware(responseCache: cache, cacheKeyProvider: new TestKeyProvider(new[] { "BaseKey", "BaseKey2" })); + var store = new TestResponseCacheStore(); + var middleware = TestUtils.CreateTestMiddleware(store: store, keyProvider: new TestResponseCacheKeyProvider("BaseKey")); var context = TestUtils.CreateTestContext(); - cache.Set( - "BaseKey2", + store.Set( + "BaseKey", new CachedResponse() { Body = new byte[0] @@ -58,38 +58,38 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests TimeSpan.Zero); Assert.True(await middleware.TryServeFromCacheAsync(context)); - Assert.Equal(2, cache.GetCount); + Assert.Equal(1, store.GetCount); } [Fact] - public async Task TryServeFromCacheAsync_VaryRuleFound_CachedResponseNotFound_Fails() + public async Task TryServeFromCacheAsync_VaryByRuleFound_CachedResponseNotFound_Fails() { - var cache = new TestResponseCache(); - var middleware = TestUtils.CreateTestMiddleware(responseCache: cache, cacheKeyProvider: new TestKeyProvider(new[] { "BaseKey", "BaseKey2" })); + var store = new TestResponseCacheStore(); + var middleware = TestUtils.CreateTestMiddleware(store: store, keyProvider: new TestResponseCacheKeyProvider("BaseKey")); var context = TestUtils.CreateTestContext(); - cache.Set( - "BaseKey2", - new CachedVaryRules(), + store.Set( + "BaseKey", + new CachedVaryByRules(), TimeSpan.Zero); Assert.False(await middleware.TryServeFromCacheAsync(context)); - Assert.Equal(2, cache.GetCount); + Assert.Equal(1, store.GetCount); } [Fact] - public async Task TryServeFromCacheAsync_VaryRuleFound_CachedResponseFound_Succeeds() + public async Task TryServeFromCacheAsync_VaryByRuleFound_CachedResponseFound_Succeeds() { - var cache = new TestResponseCache(); - var middleware = TestUtils.CreateTestMiddleware(responseCache: cache, cacheKeyProvider: new TestKeyProvider(new[] { "BaseKey", "BaseKey2" }, new[] { "VaryKey", "VaryKey2" })); + var store = new TestResponseCacheStore(); + var middleware = TestUtils.CreateTestMiddleware(store: store, keyProvider: new TestResponseCacheKeyProvider("BaseKey", new[] { "VaryKey", "VaryKey2" })); var context = TestUtils.CreateTestContext(); - cache.Set( - "BaseKey2", - new CachedVaryRules(), + store.Set( + "BaseKey", + new CachedVaryByRules(), TimeSpan.Zero); - cache.Set( - "BaseKey2VaryKey2", + store.Set( + "BaseKeyVaryKey2", new CachedResponse() { Body = new byte[0] @@ -97,7 +97,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests TimeSpan.Zero); Assert.True(await middleware.TryServeFromCacheAsync(context)); - Assert.Equal(6, cache.GetCount); + Assert.Equal(3, store.GetCount); } [Fact] @@ -106,7 +106,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var context = TestUtils.CreateTestContext(); context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); - Assert.False(ResponseCachingMiddleware.ConditionalRequestSatisfied(context)); + Assert.False(ResponseCacheMiddleware.ConditionalRequestSatisfied(context)); } [Fact] @@ -120,15 +120,15 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests // Verify modifications in the past succeeds context.CachedResponseHeaders.Date = utcNow - TimeSpan.FromSeconds(10); - Assert.True(ResponseCachingMiddleware.ConditionalRequestSatisfied(context)); + Assert.True(ResponseCacheMiddleware.ConditionalRequestSatisfied(context)); // Verify modifications at present succeeds context.CachedResponseHeaders.Date = utcNow; - Assert.True(ResponseCachingMiddleware.ConditionalRequestSatisfied(context)); + Assert.True(ResponseCacheMiddleware.ConditionalRequestSatisfied(context)); // Verify modifications in the future fails context.CachedResponseHeaders.Date = utcNow + TimeSpan.FromSeconds(10); - Assert.False(ResponseCachingMiddleware.ConditionalRequestSatisfied(context)); + Assert.False(ResponseCacheMiddleware.ConditionalRequestSatisfied(context)); } [Fact] @@ -143,17 +143,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests // Verify modifications in the past succeeds context.CachedResponseHeaders.Date = utcNow + TimeSpan.FromSeconds(10); context.CachedResponseHeaders.LastModified = utcNow - TimeSpan.FromSeconds(10); - Assert.True(ResponseCachingMiddleware.ConditionalRequestSatisfied(context)); + Assert.True(ResponseCacheMiddleware.ConditionalRequestSatisfied(context)); // Verify modifications at present context.CachedResponseHeaders.Date = utcNow + TimeSpan.FromSeconds(10); context.CachedResponseHeaders.LastModified = utcNow; - Assert.True(ResponseCachingMiddleware.ConditionalRequestSatisfied(context)); + Assert.True(ResponseCacheMiddleware.ConditionalRequestSatisfied(context)); // Verify modifications in the future fails context.CachedResponseHeaders.Date = utcNow - TimeSpan.FromSeconds(10); context.CachedResponseHeaders.LastModified = utcNow + TimeSpan.FromSeconds(10); - Assert.False(ResponseCachingMiddleware.ConditionalRequestSatisfied(context)); + Assert.False(ResponseCacheMiddleware.ConditionalRequestSatisfied(context)); } [Fact] @@ -168,7 +168,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.CachedResponseHeaders.LastModified = utcNow + TimeSpan.FromSeconds(10); context.TypedRequestHeaders.IfNoneMatch = new List(new[] { EntityTagHeaderValue.Any }); - Assert.True(ResponseCachingMiddleware.ConditionalRequestSatisfied(context)); + Assert.True(ResponseCacheMiddleware.ConditionalRequestSatisfied(context)); } [Fact] @@ -183,7 +183,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.CachedResponseHeaders.LastModified = utcNow - TimeSpan.FromSeconds(10); context.TypedRequestHeaders.IfNoneMatch = new List(new[] { new EntityTagHeaderValue("\"E1\"") }); - Assert.False(ResponseCachingMiddleware.ConditionalRequestSatisfied(context)); + Assert.False(ResponseCacheMiddleware.ConditionalRequestSatisfied(context)); } [Fact] @@ -194,7 +194,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.TypedRequestHeaders.IfNoneMatch = new List(new[] { new EntityTagHeaderValue("\"E1\"") }); - Assert.False(ResponseCachingMiddleware.ConditionalRequestSatisfied(context)); + Assert.False(ResponseCacheMiddleware.ConditionalRequestSatisfied(context)); } [Fact] @@ -208,7 +208,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.TypedRequestHeaders.IfNoneMatch = new List(new[] { new EntityTagHeaderValue("\"E1\"") }); - Assert.True(ResponseCachingMiddleware.ConditionalRequestSatisfied(context)); + Assert.True(ResponseCacheMiddleware.ConditionalRequestSatisfied(context)); } [Fact] @@ -222,27 +222,27 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.TypedRequestHeaders.IfNoneMatch = new List(new[] { new EntityTagHeaderValue("\"E1\"") }); - Assert.False(ResponseCachingMiddleware.ConditionalRequestSatisfied(context)); + Assert.False(ResponseCacheMiddleware.ConditionalRequestSatisfied(context)); } [Fact] - public void FinalizeCachingHeaders_DoNotUpdateShouldCacheResponse_IfResponseIsNotCacheable() + public void FinalizeCacheHeaders_DoNotUpdateShouldCacheResponse_IfResponseIsNotCacheable() { - var middleware = TestUtils.CreateTestMiddleware(cacheabilityValidator: new CacheabilityValidator()); + var middleware = TestUtils.CreateTestMiddleware(policyProvider: new ResponseCachePolicyProvider()); var context = TestUtils.CreateTestContext(); Assert.False(context.ShouldCacheResponse); middleware.ShimResponseStream(context); - middleware.FinalizeCachingHeaders(context); + middleware.FinalizeCacheHeaders(context); Assert.False(context.ShouldCacheResponse); } [Fact] - public void FinalizeCachingHeaders_UpdateShouldCacheResponse_IfResponseIsCacheable() + public void FinalizeCacheHeaders_UpdateShouldCacheResponse_IfResponseIsCacheable() { - var middleware = TestUtils.CreateTestMiddleware(cacheabilityValidator: new CacheabilityValidator()); + var middleware = TestUtils.CreateTestMiddleware(policyProvider: new ResponseCachePolicyProvider()); var context = TestUtils.CreateTestContext(); context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() { @@ -251,24 +251,24 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Assert.False(context.ShouldCacheResponse); - middleware.FinalizeCachingHeaders(context); + middleware.FinalizeCacheHeaders(context); Assert.True(context.ShouldCacheResponse); } [Fact] - public void FinalizeCachingHeaders_DefaultResponseValidity_Is10Seconds() + public void FinalizeCacheHeaders_DefaultResponseValidity_Is10Seconds() { var middleware = TestUtils.CreateTestMiddleware(); var context = TestUtils.CreateTestContext(); - middleware.FinalizeCachingHeaders(context); + middleware.FinalizeCacheHeaders(context); Assert.Equal(TimeSpan.FromSeconds(10), context.CachedResponseValidFor); } [Fact] - public void FinalizeCachingHeaders_ResponseValidity_UseExpiryIfAvailable() + public void FinalizeCacheHeaders_ResponseValidity_UseExpiryIfAvailable() { var utcNow = DateTimeOffset.MinValue; var middleware = TestUtils.CreateTestMiddleware(); @@ -277,13 +277,13 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.ResponseTime = utcNow; context.TypedResponseHeaders.Expires = utcNow + TimeSpan.FromSeconds(11); - middleware.FinalizeCachingHeaders(context); + middleware.FinalizeCacheHeaders(context); Assert.Equal(TimeSpan.FromSeconds(11), context.CachedResponseValidFor); } [Fact] - public void FinalizeCachingHeaders_ResponseValidity_UseMaxAgeIfAvailable() + public void FinalizeCacheHeaders_ResponseValidity_UseMaxAgeIfAvailable() { var middleware = TestUtils.CreateTestMiddleware(); var context = TestUtils.CreateTestContext(); @@ -295,13 +295,13 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.ResponseTime = DateTimeOffset.UtcNow; context.TypedResponseHeaders.Expires = context.ResponseTime + TimeSpan.FromSeconds(11); - middleware.FinalizeCachingHeaders(context); + middleware.FinalizeCacheHeaders(context); Assert.Equal(TimeSpan.FromSeconds(12), context.CachedResponseValidFor); } [Fact] - public void FinalizeCachingHeaders_ResponseValidity_UseSharedMaxAgeIfAvailable() + public void FinalizeCacheHeaders_ResponseValidity_UseSharedMaxAgeIfAvailable() { var middleware = TestUtils.CreateTestMiddleware(); var context = TestUtils.CreateTestContext(); @@ -314,60 +314,60 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.ResponseTime = DateTimeOffset.UtcNow; context.TypedResponseHeaders.Expires = context.ResponseTime + TimeSpan.FromSeconds(11); - middleware.FinalizeCachingHeaders(context); + middleware.FinalizeCacheHeaders(context); Assert.Equal(TimeSpan.FromSeconds(13), context.CachedResponseValidFor); } [Fact] - public void FinalizeCachingHeaders_UpdateCachedVaryRules_IfNotEquivalentToPrevious() + public void FinalizeCacheHeaders_UpdateCachedVaryByRules_IfNotEquivalentToPrevious() { - var cache = new TestResponseCache(); - var middleware = TestUtils.CreateTestMiddleware(cache); + var store = new TestResponseCacheStore(); + var middleware = TestUtils.CreateTestMiddleware(store); var context = TestUtils.CreateTestContext(); context.HttpContext.Response.Headers[HeaderNames.Vary] = new StringValues(new[] { "headerA", "HEADERB", "HEADERc" }); - context.HttpContext.AddResponseCachingFeature(); - context.HttpContext.GetResponseCachingFeature().VaryParams = new StringValues(new[] { "paramB", "PARAMAA" }); - var cachedVaryRules = new CachedVaryRules() + context.HttpContext.AddResponseCacheFeature(); + context.HttpContext.GetResponseCacheFeature().VaryByParams = new StringValues(new[] { "paramB", "PARAMAA" }); + var cachedVaryByRules = new CachedVaryByRules() { Headers = new StringValues(new[] { "HeaderA", "HeaderB" }), Params = new StringValues(new[] { "ParamA", "ParamB" }) }; - context.CachedVaryRules = cachedVaryRules; + context.CachedVaryByRules = cachedVaryByRules; - middleware.FinalizeCachingHeaders(context); + middleware.FinalizeCacheHeaders(context); - Assert.Equal(1, cache.SetCount); - Assert.NotSame(cachedVaryRules, context.CachedVaryRules); + Assert.Equal(1, store.SetCount); + Assert.NotSame(cachedVaryByRules, context.CachedVaryByRules); } [Fact] - public void FinalizeCachingHeaders_DoNotUpdateCachedVaryRules_IfEquivalentToPrevious() + public void FinalizeCacheHeaders_DoNotUpdateCachedVaryByRules_IfEquivalentToPrevious() { - var cache = new TestResponseCache(); - var middleware = TestUtils.CreateTestMiddleware(cache); + var store = new TestResponseCacheStore(); + var middleware = TestUtils.CreateTestMiddleware(store); var context = TestUtils.CreateTestContext(); context.HttpContext.Response.Headers[HeaderNames.Vary] = new StringValues(new[] { "headerA", "HEADERB" }); - context.HttpContext.AddResponseCachingFeature(); - context.HttpContext.GetResponseCachingFeature().VaryParams = new StringValues(new[] { "paramB", "PARAMA" }); - var cachedVaryRules = new CachedVaryRules() + context.HttpContext.AddResponseCacheFeature(); + context.HttpContext.GetResponseCacheFeature().VaryByParams = new StringValues(new[] { "paramB", "PARAMA" }); + var cachedVaryByRules = new CachedVaryByRules() { - VaryKeyPrefix = FastGuid.NewGuid().IdString, + VaryByKeyPrefix = FastGuid.NewGuid().IdString, Headers = new StringValues(new[] { "HEADERA", "HEADERB" }), Params = new StringValues(new[] { "PARAMA", "PARAMB" }) }; - context.CachedVaryRules = cachedVaryRules; + context.CachedVaryByRules = cachedVaryByRules; - middleware.FinalizeCachingHeaders(context); + middleware.FinalizeCacheHeaders(context); - Assert.Equal(0, cache.SetCount); - Assert.Same(cachedVaryRules, context.CachedVaryRules); + Assert.Equal(0, store.SetCount); + Assert.Same(cachedVaryByRules, context.CachedVaryByRules); } [Fact] - public void FinalizeCachingHeaders_DoNotAddDate_IfSpecified() + public void FinalizeCacheHeaders_DoNotAddDate_IfSpecified() { var utcNow = DateTimeOffset.MinValue; var middleware = TestUtils.CreateTestMiddleware(); @@ -376,13 +376,13 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Assert.Null(context.TypedResponseHeaders.Date); - middleware.FinalizeCachingHeaders(context); + middleware.FinalizeCacheHeaders(context); Assert.Equal(utcNow, context.TypedResponseHeaders.Date); } [Fact] - public void FinalizeCachingHeaders_AddsDate_IfNoneSpecified() + public void FinalizeCacheHeaders_AddsDate_IfNoneSpecified() { var utcNow = DateTimeOffset.MinValue; var middleware = TestUtils.CreateTestMiddleware(); @@ -392,29 +392,29 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Assert.Equal(utcNow, context.TypedResponseHeaders.Date); - middleware.FinalizeCachingHeaders(context); + middleware.FinalizeCacheHeaders(context); Assert.Equal(utcNow, context.TypedResponseHeaders.Date); } [Fact] - public void FinalizeCachingHeaders_StoresCachedResponse_InState() + public void FinalizeCacheHeaders_StoresCachedResponse_InState() { var middleware = TestUtils.CreateTestMiddleware(); var context = TestUtils.CreateTestContext(); Assert.Null(context.CachedResponse); - middleware.FinalizeCachingHeaders(context); + middleware.FinalizeCacheHeaders(context); Assert.NotNull(context.CachedResponse); } [Fact] - public async Task FinalizeCachingBody_StoreResponseBodySeparately_IfLargerThanLimit() + public async Task FinalizeCacheBody_StoreResponseBodySeparately_IfLargerThanLimit() { - var cache = new TestResponseCache(); - var middleware = TestUtils.CreateTestMiddleware(cache); + var store = new TestResponseCacheStore(); + var middleware = TestUtils.CreateTestMiddleware(store); var context = TestUtils.CreateTestContext(); middleware.ShimResponseStream(context); @@ -425,19 +425,19 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { BodyKeyPrefix = FastGuid.NewGuid().IdString }; - context.StorageBaseKey = "BaseKey"; + context.BaseKey = "BaseKey"; context.CachedResponseValidFor = TimeSpan.FromSeconds(10); - middleware.FinalizeCachingBody(context); + middleware.FinalizeCacheBody(context); - Assert.Equal(2, cache.SetCount); + Assert.Equal(2, store.SetCount); } [Fact] - public async Task FinalizeCachingBody_StoreResponseBodyInCachedResponse_IfSmallerThanLimit() + public async Task FinalizeCacheBody_StoreResponseBodyInCachedResponse_IfSmallerThanLimit() { - var cache = new TestResponseCache(); - var middleware = TestUtils.CreateTestMiddleware(cache); + var store = new TestResponseCacheStore(); + var middleware = TestUtils.CreateTestMiddleware(store); var context = TestUtils.CreateTestContext(); middleware.ShimResponseStream(context); @@ -448,19 +448,19 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { BodyKeyPrefix = FastGuid.NewGuid().IdString }; - context.StorageBaseKey = "BaseKey"; + context.BaseKey = "BaseKey"; context.CachedResponseValidFor = TimeSpan.FromSeconds(10); - middleware.FinalizeCachingBody(context); + middleware.FinalizeCacheBody(context); - Assert.Equal(1, cache.SetCount); + Assert.Equal(1, store.SetCount); } [Fact] - public async Task FinalizeCachingBody_StoreResponseBodySeparately_LimitIsConfigurable() + public async Task FinalizeCacheBody_StoreResponseBodySeparately_LimitIsConfigurable() { - var cache = new TestResponseCache(); - var middleware = TestUtils.CreateTestMiddleware(cache, new ResponseCachingOptions() + var store = new TestResponseCacheStore(); + var middleware = TestUtils.CreateTestMiddleware(store, new ResponseCacheOptions() { MinimumSplitBodySize = 2048 }); @@ -474,19 +474,19 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { BodyKeyPrefix = FastGuid.NewGuid().IdString }; - context.StorageBaseKey = "BaseKey"; + context.BaseKey = "BaseKey"; context.CachedResponseValidFor = TimeSpan.FromSeconds(10); - middleware.FinalizeCachingBody(context); + middleware.FinalizeCacheBody(context); - Assert.Equal(1, cache.SetCount); + Assert.Equal(1, store.SetCount); } [Fact] - public async Task FinalizeCachingBody_Cache_IfContentLengthMatches() + public async Task FinalizeCacheBody_Cache_IfContentLengthMatches() { - var cache = new TestResponseCache(); - var middleware = TestUtils.CreateTestMiddleware(cache); + var store = new TestResponseCacheStore(); + var middleware = TestUtils.CreateTestMiddleware(store); var context = TestUtils.CreateTestContext(); middleware.ShimResponseStream(context); @@ -498,19 +498,19 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { BodyKeyPrefix = FastGuid.NewGuid().IdString }; - context.StorageBaseKey = "BaseKey"; + context.BaseKey = "BaseKey"; context.CachedResponseValidFor = TimeSpan.FromSeconds(10); - middleware.FinalizeCachingBody(context); + middleware.FinalizeCacheBody(context); - Assert.Equal(1, cache.SetCount); + Assert.Equal(1, store.SetCount); } [Fact] - public async Task FinalizeCachingBody_DoNotCache_IfContentLengthMismatches() + public async Task FinalizeCacheBody_DoNotCache_IfContentLengthMismatches() { - var cache = new TestResponseCache(); - var middleware = TestUtils.CreateTestMiddleware(cache); + var store = new TestResponseCacheStore(); + var middleware = TestUtils.CreateTestMiddleware(store); var context = TestUtils.CreateTestContext(); middleware.ShimResponseStream(context); @@ -522,19 +522,19 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { BodyKeyPrefix = FastGuid.NewGuid().IdString }; - context.StorageBaseKey = "BaseKey"; + context.BaseKey = "BaseKey"; context.CachedResponseValidFor = TimeSpan.FromSeconds(10); - middleware.FinalizeCachingBody(context); + middleware.FinalizeCacheBody(context); - Assert.Equal(0, cache.SetCount); + Assert.Equal(0, store.SetCount); } [Fact] - public async Task FinalizeCachingBody_Cache_IfContentLengthAbsent() + public async Task FinalizeCacheBody_Cache_IfContentLengthAbsent() { - var cache = new TestResponseCache(); - var middleware = TestUtils.CreateTestMiddleware(cache); + var store = new TestResponseCacheStore(); + var middleware = TestUtils.CreateTestMiddleware(store); var context = TestUtils.CreateTestContext(); middleware.ShimResponseStream(context); @@ -545,12 +545,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { BodyKeyPrefix = FastGuid.NewGuid().IdString }; - context.StorageBaseKey = "BaseKey"; + context.BaseKey = "BaseKey"; context.CachedResponseValidFor = TimeSpan.FromSeconds(10); - middleware.FinalizeCachingBody(context); + middleware.FinalizeCacheBody(context); - Assert.Equal(1, cache.SetCount); + Assert.Equal(1, store.SetCount); } [Fact] @@ -559,7 +559,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var uppercaseStrings = new StringValues(new[] { "STRINGA", "STRINGB" }); var lowercaseStrings = new StringValues(new[] { "stringA", "stringB" }); - var normalizedStrings = ResponseCachingMiddleware.GetNormalizedStringValues(lowercaseStrings); + var normalizedStrings = ResponseCacheMiddleware.GetNormalizedStringValues(lowercaseStrings); Assert.Equal(uppercaseStrings, normalizedStrings); } @@ -570,9 +570,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var orderedStrings = new StringValues(new[] { "STRINGA", "STRINGB" }); var reverseOrderStrings = new StringValues(new[] { "STRINGB", "STRINGA" }); - var normalizedStrings = ResponseCachingMiddleware.GetNormalizedStringValues(reverseOrderStrings); + var normalizedStrings = ResponseCacheMiddleware.GetNormalizedStringValues(reverseOrderStrings); Assert.Equal(orderedStrings, normalizedStrings); } + } } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheabilityValidatorTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachePolicyProviderTests.cs similarity index 77% rename from test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheabilityValidatorTests.cs rename to test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachePolicyProviderTests.cs index 88be66b80a..fb1b817e57 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheabilityValidatorTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachePolicyProviderTests.cs @@ -9,17 +9,17 @@ using Xunit; namespace Microsoft.AspNetCore.ResponseCaching.Tests { - public class CacheabilityValidatorTests + public class ResponseCachePolicyProviderTests { [Theory] [InlineData("GET")] [InlineData("HEAD")] - public void RequestIsCacheable_CacheableMethods_Allowed(string method) + public void IsRequestCacheable_CacheableMethods_Allowed(string method) { var context = TestUtils.CreateTestContext(); context.HttpContext.Request.Method = method; - Assert.True(new CacheabilityValidator().IsRequestCacheable(context)); + Assert.True(new ResponseCachePolicyProvider().IsRequestCacheable(context)); } [Theory] @@ -31,26 +31,26 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [InlineData("CONNECT")] [InlineData("")] [InlineData(null)] - public void RequestIsCacheable_UncacheableMethods_NotAllowed(string method) + public void IsRequestCacheable_UncacheableMethods_NotAllowed(string method) { var context = TestUtils.CreateTestContext(); context.HttpContext.Request.Method = method; - Assert.False(new CacheabilityValidator().IsRequestCacheable(context)); + Assert.False(new ResponseCachePolicyProvider().IsRequestCacheable(context)); } [Fact] - public void RequestIsCacheable_AuthorizationHeaders_NotAllowed() + public void IsRequestCacheable_AuthorizationHeaders_NotAllowed() { var context = TestUtils.CreateTestContext(); context.HttpContext.Request.Method = "GET"; context.HttpContext.Request.Headers[HeaderNames.Authorization] = "Basic plaintextUN:plaintextPW"; - Assert.False(new CacheabilityValidator().IsRequestCacheable(context)); + Assert.False(new ResponseCachePolicyProvider().IsRequestCacheable(context)); } [Fact] - public void RequestIsCacheable_NoCache_NotAllowed() + public void IsRequestCacheable_NoCache_NotAllowed() { var context = TestUtils.CreateTestContext(); context.HttpContext.Request.Method = "GET"; @@ -59,11 +59,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests NoCache = true }; - Assert.False(new CacheabilityValidator().IsRequestCacheable(context)); + Assert.False(new ResponseCachePolicyProvider().IsRequestCacheable(context)); } [Fact] - public void RequestIsCacheable_NoStore_Allowed() + public void IsRequestCacheable_NoStore_Allowed() { var context = TestUtils.CreateTestContext(); context.HttpContext.Request.Method = "GET"; @@ -72,40 +72,40 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests NoStore = true }; - Assert.True(new CacheabilityValidator().IsRequestCacheable(context)); + Assert.True(new ResponseCachePolicyProvider().IsRequestCacheable(context)); } [Fact] - public void RequestIsCacheable_LegacyDirectives_NotAllowed() + public void IsRequestCacheable_LegacyDirectives_NotAllowed() { var context = TestUtils.CreateTestContext(); context.HttpContext.Request.Method = "GET"; context.HttpContext.Request.Headers[HeaderNames.Pragma] = "no-cache"; - Assert.False(new CacheabilityValidator().IsRequestCacheable(context)); + Assert.False(new ResponseCachePolicyProvider().IsRequestCacheable(context)); } [Fact] - public void RequestIsCacheable_LegacyDirectives_OverridenByCacheControl() + public void IsRequestCacheable_LegacyDirectives_OverridenByCacheControl() { var context = TestUtils.CreateTestContext(); context.HttpContext.Request.Method = "GET"; context.HttpContext.Request.Headers[HeaderNames.Pragma] = "no-cache"; context.HttpContext.Request.Headers[HeaderNames.CacheControl] = "max-age=10"; - Assert.True(new CacheabilityValidator().IsRequestCacheable(context)); + Assert.True(new ResponseCachePolicyProvider().IsRequestCacheable(context)); } [Fact] - public void ResponseIsCacheable_NoPublic_NotAllowed() + public void IsResponseCacheable_NoPublic_NotAllowed() { var context = TestUtils.CreateTestContext(); - Assert.False(new CacheabilityValidator().IsResponseCacheable(context)); + Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); } [Fact] - public void ResponseIsCacheable_Public_Allowed() + public void IsResponseCacheable_Public_Allowed() { var context = TestUtils.CreateTestContext(); context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() @@ -113,11 +113,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Public = true }; - Assert.True(new CacheabilityValidator().IsResponseCacheable(context)); + Assert.True(new ResponseCachePolicyProvider().IsResponseCacheable(context)); } [Fact] - public void ResponseIsCacheable_NoCache_NotAllowed() + public void IsResponseCacheable_NoCache_NotAllowed() { var context = TestUtils.CreateTestContext(); context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() @@ -126,11 +126,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests NoCache = true }; - Assert.False(new CacheabilityValidator().IsResponseCacheable(context)); + Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); } [Fact] - public void ResponseIsCacheable_RequestNoStore_NotAllowed() + public void IsResponseCacheable_RequestNoStore_NotAllowed() { var context = TestUtils.CreateTestContext(); context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() @@ -142,11 +142,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Public = true }; - Assert.False(new CacheabilityValidator().IsResponseCacheable(context)); + Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); } [Fact] - public void ResponseIsCacheable_ResponseNoStore_NotAllowed() + public void IsResponseCacheable_ResponseNoStore_NotAllowed() { var context = TestUtils.CreateTestContext(); context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() @@ -155,11 +155,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests NoStore = true }; - Assert.False(new CacheabilityValidator().IsResponseCacheable(context)); + Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); } [Fact] - public void ResponseIsCacheable_SetCookieHeader_NotAllowed() + public void IsResponseCacheable_SetCookieHeader_NotAllowed() { var context = TestUtils.CreateTestContext(); context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() @@ -168,11 +168,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests }; context.HttpContext.Response.Headers[HeaderNames.SetCookie] = "cookieName=cookieValue"; - Assert.False(new CacheabilityValidator().IsResponseCacheable(context)); + Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); } [Fact] - public void ResponseIsCacheable_VaryHeaderByStar_NotAllowed() + public void IsResponseCacheable_VaryHeaderByStar_NotAllowed() { var context = TestUtils.CreateTestContext(); context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() @@ -181,11 +181,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests }; context.HttpContext.Response.Headers[HeaderNames.Vary] = "*"; - Assert.False(new CacheabilityValidator().IsResponseCacheable(context)); + Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); } [Fact] - public void ResponseIsCacheable_Private_NotAllowed() + public void IsResponseCacheable_Private_NotAllowed() { var context = TestUtils.CreateTestContext(); context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() @@ -194,12 +194,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Private = true }; - Assert.False(new CacheabilityValidator().IsResponseCacheable(context)); + Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); } [Theory] [InlineData(StatusCodes.Status200OK)] - public void ResponseIsCacheable_SuccessStatusCodes_Allowed(int statusCode) + public void IsResponseCacheable_SuccessStatusCodes_Allowed(int statusCode) { var context = TestUtils.CreateTestContext(); context.HttpContext.Response.StatusCode = statusCode; @@ -208,7 +208,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Public = true }; - Assert.True(new CacheabilityValidator().IsResponseCacheable(context)); + Assert.True(new ResponseCachePolicyProvider().IsResponseCacheable(context)); } [Theory] @@ -260,7 +260,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [InlineData(StatusCodes.Status505HttpVersionNotsupported)] [InlineData(StatusCodes.Status506VariantAlsoNegotiates)] [InlineData(StatusCodes.Status507InsufficientStorage)] - public void ResponseIsCacheable_NonSuccessStatusCodes_NotAllowed(int statusCode) + public void IsResponseCacheable_NonSuccessStatusCodes_NotAllowed(int statusCode) { var context = TestUtils.CreateTestContext(); context.HttpContext.Response.StatusCode = statusCode; @@ -269,11 +269,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Public = true }; - Assert.False(new CacheabilityValidator().IsResponseCacheable(context)); + Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); } [Fact] - public void ResponseIsCacheable_NoExpiryRequirements_IsAllowed() + public void IsResponseCacheable_NoExpiryRequirements_IsAllowed() { var context = TestUtils.CreateTestContext(); context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; @@ -286,11 +286,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.TypedResponseHeaders.Date = utcNow; context.ResponseTime = DateTimeOffset.MaxValue; - Assert.True(new CacheabilityValidator().IsResponseCacheable(context)); + Assert.True(new ResponseCachePolicyProvider().IsResponseCacheable(context)); } [Fact] - public void ResponseIsCacheable_PastExpiry_NotAllowed() + public void IsResponseCacheable_PastExpiry_NotAllowed() { var context = TestUtils.CreateTestContext(); context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; @@ -304,11 +304,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.TypedResponseHeaders.Date = utcNow; context.ResponseTime = DateTimeOffset.MaxValue; - Assert.False(new CacheabilityValidator().IsResponseCacheable(context)); + Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); } [Fact] - public void ResponseIsCacheable_MaxAgeOverridesExpiry_ToAllowed() + public void IsResponseCacheable_MaxAgeOverridesExpiry_ToAllowed() { var utcNow = DateTimeOffset.UtcNow; var context = TestUtils.CreateTestContext(); @@ -322,11 +322,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.TypedResponseHeaders.Date = utcNow; context.ResponseTime = utcNow + TimeSpan.FromSeconds(9); - Assert.True(new CacheabilityValidator().IsResponseCacheable(context)); + Assert.True(new ResponseCachePolicyProvider().IsResponseCacheable(context)); } [Fact] - public void ResponseIsCacheable_MaxAgeOverridesExpiry_ToNotAllowed() + public void IsResponseCacheable_MaxAgeOverridesExpiry_ToNotAllowed() { var utcNow = DateTimeOffset.UtcNow; var context = TestUtils.CreateTestContext(); @@ -340,11 +340,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.TypedResponseHeaders.Date = utcNow; context.ResponseTime = utcNow + TimeSpan.FromSeconds(11); - Assert.False(new CacheabilityValidator().IsResponseCacheable(context)); + Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); } [Fact] - public void ResponseIsCacheable_SharedMaxAgeOverridesMaxAge_ToAllowed() + public void IsResponseCacheable_SharedMaxAgeOverridesMaxAge_ToAllowed() { var utcNow = DateTimeOffset.UtcNow; var context = TestUtils.CreateTestContext(); @@ -358,11 +358,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.TypedResponseHeaders.Date = utcNow; context.ResponseTime = utcNow + TimeSpan.FromSeconds(11); - Assert.True(new CacheabilityValidator().IsResponseCacheable(context)); + Assert.True(new ResponseCachePolicyProvider().IsResponseCacheable(context)); } [Fact] - public void ResponseIsCacheable_SharedMaxAgeOverridesMaxAge_ToNotFresh() + public void IsResponseCacheable_SharedMaxAgeOverridesMaxAge_ToNotFresh() { var utcNow = DateTimeOffset.UtcNow; var context = TestUtils.CreateTestContext(); @@ -376,22 +376,22 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.TypedResponseHeaders.Date = utcNow; context.ResponseTime = utcNow + TimeSpan.FromSeconds(6); - Assert.False(new CacheabilityValidator().IsResponseCacheable(context)); + Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); } [Fact] - public void EntryIsFresh_NoCachedCacheControl_FallsbackToEmptyCacheControl() + public void IsCachedEntryFresh_NoCachedCacheControl_FallsbackToEmptyCacheControl() { var utcNow = DateTimeOffset.UtcNow; var context = TestUtils.CreateTestContext(); context.ResponseTime = DateTimeOffset.MaxValue; context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); - Assert.True(new CacheabilityValidator().IsCachedEntryFresh(context)); + Assert.True(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); } [Fact] - public void EntryIsFresh_NoExpiryRequirements_IsFresh() + public void IsCachedEntryFresh_NoExpiryRequirements_IsFresh() { var utcNow = DateTimeOffset.UtcNow; var context = TestUtils.CreateTestContext(); @@ -404,11 +404,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } }; - Assert.True(new CacheabilityValidator().IsCachedEntryFresh(context)); + Assert.True(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); } [Fact] - public void EntryIsFresh_PastExpiry_IsNotFresh() + public void IsCachedEntryFresh_PastExpiry_IsNotFresh() { var context = TestUtils.CreateTestContext(); context.ResponseTime = DateTimeOffset.MaxValue; @@ -421,11 +421,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Expires = DateTimeOffset.UtcNow }; - Assert.False(new CacheabilityValidator().IsCachedEntryFresh(context)); + Assert.False(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); } [Fact] - public void EntryIsFresh_MaxAgeOverridesExpiry_ToFresh() + public void IsCachedEntryFresh_MaxAgeOverridesExpiry_ToFresh() { var utcNow = DateTimeOffset.UtcNow; var context = TestUtils.CreateTestContext(); @@ -441,11 +441,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Expires = utcNow }; - Assert.True(new CacheabilityValidator().IsCachedEntryFresh(context)); + Assert.True(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); } [Fact] - public void EntryIsFresh_MaxAgeOverridesExpiry_ToNotFresh() + public void IsCachedEntryFresh_MaxAgeOverridesExpiry_ToNotFresh() { var utcNow = DateTimeOffset.UtcNow; var context = TestUtils.CreateTestContext(); @@ -461,11 +461,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Expires = utcNow }; - Assert.False(new CacheabilityValidator().IsCachedEntryFresh(context)); + Assert.False(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); } [Fact] - public void EntryIsFresh_SharedMaxAgeOverridesMaxAge_ToFresh() + public void IsCachedEntryFresh_SharedMaxAgeOverridesMaxAge_ToFresh() { var utcNow = DateTimeOffset.UtcNow; var context = TestUtils.CreateTestContext(); @@ -482,11 +482,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Expires = utcNow }; - Assert.True(new CacheabilityValidator().IsCachedEntryFresh(context)); + Assert.True(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); } [Fact] - public void EntryIsFresh_SharedMaxAgeOverridesMaxAge_ToNotFresh() + public void IsCachedEntryFresh_SharedMaxAgeOverridesMaxAge_ToNotFresh() { var utcNow = DateTimeOffset.UtcNow; var context = TestUtils.CreateTestContext(); @@ -503,11 +503,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Expires = utcNow }; - Assert.False(new CacheabilityValidator().IsCachedEntryFresh(context)); + Assert.False(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); } [Fact] - public void EntryIsFresh_MinFreshReducesFreshness_ToNotFresh() + public void IsCachedEntryFresh_MinFreshReducesFreshness_ToNotFresh() { var context = TestUtils.CreateTestContext(); context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() @@ -524,11 +524,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests }; context.CachedEntryAge = TimeSpan.FromSeconds(3); - Assert.False(new CacheabilityValidator().IsCachedEntryFresh(context)); + Assert.False(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); } [Fact] - public void EntryIsFresh_RequestMaxAgeRestrictAge_ToNotFresh() + public void IsCachedEntryFresh_RequestMaxAgeRestrictAge_ToNotFresh() { var context = TestUtils.CreateTestContext(); context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() @@ -544,11 +544,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests }; context.CachedEntryAge = TimeSpan.FromSeconds(6); - Assert.False(new CacheabilityValidator().IsCachedEntryFresh(context)); + Assert.False(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); } [Fact] - public void EntryIsFresh_MaxStaleOverridesFreshness_ToFresh() + public void IsCachedEntryFresh_MaxStaleOverridesFreshness_ToFresh() { var context = TestUtils.CreateTestContext(); context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() @@ -566,11 +566,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests }; context.CachedEntryAge = TimeSpan.FromSeconds(6); - Assert.True(new CacheabilityValidator().IsCachedEntryFresh(context)); + Assert.True(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); } [Fact] - public void EntryIsFresh_MustRevalidateOverridesRequestMaxStale_ToNotFresh() + public void IsCachedEntryFresh_MustRevalidateOverridesRequestMaxStale_ToNotFresh() { var context = TestUtils.CreateTestContext(); context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() @@ -589,11 +589,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests }; context.CachedEntryAge = TimeSpan.FromSeconds(6); - Assert.False(new CacheabilityValidator().IsCachedEntryFresh(context)); + Assert.False(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); } [Fact] - public void EntryIsFresh_IgnoresRequestVerificationWhenSpecified() + public void IsCachedEntryFresh_IgnoresRequestVerificationWhenSpecified() { var context = TestUtils.CreateTestContext(); context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() @@ -611,7 +611,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests }; context.CachedEntryAge = TimeSpan.FromSeconds(3); - Assert.True(new CacheabilityValidator().IsCachedEntryFresh(context)); + Assert.True(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); } } } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheTests.cs similarity index 89% rename from test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs rename to test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheTests.cs index e910ad5478..54060d0fec 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheTests.cs @@ -14,12 +14,12 @@ using Xunit; namespace Microsoft.AspNetCore.ResponseCaching.Tests { - public class ResponseCachingTests + public class ResponseCacheTests { [Fact] public async void ServesCachedContent_IfAvailable() { - var builder = TestUtils.CreateBuilderWithResponseCaching(); + var builder = TestUtils.CreateBuilderWithResponseCache(); using (var server = new TestServer(builder)) { @@ -34,7 +34,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfNotAvailable() { - var builder = TestUtils.CreateBuilderWithResponseCaching(); + var builder = TestUtils.CreateBuilderWithResponseCache(); using (var server = new TestServer(builder)) { @@ -49,10 +49,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfVaryHeader_Matches() { - var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) => + var builder = TestUtils.CreateBuilderWithResponseCache(requestDelegate: async (context) => { context.Response.Headers[HeaderNames.Vary] = HeaderNames.From; - await TestUtils.DefaultRequestDelegate(context); + await TestUtils.TestRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -69,10 +69,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfVaryHeader_Mismatches() { - var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) => + var builder = TestUtils.CreateBuilderWithResponseCache(requestDelegate: async (context) => { context.Response.Headers[HeaderNames.Vary] = HeaderNames.From; - await TestUtils.DefaultRequestDelegate(context); + await TestUtils.TestRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -90,10 +90,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfVaryParams_Matches() { - var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) => + var builder = TestUtils.CreateBuilderWithResponseCache(requestDelegate: async (context) => { - context.GetResponseCachingFeature().VaryParams = "param"; - await TestUtils.DefaultRequestDelegate(context); + context.GetResponseCacheFeature().VaryByParams = "param"; + await TestUtils.TestRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -109,10 +109,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfVaryParamsExplicit_Matches_ParamNameCaseInsensitive() { - var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) => + var builder = TestUtils.CreateBuilderWithResponseCache(requestDelegate: async (context) => { - context.GetResponseCachingFeature().VaryParams = new[] { "ParamA", "paramb" }; - await TestUtils.DefaultRequestDelegate(context); + context.GetResponseCacheFeature().VaryByParams = new[] { "ParamA", "paramb" }; + await TestUtils.TestRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -128,10 +128,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfVaryParamsStar_Matches_ParamNameCaseInsensitive() { - var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) => + var builder = TestUtils.CreateBuilderWithResponseCache(requestDelegate: async (context) => { - context.GetResponseCachingFeature().VaryParams = new[] { "*" }; - await TestUtils.DefaultRequestDelegate(context); + context.GetResponseCacheFeature().VaryByParams = new[] { "*" }; + await TestUtils.TestRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -147,10 +147,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfVaryParamsExplicit_Matches_OrderInsensitive() { - var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) => + var builder = TestUtils.CreateBuilderWithResponseCache(requestDelegate: async (context) => { - context.GetResponseCachingFeature().VaryParams = new[] { "ParamB", "ParamA" }; - await TestUtils.DefaultRequestDelegate(context); + context.GetResponseCacheFeature().VaryByParams = new[] { "ParamB", "ParamA" }; + await TestUtils.TestRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -166,10 +166,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfVaryParamsStar_Matches_OrderInsensitive() { - var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) => + var builder = TestUtils.CreateBuilderWithResponseCache(requestDelegate: async (context) => { - context.GetResponseCachingFeature().VaryParams = new[] { "*" }; - await TestUtils.DefaultRequestDelegate(context); + context.GetResponseCacheFeature().VaryByParams = new[] { "*" }; + await TestUtils.TestRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -185,10 +185,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfVaryParams_Mismatches() { - var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) => + var builder = TestUtils.CreateBuilderWithResponseCache(requestDelegate: async (context) => { - context.GetResponseCachingFeature().VaryParams = "param"; - await TestUtils.DefaultRequestDelegate(context); + context.GetResponseCacheFeature().VaryByParams = "param"; + await TestUtils.TestRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -204,10 +204,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfVaryParamsExplicit_Mismatch_ParamValueCaseSensitive() { - var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) => + var builder = TestUtils.CreateBuilderWithResponseCache(requestDelegate: async (context) => { - context.GetResponseCachingFeature().VaryParams = new[] { "ParamA", "ParamB" }; - await TestUtils.DefaultRequestDelegate(context); + context.GetResponseCacheFeature().VaryByParams = new[] { "ParamA", "ParamB" }; + await TestUtils.TestRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -223,10 +223,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfVaryParamsStar_Mismatch_ParamValueCaseSensitive() { - var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) => + var builder = TestUtils.CreateBuilderWithResponseCache(requestDelegate: async (context) => { - context.GetResponseCachingFeature().VaryParams = new[] { "*" }; - await TestUtils.DefaultRequestDelegate(context); + context.GetResponseCacheFeature().VaryByParams = new[] { "*" }; + await TestUtils.TestRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -242,7 +242,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfRequestRequirements_NotMet() { - var builder = TestUtils.CreateBuilderWithResponseCaching(); + var builder = TestUtils.CreateBuilderWithResponseCache(); using (var server = new TestServer(builder)) { @@ -261,7 +261,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void Serves504_IfOnlyIfCachedHeader_IsSpecified() { - var builder = TestUtils.CreateBuilderWithResponseCaching(); + var builder = TestUtils.CreateBuilderWithResponseCache(); using (var server = new TestServer(builder)) { @@ -281,10 +281,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfSetCookie_IsSpecified() { - var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) => + var builder = TestUtils.CreateBuilderWithResponseCache(requestDelegate: async (context) => { var headers = context.Response.Headers[HeaderNames.SetCookie] = "cookieName=cookieValue"; - await TestUtils.DefaultRequestDelegate(context); + await TestUtils.TestRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -300,7 +300,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfIHttpSendFileFeature_NotUsed() { - var builder = TestUtils.CreateBuilderWithResponseCaching(app => + var builder = TestUtils.CreateBuilderWithResponseCache(app => { app.Use(async (context, next) => { @@ -322,7 +322,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfIHttpSendFileFeature_Used() { - var builder = TestUtils.CreateBuilderWithResponseCaching( + var builder = TestUtils.CreateBuilderWithResponseCache( app => { app.Use(async (context, next) => @@ -334,7 +334,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests requestDelegate: async (context) => { await context.Features.Get().SendFileAsync("dummy", 0, 0, CancellationToken.None); - await TestUtils.DefaultRequestDelegate(context); + await TestUtils.TestRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -350,7 +350,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfSubsequentRequest_ContainsNoStore() { - var builder = TestUtils.CreateBuilderWithResponseCaching(); + var builder = TestUtils.CreateBuilderWithResponseCache(); using (var server = new TestServer(builder)) { @@ -369,7 +369,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfInitialRequestContains_NoStore() { - var builder = TestUtils.CreateBuilderWithResponseCaching(); + var builder = TestUtils.CreateBuilderWithResponseCache(); using (var server = new TestServer(builder)) { @@ -388,7 +388,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void Serves304_IfIfModifiedSince_Satisfied() { - var builder = TestUtils.CreateBuilderWithResponseCaching(); + var builder = TestUtils.CreateBuilderWithResponseCache(); using (var server = new TestServer(builder)) { @@ -405,7 +405,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfIfModifiedSince_NotSatisfied() { - var builder = TestUtils.CreateBuilderWithResponseCaching(); + var builder = TestUtils.CreateBuilderWithResponseCache(); using (var server = new TestServer(builder)) { @@ -421,10 +421,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void Serves304_IfIfNoneMatch_Satisfied() { - var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) => + var builder = TestUtils.CreateBuilderWithResponseCache(requestDelegate: async (context) => { var headers = context.Response.GetTypedHeaders().ETag = new EntityTagHeaderValue("\"E1\""); - await TestUtils.DefaultRequestDelegate(context); + await TestUtils.TestRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -442,10 +442,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfIfNoneMatch_NotSatisfied() { - var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) => + var builder = TestUtils.CreateBuilderWithResponseCache(requestDelegate: async (context) => { var headers = context.Response.GetTypedHeaders().ETag = new EntityTagHeaderValue("\"E1\""); - await TestUtils.DefaultRequestDelegate(context); + await TestUtils.TestRequestDelegate(context); }); using (var server = new TestServer(builder)) @@ -462,7 +462,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfBodySize_IsCacheable() { - var builder = TestUtils.CreateBuilderWithResponseCaching(options: new ResponseCachingOptions() + var builder = TestUtils.CreateBuilderWithResponseCache(options: new ResponseCacheOptions() { MaximumCachedBodySize = 100 }); @@ -480,7 +480,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfBodySize_IsNotCacheable() { - var builder = TestUtils.CreateBuilderWithResponseCaching(options: new ResponseCachingOptions() + var builder = TestUtils.CreateBuilderWithResponseCache(options: new ResponseCacheOptions() { MaximumCachedBodySize = 1 }); @@ -498,10 +498,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_WithoutReplacingCachedVaryBy_OnCacheMiss() { - var builder = TestUtils.CreateBuilderWithResponseCaching(requestDelegate: async (context) => + var builder = TestUtils.CreateBuilderWithResponseCache(requestDelegate: async (context) => { context.Response.Headers[HeaderNames.Vary] = HeaderNames.From; - await TestUtils.DefaultRequestDelegate(context); + await TestUtils.TestRequestDelegate(context); }); using (var server = new TestServer(builder)) diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs index 23c80cddf4..96d45f71d2 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs @@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { internal class TestUtils { - internal static RequestDelegate DefaultRequestDelegate = async (context) => + internal static RequestDelegate TestRequestDelegate = async (context) => { var uniqueId = Guid.NewGuid().ToString(); var headers = context.Response.GetTypedHeaders(); @@ -34,19 +34,19 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests await context.Response.WriteAsync(uniqueId); }; - internal static ICacheKeyProvider CreateTestKeyProvider() + internal static IResponseCacheKeyProvider CreateTestKeyProvider() { - return CreateTestKeyProvider(new ResponseCachingOptions()); + return CreateTestKeyProvider(new ResponseCacheOptions()); } - internal static ICacheKeyProvider CreateTestKeyProvider(ResponseCachingOptions options) + internal static IResponseCacheKeyProvider CreateTestKeyProvider(ResponseCacheOptions options) { - return new CacheKeyProvider(new DefaultObjectPoolProvider(), Options.Create(options)); + return new ResponseCacheKeyProvider(new DefaultObjectPoolProvider(), Options.Create(options)); } - internal static IWebHostBuilder CreateBuilderWithResponseCaching( + internal static IWebHostBuilder CreateBuilderWithResponseCache( Action configureDelegate = null, - ResponseCachingOptions options = null, + ResponseCacheOptions options = null, RequestDelegate requestDelegate = null) { if (configureDelegate == null) @@ -55,11 +55,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } if (options == null) { - options = new ResponseCachingOptions(); + options = new ResponseCacheOptions(); } if (requestDelegate == null) { - requestDelegate = DefaultRequestDelegate; + requestDelegate = TestRequestDelegate; } return new WebHostBuilder() @@ -70,45 +70,45 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests .Configure(app => { configureDelegate(app); - app.UseResponseCaching(options); + app.UseResponseCache(options); app.Run(requestDelegate); }); } - internal static ResponseCachingMiddleware CreateTestMiddleware( - IResponseCache responseCache = null, - ResponseCachingOptions options = null, - ICacheKeyProvider cacheKeyProvider = null, - ICacheabilityValidator cacheabilityValidator = null) + internal static ResponseCacheMiddleware CreateTestMiddleware( + IResponseCacheStore store = null, + ResponseCacheOptions options = null, + IResponseCacheKeyProvider keyProvider = null, + IResponseCachePolicyProvider policyProvider = null) { - if (responseCache == null) + if (store == null) { - responseCache = new TestResponseCache(); + store = new TestResponseCacheStore(); } if (options == null) { - options = new ResponseCachingOptions(); + options = new ResponseCacheOptions(); } - if (cacheKeyProvider == null) + if (keyProvider == null) { - cacheKeyProvider = new CacheKeyProvider(new DefaultObjectPoolProvider(), Options.Create(options)); + keyProvider = new ResponseCacheKeyProvider(new DefaultObjectPoolProvider(), Options.Create(options)); } - if (cacheabilityValidator == null) + if (policyProvider == null) { - cacheabilityValidator = new TestCacheabilityValidator(); + policyProvider = new TestResponseCachePolicyProvider(); } - return new ResponseCachingMiddleware( + return new ResponseCacheMiddleware( httpContext => TaskCache.CompletedTask, - responseCache, + store, Options.Create(options), - cacheabilityValidator, - cacheKeyProvider); + policyProvider, + keyProvider); } - internal static ResponseCachingContext CreateTestContext() + internal static ResponseCacheContext CreateTestContext() { - return new ResponseCachingContext(new DefaultHttpContext()); + return new ResponseCacheContext(new DefaultHttpContext()); } } @@ -120,58 +120,49 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } } - internal class TestCacheabilityValidator : ICacheabilityValidator + internal class TestResponseCachePolicyProvider : IResponseCachePolicyProvider { - public bool IsCachedEntryFresh(ResponseCachingContext context) => true; + public bool IsCachedEntryFresh(ResponseCacheContext context) => true; - public bool IsRequestCacheable(ResponseCachingContext context) => true; + public bool IsRequestCacheable(ResponseCacheContext context) => true; - public bool IsResponseCacheable(ResponseCachingContext context) => true; + public bool IsResponseCacheable(ResponseCacheContext context) => true; } - internal class TestKeyProvider : ICacheKeyProvider + internal class TestResponseCacheKeyProvider : IResponseCacheKeyProvider { - private readonly StringValues _baseKey; + private readonly string _baseKey; private readonly StringValues _varyKey; - public TestKeyProvider(StringValues? lookupBaseKey = null, StringValues? lookupVaryKey = null) + public TestResponseCacheKeyProvider(string lookupBaseKey = null, StringValues? lookupVaryKey = null) { - if (lookupBaseKey.HasValue) - { - _baseKey = lookupBaseKey.Value; - } + _baseKey = lookupBaseKey; if (lookupVaryKey.HasValue) { _varyKey = lookupVaryKey.Value; } } - public IEnumerable CreateLookupBaseKeys(ResponseCachingContext context) => _baseKey; - - - public IEnumerable CreateLookupVaryKeys(ResponseCachingContext context) + public IEnumerable CreateLookupVaryByKeys(ResponseCacheContext context) { - foreach (var baseKey in _baseKey) + foreach (var varyKey in _varyKey) { - foreach (var varyKey in _varyKey) - { - yield return baseKey + varyKey; - } + yield return _baseKey + varyKey; } } - public string CreateStorageBaseKey(ResponseCachingContext context) + public string CreateBaseKey(ResponseCacheContext context) { - throw new NotImplementedException(); + return _baseKey; } - public string CreateStorageVaryKey(ResponseCachingContext context) + public string CreateStorageVaryByKey(ResponseCacheContext context) { throw new NotImplementedException(); } } - internal class TestResponseCache : IResponseCache + internal class TestResponseCacheStore : IResponseCacheStore { private readonly IDictionary _storage = new Dictionary(); public int GetCount { get; private set; } @@ -200,12 +191,4 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests _storage[key] = entry; } } - - internal class TestHttpSendFileFeature : IHttpSendFileFeature - { - public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellation) - { - return TaskCache.CompletedTask; - } - } } From dd4799adfdf8e1166c7b2c95036ceaaf928f97df Mon Sep 17 00:00:00 2001 From: John Luo Date: Wed, 14 Sep 2016 11:53:54 -0700 Subject: [PATCH 030/188] Split by commas when normalizing headers --- .../ResponseCacheMiddleware.cs | 9 +++--- .../ResponseCacheMiddlewareTests.cs | 29 ++++++++++++++++--- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs index 5c395ad306..85b8fec0ea 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs @@ -2,6 +2,7 @@ // 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.Globalization; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; @@ -211,7 +212,7 @@ namespace Microsoft.AspNetCore.ResponseCaching // Create the cache entry now var response = context.HttpContext.Response; - var varyHeaderValue = response.Headers[HeaderNames.Vary]; + var varyHeaderValue = new StringValues(response.Headers.GetCommaSeparatedValues(HeaderNames.Vary)); var varyParamsValue = context.HttpContext.GetResponseCacheFeature()?.VaryByParams ?? StringValues.Empty; context.CachedResponseValidFor = context.ResponseCacheControlHeaderValue.SharedMaxAge ?? context.ResponseCacheControlHeaderValue.MaxAge ?? @@ -222,8 +223,8 @@ namespace Microsoft.AspNetCore.ResponseCaching if (!StringValues.IsNullOrEmpty(varyHeaderValue) || !StringValues.IsNullOrEmpty(varyParamsValue)) { // Normalize order and casing of vary by rules - var normalizedVaryHeaderValue = GetNormalizedStringValues(varyHeaderValue); - var normalizedVaryParamsValue = GetNormalizedStringValues(varyParamsValue); + var normalizedVaryHeaderValue = GetOrderCasingNormalizedStringValues(varyHeaderValue); + var normalizedVaryParamsValue = GetOrderCasingNormalizedStringValues(varyParamsValue); // Update vary rules if they are different if (context.CachedVaryByRules == null || @@ -374,7 +375,7 @@ namespace Microsoft.AspNetCore.ResponseCaching } // Normalize order and casing - internal static StringValues GetNormalizedStringValues(StringValues stringValues) + internal static StringValues GetOrderCasingNormalizedStringValues(StringValues stringValues) { if (stringValues.Count == 1) { diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheMiddlewareTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheMiddlewareTests.cs index a329104ed1..8ef4b51bf0 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheMiddlewareTests.cs @@ -410,6 +410,18 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Assert.NotNull(context.CachedResponse); } + [Fact] + public void FinalizeCachingHeaders_SplitsVaryHeaderByCommas() + { + var middleware = TestUtils.CreateTestMiddleware(); + var context = TestUtils.CreateTestContext(); + context.HttpContext.Response.Headers[HeaderNames.Vary] = "HeaderB, heaDera"; + + middleware.FinalizeCacheHeaders(context); + + Assert.Equal(new StringValues(new[] { "HEADERA", "HEADERB" }), context.CachedVaryByRules.Headers); + } + [Fact] public async Task FinalizeCacheBody_StoreResponseBodySeparately_IfLargerThanLimit() { @@ -554,26 +566,35 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public void NormalizeStringValues_NormalizesCasingToUpper() + public void GetOrderCasingNormalizedStringValues_NormalizesCasingToUpper() { var uppercaseStrings = new StringValues(new[] { "STRINGA", "STRINGB" }); var lowercaseStrings = new StringValues(new[] { "stringA", "stringB" }); - var normalizedStrings = ResponseCacheMiddleware.GetNormalizedStringValues(lowercaseStrings); + var normalizedStrings = ResponseCacheMiddleware.GetOrderCasingNormalizedStringValues(lowercaseStrings); Assert.Equal(uppercaseStrings, normalizedStrings); } [Fact] - public void NormalizeStringValues_NormalizesOrder() + public void GetOrderCasingNormalizedStringValues_NormalizesOrder() { var orderedStrings = new StringValues(new[] { "STRINGA", "STRINGB" }); var reverseOrderStrings = new StringValues(new[] { "STRINGB", "STRINGA" }); - var normalizedStrings = ResponseCacheMiddleware.GetNormalizedStringValues(reverseOrderStrings); + var normalizedStrings = ResponseCacheMiddleware.GetOrderCasingNormalizedStringValues(reverseOrderStrings); Assert.Equal(orderedStrings, normalizedStrings); } + [Fact] + public void GetOrderCasingNormalizedStringValues_PreservesCommas() + { + var originalStrings = new StringValues(new[] { "STRINGA, STRINGB" }); + + var normalizedStrings = ResponseCacheMiddleware.GetOrderCasingNormalizedStringValues(originalStrings); + + Assert.Equal(originalStrings, normalizedStrings); + } } } From 673115a29292d6dfe8bb8f0974f6a16088c7772b Mon Sep 17 00:00:00 2001 From: John Luo Date: Thu, 15 Sep 2016 16:52:46 -0700 Subject: [PATCH 031/188] Remove todos and add a test Issues are now resolved or are tracked in issues. --- .../ResponseCacheHttpContextExtensions.cs | 1 - .../Internal/DistributedResponseCacheStore.cs | 11 +--- .../ResponseCacheFeature.cs | 1 - .../ResponseCacheMiddleware.cs | 10 --- .../ResponseCachePolicyProvider.cs | 11 ---- .../ResponseCacheTests.cs | 64 +++++++++++++++++++ 6 files changed, 66 insertions(+), 32 deletions(-) diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCacheHttpContextExtensions.cs b/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCacheHttpContextExtensions.cs index 7c1915b14d..d8349bf594 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCacheHttpContextExtensions.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCacheHttpContextExtensions.cs @@ -5,7 +5,6 @@ using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.ResponseCaching { - // TODO: Temporary interface for endpoints to specify options for response caching public static class ResponseCacheHttpContextExtensions { public static ResponseCacheFeature GetResponseCacheFeature(this HttpContext httpContext) diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/DistributedResponseCacheStore.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/DistributedResponseCacheStore.cs index aba8990b42..88fa05f3ea 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/DistributedResponseCacheStore.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/DistributedResponseCacheStore.cs @@ -28,7 +28,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal } catch { - // TODO: Log error return null; } } @@ -39,10 +38,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal { _cache.Remove(key); } - catch - { - // TODO: Log error - } + catch { } } public void Set(string key, object entry, TimeSpan validFor) @@ -57,10 +53,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal AbsoluteExpirationRelativeToNow = validFor }); } - catch - { - // TODO: Log error - } + catch { } } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheFeature.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheFeature.cs index 1233aff724..5646d15867 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheFeature.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheFeature.cs @@ -5,7 +5,6 @@ using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.ResponseCaching { - // TODO: Temporary interface for endpoints to specify options for response caching public class ResponseCacheFeature { public StringValues VaryByParams { get; set; } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs index 85b8fec0ea..cd2ec42598 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs @@ -2,7 +2,6 @@ // 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.Globalization; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; @@ -104,7 +103,6 @@ namespace Microsoft.AspNetCore.ResponseCaching } else { - // TODO: Invalidate resources for successful unsafe methods? Required by RFC await _next(httpContext); } } @@ -159,10 +157,6 @@ namespace Microsoft.AspNetCore.ResponseCaching return true; } - else - { - // TODO: Validate with endpoint instead - } return false; } @@ -313,8 +307,6 @@ namespace Microsoft.AspNetCore.ResponseCaching internal void ShimResponseStream(ResponseCacheContext context) { - // TODO: Consider caching large responses on disk and serving them from there. - // Shim response stream context.OriginalResponseStream = context.HttpContext.Response.Body; context.ResponseCacheStream = new ResponseCacheStream(context.OriginalResponseStream, _options.MaximumCachedBodySize); @@ -327,7 +319,6 @@ namespace Microsoft.AspNetCore.ResponseCaching context.HttpContext.Features.Set(new SendFileFeatureWrapper(context.OriginalSendFileFeature, context.ResponseCacheStream)); } - // TODO: Move this temporary interface with endpoint to HttpAbstractions context.HttpContext.AddResponseCacheFeature(); } @@ -339,7 +330,6 @@ namespace Microsoft.AspNetCore.ResponseCaching // Unshim IHttpSendFileFeature context.HttpContext.Features.Set(context.OriginalSendFileFeature); - // TODO: Move this temporary interface with endpoint to HttpAbstractions context.HttpContext.RemoveResponseCacheFeature(); } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachePolicyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachePolicyProvider.cs index f92527be20..912e6b48a4 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachePolicyProvider.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachePolicyProvider.cs @@ -15,7 +15,6 @@ namespace Microsoft.AspNetCore.ResponseCaching public virtual bool IsRequestCacheable(ResponseCacheContext context) { // Verify the method - // TODO: RFC lists POST as a cacheable method when explicit freshness information is provided, but this is not widely implemented. Will revisit. var request = context.HttpContext.Request; if (!string.Equals("GET", request.Method, StringComparison.OrdinalIgnoreCase) && !string.Equals("HEAD", request.Method, StringComparison.OrdinalIgnoreCase)) @@ -24,14 +23,12 @@ namespace Microsoft.AspNetCore.ResponseCaching } // Verify existence of authorization headers - // TODO: The server may indicate that the response to these request are cacheable if (!StringValues.IsNullOrEmpty(request.Headers[HeaderNames.Authorization])) { return false; } // Verify request cache-control parameters - // TODO: no-cache requests can be retrieved upon validation with origin if (!StringValues.IsNullOrEmpty(request.Headers[HeaderNames.CacheControl])) { if (context.RequestCacheControlHeaderValue.NoCache) @@ -52,14 +49,12 @@ namespace Microsoft.AspNetCore.ResponseCaching } } - // TODO: Verify global middleware settings? Explicit ignore list, range requests, etc. return true; } public virtual bool IsResponseCacheable(ResponseCacheContext context) { // Only cache pages explicitly marked with public - // TODO: Consider caching responses that are not marked as public but otherwise cacheable? if (!context.ResponseCacheControlHeaderValue.Public) { return false; @@ -72,7 +67,6 @@ namespace Microsoft.AspNetCore.ResponseCaching } // Check no-cache - // TODO: Handle no-cache with headers if (context.ResponseCacheControlHeaderValue.NoCache) { return false; @@ -93,8 +87,6 @@ namespace Microsoft.AspNetCore.ResponseCaching return false; } - // TODO: public MAY override the cacheability checks for private and status codes - // Check private if (context.ResponseCacheControlHeaderValue.Private) { @@ -102,14 +94,12 @@ namespace Microsoft.AspNetCore.ResponseCaching } // Check response code - // TODO: RFC also lists 203, 204, 206, 300, 301, 404, 405, 410, 414, and 501 as cacheable by default if (response.StatusCode != StatusCodes.Status200OK) { return false; } // Check response freshness - // TODO: apparent age vs corrected age value if (context.TypedResponseHeaders.Date == null) { if (context.ResponseCacheControlHeaderValue.SharedMaxAge == null && @@ -180,7 +170,6 @@ namespace Microsoft.AspNetCore.ResponseCaching // Request allows stale values if (age < context.RequestCacheControlHeaderValue.MaxStaleLimit) { - // TODO: Add warning header indicating the response is stale return true; } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheTests.cs index 54060d0fec..45fd90eb96 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheTests.cs @@ -518,6 +518,70 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } } + [Fact] + public async void ServesFreshContent_IfCachedVaryByUpdated_OnCacheMiss() + { + var builder = TestUtils.CreateBuilderWithResponseCache(requestDelegate: async (context) => + { + context.Response.Headers[HeaderNames.Vary] = context.Request.Headers[HeaderNames.Pragma]; + await TestUtils.TestRequestDelegate(context); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + client.DefaultRequestHeaders.From = "user@example.com"; + client.DefaultRequestHeaders.Pragma.Clear(); + client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("From")); + client.DefaultRequestHeaders.MaxForwards = 1; + var initialResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.From = "user2@example.com"; + client.DefaultRequestHeaders.Pragma.Clear(); + client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("Max-Forwards")); + client.DefaultRequestHeaders.MaxForwards = 2; + var otherResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.From = "user@example.com"; + client.DefaultRequestHeaders.Pragma.Clear(); + client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("From")); + client.DefaultRequestHeaders.MaxForwards = 1; + var subsequentResponse = await client.GetAsync(""); + + await AssertResponseNotCachedAsync(initialResponse, subsequentResponse); + } + } + + [Fact] + public async void ServesCachedContent_IfCachedVaryByNotUpdated_OnCacheMiss() + { + var builder = TestUtils.CreateBuilderWithResponseCache(requestDelegate: async (context) => + { + context.Response.Headers[HeaderNames.Vary] = context.Request.Headers[HeaderNames.Pragma]; + await TestUtils.TestRequestDelegate(context); + }); + + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + client.DefaultRequestHeaders.From = "user@example.com"; + client.DefaultRequestHeaders.Pragma.Clear(); + client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("From")); + client.DefaultRequestHeaders.MaxForwards = 1; + var initialResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.From = "user2@example.com"; + client.DefaultRequestHeaders.Pragma.Clear(); + client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("From")); + client.DefaultRequestHeaders.MaxForwards = 2; + var otherResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.From = "user@example.com"; + client.DefaultRequestHeaders.Pragma.Clear(); + client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("From")); + client.DefaultRequestHeaders.MaxForwards = 1; + var subsequentResponse = await client.GetAsync(""); + + await AssertResponseCachedAsync(initialResponse, subsequentResponse); + } + } + private static async Task AssertResponseCachedAsync(HttpResponseMessage initialResponse, HttpResponseMessage subsequentResponse) { initialResponse.EnsureSuccessStatusCode(); From d710e44c6365872071b40de8c86621efc6f6e30e Mon Sep 17 00:00:00 2001 From: BrennanConroy Date: Fri, 16 Sep 2016 09:59:03 -0700 Subject: [PATCH 032/188] Remove some duplicate work --- .../ResponseCacheMiddleware.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs index cd2ec42598..1e5da7d23d 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs @@ -181,8 +181,7 @@ namespace Microsoft.AspNetCore.ResponseCaching } } } - - if (cacheEntry is CachedResponse && await TryServeCachedResponseAsync(context, (CachedResponse)cacheEntry)) + else if (cacheEntry is CachedResponse && await TryServeCachedResponseAsync(context, (CachedResponse)cacheEntry)) { return true; } From 7f638c1385b1dfa3c8638f70603e63546a72bd36 Mon Sep 17 00:00:00 2001 From: John Luo Date: Fri, 16 Sep 2016 13:40:35 -0700 Subject: [PATCH 033/188] Always overwrite the VaryBy entry to ensure expiry is updated --- .../ResponseCacheMiddleware.cs | 6 +++--- .../ResponseCacheMiddlewareTests.cs | 12 ++++++++---- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs index 1e5da7d23d..b57bea561b 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs @@ -201,7 +201,6 @@ namespace Microsoft.AspNetCore.ResponseCaching if (_policyProvider.IsResponseCacheable(context)) { context.ShouldCacheResponse = true; - context.BaseKey = _keyProvider.CreateBaseKey(context); // Create the cache entry now var response = context.HttpContext.Response; @@ -230,10 +229,11 @@ namespace Microsoft.AspNetCore.ResponseCaching Headers = normalizedVaryHeaderValue, Params = normalizedVaryParamsValue }; - - _store.Set(context.BaseKey, context.CachedVaryByRules, context.CachedResponseValidFor); } + // Always overwrite the CachedVaryByRules to update the expiry information + _store.Set(context.BaseKey, context.CachedVaryByRules, context.CachedResponseValidFor); + context.StorageVaryKey = _keyProvider.CreateStorageVaryByKey(context); } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheMiddlewareTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheMiddlewareTests.cs index 8ef4b51bf0..4b66d8b21b 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheMiddlewareTests.cs @@ -320,7 +320,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public void FinalizeCacheHeaders_UpdateCachedVaryByRules_IfNotEquivalentToPrevious() + public async Task FinalizeCacheHeaders_UpdateCachedVaryByRules_IfNotEquivalentToPrevious() { var store = new TestResponseCacheStore(); var middleware = TestUtils.CreateTestMiddleware(store); @@ -336,6 +336,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests }; context.CachedVaryByRules = cachedVaryByRules; + await middleware.TryServeFromCacheAsync(context); middleware.FinalizeCacheHeaders(context); Assert.Equal(1, store.SetCount); @@ -343,7 +344,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public void FinalizeCacheHeaders_DoNotUpdateCachedVaryByRules_IfEquivalentToPrevious() + public async Task FinalizeCacheHeaders_DoNotUpdateCachedVaryByRules_IfEquivalentToPrevious() { var store = new TestResponseCacheStore(); var middleware = TestUtils.CreateTestMiddleware(store); @@ -360,9 +361,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests }; context.CachedVaryByRules = cachedVaryByRules; + await middleware.TryServeFromCacheAsync(context); middleware.FinalizeCacheHeaders(context); - Assert.Equal(0, store.SetCount); + // An update to the cache is always made but the entry should be the same + Assert.Equal(1, store.SetCount); Assert.Same(cachedVaryByRules, context.CachedVaryByRules); } @@ -411,12 +414,13 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public void FinalizeCachingHeaders_SplitsVaryHeaderByCommas() + public async Task FinalizeCacheHeaders_SplitsVaryHeaderByCommas() { var middleware = TestUtils.CreateTestMiddleware(); var context = TestUtils.CreateTestContext(); context.HttpContext.Response.Headers[HeaderNames.Vary] = "HeaderB, heaDera"; + await middleware.TryServeFromCacheAsync(context); middleware.FinalizeCacheHeaders(context); Assert.Equal(new StringValues(new[] { "HEADERA", "HEADERB" }), context.CachedVaryByRules.Headers); From fbac81a471a10232996cecb00563abc78055c3e7 Mon Sep 17 00:00:00 2001 From: BrennanConroy Date: Fri, 16 Sep 2016 16:59:29 -0700 Subject: [PATCH 034/188] Short circuit to avoid DateTimeOffset parsing --- .../ResponseCacheMiddleware.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs index b57bea561b..4b0889fc5d 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs @@ -355,7 +355,7 @@ namespace Microsoft.AspNetCore.ResponseCaching } } } - else if ((cachedResponseHeaders.LastModified ?? cachedResponseHeaders.Date) <= context.TypedRequestHeaders.IfUnmodifiedSince) + else if (context.TypedRequestHeaders.IfUnmodifiedSince != null && (cachedResponseHeaders.LastModified ?? cachedResponseHeaders.Date) <= context.TypedRequestHeaders.IfUnmodifiedSince) { return true; } From c30d471c27e7f80ffafe30c4905174036fdaa230 Mon Sep 17 00:00:00 2001 From: John Luo Date: Fri, 16 Sep 2016 11:39:53 -0700 Subject: [PATCH 035/188] Make IResponseCacheStore APIs async --- .../Interfaces/IResponseCacheStore.cs | 7 ++- .../Internal/DistributedResponseCacheStore.cs | 13 ++-- .../Internal/MemoryResponseCacheStore.cs | 12 ++-- .../ResponseCacheMiddleware.cs | 36 +++++------ .../ResponseCacheMiddlewareTests.cs | 62 +++++++++---------- .../TestUtils.cs | 12 ++-- 6 files changed, 75 insertions(+), 67 deletions(-) diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCacheStore.cs b/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCacheStore.cs index 90998a71d4..55ed237786 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCacheStore.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCacheStore.cs @@ -2,13 +2,14 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Threading.Tasks; namespace Microsoft.AspNetCore.ResponseCaching { public interface IResponseCacheStore { - object Get(string key); - void Set(string key, object entry, TimeSpan validFor); - void Remove(string key); + Task GetAsync(string key); + Task SetAsync(string key, object entry, TimeSpan validFor); + Task RemoveAsync(string key); } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/DistributedResponseCacheStore.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/DistributedResponseCacheStore.cs index 88fa05f3ea..170f7e65cd 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/DistributedResponseCacheStore.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/DistributedResponseCacheStore.cs @@ -2,6 +2,7 @@ // 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.Extensions.Caching.Distributed; namespace Microsoft.AspNetCore.ResponseCaching.Internal @@ -20,11 +21,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal _cache = cache; } - public object Get(string key) + public async Task GetAsync(string key) { try { - return CacheEntrySerializer.Deserialize(_cache.Get(key)); + return CacheEntrySerializer.Deserialize(await _cache.GetAsync(key)); } catch { @@ -32,20 +33,20 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal } } - public void Remove(string key) + public async Task RemoveAsync(string key) { try { - _cache.Remove(key); + await _cache.RemoveAsync(key); } catch { } } - public void Set(string key, object entry, TimeSpan validFor) + public async Task SetAsync(string key, object entry, TimeSpan validFor) { try { - _cache.Set( + await _cache.SetAsync( key, CacheEntrySerializer.Serialize(entry), new DistributedCacheEntryOptions() diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCacheStore.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCacheStore.cs index b92693137b..33b7f4367b 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCacheStore.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCacheStore.cs @@ -2,7 +2,9 @@ // 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.Extensions.Caching.Memory; +using Microsoft.Extensions.Internal; namespace Microsoft.AspNetCore.ResponseCaching.Internal { @@ -20,17 +22,18 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal _cache = cache; } - public object Get(string key) + public Task GetAsync(string key) { - return _cache.Get(key); + return Task.FromResult(_cache.Get(key)); } - public void Remove(string key) + public Task RemoveAsync(string key) { _cache.Remove(key); + return TaskCache.CompletedTask; } - public void Set(string key, object entry, TimeSpan validFor) + public Task SetAsync(string key, object entry, TimeSpan validFor) { _cache.Set( key, @@ -39,6 +42,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal { AbsoluteExpirationRelativeToNow = validFor }); + return TaskCache.CompletedTask; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs index 4b0889fc5d..850bfdb236 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs @@ -60,11 +60,7 @@ namespace Microsoft.AspNetCore.ResponseCaching _options = options.Value; _policyProvider = policyProvider; _keyProvider = keyProvider; - _onStartingCallback = state => - { - OnResponseStarting((ResponseCacheContext)state); - return TaskCache.CompletedTask; - }; + _onStartingCallback = state => OnResponseStartingAsync((ResponseCacheContext)state); } public async Task Invoke(HttpContext httpContext) @@ -91,10 +87,10 @@ namespace Microsoft.AspNetCore.ResponseCaching await _next(httpContext); // If there was no response body, check the response headers now. We can cache things like redirects. - OnResponseStarting(context); + await OnResponseStartingAsync(context); // Finalize the cache entry - FinalizeCacheBody(context); + await FinalizeCacheBodyAsync(context); } finally { @@ -135,7 +131,7 @@ namespace Microsoft.AspNetCore.ResponseCaching response.Headers[HeaderNames.Age] = context.CachedEntryAge.TotalSeconds.ToString("F0", CultureInfo.InvariantCulture); var body = context.CachedResponse.Body ?? - ((CachedResponseBody)_store.Get(context.CachedResponse.BodyKeyPrefix))?.Body; + ((CachedResponseBody) await _store.GetAsync(context.CachedResponse.BodyKeyPrefix))?.Body; // If the body is not found, something went wrong. if (body == null) @@ -164,7 +160,7 @@ namespace Microsoft.AspNetCore.ResponseCaching internal async Task TryServeFromCacheAsync(ResponseCacheContext context) { context.BaseKey = _keyProvider.CreateBaseKey(context); - var cacheEntry = _store.Get(context.BaseKey); + var cacheEntry = await _store.GetAsync(context.BaseKey); if (cacheEntry is CachedVaryByRules) { @@ -173,7 +169,7 @@ namespace Microsoft.AspNetCore.ResponseCaching foreach (var varyKey in _keyProvider.CreateLookupVaryByKeys(context)) { - cacheEntry = _store.Get(varyKey); + cacheEntry = await _store.GetAsync(varyKey); if (cacheEntry is CachedResponse && await TryServeCachedResponseAsync(context, (CachedResponse)cacheEntry)) { @@ -196,7 +192,7 @@ namespace Microsoft.AspNetCore.ResponseCaching return false; } - internal void FinalizeCacheHeaders(ResponseCacheContext context) + internal async Task FinalizeCacheHeadersAsync(ResponseCacheContext context) { if (_policyProvider.IsResponseCacheable(context)) { @@ -232,7 +228,7 @@ namespace Microsoft.AspNetCore.ResponseCaching } // Always overwrite the CachedVaryByRules to update the expiry information - _store.Set(context.BaseKey, context.CachedVaryByRules, context.CachedResponseValidFor); + await _store.SetAsync(context.BaseKey, context.CachedVaryByRules, context.CachedResponseValidFor); context.StorageVaryKey = _keyProvider.CreateStorageVaryByKey(context); } @@ -265,7 +261,7 @@ namespace Microsoft.AspNetCore.ResponseCaching } } - internal void FinalizeCacheBody(ResponseCacheContext context) + internal async Task FinalizeCacheBodyAsync(ResponseCacheContext context) { if (context.ShouldCacheResponse && context.ResponseCacheStream.BufferingEnabled && @@ -275,32 +271,36 @@ namespace Microsoft.AspNetCore.ResponseCaching if (context.ResponseCacheStream.BufferedStream.Length >= _options.MinimumSplitBodySize) { // Store response and response body separately - _store.Set(context.StorageVaryKey ?? context.BaseKey, context.CachedResponse, context.CachedResponseValidFor); + await _store.SetAsync(context.StorageVaryKey ?? context.BaseKey, context.CachedResponse, context.CachedResponseValidFor); var cachedResponseBody = new CachedResponseBody() { Body = context.ResponseCacheStream.BufferedStream.ToArray() }; - _store.Set(context.CachedResponse.BodyKeyPrefix, cachedResponseBody, context.CachedResponseValidFor); + await _store.SetAsync(context.CachedResponse.BodyKeyPrefix, cachedResponseBody, context.CachedResponseValidFor); } else { // Store response and response body together context.CachedResponse.Body = context.ResponseCacheStream.BufferedStream.ToArray(); - _store.Set(context.StorageVaryKey ?? context.BaseKey, context.CachedResponse, context.CachedResponseValidFor); + await _store.SetAsync(context.StorageVaryKey ?? context.BaseKey, context.CachedResponse, context.CachedResponseValidFor); } } } - internal void OnResponseStarting(ResponseCacheContext context) + internal Task OnResponseStartingAsync(ResponseCacheContext context) { if (!context.ResponseStarted) { context.ResponseStarted = true; context.ResponseTime = _options.SystemClock.UtcNow; - FinalizeCacheHeaders(context); + return FinalizeCacheHeadersAsync(context); + } + else + { + return TaskCache.CompletedTask; } } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheMiddlewareTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheMiddlewareTests.cs index 4b66d8b21b..d7db493fd9 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheMiddlewareTests.cs @@ -49,7 +49,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var middleware = TestUtils.CreateTestMiddleware(store: store, keyProvider: new TestResponseCacheKeyProvider("BaseKey")); var context = TestUtils.CreateTestContext(); - store.Set( + await store.SetAsync( "BaseKey", new CachedResponse() { @@ -68,7 +68,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var middleware = TestUtils.CreateTestMiddleware(store: store, keyProvider: new TestResponseCacheKeyProvider("BaseKey")); var context = TestUtils.CreateTestContext(); - store.Set( + await store.SetAsync( "BaseKey", new CachedVaryByRules(), TimeSpan.Zero); @@ -84,11 +84,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var middleware = TestUtils.CreateTestMiddleware(store: store, keyProvider: new TestResponseCacheKeyProvider("BaseKey", new[] { "VaryKey", "VaryKey2" })); var context = TestUtils.CreateTestContext(); - store.Set( + await store.SetAsync( "BaseKey", new CachedVaryByRules(), TimeSpan.Zero); - store.Set( + await store.SetAsync( "BaseKeyVaryKey2", new CachedResponse() { @@ -226,7 +226,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public void FinalizeCacheHeaders_DoNotUpdateShouldCacheResponse_IfResponseIsNotCacheable() + public async Task FinalizeCacheHeaders_DoNotUpdateShouldCacheResponse_IfResponseIsNotCacheable() { var middleware = TestUtils.CreateTestMiddleware(policyProvider: new ResponseCachePolicyProvider()); var context = TestUtils.CreateTestContext(); @@ -234,13 +234,13 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Assert.False(context.ShouldCacheResponse); middleware.ShimResponseStream(context); - middleware.FinalizeCacheHeaders(context); + await middleware.FinalizeCacheHeadersAsync(context); Assert.False(context.ShouldCacheResponse); } [Fact] - public void FinalizeCacheHeaders_UpdateShouldCacheResponse_IfResponseIsCacheable() + public async Task FinalizeCacheHeaders_UpdateShouldCacheResponse_IfResponseIsCacheable() { var middleware = TestUtils.CreateTestMiddleware(policyProvider: new ResponseCachePolicyProvider()); var context = TestUtils.CreateTestContext(); @@ -251,24 +251,24 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Assert.False(context.ShouldCacheResponse); - middleware.FinalizeCacheHeaders(context); + await middleware.FinalizeCacheHeadersAsync(context); Assert.True(context.ShouldCacheResponse); } [Fact] - public void FinalizeCacheHeaders_DefaultResponseValidity_Is10Seconds() + public async Task FinalizeCacheHeaders_DefaultResponseValidity_Is10Seconds() { var middleware = TestUtils.CreateTestMiddleware(); var context = TestUtils.CreateTestContext(); - middleware.FinalizeCacheHeaders(context); + await middleware.FinalizeCacheHeadersAsync(context); Assert.Equal(TimeSpan.FromSeconds(10), context.CachedResponseValidFor); } [Fact] - public void FinalizeCacheHeaders_ResponseValidity_UseExpiryIfAvailable() + public async Task FinalizeCacheHeaders_ResponseValidity_UseExpiryIfAvailable() { var utcNow = DateTimeOffset.MinValue; var middleware = TestUtils.CreateTestMiddleware(); @@ -277,13 +277,13 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.ResponseTime = utcNow; context.TypedResponseHeaders.Expires = utcNow + TimeSpan.FromSeconds(11); - middleware.FinalizeCacheHeaders(context); + await middleware.FinalizeCacheHeadersAsync(context); Assert.Equal(TimeSpan.FromSeconds(11), context.CachedResponseValidFor); } [Fact] - public void FinalizeCacheHeaders_ResponseValidity_UseMaxAgeIfAvailable() + public async Task FinalizeCacheHeaders_ResponseValidity_UseMaxAgeIfAvailable() { var middleware = TestUtils.CreateTestMiddleware(); var context = TestUtils.CreateTestContext(); @@ -295,13 +295,13 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.ResponseTime = DateTimeOffset.UtcNow; context.TypedResponseHeaders.Expires = context.ResponseTime + TimeSpan.FromSeconds(11); - middleware.FinalizeCacheHeaders(context); + await middleware.FinalizeCacheHeadersAsync(context); Assert.Equal(TimeSpan.FromSeconds(12), context.CachedResponseValidFor); } [Fact] - public void FinalizeCacheHeaders_ResponseValidity_UseSharedMaxAgeIfAvailable() + public async Task FinalizeCacheHeaders_ResponseValidity_UseSharedMaxAgeIfAvailable() { var middleware = TestUtils.CreateTestMiddleware(); var context = TestUtils.CreateTestContext(); @@ -314,7 +314,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.ResponseTime = DateTimeOffset.UtcNow; context.TypedResponseHeaders.Expires = context.ResponseTime + TimeSpan.FromSeconds(11); - middleware.FinalizeCacheHeaders(context); + await middleware.FinalizeCacheHeadersAsync(context); Assert.Equal(TimeSpan.FromSeconds(13), context.CachedResponseValidFor); } @@ -337,7 +337,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.CachedVaryByRules = cachedVaryByRules; await middleware.TryServeFromCacheAsync(context); - middleware.FinalizeCacheHeaders(context); + await middleware.FinalizeCacheHeadersAsync(context); Assert.Equal(1, store.SetCount); Assert.NotSame(cachedVaryByRules, context.CachedVaryByRules); @@ -362,7 +362,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.CachedVaryByRules = cachedVaryByRules; await middleware.TryServeFromCacheAsync(context); - middleware.FinalizeCacheHeaders(context); + await middleware.FinalizeCacheHeadersAsync(context); // An update to the cache is always made but the entry should be the same Assert.Equal(1, store.SetCount); @@ -370,7 +370,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public void FinalizeCacheHeaders_DoNotAddDate_IfSpecified() + public async Task FinalizeCacheHeaders_DoNotAddDate_IfSpecified() { var utcNow = DateTimeOffset.MinValue; var middleware = TestUtils.CreateTestMiddleware(); @@ -379,13 +379,13 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Assert.Null(context.TypedResponseHeaders.Date); - middleware.FinalizeCacheHeaders(context); + await middleware.FinalizeCacheHeadersAsync(context); Assert.Equal(utcNow, context.TypedResponseHeaders.Date); } [Fact] - public void FinalizeCacheHeaders_AddsDate_IfNoneSpecified() + public async Task FinalizeCacheHeaders_AddsDate_IfNoneSpecified() { var utcNow = DateTimeOffset.MinValue; var middleware = TestUtils.CreateTestMiddleware(); @@ -395,20 +395,20 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Assert.Equal(utcNow, context.TypedResponseHeaders.Date); - middleware.FinalizeCacheHeaders(context); + await middleware.FinalizeCacheHeadersAsync(context); Assert.Equal(utcNow, context.TypedResponseHeaders.Date); } [Fact] - public void FinalizeCacheHeaders_StoresCachedResponse_InState() + public async Task FinalizeCacheHeaders_StoresCachedResponse_InState() { var middleware = TestUtils.CreateTestMiddleware(); var context = TestUtils.CreateTestContext(); Assert.Null(context.CachedResponse); - middleware.FinalizeCacheHeaders(context); + await middleware.FinalizeCacheHeadersAsync(context); Assert.NotNull(context.CachedResponse); } @@ -421,7 +421,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.HttpContext.Response.Headers[HeaderNames.Vary] = "HeaderB, heaDera"; await middleware.TryServeFromCacheAsync(context); - middleware.FinalizeCacheHeaders(context); + await middleware.FinalizeCacheHeadersAsync(context); Assert.Equal(new StringValues(new[] { "HEADERA", "HEADERB" }), context.CachedVaryByRules.Headers); } @@ -444,7 +444,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.BaseKey = "BaseKey"; context.CachedResponseValidFor = TimeSpan.FromSeconds(10); - middleware.FinalizeCacheBody(context); + await middleware.FinalizeCacheBodyAsync(context); Assert.Equal(2, store.SetCount); } @@ -467,7 +467,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.BaseKey = "BaseKey"; context.CachedResponseValidFor = TimeSpan.FromSeconds(10); - middleware.FinalizeCacheBody(context); + await middleware.FinalizeCacheBodyAsync(context); Assert.Equal(1, store.SetCount); } @@ -493,7 +493,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.BaseKey = "BaseKey"; context.CachedResponseValidFor = TimeSpan.FromSeconds(10); - middleware.FinalizeCacheBody(context); + await middleware.FinalizeCacheBodyAsync(context); Assert.Equal(1, store.SetCount); } @@ -517,7 +517,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.BaseKey = "BaseKey"; context.CachedResponseValidFor = TimeSpan.FromSeconds(10); - middleware.FinalizeCacheBody(context); + await middleware.FinalizeCacheBodyAsync(context); Assert.Equal(1, store.SetCount); } @@ -541,7 +541,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.BaseKey = "BaseKey"; context.CachedResponseValidFor = TimeSpan.FromSeconds(10); - middleware.FinalizeCacheBody(context); + await middleware.FinalizeCacheBodyAsync(context); Assert.Equal(0, store.SetCount); } @@ -564,7 +564,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.BaseKey = "BaseKey"; context.CachedResponseValidFor = TimeSpan.FromSeconds(10); - middleware.FinalizeCacheBody(context); + await middleware.FinalizeCacheBodyAsync(context); Assert.Equal(1, store.SetCount); } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs index 96d45f71d2..3ccb72fe0c 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs @@ -168,27 +168,29 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests public int GetCount { get; private set; } public int SetCount { get; private set; } - public object Get(string key) + public Task GetAsync(string key) { GetCount++; try { - return _storage[key]; + return Task.FromResult(_storage[key]); } catch { - return null; + return Task.FromResult(null); } } - public void Remove(string key) + public Task RemoveAsync(string key) { + return TaskCache.CompletedTask; } - public void Set(string key, object entry, TimeSpan validFor) + public Task SetAsync(string key, object entry, TimeSpan validFor) { SetCount++; _storage[key] = entry; + return TaskCache.CompletedTask; } } } From 6891d00032577651b97f59e2a81d7c60f4bed323 Mon Sep 17 00:00:00 2001 From: John Luo Date: Tue, 20 Sep 2016 12:32:51 -0700 Subject: [PATCH 036/188] Stricter expiration checks to avoid serving responses when max-age is 0 Cache parsed response headers for performance --- .../ResponseCacheContext.cs | 36 ++++++++++ .../ResponseCacheMiddleware.cs | 24 ++++--- .../ResponseCachePolicyProvider.cs | 32 ++++----- .../ResponseCachePolicyProviderTests.cs | 69 ++++++++++--------- 4 files changed, 102 insertions(+), 59 deletions(-) diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheContext.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheContext.cs index 70f96d7537..19b77538f9 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheContext.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheContext.cs @@ -19,6 +19,10 @@ namespace Microsoft.AspNetCore.ResponseCaching private ResponseHeaders _responseHeaders; private CacheControlHeaderValue _requestCacheControl; private CacheControlHeaderValue _responseCacheControl; + private DateTimeOffset? _responseDate; + private bool _parsedResponseDate; + private DateTimeOffset? _responseExpires; + private bool _parsedResponseExpires; internal ResponseCacheContext( HttpContext httpContext) @@ -101,5 +105,37 @@ namespace Microsoft.AspNetCore.ResponseCaching return _responseCacheControl; } } + + internal DateTimeOffset? ResponseDate + { + get + { + if (!_parsedResponseDate) + { + _parsedResponseDate = true; + _responseDate = TypedResponseHeaders.Date; + } + return _responseDate; + } + set + { + // Don't reparse the response date again if it's explicitly set + _parsedResponseDate = true; + _responseDate = value; + } + } + + internal DateTimeOffset? ResponseExpires + { + get + { + if (!_parsedResponseExpires) + { + _parsedResponseExpires = true; + _responseExpires = TypedResponseHeaders.Expires; + } + return _responseExpires; + } + } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs index 850bfdb236..8697aecc5c 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs @@ -143,7 +143,7 @@ namespace Microsoft.AspNetCore.ResponseCaching if (body.Length > 0) { // Add a content-length if required - if (response.ContentLength == null && StringValues.IsNullOrEmpty(response.Headers[HeaderNames.TransferEncoding])) + if (!response.ContentLength.HasValue && StringValues.IsNullOrEmpty(response.Headers[HeaderNames.TransferEncoding])) { response.ContentLength = body.Length; } @@ -204,7 +204,7 @@ namespace Microsoft.AspNetCore.ResponseCaching var varyParamsValue = context.HttpContext.GetResponseCacheFeature()?.VaryByParams ?? StringValues.Empty; context.CachedResponseValidFor = context.ResponseCacheControlHeaderValue.SharedMaxAge ?? context.ResponseCacheControlHeaderValue.MaxAge ?? - (context.TypedResponseHeaders.Expires - context.ResponseTime) ?? + (context.ResponseExpires - context.ResponseTime) ?? DefaultExpirationTimeSpan; // Check if any vary rules exist @@ -234,16 +234,18 @@ namespace Microsoft.AspNetCore.ResponseCaching } // Ensure date header is set - if (context.TypedResponseHeaders.Date == null) + if (!context.ResponseDate.HasValue) { - context.TypedResponseHeaders.Date = context.ResponseTime; + context.ResponseDate = context.ResponseTime; + // Setting the date on the raw response headers. + context.TypedResponseHeaders.Date = context.ResponseDate; } // Store the response on the state context.CachedResponse = new CachedResponse { BodyKeyPrefix = FastGuid.NewGuid().IdString, - Created = context.TypedResponseHeaders.Date.Value, + Created = context.ResponseDate.Value, StatusCode = context.HttpContext.Response.StatusCode }; @@ -263,10 +265,10 @@ namespace Microsoft.AspNetCore.ResponseCaching internal async Task FinalizeCacheBodyAsync(ResponseCacheContext context) { + var contentLength = context.TypedResponseHeaders.ContentLength; if (context.ShouldCacheResponse && context.ResponseCacheStream.BufferingEnabled && - (context.TypedResponseHeaders.ContentLength == null || - context.TypedResponseHeaders.ContentLength == context.ResponseCacheStream.BufferedStream.Length)) + (!contentLength.HasValue || contentLength == context.ResponseCacheStream.BufferedStream.Length)) { if (context.ResponseCacheStream.BufferedStream.Length >= _options.MinimumSplitBodySize) { @@ -355,9 +357,13 @@ namespace Microsoft.AspNetCore.ResponseCaching } } } - else if (context.TypedRequestHeaders.IfUnmodifiedSince != null && (cachedResponseHeaders.LastModified ?? cachedResponseHeaders.Date) <= context.TypedRequestHeaders.IfUnmodifiedSince) + else { - return true; + var ifUnmodifiedSince = context.TypedRequestHeaders.IfUnmodifiedSince; + if (ifUnmodifiedSince != null && (cachedResponseHeaders.LastModified ?? cachedResponseHeaders.Date) <= ifUnmodifiedSince) + { + return true; + } } return false; diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachePolicyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachePolicyProvider.cs index 912e6b48a4..fc6d51dae9 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachePolicyProvider.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachePolicyProvider.cs @@ -100,35 +100,35 @@ namespace Microsoft.AspNetCore.ResponseCaching } // Check response freshness - if (context.TypedResponseHeaders.Date == null) + if (!context.ResponseDate.HasValue) { - if (context.ResponseCacheControlHeaderValue.SharedMaxAge == null && - context.ResponseCacheControlHeaderValue.MaxAge == null && - context.ResponseTime > context.TypedResponseHeaders.Expires) + if (!context.ResponseCacheControlHeaderValue.SharedMaxAge.HasValue && + !context.ResponseCacheControlHeaderValue.MaxAge.HasValue && + context.ResponseTime >= context.ResponseExpires) { return false; } } else { - var age = context.ResponseTime - context.TypedResponseHeaders.Date.Value; + var age = context.ResponseTime - context.ResponseDate.Value; // Validate shared max age - if (age > context.ResponseCacheControlHeaderValue.SharedMaxAge) + if (age >= context.ResponseCacheControlHeaderValue.SharedMaxAge) { return false; } - else if (context.ResponseCacheControlHeaderValue.SharedMaxAge == null) + else if (!context.ResponseCacheControlHeaderValue.SharedMaxAge.HasValue) { // Validate max age - if (age > context.ResponseCacheControlHeaderValue.MaxAge) + if (age >= context.ResponseCacheControlHeaderValue.MaxAge) { return false; } - else if (context.ResponseCacheControlHeaderValue.MaxAge == null) + else if (!context.ResponseCacheControlHeaderValue.MaxAge.HasValue) { // Validate expiration - if (context.ResponseTime > context.TypedResponseHeaders.Expires) + if (context.ResponseTime >= context.ResponseExpires) { return false; } @@ -145,21 +145,21 @@ namespace Microsoft.AspNetCore.ResponseCaching var cachedControlHeaders = context.CachedResponseHeaders.CacheControl ?? EmptyCacheControl; // Add min-fresh requirements - if (context.RequestCacheControlHeaderValue.MinFresh != null) + if (context.RequestCacheControlHeaderValue.MinFresh.HasValue) { age += context.RequestCacheControlHeaderValue.MinFresh.Value; } // Validate shared max age, this overrides any max age settings for shared caches - if (age > cachedControlHeaders.SharedMaxAge) + if (age >= cachedControlHeaders.SharedMaxAge) { // shared max age implies must revalidate return false; } - else if (cachedControlHeaders.SharedMaxAge == null) + else if (!cachedControlHeaders.SharedMaxAge.HasValue) { // Validate max age - if (age > cachedControlHeaders.MaxAge || age > context.RequestCacheControlHeaderValue.MaxAge) + if (age >= cachedControlHeaders.MaxAge || age >= context.RequestCacheControlHeaderValue.MaxAge) { // Must revalidate if (cachedControlHeaders.MustRevalidate) @@ -175,10 +175,10 @@ namespace Microsoft.AspNetCore.ResponseCaching return false; } - else if (cachedControlHeaders.MaxAge == null && context.RequestCacheControlHeaderValue.MaxAge == null) + else if (!cachedControlHeaders.MaxAge.HasValue && !context.RequestCacheControlHeaderValue.MaxAge.HasValue) { // Validate expiration - if (context.ResponseTime > context.CachedResponseHeaders.Expires) + if (context.ResponseTime >= context.CachedResponseHeaders.Expires) { return false; } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachePolicyProviderTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachePolicyProviderTests.cs index fb1b817e57..0afbf329e3 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachePolicyProviderTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachePolicyProviderTests.cs @@ -290,7 +290,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public void IsResponseCacheable_PastExpiry_NotAllowed() + public void IsResponseCacheable_AtExpiry_NotAllowed() { var context = TestUtils.CreateTestContext(); context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; @@ -302,7 +302,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.TypedResponseHeaders.Expires = utcNow; context.TypedResponseHeaders.Date = utcNow; - context.ResponseTime = DateTimeOffset.MaxValue; + context.ResponseTime = utcNow; Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); } @@ -338,7 +338,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests }; context.TypedResponseHeaders.Expires = utcNow; context.TypedResponseHeaders.Date = utcNow; - context.ResponseTime = utcNow + TimeSpan.FromSeconds(11); + context.ResponseTime = utcNow + TimeSpan.FromSeconds(10); Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); } @@ -362,7 +362,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public void IsResponseCacheable_SharedMaxAgeOverridesMaxAge_ToNotFresh() + public void IsResponseCacheable_SharedMaxAgeOverridesMaxAge_ToNotAllowed() { var utcNow = DateTimeOffset.UtcNow; var context = TestUtils.CreateTestContext(); @@ -374,7 +374,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests SharedMaxAge = TimeSpan.FromSeconds(5) }; context.TypedResponseHeaders.Date = utcNow; - context.ResponseTime = utcNow + TimeSpan.FromSeconds(6); + context.ResponseTime = utcNow + TimeSpan.FromSeconds(5); Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); } @@ -408,17 +408,18 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public void IsCachedEntryFresh_PastExpiry_IsNotFresh() + public void IsCachedEntryFresh_AtExpiry_IsNotFresh() { + var utcNow = DateTimeOffset.UtcNow; var context = TestUtils.CreateTestContext(); - context.ResponseTime = DateTimeOffset.MaxValue; + context.ResponseTime = utcNow; context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) { CacheControl = new CacheControlHeaderValue() { Public = true }, - Expires = DateTimeOffset.UtcNow + Expires = utcNow }; Assert.False(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); @@ -449,7 +450,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var utcNow = DateTimeOffset.UtcNow; var context = TestUtils.CreateTestContext(); - context.CachedEntryAge = TimeSpan.FromSeconds(11); + context.CachedEntryAge = TimeSpan.FromSeconds(10); context.ResponseTime = utcNow + context.CachedEntryAge; context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) { @@ -490,7 +491,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var utcNow = DateTimeOffset.UtcNow; var context = TestUtils.CreateTestContext(); - context.CachedEntryAge = TimeSpan.FromSeconds(6); + context.CachedEntryAge = TimeSpan.FromSeconds(5); context.ResponseTime = utcNow + context.CachedEntryAge; context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) { @@ -512,7 +513,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var context = TestUtils.CreateTestContext(); context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() { - MinFresh = TimeSpan.FromSeconds(3) + MinFresh = TimeSpan.FromSeconds(2) }; context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) { @@ -542,7 +543,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests MaxAge = TimeSpan.FromSeconds(10), } }; - context.CachedEntryAge = TimeSpan.FromSeconds(6); + context.CachedEntryAge = TimeSpan.FromSeconds(5); Assert.False(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); } @@ -569,6 +570,28 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Assert.True(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); } + [Fact] + public void IsCachedEntryFresh_MaxStaleOverridesFreshness_ButStillNotFresh() + { + var context = TestUtils.CreateTestContext(); + context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() + { + MaxAge = TimeSpan.FromSeconds(5), + MaxStale = true, // This value must be set to true in order to specify MaxStaleLimit + MaxStaleLimit = TimeSpan.FromSeconds(6) + }; + context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) + { + CacheControl = new CacheControlHeaderValue() + { + MaxAge = TimeSpan.FromSeconds(5), + } + }; + context.CachedEntryAge = TimeSpan.FromSeconds(6); + + Assert.False(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); + } + [Fact] public void IsCachedEntryFresh_MustRevalidateOverridesRequestMaxStale_ToNotFresh() { @@ -591,27 +614,5 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Assert.False(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); } - - [Fact] - public void IsCachedEntryFresh_IgnoresRequestVerificationWhenSpecified() - { - var context = TestUtils.CreateTestContext(); - context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() - { - MinFresh = TimeSpan.FromSeconds(1), - MaxAge = TimeSpan.FromSeconds(3) - }; - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) - { - CacheControl = new CacheControlHeaderValue() - { - MaxAge = TimeSpan.FromSeconds(10), - SharedMaxAge = TimeSpan.FromSeconds(5) - } - }; - context.CachedEntryAge = TimeSpan.FromSeconds(3); - - Assert.True(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); - } } } From 5e12a103a4c204d4b3fb68d07e43ce2299b29cf9 Mon Sep 17 00:00:00 2001 From: John Luo Date: Tue, 20 Sep 2016 14:29:42 -0700 Subject: [PATCH 037/188] API updates - Internalize properties on ResponseCacheContext unless required by extension points - Rename VaryByParams -> VaryByQueryKeys --- .../CacheEntry/CacheEntrySerializer.cs | 20 +++--- .../CacheEntry/CachedVaryByRules.cs | 2 +- .../ResponseCacheContext.cs | 24 +++---- .../ResponseCacheFeature.cs | 2 +- .../ResponseCacheKeyProvider.cs | 20 +++--- .../ResponseCacheMiddleware.cs | 26 ++++---- .../ResponseCachePolicyProvider.cs | 10 +-- .../CacheEntrySerializerTests.cs | 14 ++-- .../ResponseCacheKeyProviderTests.cs | 36 +++++------ .../ResponseCacheMiddlewareTests.cs | 10 ++- .../ResponseCachePolicyProviderTests.cs | 3 + .../ResponseCacheTests.cs | 64 +++++++++---------- .../TestUtils.cs | 5 +- 13 files changed, 120 insertions(+), 116 deletions(-) diff --git a/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CacheEntrySerializer.cs b/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CacheEntrySerializer.cs index 81e89f7304..385d5ebf78 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CacheEntrySerializer.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CacheEntrySerializer.cs @@ -127,8 +127,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal // Guid (long) // Headers count // Header(s) (comma separated string) - // Params count - // Param(s) (comma separated string) + // QueryKey count + // QueryKey(s) (comma separated string) private static CachedVaryByRules ReadCachedVaryByRules(BinaryReader reader) { var varyKeyPrefix = reader.ReadString(); @@ -139,14 +139,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal { headers[index] = reader.ReadString(); } - var paramCount = reader.ReadInt32(); - var param = new string[paramCount]; - for (var index = 0; index < paramCount; index++) + var queryKeysCount = reader.ReadInt32(); + var queryKeys = new string[queryKeysCount]; + for (var index = 0; index < queryKeysCount; index++) { - param[index] = reader.ReadString(); + queryKeys[index] = reader.ReadString(); } - return new CachedVaryByRules { VaryByKeyPrefix = varyKeyPrefix, Headers = headers, Params = param }; + return new CachedVaryByRules { VaryByKeyPrefix = varyKeyPrefix, Headers = headers, QueryKeys = queryKeys }; } // See serialization format above @@ -228,10 +228,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal writer.Write(header); } - writer.Write(varyByRules.Params.Count); - foreach (var param in varyByRules.Params) + writer.Write(varyByRules.QueryKeys.Count); + foreach (var queryKey in varyByRules.QueryKeys) { - writer.Write(param); + writer.Write(queryKey); } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedVaryByRules.cs b/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedVaryByRules.cs index e301edbf4c..c79d72ddc0 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedVaryByRules.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedVaryByRules.cs @@ -11,6 +11,6 @@ namespace Microsoft.AspNetCore.ResponseCaching public StringValues Headers { get; set; } - public StringValues Params { get; set; } + public StringValues QueryKeys { get; set; } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheContext.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheContext.cs index 19b77538f9..ac86b028ad 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheContext.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheContext.cs @@ -32,22 +32,22 @@ namespace Microsoft.AspNetCore.ResponseCaching public HttpContext HttpContext { get; } - public bool ShouldCacheResponse { get; internal set; } + public DateTimeOffset? ResponseTime { get; internal set; } - public string BaseKey { get; internal set; } - - public string StorageVaryKey { get; internal set; } - - public DateTimeOffset ResponseTime { get; internal set; } - - public TimeSpan CachedEntryAge { get; internal set; } - - public TimeSpan CachedResponseValidFor { get; internal set; } - - public CachedResponse CachedResponse { get; internal set; } + public TimeSpan? CachedEntryAge { get; internal set; } public CachedVaryByRules CachedVaryByRules { get; internal set; } + internal bool ShouldCacheResponse { get; set; } + + internal string BaseKey { get; set; } + + internal string StorageVaryKey { get; set; } + + internal TimeSpan CachedResponseValidFor { get; set; } + + internal CachedResponse CachedResponse { get; set; } + internal bool ResponseStarted { get; set; } internal Stream OriginalResponseStream { get; set; } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheFeature.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheFeature.cs index 5646d15867..eec1a64887 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheFeature.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheFeature.cs @@ -7,6 +7,6 @@ namespace Microsoft.AspNetCore.ResponseCaching { public class ResponseCacheFeature { - public StringValues VaryByParams { get; set; } + public StringValues VaryByQueryKeys { get; set; } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheKeyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheKeyProvider.cs index 318842f47f..a0db9a0a63 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheKeyProvider.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheKeyProvider.cs @@ -75,12 +75,12 @@ namespace Microsoft.AspNetCore.ResponseCaching } var varyByRules = context.CachedVaryByRules; - if (varyByRules == null) + if (varyByRules == null) { throw new InvalidOperationException($"{nameof(CachedVaryByRules)} must not be null on the {nameof(ResponseCacheContext)}"); } - if ((StringValues.IsNullOrEmpty(varyByRules.Headers) && StringValues.IsNullOrEmpty(varyByRules.Params))) + if ((StringValues.IsNullOrEmpty(varyByRules.Headers) && StringValues.IsNullOrEmpty(varyByRules.QueryKeys))) { return varyByRules.VaryByKeyPrefix; } @@ -110,16 +110,16 @@ namespace Microsoft.AspNetCore.ResponseCaching } } - // Vary by query params - if (varyByRules?.Params.Count > 0) + // Vary by query keys + if (varyByRules?.QueryKeys.Count > 0) { - // Append a group separator for the query parameter segment of the cache key + // Append a group separator for the query key segment of the cache key builder.Append(KeyDelimiter) .Append('Q'); - if (varyByRules.Params.Count == 1 && string.Equals(varyByRules.Params[0], "*", StringComparison.Ordinal)) + if (varyByRules.QueryKeys.Count == 1 && string.Equals(varyByRules.QueryKeys[0], "*", StringComparison.Ordinal)) { - // Vary by all available query params + // Vary by all available query keys foreach (var query in context.HttpContext.Request.Query.OrderBy(q => q.Key, StringComparer.OrdinalIgnoreCase)) { builder.Append(KeyDelimiter) @@ -130,13 +130,13 @@ namespace Microsoft.AspNetCore.ResponseCaching } else { - foreach (var param in varyByRules.Params) + foreach (var queryKey in varyByRules.QueryKeys) { builder.Append(KeyDelimiter) - .Append(param) + .Append(queryKey) .Append("=") // TODO: Perf - iterate the string values instead? - .Append(context.HttpContext.Request.Query[param]); + .Append(context.HttpContext.Request.Query[queryKey]); } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs index 8697aecc5c..6ceb6ad8d0 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs @@ -108,7 +108,7 @@ namespace Microsoft.AspNetCore.ResponseCaching context.CachedResponse = cachedResponse; context.CachedResponseHeaders = new ResponseHeaders(cachedResponse.Headers); context.ResponseTime = _options.SystemClock.UtcNow; - var cachedEntryAge = context.ResponseTime - context.CachedResponse.Created; + var cachedEntryAge = context.ResponseTime.Value - context.CachedResponse.Created; context.CachedEntryAge = cachedEntryAge > TimeSpan.Zero ? cachedEntryAge : TimeSpan.Zero; if (_policyProvider.IsCachedEntryFresh(context)) @@ -128,7 +128,7 @@ namespace Microsoft.AspNetCore.ResponseCaching response.Headers.Add(header); } - response.Headers[HeaderNames.Age] = context.CachedEntryAge.TotalSeconds.ToString("F0", CultureInfo.InvariantCulture); + response.Headers[HeaderNames.Age] = context.CachedEntryAge.Value.TotalSeconds.ToString("F0", CultureInfo.InvariantCulture); var body = context.CachedResponse.Body ?? ((CachedResponseBody) await _store.GetAsync(context.CachedResponse.BodyKeyPrefix))?.Body; @@ -200,30 +200,30 @@ namespace Microsoft.AspNetCore.ResponseCaching // Create the cache entry now var response = context.HttpContext.Response; - var varyHeaderValue = new StringValues(response.Headers.GetCommaSeparatedValues(HeaderNames.Vary)); - var varyParamsValue = context.HttpContext.GetResponseCacheFeature()?.VaryByParams ?? StringValues.Empty; + var varyHeaders = new StringValues(response.Headers.GetCommaSeparatedValues(HeaderNames.Vary)); + var varyQueryKeys = context.HttpContext.GetResponseCacheFeature()?.VaryByQueryKeys ?? StringValues.Empty; context.CachedResponseValidFor = context.ResponseCacheControlHeaderValue.SharedMaxAge ?? context.ResponseCacheControlHeaderValue.MaxAge ?? - (context.ResponseExpires - context.ResponseTime) ?? + (context.ResponseExpires - context.ResponseTime.Value) ?? DefaultExpirationTimeSpan; // Check if any vary rules exist - if (!StringValues.IsNullOrEmpty(varyHeaderValue) || !StringValues.IsNullOrEmpty(varyParamsValue)) + if (!StringValues.IsNullOrEmpty(varyHeaders) || !StringValues.IsNullOrEmpty(varyQueryKeys)) { // Normalize order and casing of vary by rules - var normalizedVaryHeaderValue = GetOrderCasingNormalizedStringValues(varyHeaderValue); - var normalizedVaryParamsValue = GetOrderCasingNormalizedStringValues(varyParamsValue); + var normalizedVaryHeaders = GetOrderCasingNormalizedStringValues(varyHeaders); + var normalizedVaryQueryKeys = GetOrderCasingNormalizedStringValues(varyQueryKeys); // Update vary rules if they are different if (context.CachedVaryByRules == null || - !StringValues.Equals(context.CachedVaryByRules.Params, normalizedVaryParamsValue) || - !StringValues.Equals(context.CachedVaryByRules.Headers, normalizedVaryHeaderValue)) + !StringValues.Equals(context.CachedVaryByRules.QueryKeys, normalizedVaryQueryKeys) || + !StringValues.Equals(context.CachedVaryByRules.Headers, normalizedVaryHeaders)) { context.CachedVaryByRules = new CachedVaryByRules { VaryByKeyPrefix = FastGuid.NewGuid().IdString, - Headers = normalizedVaryHeaderValue, - Params = normalizedVaryParamsValue + Headers = normalizedVaryHeaders, + QueryKeys = normalizedVaryQueryKeys }; } @@ -236,7 +236,7 @@ namespace Microsoft.AspNetCore.ResponseCaching // Ensure date header is set if (!context.ResponseDate.HasValue) { - context.ResponseDate = context.ResponseTime; + context.ResponseDate = context.ResponseTime.Value; // Setting the date on the raw response headers. context.TypedResponseHeaders.Date = context.ResponseDate; } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachePolicyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachePolicyProvider.cs index fc6d51dae9..351faa0df9 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachePolicyProvider.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachePolicyProvider.cs @@ -104,14 +104,14 @@ namespace Microsoft.AspNetCore.ResponseCaching { if (!context.ResponseCacheControlHeaderValue.SharedMaxAge.HasValue && !context.ResponseCacheControlHeaderValue.MaxAge.HasValue && - context.ResponseTime >= context.ResponseExpires) + context.ResponseTime.Value >= context.ResponseExpires) { return false; } } else { - var age = context.ResponseTime - context.ResponseDate.Value; + var age = context.ResponseTime.Value - context.ResponseDate.Value; // Validate shared max age if (age >= context.ResponseCacheControlHeaderValue.SharedMaxAge) @@ -128,7 +128,7 @@ namespace Microsoft.AspNetCore.ResponseCaching else if (!context.ResponseCacheControlHeaderValue.MaxAge.HasValue) { // Validate expiration - if (context.ResponseTime >= context.ResponseExpires) + if (context.ResponseTime.Value >= context.ResponseExpires) { return false; } @@ -141,7 +141,7 @@ namespace Microsoft.AspNetCore.ResponseCaching public virtual bool IsCachedEntryFresh(ResponseCacheContext context) { - var age = context.CachedEntryAge; + var age = context.CachedEntryAge.Value; var cachedControlHeaders = context.CachedResponseHeaders.CacheControl ?? EmptyCacheControl; // Add min-fresh requirements @@ -178,7 +178,7 @@ namespace Microsoft.AspNetCore.ResponseCaching else if (!cachedControlHeaders.MaxAge.HasValue && !context.RequestCacheControlHeaderValue.MaxAge.HasValue) { // Validate expiration - if (context.ResponseTime >= context.CachedResponseHeaders.Expires) + if (context.ResponseTime.Value >= context.CachedResponseHeaders.Expires) { return false; } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheEntrySerializerTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheEntrySerializerTests.cs index a88caa995c..493906f20b 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheEntrySerializerTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheEntrySerializerTests.cs @@ -101,28 +101,28 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public void RoundTrip_CachedVaryByRule_ParamsOnly_Succeeds() + public void RoundTrip_CachedVaryByRule_QueryKeysOnly_Succeeds() { - var param = new[] { "paramA", "paramB" }; + var queryKeys = new[] { "queryA", "queryB" }; var cachedVaryByRule = new CachedVaryByRules() { VaryByKeyPrefix = FastGuid.NewGuid().IdString, - Params = param + QueryKeys = queryKeys }; AssertCachedVaryByRuleEqual(cachedVaryByRule, (CachedVaryByRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryByRule))); } [Fact] - public void RoundTrip_CachedVaryByRule_HeadersAndParams_Succeeds() + public void RoundTrip_CachedVaryByRule_HeadersAndQueryKeys_Succeeds() { var headers = new[] { "headerA", "headerB" }; - var param = new[] { "paramA", "paramB" }; + var queryKeys = new[] { "queryA", "queryB" }; var cachedVaryByRule = new CachedVaryByRules() { VaryByKeyPrefix = FastGuid.NewGuid().IdString, Headers = headers, - Params = param + QueryKeys = queryKeys }; AssertCachedVaryByRuleEqual(cachedVaryByRule, (CachedVaryByRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryByRule))); @@ -176,7 +176,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Assert.NotNull(expected); Assert.Equal(expected.VaryByKeyPrefix, actual.VaryByKeyPrefix); Assert.Equal(expected.Headers, actual.Headers); - Assert.Equal(expected.Params, actual.Params); + Assert.Equal(expected.QueryKeys, actual.QueryKeys); } } } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheKeyProviderTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheKeyProviderTests.cs index 621c9fb5fd..99d42298b2 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheKeyProviderTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheKeyProviderTests.cs @@ -95,71 +95,71 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public void ResponseCacheKeyProvider_CreateStorageVaryKey_IncludesListedParamsOnly() + public void ResponseCacheKeyProvider_CreateStorageVaryKey_IncludesListedQueryKeysOnly() { var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); var context = TestUtils.CreateTestContext(); - context.HttpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB"); + context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueA&QueryB=ValueB"); context.CachedVaryByRules = new CachedVaryByRules() { VaryByKeyPrefix = FastGuid.NewGuid().IdString, - Params = new string[] { "ParamA", "ParamC" } + QueryKeys = new string[] { "QueryA", "QueryC" } }; - Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=", + Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}QueryA=ValueA{KeyDelimiter}QueryC=", cacheKeyProvider.CreateStorageVaryByKey(context)); } [Fact] - public void ResponseCacheKeyProvider_CreateStorageVaryKey_IncludesParams_ParamNameCaseInsensitive_UseParamCasing() + public void ResponseCacheKeyProvider_CreateStorageVaryKey_IncludesQueryKeys_QueryKeyCaseInsensitive_UseQueryKeyCasing() { var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); var context = TestUtils.CreateTestContext(); - context.HttpContext.Request.QueryString = new QueryString("?parama=ValueA¶mB=ValueB"); + context.HttpContext.Request.QueryString = new QueryString("?queryA=ValueA&queryB=ValueB"); context.CachedVaryByRules = new CachedVaryByRules() { VaryByKeyPrefix = FastGuid.NewGuid().IdString, - Params = new string[] { "ParamA", "ParamC" } + QueryKeys = new string[] { "QueryA", "QueryC" } }; - Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=", + Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}QueryA=ValueA{KeyDelimiter}QueryC=", cacheKeyProvider.CreateStorageVaryByKey(context)); } [Fact] - public void ResponseCacheKeyProvider_CreateStorageVaryKey_IncludesAllQueryParamsGivenAsterisk() + public void ResponseCacheKeyProvider_CreateStorageVaryKey_IncludesAllQueryKeysGivenAsterisk() { var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); var context = TestUtils.CreateTestContext(); - context.HttpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB"); + context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueA&QueryB=ValueB"); context.CachedVaryByRules = new CachedVaryByRules() { VaryByKeyPrefix = FastGuid.NewGuid().IdString, - Params = new string[] { "*" } + QueryKeys = new string[] { "*" } }; - // To support case insensitivity, all param keys are converted to upper case. - // Explicit params uses the casing specified in the setting. - Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}PARAMA=ValueA{KeyDelimiter}PARAMB=ValueB", + // To support case insensitivity, all query keys are converted to upper case. + // Explicit query keys uses the casing specified in the setting. + Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}QUERYA=ValueA{KeyDelimiter}QUERYB=ValueB", cacheKeyProvider.CreateStorageVaryByKey(context)); } [Fact] - public void ResponseCacheKeyProvider_CreateStorageVaryKey_IncludesListedHeadersAndParams() + public void ResponseCacheKeyProvider_CreateStorageVaryKey_IncludesListedHeadersAndQueryKeys() { var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); var context = TestUtils.CreateTestContext(); context.HttpContext.Request.Headers["HeaderA"] = "ValueA"; context.HttpContext.Request.Headers["HeaderB"] = "ValueB"; - context.HttpContext.Request.QueryString = new QueryString("?ParamA=ValueA&ParamB=ValueB"); + context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueA&QueryB=ValueB"); context.CachedVaryByRules = new CachedVaryByRules() { VaryByKeyPrefix = FastGuid.NewGuid().IdString, Headers = new string[] { "HeaderA", "HeaderC" }, - Params = new string[] { "ParamA", "ParamC" } + QueryKeys = new string[] { "QueryA", "QueryC" } }; - Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC={KeyDelimiter}Q{KeyDelimiter}ParamA=ValueA{KeyDelimiter}ParamC=", + Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC={KeyDelimiter}Q{KeyDelimiter}QueryA=ValueA{KeyDelimiter}QueryC=", cacheKeyProvider.CreateStorageVaryByKey(context)); } } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheMiddlewareTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheMiddlewareTests.cs index d7db493fd9..5d8bebc4a4 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheMiddlewareTests.cs @@ -292,7 +292,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests MaxAge = TimeSpan.FromSeconds(12) }; - context.ResponseTime = DateTimeOffset.UtcNow; context.TypedResponseHeaders.Expires = context.ResponseTime + TimeSpan.FromSeconds(11); await middleware.FinalizeCacheHeadersAsync(context); @@ -311,7 +310,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests SharedMaxAge = TimeSpan.FromSeconds(13) }; - context.ResponseTime = DateTimeOffset.UtcNow; context.TypedResponseHeaders.Expires = context.ResponseTime + TimeSpan.FromSeconds(11); await middleware.FinalizeCacheHeadersAsync(context); @@ -328,11 +326,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.HttpContext.Response.Headers[HeaderNames.Vary] = new StringValues(new[] { "headerA", "HEADERB", "HEADERc" }); context.HttpContext.AddResponseCacheFeature(); - context.HttpContext.GetResponseCacheFeature().VaryByParams = new StringValues(new[] { "paramB", "PARAMAA" }); + context.HttpContext.GetResponseCacheFeature().VaryByQueryKeys = new StringValues(new[] { "queryB", "QUERYA" }); var cachedVaryByRules = new CachedVaryByRules() { Headers = new StringValues(new[] { "HeaderA", "HeaderB" }), - Params = new StringValues(new[] { "ParamA", "ParamB" }) + QueryKeys = new StringValues(new[] { "QueryA", "QueryB" }) }; context.CachedVaryByRules = cachedVaryByRules; @@ -352,12 +350,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.HttpContext.Response.Headers[HeaderNames.Vary] = new StringValues(new[] { "headerA", "HEADERB" }); context.HttpContext.AddResponseCacheFeature(); - context.HttpContext.GetResponseCacheFeature().VaryByParams = new StringValues(new[] { "paramB", "PARAMA" }); + context.HttpContext.GetResponseCacheFeature().VaryByQueryKeys = new StringValues(new[] { "queryB", "QUERYA" }); var cachedVaryByRules = new CachedVaryByRules() { VaryByKeyPrefix = FastGuid.NewGuid().IdString, Headers = new StringValues(new[] { "HEADERA", "HEADERB" }), - Params = new StringValues(new[] { "PARAMA", "PARAMB" }) + QueryKeys = new StringValues(new[] { "QUERYA", "QUERYB" }) }; context.CachedVaryByRules = cachedVaryByRules; diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachePolicyProviderTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachePolicyProviderTests.cs index 0afbf329e3..071ab2fbb3 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachePolicyProviderTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachePolicyProviderTests.cs @@ -385,6 +385,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var utcNow = DateTimeOffset.UtcNow; var context = TestUtils.CreateTestContext(); context.ResponseTime = DateTimeOffset.MaxValue; + context.CachedEntryAge = TimeSpan.MaxValue; context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); Assert.True(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); @@ -396,6 +397,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var utcNow = DateTimeOffset.UtcNow; var context = TestUtils.CreateTestContext(); context.ResponseTime = DateTimeOffset.MaxValue; + context.CachedEntryAge = TimeSpan.MaxValue; context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) { CacheControl = new CacheControlHeaderValue() @@ -413,6 +415,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var utcNow = DateTimeOffset.UtcNow; var context = TestUtils.CreateTestContext(); context.ResponseTime = utcNow; + context.CachedEntryAge = TimeSpan.Zero; context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) { CacheControl = new CacheControlHeaderValue() diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheTests.cs index 45fd90eb96..09f432d3b7 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheTests.cs @@ -88,152 +88,152 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public async void ServesCachedContent_IfVaryParams_Matches() + public async void ServesCachedContent_IfVaryQueryKeys_Matches() { var builder = TestUtils.CreateBuilderWithResponseCache(requestDelegate: async (context) => { - context.GetResponseCacheFeature().VaryByParams = "param"; + context.GetResponseCacheFeature().VaryByQueryKeys = "query"; await TestUtils.TestRequestDelegate(context); }); using (var server = new TestServer(builder)) { var client = server.CreateClient(); - var initialResponse = await client.GetAsync("?param=value"); - var subsequentResponse = await client.GetAsync("?param=value"); + var initialResponse = await client.GetAsync("?query=value"); + var subsequentResponse = await client.GetAsync("?query=value"); await AssertResponseCachedAsync(initialResponse, subsequentResponse); } } [Fact] - public async void ServesCachedContent_IfVaryParamsExplicit_Matches_ParamNameCaseInsensitive() + public async void ServesCachedContent_IfVaryQueryKeysExplicit_Matches_QueryKeyCaseInsensitive() { var builder = TestUtils.CreateBuilderWithResponseCache(requestDelegate: async (context) => { - context.GetResponseCacheFeature().VaryByParams = new[] { "ParamA", "paramb" }; + context.GetResponseCacheFeature().VaryByQueryKeys = new[] { "QueryA", "queryb" }; await TestUtils.TestRequestDelegate(context); }); using (var server = new TestServer(builder)) { var client = server.CreateClient(); - var initialResponse = await client.GetAsync("?parama=valuea¶mb=valueb"); - var subsequentResponse = await client.GetAsync("?ParamA=valuea&ParamB=valueb"); + var initialResponse = await client.GetAsync("?querya=valuea&queryb=valueb"); + var subsequentResponse = await client.GetAsync("?QueryA=valuea&QueryB=valueb"); await AssertResponseCachedAsync(initialResponse, subsequentResponse); } } [Fact] - public async void ServesCachedContent_IfVaryParamsStar_Matches_ParamNameCaseInsensitive() + public async void ServesCachedContent_IfVaryQueryKeyStar_Matches_QueryKeyCaseInsensitive() { var builder = TestUtils.CreateBuilderWithResponseCache(requestDelegate: async (context) => { - context.GetResponseCacheFeature().VaryByParams = new[] { "*" }; + context.GetResponseCacheFeature().VaryByQueryKeys = new[] { "*" }; await TestUtils.TestRequestDelegate(context); }); using (var server = new TestServer(builder)) { var client = server.CreateClient(); - var initialResponse = await client.GetAsync("?parama=valuea¶mb=valueb"); - var subsequentResponse = await client.GetAsync("?ParamA=valuea&ParamB=valueb"); + var initialResponse = await client.GetAsync("?querya=valuea&queryb=valueb"); + var subsequentResponse = await client.GetAsync("?QueryA=valuea&QueryB=valueb"); await AssertResponseCachedAsync(initialResponse, subsequentResponse); } } [Fact] - public async void ServesCachedContent_IfVaryParamsExplicit_Matches_OrderInsensitive() + public async void ServesCachedContent_IfVaryQueryKeyExplicit_Matches_OrderInsensitive() { var builder = TestUtils.CreateBuilderWithResponseCache(requestDelegate: async (context) => { - context.GetResponseCacheFeature().VaryByParams = new[] { "ParamB", "ParamA" }; + context.GetResponseCacheFeature().VaryByQueryKeys = new[] { "QueryB", "QueryA" }; await TestUtils.TestRequestDelegate(context); }); using (var server = new TestServer(builder)) { var client = server.CreateClient(); - var initialResponse = await client.GetAsync("?ParamA=ValueA&ParamB=ValueB"); - var subsequentResponse = await client.GetAsync("?ParamB=ValueB&ParamA=ValueA"); + var initialResponse = await client.GetAsync("?QueryA=ValueA&QueryB=ValueB"); + var subsequentResponse = await client.GetAsync("?QueryB=ValueB&QueryA=ValueA"); await AssertResponseCachedAsync(initialResponse, subsequentResponse); } } [Fact] - public async void ServesCachedContent_IfVaryParamsStar_Matches_OrderInsensitive() + public async void ServesCachedContent_IfVaryQueryKeyStar_Matches_OrderInsensitive() { var builder = TestUtils.CreateBuilderWithResponseCache(requestDelegate: async (context) => { - context.GetResponseCacheFeature().VaryByParams = new[] { "*" }; + context.GetResponseCacheFeature().VaryByQueryKeys = new[] { "*" }; await TestUtils.TestRequestDelegate(context); }); using (var server = new TestServer(builder)) { var client = server.CreateClient(); - var initialResponse = await client.GetAsync("?ParamA=ValueA&ParamB=ValueB"); - var subsequentResponse = await client.GetAsync("?ParamB=ValueB&ParamA=ValueA"); + var initialResponse = await client.GetAsync("?QueryA=ValueA&QueryB=ValueB"); + var subsequentResponse = await client.GetAsync("?QueryB=ValueB&QueryA=ValueA"); await AssertResponseCachedAsync(initialResponse, subsequentResponse); } } [Fact] - public async void ServesFreshContent_IfVaryParams_Mismatches() + public async void ServesFreshContent_IfVaryQueryKey_Mismatches() { var builder = TestUtils.CreateBuilderWithResponseCache(requestDelegate: async (context) => { - context.GetResponseCacheFeature().VaryByParams = "param"; + context.GetResponseCacheFeature().VaryByQueryKeys = "query"; await TestUtils.TestRequestDelegate(context); }); using (var server = new TestServer(builder)) { var client = server.CreateClient(); - var initialResponse = await client.GetAsync("?param=value"); - var subsequentResponse = await client.GetAsync("?param=value2"); + var initialResponse = await client.GetAsync("?query=value"); + var subsequentResponse = await client.GetAsync("?query=value2"); await AssertResponseNotCachedAsync(initialResponse, subsequentResponse); } } [Fact] - public async void ServesFreshContent_IfVaryParamsExplicit_Mismatch_ParamValueCaseSensitive() + public async void ServesFreshContent_IfVaryQueryKeyExplicit_Mismatch_QueryKeyCaseSensitive() { var builder = TestUtils.CreateBuilderWithResponseCache(requestDelegate: async (context) => { - context.GetResponseCacheFeature().VaryByParams = new[] { "ParamA", "ParamB" }; + context.GetResponseCacheFeature().VaryByQueryKeys = new[] { "QueryA", "QueryB" }; await TestUtils.TestRequestDelegate(context); }); using (var server = new TestServer(builder)) { var client = server.CreateClient(); - var initialResponse = await client.GetAsync("?parama=valuea¶mb=valueb"); - var subsequentResponse = await client.GetAsync("?parama=ValueA¶mb=ValueB"); + var initialResponse = await client.GetAsync("?querya=valuea&queryb=valueb"); + var subsequentResponse = await client.GetAsync("?querya=ValueA&queryb=ValueB"); await AssertResponseNotCachedAsync(initialResponse, subsequentResponse); } } [Fact] - public async void ServesFreshContent_IfVaryParamsStar_Mismatch_ParamValueCaseSensitive() + public async void ServesFreshContent_IfVaryQueryKeyStar_Mismatch_QueryKeyValueCaseSensitive() { var builder = TestUtils.CreateBuilderWithResponseCache(requestDelegate: async (context) => { - context.GetResponseCacheFeature().VaryByParams = new[] { "*" }; + context.GetResponseCacheFeature().VaryByQueryKeys = new[] { "*" }; await TestUtils.TestRequestDelegate(context); }); using (var server = new TestServer(builder)) { var client = server.CreateClient(); - var initialResponse = await client.GetAsync("?parama=valuea¶mb=valueb"); - var subsequentResponse = await client.GetAsync("?parama=ValueA¶mb=ValueB"); + var initialResponse = await client.GetAsync("?querya=valuea&queryb=valueb"); + var subsequentResponse = await client.GetAsync("?querya=ValueA&queryb=ValueB"); await AssertResponseNotCachedAsync(initialResponse, subsequentResponse); } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs index 3ccb72fe0c..d938065892 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs @@ -108,7 +108,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests internal static ResponseCacheContext CreateTestContext() { - return new ResponseCacheContext(new DefaultHttpContext()); + return new ResponseCacheContext(new DefaultHttpContext()) + { + ResponseTime = DateTimeOffset.UtcNow + }; } } From a069f6b636e343f7a1839783ee23f9ae7eca32c3 Mon Sep 17 00:00:00 2001 From: John Luo Date: Wed, 21 Sep 2016 12:48:00 -0700 Subject: [PATCH 038/188] Store each header value separately --- .../CacheEntry/CacheEntrySerializer.cs | 26 +++++++++++--- .../CacheEntrySerializerTests.cs | 35 +++++++++++++++++++ 2 files changed, 57 insertions(+), 4 deletions(-) diff --git a/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CacheEntrySerializer.cs b/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CacheEntrySerializer.cs index 385d5ebf78..21534dd13f 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CacheEntrySerializer.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CacheEntrySerializer.cs @@ -93,7 +93,9 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal // Header count (int) // Header(s) // Key (string) - // Value (string) + // ValueCount (int) + // Value(s) + // Value (string) // ContainsBody (bool) // Body length (int) // Body (byte[]) @@ -107,8 +109,20 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal for (var index = 0; index < headerCount; index++) { var key = reader.ReadString(); - var value = reader.ReadString(); - headers[key] = value; + var headerValueCount = reader.ReadInt32(); + if (headerValueCount > 1) + { + var headerValues = new string[headerValueCount]; + for (var valueIndex = 0; valueIndex < headerValueCount; valueIndex++) + { + headerValues[valueIndex] = reader.ReadString(); + } + headers[key] = headerValues; + } + else if (headerValueCount == 1) + { + headers[key] = reader.ReadString(); + } } var containsBody = reader.ReadBoolean(); @@ -202,7 +216,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal foreach (var header in entry.Headers) { writer.Write(header.Key); - writer.Write(header.Value); + writer.Write(header.Value.Count); + foreach (var headerValue in header.Value) + { + writer.Write(headerValue); + } } if (entry.Body == null) diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheEntrySerializerTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheEntrySerializerTests.cs index 493906f20b..966fdee6e0 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheEntrySerializerTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheEntrySerializerTests.cs @@ -6,6 +6,7 @@ using System.Linq; using System.Text; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.ResponseCaching.Internal; +using Microsoft.Extensions.Primitives; using Xunit; namespace Microsoft.AspNetCore.ResponseCaching.Tests @@ -76,6 +77,40 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests AssertCachedResponseEqual(cachedResponse, (CachedResponse)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedResponse))); } + [Fact] + public void RoundTrip_CachedResponseWithMultivalueHeaders_Succeeds() + { + var headers = new HeaderDictionary(); + headers["keyA"] = new StringValues(new[] { "ValueA", "ValueB" }); + var cachedResponse = new CachedResponse() + { + BodyKeyPrefix = FastGuid.NewGuid().IdString, + Created = DateTimeOffset.UtcNow, + StatusCode = StatusCodes.Status200OK, + Body = Encoding.ASCII.GetBytes("Hello world"), + Headers = headers + }; + + AssertCachedResponseEqual(cachedResponse, (CachedResponse)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedResponse))); + } + + [Fact] + public void RoundTrip_CachedResponseWithEmptyHeaders_Succeeds() + { + var headers = new HeaderDictionary(); + headers["keyA"] = StringValues.Empty; + var cachedResponse = new CachedResponse() + { + BodyKeyPrefix = FastGuid.NewGuid().IdString, + Created = DateTimeOffset.UtcNow, + StatusCode = StatusCodes.Status200OK, + Body = Encoding.ASCII.GetBytes("Hello world"), + Headers = headers + }; + + AssertCachedResponseEqual(cachedResponse, (CachedResponse)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedResponse))); + } + [Fact] public void RoundTrip_CachedVaryByRule_EmptyRules_Succeeds() { From 44b0dfd5bb41dd4bab34e3c117d52fd1bb8bd3e7 Mon Sep 17 00:00:00 2001 From: John Luo Date: Wed, 28 Sep 2016 11:58:16 -0700 Subject: [PATCH 039/188] Sharding (#57) Add sharding support for MemoryResponseCacheStore - update CachedResponse API to use streams - added empty IResponseCacheEntry interface --- samples/ResponseCachingSample/Startup.cs | 2 +- .../CacheEntry/CachedResponse.cs | 7 +- .../CacheEntry/CachedVaryByRules.cs | 2 +- ...esponseCacheServiceCollectionExtensions.cs | 4 +- .../IResponseCacheEntry.cs} | 3 +- .../Interfaces/IResponseCacheStore.cs | 5 +- .../Internal/DistributedResponseCacheStore.cs | 17 +- .../Internal/MemoryCachedResponse.cs | 22 + .../Internal/MemoryResponseCacheStore.cs | 68 ++- .../ResponseCacheEntrySerializer.cs} | 111 ++-- .../ResponseCacheMiddleware.cs | 47 +- .../ResponseCacheOptions.cs | 7 +- .../ResponseCacheStream.cs | 68 +-- .../Streams/SegmentReadStream.cs | 238 ++++++++ .../Streams/SegmentWriteStream.cs | 215 +++++++ .../Streams/StreamUtilities.cs | 41 ++ .../CacheEntrySerializerTests.cs | 92 ++- .../ResponseCacheMiddlewareTests.cs | 96 +--- .../ResponseCacheTests.cs | 528 ++++++++++-------- .../SegmentReadStreamTests.cs | 285 ++++++++++ .../SegmentWriteStreamTests.cs | 113 ++++ .../TestUtils.cs | 40 +- 22 files changed, 1449 insertions(+), 562 deletions(-) rename src/Microsoft.AspNetCore.ResponseCaching/{CacheEntry/CachedResponseBody.cs => Interfaces/IResponseCacheEntry.cs} (75%) create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryCachedResponse.cs rename src/Microsoft.AspNetCore.ResponseCaching/{CacheEntry/CacheEntrySerializer.cs => Internal/ResponseCacheEntrySerializer.cs} (72%) rename src/Microsoft.AspNetCore.ResponseCaching/{Internal => Streams}/ResponseCacheStream.cs (70%) create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Streams/SegmentReadStream.cs create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Streams/SegmentWriteStream.cs create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Streams/StreamUtilities.cs create mode 100644 test/Microsoft.AspNetCore.ResponseCaching.Tests/SegmentReadStreamTests.cs create mode 100644 test/Microsoft.AspNetCore.ResponseCaching.Tests/SegmentWriteStreamTests.cs diff --git a/samples/ResponseCachingSample/Startup.cs b/samples/ResponseCachingSample/Startup.cs index 1cad8d6c1f..66708440a9 100644 --- a/samples/ResponseCachingSample/Startup.cs +++ b/samples/ResponseCachingSample/Startup.cs @@ -15,7 +15,7 @@ namespace ResponseCachingSample { public void ConfigureServices(IServiceCollection services) { - services.AddDistributedResponseCache(); + services.AddDistributedResponseCacheStore(); } public void Configure(IApplicationBuilder app) diff --git a/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedResponse.cs b/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedResponse.cs index 7e32ec2959..1accf9a759 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedResponse.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedResponse.cs @@ -2,20 +2,19 @@ // 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 { - public class CachedResponse + public class CachedResponse : IResponseCacheEntry { - public string BodyKeyPrefix { get; set; } - public DateTimeOffset Created { get; set; } public int StatusCode { get; set; } public IHeaderDictionary Headers { get; set; } = new HeaderDictionary(); - public byte[] Body { get; set; } + public Stream Body { get; set; } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedVaryByRules.cs b/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedVaryByRules.cs index c79d72ddc0..e0e82bd60a 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedVaryByRules.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedVaryByRules.cs @@ -5,7 +5,7 @@ using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.ResponseCaching { - public class CachedVaryByRules + public class CachedVaryByRules : IResponseCacheEntry { public string VaryByKeyPrefix { get; set; } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCacheServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCacheServiceCollectionExtensions.cs index 51af53847e..e57dc5856f 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCacheServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCacheServiceCollectionExtensions.cs @@ -10,7 +10,7 @@ namespace Microsoft.Extensions.DependencyInjection { public static class ResponseCacheServiceCollectionExtensions { - public static IServiceCollection AddMemoryResponseCache(this IServiceCollection services) + public static IServiceCollection AddMemoryResponseCacheStore(this IServiceCollection services) { if (services == null) { @@ -24,7 +24,7 @@ namespace Microsoft.Extensions.DependencyInjection return services; } - public static IServiceCollection AddDistributedResponseCache(this IServiceCollection services) + public static IServiceCollection AddDistributedResponseCacheStore(this IServiceCollection services) { if (services == null) { diff --git a/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedResponseBody.cs b/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCacheEntry.cs similarity index 75% rename from src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedResponseBody.cs rename to src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCacheEntry.cs index a5ce8d6aca..d09fd4da48 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedResponseBody.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCacheEntry.cs @@ -3,8 +3,7 @@ namespace Microsoft.AspNetCore.ResponseCaching { - public class CachedResponseBody + public interface IResponseCacheEntry { - public byte[] Body { get; set; } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCacheStore.cs b/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCacheStore.cs index 55ed237786..9a12b9b6df 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCacheStore.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCacheStore.cs @@ -8,8 +8,7 @@ namespace Microsoft.AspNetCore.ResponseCaching { public interface IResponseCacheStore { - Task GetAsync(string key); - Task SetAsync(string key, object entry, TimeSpan validFor); - Task RemoveAsync(string key); + Task GetAsync(string key); + Task SetAsync(string key, IResponseCacheEntry entry, TimeSpan validFor); } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/DistributedResponseCacheStore.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/DistributedResponseCacheStore.cs index 170f7e65cd..ab708fc81b 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/DistributedResponseCacheStore.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/DistributedResponseCacheStore.cs @@ -21,11 +21,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal _cache = cache; } - public async Task GetAsync(string key) + public async Task GetAsync(string key) { try { - return CacheEntrySerializer.Deserialize(await _cache.GetAsync(key)); + return ResponseCacheEntrySerializer.Deserialize(await _cache.GetAsync(key)); } catch { @@ -33,22 +33,13 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal } } - public async Task RemoveAsync(string key) - { - try - { - await _cache.RemoveAsync(key); - } - catch { } - } - - public async Task SetAsync(string key, object entry, TimeSpan validFor) + public async Task SetAsync(string key, IResponseCacheEntry entry, TimeSpan validFor) { try { await _cache.SetAsync( key, - CacheEntrySerializer.Serialize(entry), + ResponseCacheEntrySerializer.Serialize(entry), new DistributedCacheEntryOptions() { AbsoluteExpirationRelativeToNow = validFor diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryCachedResponse.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryCachedResponse.cs new file mode 100644 index 0000000000..d24e63a9ff --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryCachedResponse.cs @@ -0,0 +1,22 @@ +// 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 Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.ResponseCaching.Internal +{ + internal class MemoryCachedResponse + { + public DateTimeOffset Created { get; set; } + + public int StatusCode { get; set; } + + public IHeaderDictionary Headers { get; set; } = new HeaderDictionary(); + + public List BodySegments { get; set; } + + public long BodyLength { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCacheStore.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCacheStore.cs index 33b7f4367b..1a49f1917e 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCacheStore.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCacheStore.cs @@ -4,7 +4,6 @@ using System; using System.Threading.Tasks; using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Internal; namespace Microsoft.AspNetCore.ResponseCaching.Internal { @@ -22,27 +21,60 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal _cache = cache; } - public Task GetAsync(string key) + public Task GetAsync(string key) { - return Task.FromResult(_cache.Get(key)); - } + var entry = _cache.Get(key); - public Task RemoveAsync(string key) - { - _cache.Remove(key); - return TaskCache.CompletedTask; - } - - public Task SetAsync(string key, object entry, TimeSpan validFor) - { - _cache.Set( - key, - entry, - new MemoryCacheEntryOptions() + if (entry is MemoryCachedResponse) + { + var memoryCachedResponse = (MemoryCachedResponse)entry; + return Task.FromResult(new CachedResponse() { - AbsoluteExpirationRelativeToNow = validFor + Created = memoryCachedResponse.Created, + StatusCode = memoryCachedResponse.StatusCode, + Headers = memoryCachedResponse.Headers, + Body = new SegmentReadStream(memoryCachedResponse.BodySegments, memoryCachedResponse.BodyLength) }); - return TaskCache.CompletedTask; + } + else + { + return Task.FromResult(entry as IResponseCacheEntry); + } + } + + public async Task SetAsync(string key, IResponseCacheEntry entry, TimeSpan validFor) + { + if (entry is CachedResponse) + { + var cachedResponse = (CachedResponse)entry; + var segmentStream = new SegmentWriteStream(StreamUtilities.BodySegmentSize); + await cachedResponse.Body.CopyToAsync(segmentStream); + + _cache.Set( + key, + new MemoryCachedResponse() + { + Created = cachedResponse.Created, + StatusCode = cachedResponse.StatusCode, + Headers = cachedResponse.Headers, + BodySegments = segmentStream.GetSegments(), + BodyLength = segmentStream.Length + }, + new MemoryCacheEntryOptions() + { + AbsoluteExpirationRelativeToNow = validFor + }); + } + else + { + _cache.Set( + key, + entry, + new MemoryCacheEntryOptions() + { + AbsoluteExpirationRelativeToNow = validFor + }); + } } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CacheEntrySerializer.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCacheEntrySerializer.cs similarity index 72% rename from src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CacheEntrySerializer.cs rename to src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCacheEntrySerializer.cs index 21534dd13f..46c679d241 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CacheEntrySerializer.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCacheEntrySerializer.cs @@ -7,11 +7,11 @@ using Microsoft.AspNetCore.Http; namespace Microsoft.AspNetCore.ResponseCaching.Internal { - internal static class CacheEntrySerializer + internal static class ResponseCacheEntrySerializer { private const int FormatVersion = 1; - public static object Deserialize(byte[] serializedEntry) + internal static IResponseCacheEntry Deserialize(byte[] serializedEntry) { if (serializedEntry == null) { @@ -27,7 +27,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal } } - public static byte[] Serialize(object entry) + internal static byte[] Serialize(IResponseCacheEntry entry) { using (var memory = new MemoryStream()) { @@ -42,9 +42,9 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal // Serialization Format // Format version (int) - // Type (char: 'B' for CachedResponseBody, 'R' for CachedResponse, 'V' for CachedVaryByRules) + // Type (char: 'R' for CachedResponse, 'V' for CachedVaryByRules) // Type-dependent data (see CachedResponse and CachedVaryByRules) - public static object Read(BinaryReader reader) + private static IResponseCacheEntry Read(BinaryReader reader) { if (reader == null) { @@ -58,11 +58,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal var type = reader.ReadChar(); - if (type == 'B') - { - return ReadCachedResponseBody(reader); - } - else if (type == 'R') + if (type == 'R') { return ReadCachedResponse(reader); } @@ -76,18 +72,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal } // Serialization Format - // Body length (int) - // Body (byte[]) - private static CachedResponseBody ReadCachedResponseBody(BinaryReader reader) - { - var bodyLength = reader.ReadInt32(); - var body = reader.ReadBytes(bodyLength); - - return new CachedResponseBody() { Body = body }; - } - - // Serialization Format - // BodyKeyPrefix (string) // Creation time - UtcTicks (long) // Status code (int) // Header count (int) @@ -96,12 +80,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal // ValueCount (int) // Value(s) // Value (string) - // ContainsBody (bool) - // Body length (int) - // Body (byte[]) + // BodyLength (int) + // Body (byte[]) private static CachedResponse ReadCachedResponse(BinaryReader reader) { - var bodyKeyPrefix = reader.ReadString(); var created = new DateTimeOffset(reader.ReadInt64(), TimeSpan.Zero); var statusCode = reader.ReadInt32(); var headerCount = reader.ReadInt32(); @@ -125,20 +107,20 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal } } - var containsBody = reader.ReadBoolean(); - int bodyLength; - byte[] body = null; - if (containsBody) - { - bodyLength = reader.ReadInt32(); - body = reader.ReadBytes(bodyLength); - } + var bodyLength = reader.ReadInt32(); + var bodyBytes = reader.ReadBytes(bodyLength); - return new CachedResponse { BodyKeyPrefix = bodyKeyPrefix, Created = created, StatusCode = statusCode, Headers = headers, Body = body }; + return new CachedResponse + { + Created = created, + StatusCode = statusCode, + Headers = headers, + Body = new MemoryStream(bodyBytes, writable: false) + }; } // Serialization Format - // Guid (long) + // VaryKeyPrefix (string) // Headers count // Header(s) (comma separated string) // QueryKey count @@ -164,7 +146,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal } // See serialization format above - public static void Write(BinaryWriter writer, object entry) + private static void Write(BinaryWriter writer, IResponseCacheEntry entry) { if (writer == null) { @@ -178,38 +160,25 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal writer.Write(FormatVersion); - if (entry is CachedResponseBody) - { - writer.Write('B'); - WriteCachedResponseBody(writer, entry as CachedResponseBody); - } - else if (entry is CachedResponse) + if (entry is CachedResponse) { writer.Write('R'); - WriteCachedResponse(writer, entry as CachedResponse); + WriteCachedResponse(writer, (CachedResponse)entry); } else if (entry is CachedVaryByRules) { writer.Write('V'); - WriteCachedVaryByRules(writer, entry as CachedVaryByRules); + WriteCachedVaryByRules(writer, (CachedVaryByRules)entry); } else { - throw new NotSupportedException($"Unrecognized entry format for {nameof(entry)}."); + throw new NotSupportedException($"Unrecognized entry type for {nameof(entry)}."); } } - // See serialization format above - private static void WriteCachedResponseBody(BinaryWriter writer, CachedResponseBody entry) - { - writer.Write(entry.Body.Length); - writer.Write(entry.Body); - } - // See serialization format above private static void WriteCachedResponse(BinaryWriter writer, CachedResponse entry) { - writer.Write(entry.BodyKeyPrefix); writer.Write(entry.Created.UtcTicks); writer.Write(entry.StatusCode); writer.Write(entry.Headers.Count); @@ -223,15 +192,39 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal } } - if (entry.Body == null) + if (entry.Body.CanSeek) { - writer.Write(false); + if (entry.Body.Length > int.MaxValue) + { + throw new NotSupportedException($"{nameof(entry.Body)} is too large to serialized."); + } + + var bodyLength = (int)entry.Body.Length; + var bodyBytes = new byte[bodyLength]; + var bytesRead = entry.Body.Read(bodyBytes, 0, bodyLength); + + if (bytesRead != bodyLength) + { + throw new InvalidOperationException($"Failed to fully read {nameof(entry.Body)}."); + } + + writer.Write(bodyLength); + writer.Write(bodyBytes); } else { - writer.Write(true); - writer.Write(entry.Body.Length); - writer.Write(entry.Body); + var stream = new MemoryStream(); + entry.Body.CopyTo(stream); + + if (stream.Length > int.MaxValue) + { + throw new NotSupportedException($"{nameof(entry.Body)} is too large to serialized."); + } + + var bodyLength = (int)stream.Length; + writer.Write(bodyLength); + writer.Write(stream.ToArray()); + } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs index 6ceb6ad8d0..72f6902751 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs @@ -130,16 +130,8 @@ namespace Microsoft.AspNetCore.ResponseCaching response.Headers[HeaderNames.Age] = context.CachedEntryAge.Value.TotalSeconds.ToString("F0", CultureInfo.InvariantCulture); - var body = context.CachedResponse.Body ?? - ((CachedResponseBody) await _store.GetAsync(context.CachedResponse.BodyKeyPrefix))?.Body; - - // If the body is not found, something went wrong. - if (body == null) - { - return false; - } - // Copy the cached response body + var body = context.CachedResponse.Body; if (body.Length > 0) { // Add a content-length if required @@ -147,7 +139,15 @@ namespace Microsoft.AspNetCore.ResponseCaching { response.ContentLength = body.Length; } - await response.Body.WriteAsync(body, 0, body.Length); + + try + { + await body.CopyToAsync(response.Body, StreamUtilities.BodySegmentSize, context.HttpContext.RequestAborted); + } + catch (OperationCanceledException) + { + context.HttpContext.Abort(); + } } } @@ -244,7 +244,6 @@ namespace Microsoft.AspNetCore.ResponseCaching // Store the response on the state context.CachedResponse = new CachedResponse { - BodyKeyPrefix = FastGuid.NewGuid().IdString, Created = context.ResponseDate.Value, StatusCode = context.HttpContext.Response.StatusCode }; @@ -266,26 +265,12 @@ namespace Microsoft.AspNetCore.ResponseCaching internal async Task FinalizeCacheBodyAsync(ResponseCacheContext context) { var contentLength = context.TypedResponseHeaders.ContentLength; - if (context.ShouldCacheResponse && - context.ResponseCacheStream.BufferingEnabled && - (!contentLength.HasValue || contentLength == context.ResponseCacheStream.BufferedStream.Length)) + if (context.ShouldCacheResponse && context.ResponseCacheStream.BufferingEnabled) { - if (context.ResponseCacheStream.BufferedStream.Length >= _options.MinimumSplitBodySize) + var bufferStream = context.ResponseCacheStream.GetBufferStream(); + if (!contentLength.HasValue || contentLength == bufferStream.Length) { - // Store response and response body separately - await _store.SetAsync(context.StorageVaryKey ?? context.BaseKey, context.CachedResponse, context.CachedResponseValidFor); - - var cachedResponseBody = new CachedResponseBody() - { - Body = context.ResponseCacheStream.BufferedStream.ToArray() - }; - - await _store.SetAsync(context.CachedResponse.BodyKeyPrefix, cachedResponseBody, context.CachedResponseValidFor); - } - else - { - // Store response and response body together - context.CachedResponse.Body = context.ResponseCacheStream.BufferedStream.ToArray(); + context.CachedResponse.Body = bufferStream; await _store.SetAsync(context.StorageVaryKey ?? context.BaseKey, context.CachedResponse, context.CachedResponseValidFor); } } @@ -310,7 +295,7 @@ namespace Microsoft.AspNetCore.ResponseCaching { // Shim response stream context.OriginalResponseStream = context.HttpContext.Response.Body; - context.ResponseCacheStream = new ResponseCacheStream(context.OriginalResponseStream, _options.MaximumCachedBodySize); + context.ResponseCacheStream = new ResponseCacheStream(context.OriginalResponseStream, _options.MaximumBodySize, StreamUtilities.BodySegmentSize); context.HttpContext.Response.Body = context.ResponseCacheStream; // Shim IHttpSendFileFeature @@ -381,7 +366,7 @@ namespace Microsoft.AspNetCore.ResponseCaching var originalArray = stringValues.ToArray(); var newArray = new string[originalArray.Length]; - for (int i = 0; i < originalArray.Length; i++) + for (var i = 0; i < originalArray.Length; i++) { newArray[i] = originalArray[i].ToUpperInvariant(); } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheOptions.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheOptions.cs index db9bae7575..795c80abcf 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheOptions.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheOptions.cs @@ -11,18 +11,13 @@ namespace Microsoft.AspNetCore.Builder /// /// The largest cacheable size for the response body in bytes. The default is set to 1 MB. /// - public long MaximumCachedBodySize { get; set; } = 1024 * 1024; + public long MaximumBodySize { get; set; } = 1024 * 1024; /// /// true if request paths are case-sensitive; otherwise false. The default is to treat paths as case-insensitive. /// public bool UseCaseSensitivePaths { get; set; } = false; - /// - /// The smallest size in bytes for which the headers and body of the response will be stored separately. The default is set to 70 KB. - /// - public long MinimumSplitBodySize { get; set; } = 70 * 1024; - /// /// For testing purposes only. /// diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCacheStream.cs b/src/Microsoft.AspNetCore.ResponseCaching/Streams/ResponseCacheStream.cs similarity index 70% rename from src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCacheStream.cs rename to src/Microsoft.AspNetCore.ResponseCaching/Streams/ResponseCacheStream.cs index b8921b85ba..40fe217aec 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCacheStream.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Streams/ResponseCacheStream.cs @@ -12,16 +12,18 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal { private readonly Stream _innerStream; private readonly long _maxBufferSize; + private readonly int _segmentSize; + private SegmentWriteStream _segmentWriteStream; - public ResponseCacheStream(Stream innerStream, long maxBufferSize) + internal ResponseCacheStream(Stream innerStream, long maxBufferSize, int segmentSize) { _innerStream = innerStream; _maxBufferSize = maxBufferSize; + _segmentSize = segmentSize; + _segmentWriteStream = new SegmentWriteStream(_segmentSize); } - public MemoryStream BufferedStream { get; } = new MemoryStream(); - - public bool BufferingEnabled { get; set; } = true; + internal bool BufferingEnabled { get; private set; } = true; public override bool CanRead => _innerStream.CanRead; @@ -34,15 +36,26 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal public override long Position { get { return _innerStream.Position; } - set { _innerStream.Position = value; } + set + { + DisableBuffering(); + _innerStream.Position = value; + } } - public void DisableBuffering() + internal Stream GetBufferStream() + { + if (!BufferingEnabled) + { + throw new InvalidOperationException("Buffer stream cannot be retrieved since buffering is disabled."); + } + return new SegmentReadStream(_segmentWriteStream.GetSegments(), _segmentWriteStream.Length); + } + + internal void DisableBuffering() { BufferingEnabled = false; - BufferedStream.SetLength(0); - BufferedStream.Capacity = 0; - BufferedStream.Dispose(); + _segmentWriteStream.Dispose(); } public override void SetLength(long value) @@ -81,13 +94,13 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal if (BufferingEnabled) { - if (BufferedStream.Length + count > _maxBufferSize) + if (_segmentWriteStream.Length + count > _maxBufferSize) { DisableBuffering(); } else { - BufferedStream.Write(buffer, offset, count); + _segmentWriteStream.Write(buffer, offset, count); } } } @@ -106,13 +119,13 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal if (BufferingEnabled) { - if (BufferedStream.Length + count > _maxBufferSize) + if (_segmentWriteStream.Length + count > _maxBufferSize) { DisableBuffering(); } else { - await BufferedStream.WriteAsync(buffer, offset, count, cancellationToken); + await _segmentWriteStream.WriteAsync(buffer, offset, count, cancellationToken); } } } @@ -131,13 +144,13 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal if (BufferingEnabled) { - if (BufferedStream.Length + 1 > _maxBufferSize) + if (_segmentWriteStream.Length + 1 > _maxBufferSize) { DisableBuffering(); } else { - BufferedStream.WriteByte(value); + _segmentWriteStream.WriteByte(value); } } } @@ -148,7 +161,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) #endif { - return ToIAsyncResult(WriteAsync(buffer, offset, count), callback, state); + return StreamUtilities.ToIAsyncResult(WriteAsync(buffer, offset, count), callback, state); } #if NETSTANDARD1_3 public void EndWrite(IAsyncResult asyncResult) @@ -162,28 +175,5 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal } ((Task)asyncResult).GetAwaiter().GetResult(); } - - private static IAsyncResult ToIAsyncResult(Task task, AsyncCallback callback, object state) - { - var tcs = new TaskCompletionSource(state); - task.ContinueWith(t => - { - if (t.IsFaulted) - { - tcs.TrySetException(t.Exception.InnerExceptions); - } - else if (t.IsCanceled) - { - tcs.TrySetCanceled(); - } - else - { - tcs.TrySetResult(0); - } - - callback?.Invoke(tcs.Task); - }, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default); - return tcs.Task; - } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Streams/SegmentReadStream.cs b/src/Microsoft.AspNetCore.ResponseCaching/Streams/SegmentReadStream.cs new file mode 100644 index 0000000000..51ff15ff41 --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/Streams/SegmentReadStream.cs @@ -0,0 +1,238 @@ +// 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.Threading; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.ResponseCaching.Internal +{ + internal class SegmentReadStream : Stream + { + private readonly List _segments; + private readonly long _length; + private int _segmentIndex; + private int _segmentOffset; + private long _position; + + internal SegmentReadStream(List segments, long length) + { + if (segments == null) + { + throw new ArgumentNullException(nameof(segments)); + } + + _segments = segments; + _length = length; + } + + public override bool CanRead => true; + + public override bool CanSeek => true; + + public override bool CanWrite => false; + + public override long Length => _length; + + public override long Position + { + get + { + return _position; + } + set + { + // The stream only supports a full rewind. This will need an update if random access becomes a required feature. + if (value != 0) + { + throw new ArgumentOutOfRangeException(nameof(value), value, $"{nameof(Position)} can only be set to 0."); + } + + _position = 0; + _segmentOffset = 0; + _segmentIndex = 0; + } + } + + public override void Flush() + { + throw new NotSupportedException("The stream does not support writing."); + } + + public override int Read(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset), offset, "Non-negative number required."); + } + // Read of length 0 will return zero and indicate end of stream. + if (count <= 0 ) + { + throw new ArgumentOutOfRangeException(nameof(count), count, "Positive number required."); + } + if (count > buffer.Length - offset) + { + throw new ArgumentException("Offset and length were out of bounds for the array or count is greater than the number of elements from index to the end of the source collection."); + } + + if (_segmentIndex == _segments.Count) + { + return 0; + } + + var bytesRead = 0; + while (count > 0) + { + if (_segmentOffset == _segments[_segmentIndex].Length) + { + // Move to the next segment + _segmentIndex++; + _segmentOffset = 0; + + if (_segmentIndex == _segments.Count) + { + break; + } + } + + // Read up to the end of the segment + var segmentBytesRead = Math.Min(count, _segments[_segmentIndex].Length - _segmentOffset); + Buffer.BlockCopy(_segments[_segmentIndex], _segmentOffset, buffer, offset, segmentBytesRead); + bytesRead += segmentBytesRead; + _segmentOffset += segmentBytesRead; + _position += segmentBytesRead; + offset += segmentBytesRead; + count -= segmentBytesRead; + } + + return bytesRead; + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + return Task.FromResult(Read(buffer, offset, count)); + } + + public override int ReadByte() + { + if (Position == Length) + { + return -1; + } + + if (_segmentOffset == _segments[_segmentIndex].Length) + { + // Move to the next segment + _segmentIndex++; + _segmentOffset = 0; + } + + var byteRead = _segments[_segmentIndex][_segmentOffset]; + _segmentOffset++; + _position++; + + return byteRead; + } + +#if NETSTANDARD1_3 + public IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) +#else + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) +#endif + { + var tcs = new TaskCompletionSource(state); + + try + { + tcs.TrySetResult(Read(buffer, offset, count)); + } + catch (Exception ex) + { + tcs.TrySetException(ex); + } + + if (callback != null) + { + // Offload callbacks to avoid stack dives on sync completions. + var ignored = Task.Run(() => + { + try + { + callback(tcs.Task); + } + catch (Exception) + { + // Suppress exceptions on background threads. + } + }); + } + + return tcs.Task; + } + +#if NETSTANDARD1_3 + public int EndRead(IAsyncResult asyncResult) +#else + public override int EndRead(IAsyncResult asyncResult) +#endif + { + if (asyncResult == null) + { + throw new ArgumentNullException(nameof(asyncResult)); + } + return ((Task)asyncResult).GetAwaiter().GetResult(); + } + + public override long Seek(long offset, SeekOrigin origin) + { + // The stream only supports a full rewind. This will need an update if random access becomes a required feature. + if (origin != SeekOrigin.Begin) + { + throw new ArgumentException(nameof(origin), $"{nameof(Seek)} can only be set to {nameof(SeekOrigin.Begin)}."); + } + if (offset != 0) + { + throw new ArgumentOutOfRangeException(nameof(offset), offset, $"{nameof(Seek)} can only be set to 0."); + } + + Position = 0; + return Position; + } + + public override void SetLength(long value) + { + throw new NotSupportedException("The stream does not support writing."); + } + + public override void Write(byte[] buffer, int offset, int count) + { + throw new NotSupportedException("The stream does not support writing."); + } + + public override async Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) + { + if (destination == null) + { + throw new ArgumentNullException(nameof(destination)); + } + if (!destination.CanWrite) + { + throw new NotSupportedException("The destination stream does not support writing."); + } + + for (; _segmentIndex < _segments.Count; _segmentIndex++, _segmentOffset = 0) + { + cancellationToken.ThrowIfCancellationRequested(); + var bytesCopied = _segments[_segmentIndex].Length - _segmentOffset; + await destination.WriteAsync(_segments[_segmentIndex], _segmentOffset, bytesCopied, cancellationToken); + _position += bytesCopied; + } + } + } +} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Streams/SegmentWriteStream.cs b/src/Microsoft.AspNetCore.ResponseCaching/Streams/SegmentWriteStream.cs new file mode 100644 index 0000000000..6fb93c5c7b --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/Streams/SegmentWriteStream.cs @@ -0,0 +1,215 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Internal; + +namespace Microsoft.AspNetCore.ResponseCaching.Internal +{ + internal class SegmentWriteStream : Stream + { + private readonly List _segments = new List(); + private readonly MemoryStream _bufferStream = new MemoryStream(); + private readonly int _segmentSize; + private long _length; + private bool _closed; + private bool _disposed; + + internal SegmentWriteStream(int segmentSize) + { + if (segmentSize <= 0) + { + throw new ArgumentOutOfRangeException(nameof(segmentSize), segmentSize, $"{nameof(segmentSize)} must be greater than 0."); + } + + _segmentSize = segmentSize; + } + + // Extracting the buffered segments closes the stream for writing + internal List GetSegments() + { + if (!_closed) + { + _closed = true; + FinalizeSegments(); + } + return _segments; + } + + public override bool CanRead => false; + + public override bool CanSeek => false; + + public override bool CanWrite => !_closed; + + public override long Length => _length; + + public override long Position + { + get + { + return _length; + } + set + { + throw new NotSupportedException("The stream does not support seeking."); + } + } + + private void DisposeMemoryStream() + { + // Clean up the memory stream + _bufferStream.SetLength(0); + _bufferStream.Capacity = 0; + _bufferStream.Dispose(); + } + + private void FinalizeSegments() + { + // Append any remaining segments + if (_bufferStream.Length > 0) + { + // Add the last segment + _segments.Add(_bufferStream.ToArray()); + } + + DisposeMemoryStream(); + } + + protected override void Dispose(bool disposing) + { + try + { + if (_disposed) + { + return; + } + + if (disposing) + { + _segments.Clear(); + DisposeMemoryStream(); + } + + _disposed = true; + _closed = true; + } + finally + { + base.Dispose(disposing); + } + } + + public override void Flush() + { + if (!CanWrite) + { + throw new ObjectDisposedException("The stream has been closed for writing."); + } + } + + public override int Read(byte[] buffer, int offset, int count) + { + throw new NotSupportedException("The stream does not support reading."); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException("The stream does not support seeking."); + } + + public override void SetLength(long value) + { + throw new NotSupportedException("The stream does not support seeking."); + } + + public override void Write(byte[] buffer, int offset, int count) + { + if (buffer == null) + { + throw new ArgumentNullException(nameof(buffer)); + } + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset), offset, "Non-negative number required."); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count), count, "Non-negative number required."); + } + if (count > buffer.Length - offset) + { + throw new ArgumentException("Offset and length were out of bounds for the array or count is greater than the number of elements from index to the end of the source collection."); + } + if (!CanWrite) + { + throw new ObjectDisposedException("The stream has been closed for writing."); + } + + while (count > 0) + { + if ((int)_bufferStream.Length == _segmentSize) + { + _segments.Add(_bufferStream.ToArray()); + _bufferStream.SetLength(0); + } + + var bytesWritten = Math.Min(count, _segmentSize - (int)_bufferStream.Length); + + _bufferStream.Write(buffer, offset, bytesWritten); + count -= bytesWritten; + offset += bytesWritten; + _length += bytesWritten; + } + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + Write(buffer, offset, count); + return TaskCache.CompletedTask; + } + + public override void WriteByte(byte value) + { + if (!CanWrite) + { + throw new ObjectDisposedException("The stream has been closed for writing."); + } + + if ((int)_bufferStream.Length == _segmentSize) + { + _segments.Add(_bufferStream.ToArray()); + _bufferStream.SetLength(0); + } + + _bufferStream.WriteByte(value); + _length++; + } + +#if NETSTANDARD1_3 + public IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) +#else + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) +#endif + { + return StreamUtilities.ToIAsyncResult(WriteAsync(buffer, offset, count), callback, state); + } + +#if NETSTANDARD1_3 + public void EndWrite(IAsyncResult asyncResult) +#else + public override void EndWrite(IAsyncResult asyncResult) +#endif + { + if (asyncResult == null) + { + throw new ArgumentNullException(nameof(asyncResult)); + } + ((Task)asyncResult).GetAwaiter().GetResult(); + } + } +} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Streams/StreamUtilities.cs b/src/Microsoft.AspNetCore.ResponseCaching/Streams/StreamUtilities.cs new file mode 100644 index 0000000000..4ce5a4ebe0 --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/Streams/StreamUtilities.cs @@ -0,0 +1,41 @@ +// 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; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.ResponseCaching.Internal +{ + internal static class StreamUtilities + { + /// + /// The segment size for buffering the response body in bytes. The default is set to 84 KB. + /// + // Internal for testing + internal static int BodySegmentSize { get; set; } = 84 * 1024; + + internal static IAsyncResult ToIAsyncResult(Task task, AsyncCallback callback, object state) + { + var tcs = new TaskCompletionSource(state); + task.ContinueWith(t => + { + if (t.IsFaulted) + { + tcs.TrySetException(t.Exception.InnerExceptions); + } + else if (t.IsCanceled) + { + tcs.TrySetCanceled(); + } + else + { + tcs.TrySetResult(0); + } + + callback?.Invoke(tcs.Task); + }, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default); + return tcs.Task; + } + } +} diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheEntrySerializerTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheEntrySerializerTests.cs index 966fdee6e0..7664e437cb 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheEntrySerializerTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheEntrySerializerTests.cs @@ -2,6 +2,7 @@ // 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.Linq; using System.Text; using Microsoft.AspNetCore.Http; @@ -16,47 +17,23 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public void Serialize_NullObject_Throws() { - Assert.Throws(() => CacheEntrySerializer.Serialize(null)); + Assert.Throws(() => ResponseCacheEntrySerializer.Serialize(null)); + } + + private class UnknownResponseCacheEntry : IResponseCacheEntry + { } [Fact] public void Serialize_UnknownObject_Throws() { - Assert.Throws(() => CacheEntrySerializer.Serialize(new object())); + Assert.Throws(() => ResponseCacheEntrySerializer.Serialize(new UnknownResponseCacheEntry())); } [Fact] public void Deserialize_NullObject_ReturnsNull() { - Assert.Null(CacheEntrySerializer.Deserialize(null)); - } - - [Fact] - public void RoundTrip_CachedResponseBody_Succeeds() - { - var cachedResponseBody = new CachedResponseBody() - { - Body = Encoding.ASCII.GetBytes("Hello world"), - }; - - AssertCachedResponseBodyEqual(cachedResponseBody, (CachedResponseBody)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedResponseBody))); - } - - [Fact] - public void RoundTrip_CachedResponseWithoutBody_Succeeds() - { - var headers = new HeaderDictionary(); - headers["keyA"] = "valueA"; - headers["keyB"] = "valueB"; - var cachedResponse = new CachedResponse() - { - BodyKeyPrefix = FastGuid.NewGuid().IdString, - Created = DateTimeOffset.UtcNow, - StatusCode = StatusCodes.Status200OK, - Headers = headers - }; - - AssertCachedResponseEqual(cachedResponse, (CachedResponse)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedResponse))); + Assert.Null(ResponseCacheEntrySerializer.Deserialize(null)); } [Fact] @@ -65,16 +42,16 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var headers = new HeaderDictionary(); headers["keyA"] = "valueA"; headers["keyB"] = "valueB"; + var body = Encoding.ASCII.GetBytes("Hello world"); var cachedResponse = new CachedResponse() { - BodyKeyPrefix = FastGuid.NewGuid().IdString, Created = DateTimeOffset.UtcNow, StatusCode = StatusCodes.Status200OK, - Body = Encoding.ASCII.GetBytes("Hello world"), + Body = new SegmentReadStream(new List(new[] { body }), body.Length), Headers = headers }; - AssertCachedResponseEqual(cachedResponse, (CachedResponse)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedResponse))); + AssertCachedResponseEqual(cachedResponse, (CachedResponse)ResponseCacheEntrySerializer.Deserialize(ResponseCacheEntrySerializer.Serialize(cachedResponse))); } [Fact] @@ -82,16 +59,16 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var headers = new HeaderDictionary(); headers["keyA"] = new StringValues(new[] { "ValueA", "ValueB" }); + var body = Encoding.ASCII.GetBytes("Hello world"); var cachedResponse = new CachedResponse() { - BodyKeyPrefix = FastGuid.NewGuid().IdString, Created = DateTimeOffset.UtcNow, StatusCode = StatusCodes.Status200OK, - Body = Encoding.ASCII.GetBytes("Hello world"), + Body = new SegmentReadStream(new List(new[] { body }), body.Length), Headers = headers }; - AssertCachedResponseEqual(cachedResponse, (CachedResponse)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedResponse))); + AssertCachedResponseEqual(cachedResponse, (CachedResponse)ResponseCacheEntrySerializer.Deserialize(ResponseCacheEntrySerializer.Serialize(cachedResponse))); } [Fact] @@ -99,16 +76,16 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var headers = new HeaderDictionary(); headers["keyA"] = StringValues.Empty; + var body = Encoding.ASCII.GetBytes("Hello world"); var cachedResponse = new CachedResponse() { - BodyKeyPrefix = FastGuid.NewGuid().IdString, Created = DateTimeOffset.UtcNow, StatusCode = StatusCodes.Status200OK, - Body = Encoding.ASCII.GetBytes("Hello world"), + Body = new SegmentReadStream(new List(new[] { body }), body.Length), Headers = headers }; - AssertCachedResponseEqual(cachedResponse, (CachedResponse)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedResponse))); + AssertCachedResponseEqual(cachedResponse, (CachedResponse)ResponseCacheEntrySerializer.Deserialize(ResponseCacheEntrySerializer.Serialize(cachedResponse))); } [Fact] @@ -119,7 +96,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests VaryByKeyPrefix = FastGuid.NewGuid().IdString }; - AssertCachedVaryByRuleEqual(cachedVaryByRule, (CachedVaryByRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryByRule))); + AssertCachedVaryByRuleEqual(cachedVaryByRule, (CachedVaryByRules)ResponseCacheEntrySerializer.Deserialize(ResponseCacheEntrySerializer.Serialize(cachedVaryByRule))); } [Fact] @@ -132,7 +109,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Headers = headers }; - AssertCachedVaryByRuleEqual(cachedVaryByRule, (CachedVaryByRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryByRule))); + AssertCachedVaryByRuleEqual(cachedVaryByRule, (CachedVaryByRules)ResponseCacheEntrySerializer.Deserialize(ResponseCacheEntrySerializer.Serialize(cachedVaryByRule))); } [Fact] @@ -145,7 +122,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests QueryKeys = queryKeys }; - AssertCachedVaryByRuleEqual(cachedVaryByRule, (CachedVaryByRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryByRule))); + AssertCachedVaryByRuleEqual(cachedVaryByRule, (CachedVaryByRules)ResponseCacheEntrySerializer.Deserialize(ResponseCacheEntrySerializer.Serialize(cachedVaryByRule))); } [Fact] @@ -160,7 +137,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests QueryKeys = queryKeys }; - AssertCachedVaryByRuleEqual(cachedVaryByRule, (CachedVaryByRules)CacheEntrySerializer.Deserialize(CacheEntrySerializer.Serialize(cachedVaryByRule))); + AssertCachedVaryByRuleEqual(cachedVaryByRule, (CachedVaryByRules)ResponseCacheEntrySerializer.Deserialize(ResponseCacheEntrySerializer.Serialize(cachedVaryByRule))); } [Fact] @@ -172,22 +149,16 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests VaryByKeyPrefix = FastGuid.NewGuid().IdString, Headers = headers }; - var serializedEntry = CacheEntrySerializer.Serialize(cachedVaryByRule); + var serializedEntry = ResponseCacheEntrySerializer.Serialize(cachedVaryByRule); Array.Reverse(serializedEntry); - Assert.Null(CacheEntrySerializer.Deserialize(serializedEntry)); - } - - private static void AssertCachedResponseBodyEqual(CachedResponseBody expected, CachedResponseBody actual) - { - Assert.True(expected.Body.SequenceEqual(actual.Body)); + Assert.Null(ResponseCacheEntrySerializer.Deserialize(serializedEntry)); } private static void AssertCachedResponseEqual(CachedResponse expected, CachedResponse actual) { Assert.NotNull(actual); Assert.NotNull(expected); - Assert.Equal(expected.BodyKeyPrefix, actual.BodyKeyPrefix); Assert.Equal(expected.Created, actual.Created); Assert.Equal(expected.StatusCode, actual.StatusCode); Assert.Equal(expected.Headers.Count, actual.Headers.Count); @@ -195,14 +166,15 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { Assert.Equal(expectedHeader.Value, actual.Headers[expectedHeader.Key]); } - if (expected.Body == null) - { - Assert.Null(actual.Body); - } - else - { - Assert.True(expected.Body.SequenceEqual(actual.Body)); - } + + Assert.Equal(expected.Body.Length, actual.Body.Length); + var bodyLength = (int)expected.Body.Length; + var expectedBytes = new byte[bodyLength]; + var actualBytes = new byte[bodyLength]; + expected.Body.Position = 0; // Rewind + Assert.Equal(bodyLength, expected.Body.Read(expectedBytes, 0, bodyLength)); + Assert.Equal(bodyLength, actual.Body.Read(actualBytes, 0, bodyLength)); + Assert.True(expectedBytes.SequenceEqual(actualBytes)); } private static void AssertCachedVaryByRuleEqual(CachedVaryByRules expected, CachedVaryByRules actual) diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheMiddlewareTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheMiddlewareTests.cs index 5d8bebc4a4..792c6630e5 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheMiddlewareTests.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Headers; using Microsoft.AspNetCore.ResponseCaching.Internal; @@ -53,7 +52,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests "BaseKey", new CachedResponse() { - Body = new byte[0] + Body = new SegmentReadStream(new List(0), 0) }, TimeSpan.Zero); @@ -92,7 +91,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests "BaseKeyVaryKey2", new CachedResponse() { - Body = new byte[0] + Body = new SegmentReadStream(new List(0), 0) }, TimeSpan.Zero); @@ -424,78 +423,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Assert.Equal(new StringValues(new[] { "HEADERA", "HEADERB" }), context.CachedVaryByRules.Headers); } - [Fact] - public async Task FinalizeCacheBody_StoreResponseBodySeparately_IfLargerThanLimit() - { - var store = new TestResponseCacheStore(); - var middleware = TestUtils.CreateTestMiddleware(store); - var context = TestUtils.CreateTestContext(); - - middleware.ShimResponseStream(context); - await context.HttpContext.Response.WriteAsync(new string('0', 70 * 1024)); - - context.ShouldCacheResponse = true; - context.CachedResponse = new CachedResponse() - { - BodyKeyPrefix = FastGuid.NewGuid().IdString - }; - context.BaseKey = "BaseKey"; - context.CachedResponseValidFor = TimeSpan.FromSeconds(10); - - await middleware.FinalizeCacheBodyAsync(context); - - Assert.Equal(2, store.SetCount); - } - - [Fact] - public async Task FinalizeCacheBody_StoreResponseBodyInCachedResponse_IfSmallerThanLimit() - { - var store = new TestResponseCacheStore(); - var middleware = TestUtils.CreateTestMiddleware(store); - var context = TestUtils.CreateTestContext(); - - middleware.ShimResponseStream(context); - await context.HttpContext.Response.WriteAsync(new string('0', 70 * 1024 - 1)); - - context.ShouldCacheResponse = true; - context.CachedResponse = new CachedResponse() - { - BodyKeyPrefix = FastGuid.NewGuid().IdString - }; - context.BaseKey = "BaseKey"; - context.CachedResponseValidFor = TimeSpan.FromSeconds(10); - - await middleware.FinalizeCacheBodyAsync(context); - - Assert.Equal(1, store.SetCount); - } - - [Fact] - public async Task FinalizeCacheBody_StoreResponseBodySeparately_LimitIsConfigurable() - { - var store = new TestResponseCacheStore(); - var middleware = TestUtils.CreateTestMiddleware(store, new ResponseCacheOptions() - { - MinimumSplitBodySize = 2048 - }); - var context = TestUtils.CreateTestContext(); - - middleware.ShimResponseStream(context); - await context.HttpContext.Response.WriteAsync(new string('0', 1024)); - - context.ShouldCacheResponse = true; - context.CachedResponse = new CachedResponse() - { - BodyKeyPrefix = FastGuid.NewGuid().IdString - }; - context.BaseKey = "BaseKey"; - context.CachedResponseValidFor = TimeSpan.FromSeconds(10); - - await middleware.FinalizeCacheBodyAsync(context); - - Assert.Equal(1, store.SetCount); - } - [Fact] public async Task FinalizeCacheBody_Cache_IfContentLengthMatches() { @@ -504,14 +431,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var context = TestUtils.CreateTestContext(); middleware.ShimResponseStream(context); - context.HttpContext.Response.ContentLength = 10; - await context.HttpContext.Response.WriteAsync(new string('0', 10)); + context.HttpContext.Response.ContentLength = 20; + await context.HttpContext.Response.WriteAsync(new string('0', 20)); context.ShouldCacheResponse = true; - context.CachedResponse = new CachedResponse() - { - BodyKeyPrefix = FastGuid.NewGuid().IdString - }; + context.CachedResponse = new CachedResponse(); context.BaseKey = "BaseKey"; context.CachedResponseValidFor = TimeSpan.FromSeconds(10); @@ -532,10 +456,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests await context.HttpContext.Response.WriteAsync(new string('0', 10)); context.ShouldCacheResponse = true; - context.CachedResponse = new CachedResponse() - { - BodyKeyPrefix = FastGuid.NewGuid().IdString - }; + context.CachedResponse = new CachedResponse(); context.BaseKey = "BaseKey"; context.CachedResponseValidFor = TimeSpan.FromSeconds(10); @@ -555,10 +476,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests await context.HttpContext.Response.WriteAsync(new string('0', 10)); context.ShouldCacheResponse = true; - context.CachedResponse = new CachedResponse() - { - BodyKeyPrefix = FastGuid.NewGuid().IdString - }; + context.CachedResponse = new CachedResponse(); context.BaseKey = "BaseKey"; context.CachedResponseValidFor = TimeSpan.FromSeconds(10); diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheTests.cs index 09f432d3b7..bc473d12a5 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheTests.cs @@ -19,288 +19,333 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfAvailable() { - var builder = TestUtils.CreateBuilderWithResponseCache(); + var builders = TestUtils.CreateBuildersWithResponseCache(); - using (var server = new TestServer(builder)) + foreach (var builder in builders) { - var client = server.CreateClient(); - var initialResponse = await client.GetAsync(""); - var subsequentResponse = await client.GetAsync(""); + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + var subsequentResponse = await client.GetAsync(""); - await AssertResponseCachedAsync(initialResponse, subsequentResponse); + await AssertResponseCachedAsync(initialResponse, subsequentResponse); + } } } [Fact] public async void ServesFreshContent_IfNotAvailable() { - var builder = TestUtils.CreateBuilderWithResponseCache(); + var builders = TestUtils.CreateBuildersWithResponseCache(); - using (var server = new TestServer(builder)) + foreach (var builder in builders) { - var client = server.CreateClient(); - var initialResponse = await client.GetAsync(""); - var subsequentResponse = await client.GetAsync("/different"); + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + var subsequentResponse = await client.GetAsync("/different"); - await AssertResponseNotCachedAsync(initialResponse, subsequentResponse); + await AssertResponseNotCachedAsync(initialResponse, subsequentResponse); + } } } [Fact] public async void ServesCachedContent_IfVaryHeader_Matches() { - var builder = TestUtils.CreateBuilderWithResponseCache(requestDelegate: async (context) => + var builders = TestUtils.CreateBuildersWithResponseCache(requestDelegate: async (context) => { context.Response.Headers[HeaderNames.Vary] = HeaderNames.From; await TestUtils.TestRequestDelegate(context); }); - using (var server = new TestServer(builder)) + foreach (var builder in builders) { - var client = server.CreateClient(); - client.DefaultRequestHeaders.From = "user@example.com"; - var initialResponse = await client.GetAsync(""); - var subsequentResponse = await client.GetAsync(""); + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + client.DefaultRequestHeaders.From = "user@example.com"; + var initialResponse = await client.GetAsync(""); + var subsequentResponse = await client.GetAsync(""); - await AssertResponseCachedAsync(initialResponse, subsequentResponse); + await AssertResponseCachedAsync(initialResponse, subsequentResponse); + } } } [Fact] public async void ServesFreshContent_IfVaryHeader_Mismatches() { - var builder = TestUtils.CreateBuilderWithResponseCache(requestDelegate: async (context) => + var builders = TestUtils.CreateBuildersWithResponseCache(requestDelegate: async (context) => { context.Response.Headers[HeaderNames.Vary] = HeaderNames.From; await TestUtils.TestRequestDelegate(context); }); - using (var server = new TestServer(builder)) + foreach (var builder in builders) { - var client = server.CreateClient(); - client.DefaultRequestHeaders.From = "user@example.com"; - var initialResponse = await client.GetAsync(""); - client.DefaultRequestHeaders.From = "user2@example.com"; - var subsequentResponse = await client.GetAsync(""); + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + client.DefaultRequestHeaders.From = "user@example.com"; + var initialResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.From = "user2@example.com"; + var subsequentResponse = await client.GetAsync(""); - await AssertResponseNotCachedAsync(initialResponse, subsequentResponse); + await AssertResponseNotCachedAsync(initialResponse, subsequentResponse); + } } } [Fact] public async void ServesCachedContent_IfVaryQueryKeys_Matches() { - var builder = TestUtils.CreateBuilderWithResponseCache(requestDelegate: async (context) => + var builders = TestUtils.CreateBuildersWithResponseCache(requestDelegate: async (context) => { context.GetResponseCacheFeature().VaryByQueryKeys = "query"; await TestUtils.TestRequestDelegate(context); }); - using (var server = new TestServer(builder)) + foreach (var builder in builders) { - var client = server.CreateClient(); - var initialResponse = await client.GetAsync("?query=value"); - var subsequentResponse = await client.GetAsync("?query=value"); + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync("?query=value"); + var subsequentResponse = await client.GetAsync("?query=value"); - await AssertResponseCachedAsync(initialResponse, subsequentResponse); + await AssertResponseCachedAsync(initialResponse, subsequentResponse); + } } } [Fact] public async void ServesCachedContent_IfVaryQueryKeysExplicit_Matches_QueryKeyCaseInsensitive() { - var builder = TestUtils.CreateBuilderWithResponseCache(requestDelegate: async (context) => + var builders = TestUtils.CreateBuildersWithResponseCache(requestDelegate: async (context) => { context.GetResponseCacheFeature().VaryByQueryKeys = new[] { "QueryA", "queryb" }; await TestUtils.TestRequestDelegate(context); }); - using (var server = new TestServer(builder)) + foreach (var builder in builders) { - var client = server.CreateClient(); - var initialResponse = await client.GetAsync("?querya=valuea&queryb=valueb"); - var subsequentResponse = await client.GetAsync("?QueryA=valuea&QueryB=valueb"); + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync("?querya=valuea&queryb=valueb"); + var subsequentResponse = await client.GetAsync("?QueryA=valuea&QueryB=valueb"); - await AssertResponseCachedAsync(initialResponse, subsequentResponse); + await AssertResponseCachedAsync(initialResponse, subsequentResponse); + } } } [Fact] public async void ServesCachedContent_IfVaryQueryKeyStar_Matches_QueryKeyCaseInsensitive() { - var builder = TestUtils.CreateBuilderWithResponseCache(requestDelegate: async (context) => + var builders = TestUtils.CreateBuildersWithResponseCache(requestDelegate: async (context) => { context.GetResponseCacheFeature().VaryByQueryKeys = new[] { "*" }; await TestUtils.TestRequestDelegate(context); }); - using (var server = new TestServer(builder)) + foreach (var builder in builders) { - var client = server.CreateClient(); - var initialResponse = await client.GetAsync("?querya=valuea&queryb=valueb"); - var subsequentResponse = await client.GetAsync("?QueryA=valuea&QueryB=valueb"); + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync("?querya=valuea&queryb=valueb"); + var subsequentResponse = await client.GetAsync("?QueryA=valuea&QueryB=valueb"); - await AssertResponseCachedAsync(initialResponse, subsequentResponse); + await AssertResponseCachedAsync(initialResponse, subsequentResponse); + } } } [Fact] public async void ServesCachedContent_IfVaryQueryKeyExplicit_Matches_OrderInsensitive() { - var builder = TestUtils.CreateBuilderWithResponseCache(requestDelegate: async (context) => + var builders = TestUtils.CreateBuildersWithResponseCache(requestDelegate: async (context) => { context.GetResponseCacheFeature().VaryByQueryKeys = new[] { "QueryB", "QueryA" }; await TestUtils.TestRequestDelegate(context); }); - using (var server = new TestServer(builder)) + foreach (var builder in builders) { - var client = server.CreateClient(); - var initialResponse = await client.GetAsync("?QueryA=ValueA&QueryB=ValueB"); - var subsequentResponse = await client.GetAsync("?QueryB=ValueB&QueryA=ValueA"); + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync("?QueryA=ValueA&QueryB=ValueB"); + var subsequentResponse = await client.GetAsync("?QueryB=ValueB&QueryA=ValueA"); - await AssertResponseCachedAsync(initialResponse, subsequentResponse); + await AssertResponseCachedAsync(initialResponse, subsequentResponse); + } } } [Fact] public async void ServesCachedContent_IfVaryQueryKeyStar_Matches_OrderInsensitive() { - var builder = TestUtils.CreateBuilderWithResponseCache(requestDelegate: async (context) => + var builders = TestUtils.CreateBuildersWithResponseCache(requestDelegate: async (context) => { context.GetResponseCacheFeature().VaryByQueryKeys = new[] { "*" }; await TestUtils.TestRequestDelegate(context); }); - using (var server = new TestServer(builder)) + foreach (var builder in builders) { - var client = server.CreateClient(); - var initialResponse = await client.GetAsync("?QueryA=ValueA&QueryB=ValueB"); - var subsequentResponse = await client.GetAsync("?QueryB=ValueB&QueryA=ValueA"); + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync("?QueryA=ValueA&QueryB=ValueB"); + var subsequentResponse = await client.GetAsync("?QueryB=ValueB&QueryA=ValueA"); - await AssertResponseCachedAsync(initialResponse, subsequentResponse); + await AssertResponseCachedAsync(initialResponse, subsequentResponse); + } } } [Fact] public async void ServesFreshContent_IfVaryQueryKey_Mismatches() { - var builder = TestUtils.CreateBuilderWithResponseCache(requestDelegate: async (context) => + var builders = TestUtils.CreateBuildersWithResponseCache(requestDelegate: async (context) => { context.GetResponseCacheFeature().VaryByQueryKeys = "query"; await TestUtils.TestRequestDelegate(context); }); - using (var server = new TestServer(builder)) + foreach (var builder in builders) { - var client = server.CreateClient(); - var initialResponse = await client.GetAsync("?query=value"); - var subsequentResponse = await client.GetAsync("?query=value2"); + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync("?query=value"); + var subsequentResponse = await client.GetAsync("?query=value2"); - await AssertResponseNotCachedAsync(initialResponse, subsequentResponse); + await AssertResponseNotCachedAsync(initialResponse, subsequentResponse); + } } } [Fact] public async void ServesFreshContent_IfVaryQueryKeyExplicit_Mismatch_QueryKeyCaseSensitive() { - var builder = TestUtils.CreateBuilderWithResponseCache(requestDelegate: async (context) => + var builders = TestUtils.CreateBuildersWithResponseCache(requestDelegate: async (context) => { context.GetResponseCacheFeature().VaryByQueryKeys = new[] { "QueryA", "QueryB" }; await TestUtils.TestRequestDelegate(context); }); - using (var server = new TestServer(builder)) + foreach (var builder in builders) { - var client = server.CreateClient(); - var initialResponse = await client.GetAsync("?querya=valuea&queryb=valueb"); - var subsequentResponse = await client.GetAsync("?querya=ValueA&queryb=ValueB"); + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync("?querya=valuea&queryb=valueb"); + var subsequentResponse = await client.GetAsync("?querya=ValueA&queryb=ValueB"); - await AssertResponseNotCachedAsync(initialResponse, subsequentResponse); + await AssertResponseNotCachedAsync(initialResponse, subsequentResponse); + } } } [Fact] public async void ServesFreshContent_IfVaryQueryKeyStar_Mismatch_QueryKeyValueCaseSensitive() { - var builder = TestUtils.CreateBuilderWithResponseCache(requestDelegate: async (context) => + var builders = TestUtils.CreateBuildersWithResponseCache(requestDelegate: async (context) => { context.GetResponseCacheFeature().VaryByQueryKeys = new[] { "*" }; await TestUtils.TestRequestDelegate(context); }); - using (var server = new TestServer(builder)) + foreach (var builder in builders) { - var client = server.CreateClient(); - var initialResponse = await client.GetAsync("?querya=valuea&queryb=valueb"); - var subsequentResponse = await client.GetAsync("?querya=ValueA&queryb=ValueB"); + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync("?querya=valuea&queryb=valueb"); + var subsequentResponse = await client.GetAsync("?querya=ValueA&queryb=ValueB"); - await AssertResponseNotCachedAsync(initialResponse, subsequentResponse); + await AssertResponseNotCachedAsync(initialResponse, subsequentResponse); + } } } [Fact] public async void ServesFreshContent_IfRequestRequirements_NotMet() { - var builder = TestUtils.CreateBuilderWithResponseCache(); + var builders = TestUtils.CreateBuildersWithResponseCache(); - using (var server = new TestServer(builder)) + foreach (var builder in builders) { - var client = server.CreateClient(); - var initialResponse = await client.GetAsync(""); - client.DefaultRequestHeaders.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue() + using (var server = new TestServer(builder)) { - MaxAge = TimeSpan.FromSeconds(0) - }; - var subsequentResponse = await client.GetAsync(""); + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue() + { + MaxAge = TimeSpan.FromSeconds(0) + }; + var subsequentResponse = await client.GetAsync(""); - await AssertResponseNotCachedAsync(initialResponse, subsequentResponse); + await AssertResponseNotCachedAsync(initialResponse, subsequentResponse); + } } } [Fact] public async void Serves504_IfOnlyIfCachedHeader_IsSpecified() { - var builder = TestUtils.CreateBuilderWithResponseCache(); + var builders = TestUtils.CreateBuildersWithResponseCache(); - using (var server = new TestServer(builder)) + foreach (var builder in builders) { - var client = server.CreateClient(); - var initialResponse = await client.GetAsync(""); - client.DefaultRequestHeaders.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue() + using (var server = new TestServer(builder)) { - OnlyIfCached = true - }; - var subsequentResponse = await client.GetAsync("/different"); + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue() + { + OnlyIfCached = true + }; + var subsequentResponse = await client.GetAsync("/different"); - initialResponse.EnsureSuccessStatusCode(); - Assert.Equal(System.Net.HttpStatusCode.GatewayTimeout, subsequentResponse.StatusCode); + initialResponse.EnsureSuccessStatusCode(); + Assert.Equal(System.Net.HttpStatusCode.GatewayTimeout, subsequentResponse.StatusCode); + } } } [Fact] public async void ServesFreshContent_IfSetCookie_IsSpecified() { - var builder = TestUtils.CreateBuilderWithResponseCache(requestDelegate: async (context) => + var builders = TestUtils.CreateBuildersWithResponseCache(requestDelegate: async (context) => { var headers = context.Response.Headers[HeaderNames.SetCookie] = "cookieName=cookieValue"; await TestUtils.TestRequestDelegate(context); }); - using (var server = new TestServer(builder)) + foreach (var builder in builders) { - var client = server.CreateClient(); - var initialResponse = await client.GetAsync(""); - var subsequentResponse = await client.GetAsync(""); + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + var subsequentResponse = await client.GetAsync(""); - await AssertResponseNotCachedAsync(initialResponse, subsequentResponse); + await AssertResponseNotCachedAsync(initialResponse, subsequentResponse); + } } } [Fact] public async void ServesCachedContent_IfIHttpSendFileFeature_NotUsed() { - var builder = TestUtils.CreateBuilderWithResponseCache(app => + var builders = TestUtils.CreateBuildersWithResponseCache(app => { app.Use(async (context, next) => { @@ -309,20 +354,23 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests }); }); - using (var server = new TestServer(builder)) + foreach (var builder in builders) { - var client = server.CreateClient(); - var initialResponse = await client.GetAsync(""); - var subsequentResponse = await client.GetAsync(""); + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + var subsequentResponse = await client.GetAsync(""); - await AssertResponseCachedAsync(initialResponse, subsequentResponse); + await AssertResponseCachedAsync(initialResponse, subsequentResponse); + } } } [Fact] public async void ServesFreshContent_IfIHttpSendFileFeature_Used() { - var builder = TestUtils.CreateBuilderWithResponseCache( + var builders = TestUtils.CreateBuildersWithResponseCache( app => { app.Use(async (context, next) => @@ -337,248 +385,284 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests await TestUtils.TestRequestDelegate(context); }); - using (var server = new TestServer(builder)) + foreach (var builder in builders) { - var client = server.CreateClient(); - var initialResponse = await client.GetAsync(""); - var subsequentResponse = await client.GetAsync(""); + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + var subsequentResponse = await client.GetAsync(""); - await AssertResponseNotCachedAsync(initialResponse, subsequentResponse); + await AssertResponseNotCachedAsync(initialResponse, subsequentResponse); + } } } [Fact] public async void ServesCachedContent_IfSubsequentRequest_ContainsNoStore() { - var builder = TestUtils.CreateBuilderWithResponseCache(); + var builders = TestUtils.CreateBuildersWithResponseCache(); - using (var server = new TestServer(builder)) + foreach (var builder in builders) { - var client = server.CreateClient(); - var initialResponse = await client.GetAsync(""); - client.DefaultRequestHeaders.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue() + using (var server = new TestServer(builder)) { - NoStore = true - }; - var subsequentResponse = await client.GetAsync(""); + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue() + { + NoStore = true + }; + var subsequentResponse = await client.GetAsync(""); - await AssertResponseCachedAsync(initialResponse, subsequentResponse); + await AssertResponseCachedAsync(initialResponse, subsequentResponse); + } } } [Fact] public async void ServesFreshContent_IfInitialRequestContains_NoStore() { - var builder = TestUtils.CreateBuilderWithResponseCache(); + var builders = TestUtils.CreateBuildersWithResponseCache(); - using (var server = new TestServer(builder)) + foreach (var builder in builders) { - var client = server.CreateClient(); - client.DefaultRequestHeaders.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue() + using (var server = new TestServer(builder)) { - NoStore = true - }; - var initialResponse = await client.GetAsync(""); - var subsequentResponse = await client.GetAsync(""); + var client = server.CreateClient(); + client.DefaultRequestHeaders.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue() + { + NoStore = true + }; + var initialResponse = await client.GetAsync(""); + var subsequentResponse = await client.GetAsync(""); - await AssertResponseNotCachedAsync(initialResponse, subsequentResponse); + await AssertResponseNotCachedAsync(initialResponse, subsequentResponse); + } } } [Fact] public async void Serves304_IfIfModifiedSince_Satisfied() { - var builder = TestUtils.CreateBuilderWithResponseCache(); + var builders = TestUtils.CreateBuildersWithResponseCache(); - using (var server = new TestServer(builder)) + foreach (var builder in builders) { - var client = server.CreateClient(); - var initialResponse = await client.GetAsync(""); - client.DefaultRequestHeaders.IfUnmodifiedSince = DateTimeOffset.MaxValue; - var subsequentResponse = await client.GetAsync(""); + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.IfUnmodifiedSince = DateTimeOffset.MaxValue; + var subsequentResponse = await client.GetAsync(""); - initialResponse.EnsureSuccessStatusCode(); - Assert.Equal(System.Net.HttpStatusCode.NotModified, subsequentResponse.StatusCode); + initialResponse.EnsureSuccessStatusCode(); + Assert.Equal(System.Net.HttpStatusCode.NotModified, subsequentResponse.StatusCode); + } } } [Fact] public async void ServesCachedContent_IfIfModifiedSince_NotSatisfied() { - var builder = TestUtils.CreateBuilderWithResponseCache(); + var builders = TestUtils.CreateBuildersWithResponseCache(); - using (var server = new TestServer(builder)) + foreach (var builder in builders) { - var client = server.CreateClient(); - var initialResponse = await client.GetAsync(""); - client.DefaultRequestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue; - var subsequentResponse = await client.GetAsync(""); + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue; + var subsequentResponse = await client.GetAsync(""); - await AssertResponseCachedAsync(initialResponse, subsequentResponse); + await AssertResponseCachedAsync(initialResponse, subsequentResponse); + } } } [Fact] public async void Serves304_IfIfNoneMatch_Satisfied() { - var builder = TestUtils.CreateBuilderWithResponseCache(requestDelegate: async (context) => + var builders = TestUtils.CreateBuildersWithResponseCache(requestDelegate: async (context) => { var headers = context.Response.GetTypedHeaders().ETag = new EntityTagHeaderValue("\"E1\""); await TestUtils.TestRequestDelegate(context); }); - using (var server = new TestServer(builder)) + foreach (var builder in builders) { - var client = server.CreateClient(); - var initialResponse = await client.GetAsync(""); - client.DefaultRequestHeaders.IfNoneMatch.Add(new System.Net.Http.Headers.EntityTagHeaderValue("\"E1\"")); - var subsequentResponse = await client.GetAsync(""); + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.IfNoneMatch.Add(new System.Net.Http.Headers.EntityTagHeaderValue("\"E1\"")); + var subsequentResponse = await client.GetAsync(""); - initialResponse.EnsureSuccessStatusCode(); - Assert.Equal(System.Net.HttpStatusCode.NotModified, subsequentResponse.StatusCode); + initialResponse.EnsureSuccessStatusCode(); + Assert.Equal(System.Net.HttpStatusCode.NotModified, subsequentResponse.StatusCode); + } } } [Fact] public async void ServesCachedContent_IfIfNoneMatch_NotSatisfied() { - var builder = TestUtils.CreateBuilderWithResponseCache(requestDelegate: async (context) => + var builders = TestUtils.CreateBuildersWithResponseCache(requestDelegate: async (context) => { var headers = context.Response.GetTypedHeaders().ETag = new EntityTagHeaderValue("\"E1\""); await TestUtils.TestRequestDelegate(context); }); - using (var server = new TestServer(builder)) + foreach (var builder in builders) { - var client = server.CreateClient(); - var initialResponse = await client.GetAsync(""); - client.DefaultRequestHeaders.IfNoneMatch.Add(new System.Net.Http.Headers.EntityTagHeaderValue("\"E2\"")); - var subsequentResponse = await client.GetAsync(""); + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.IfNoneMatch.Add(new System.Net.Http.Headers.EntityTagHeaderValue("\"E2\"")); + var subsequentResponse = await client.GetAsync(""); - await AssertResponseCachedAsync(initialResponse, subsequentResponse); + await AssertResponseCachedAsync(initialResponse, subsequentResponse); + } } } [Fact] public async void ServesCachedContent_IfBodySize_IsCacheable() { - var builder = TestUtils.CreateBuilderWithResponseCache(options: new ResponseCacheOptions() + var builders = TestUtils.CreateBuildersWithResponseCache(options: new ResponseCacheOptions() { - MaximumCachedBodySize = 100 + MaximumBodySize = 100 }); - using (var server = new TestServer(builder)) + foreach (var builder in builders) { - var client = server.CreateClient(); - var initialResponse = await client.GetAsync(""); - var subsequentResponse = await client.GetAsync(""); + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + var subsequentResponse = await client.GetAsync(""); - await AssertResponseCachedAsync(initialResponse, subsequentResponse); + await AssertResponseCachedAsync(initialResponse, subsequentResponse); + } } } [Fact] public async void ServesFreshContent_IfBodySize_IsNotCacheable() { - var builder = TestUtils.CreateBuilderWithResponseCache(options: new ResponseCacheOptions() + var builders = TestUtils.CreateBuildersWithResponseCache(options: new ResponseCacheOptions() { - MaximumCachedBodySize = 1 + MaximumBodySize = 1 }); - using (var server = new TestServer(builder)) + foreach (var builder in builders) { - var client = server.CreateClient(); - var initialResponse = await client.GetAsync(""); - var subsequentResponse = await client.GetAsync("/different"); + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + var subsequentResponse = await client.GetAsync("/different"); - await AssertResponseNotCachedAsync(initialResponse, subsequentResponse); + await AssertResponseNotCachedAsync(initialResponse, subsequentResponse); + } } } [Fact] public async void ServesCachedContent_WithoutReplacingCachedVaryBy_OnCacheMiss() { - var builder = TestUtils.CreateBuilderWithResponseCache(requestDelegate: async (context) => + var builders = TestUtils.CreateBuildersWithResponseCache(requestDelegate: async (context) => { context.Response.Headers[HeaderNames.Vary] = HeaderNames.From; await TestUtils.TestRequestDelegate(context); }); - using (var server = new TestServer(builder)) + foreach (var builder in builders) { - var client = server.CreateClient(); - client.DefaultRequestHeaders.From = "user@example.com"; - var initialResponse = await client.GetAsync(""); - client.DefaultRequestHeaders.From = "user2@example.com"; - var otherResponse = await client.GetAsync(""); - client.DefaultRequestHeaders.From = "user@example.com"; - var subsequentResponse = await client.GetAsync(""); + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + client.DefaultRequestHeaders.From = "user@example.com"; + var initialResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.From = "user2@example.com"; + var otherResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.From = "user@example.com"; + var subsequentResponse = await client.GetAsync(""); - await AssertResponseCachedAsync(initialResponse, subsequentResponse); + await AssertResponseCachedAsync(initialResponse, subsequentResponse); + } } } [Fact] public async void ServesFreshContent_IfCachedVaryByUpdated_OnCacheMiss() { - var builder = TestUtils.CreateBuilderWithResponseCache(requestDelegate: async (context) => + var builders = TestUtils.CreateBuildersWithResponseCache(requestDelegate: async (context) => { context.Response.Headers[HeaderNames.Vary] = context.Request.Headers[HeaderNames.Pragma]; await TestUtils.TestRequestDelegate(context); }); - using (var server = new TestServer(builder)) + foreach (var builder in builders) { - var client = server.CreateClient(); - client.DefaultRequestHeaders.From = "user@example.com"; - client.DefaultRequestHeaders.Pragma.Clear(); - client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("From")); - client.DefaultRequestHeaders.MaxForwards = 1; - var initialResponse = await client.GetAsync(""); - client.DefaultRequestHeaders.From = "user2@example.com"; - client.DefaultRequestHeaders.Pragma.Clear(); - client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("Max-Forwards")); - client.DefaultRequestHeaders.MaxForwards = 2; - var otherResponse = await client.GetAsync(""); - client.DefaultRequestHeaders.From = "user@example.com"; - client.DefaultRequestHeaders.Pragma.Clear(); - client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("From")); - client.DefaultRequestHeaders.MaxForwards = 1; - var subsequentResponse = await client.GetAsync(""); + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + client.DefaultRequestHeaders.From = "user@example.com"; + client.DefaultRequestHeaders.Pragma.Clear(); + client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("From")); + client.DefaultRequestHeaders.MaxForwards = 1; + var initialResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.From = "user2@example.com"; + client.DefaultRequestHeaders.Pragma.Clear(); + client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("Max-Forwards")); + client.DefaultRequestHeaders.MaxForwards = 2; + var otherResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.From = "user@example.com"; + client.DefaultRequestHeaders.Pragma.Clear(); + client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("From")); + client.DefaultRequestHeaders.MaxForwards = 1; + var subsequentResponse = await client.GetAsync(""); - await AssertResponseNotCachedAsync(initialResponse, subsequentResponse); + await AssertResponseNotCachedAsync(initialResponse, subsequentResponse); + } } } [Fact] public async void ServesCachedContent_IfCachedVaryByNotUpdated_OnCacheMiss() { - var builder = TestUtils.CreateBuilderWithResponseCache(requestDelegate: async (context) => + var builders = TestUtils.CreateBuildersWithResponseCache(requestDelegate: async (context) => { context.Response.Headers[HeaderNames.Vary] = context.Request.Headers[HeaderNames.Pragma]; await TestUtils.TestRequestDelegate(context); }); - using (var server = new TestServer(builder)) + foreach (var builder in builders) { - var client = server.CreateClient(); - client.DefaultRequestHeaders.From = "user@example.com"; - client.DefaultRequestHeaders.Pragma.Clear(); - client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("From")); - client.DefaultRequestHeaders.MaxForwards = 1; - var initialResponse = await client.GetAsync(""); - client.DefaultRequestHeaders.From = "user2@example.com"; - client.DefaultRequestHeaders.Pragma.Clear(); - client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("From")); - client.DefaultRequestHeaders.MaxForwards = 2; - var otherResponse = await client.GetAsync(""); - client.DefaultRequestHeaders.From = "user@example.com"; - client.DefaultRequestHeaders.Pragma.Clear(); - client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("From")); - client.DefaultRequestHeaders.MaxForwards = 1; - var subsequentResponse = await client.GetAsync(""); + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + client.DefaultRequestHeaders.From = "user@example.com"; + client.DefaultRequestHeaders.Pragma.Clear(); + client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("From")); + client.DefaultRequestHeaders.MaxForwards = 1; + var initialResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.From = "user2@example.com"; + client.DefaultRequestHeaders.Pragma.Clear(); + client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("From")); + client.DefaultRequestHeaders.MaxForwards = 2; + var otherResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.From = "user@example.com"; + client.DefaultRequestHeaders.Pragma.Clear(); + client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("From")); + client.DefaultRequestHeaders.MaxForwards = 1; + var subsequentResponse = await client.GetAsync(""); - await AssertResponseCachedAsync(initialResponse, subsequentResponse); + await AssertResponseCachedAsync(initialResponse, subsequentResponse); + } } } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/SegmentReadStreamTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/SegmentReadStreamTests.cs new file mode 100644 index 0000000000..5247df3096 --- /dev/null +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/SegmentReadStreamTests.cs @@ -0,0 +1,285 @@ +// 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.Linq; +using Microsoft.AspNetCore.ResponseCaching.Internal; +using Xunit; + +namespace Microsoft.AspNetCore.ResponseCaching.Tests +{ + public class SegmentReadStreamTests + { + public class TestStreamInitInfo + { + internal List Segments { get; set; } + internal int SegmentSize { get; set; } + internal long Length { get; set; } + } + + public static TheoryData TestStreams + { + get + { + return new TheoryData + { + // Partial Segment + new TestStreamInitInfo() + { + Segments = new List(new[] + { + new byte[] { 0, 1, 2, 3, 4 }, + new byte[] { 5, 6, 7, 8, 9 }, + new byte[] { 10, 11, 12 }, + }), + SegmentSize = 5, + Length = 13 + }, + // Full Segments + new TestStreamInitInfo() + { + Segments = new List(new[] + { + new byte[] { 0, 1, 2, 3, 4 }, + new byte[] { 5, 6, 7, 8, 9 }, + new byte[] { 10, 11, 12, 13, 14 }, + }), + SegmentSize = 5, + Length = 15 + } + }; + } + } + + [Fact] + public void SegmentReadStream_NullSegments_Throws() + { + Assert.Throws(() => new SegmentReadStream(null, 0)); + } + + [Fact] + public void Position_ResetToZero_Succeeds() + { + var stream = new SegmentReadStream(new List(), 0); + + // This should not throw + stream.Position = 0; + } + + [Theory] + [InlineData(1)] + [InlineData(-1)] + [InlineData(100)] + [InlineData(long.MaxValue)] + [InlineData(long.MinValue)] + public void Position_SetToNonZero_Throws(long position) + { + var stream = new SegmentReadStream(new List(new[] { new byte[100] }), 100); + + Assert.Throws(() => stream.Position = position); + } + + [Fact] + public void WriteOperations_Throws() + { + var stream = new SegmentReadStream(new List(), 0); + + + Assert.Throws(() => stream.Flush()); + Assert.Throws(() => stream.Write(new byte[1], 0, 0)); + } + + [Fact] + public void SetLength_Throws() + { + var stream = new SegmentReadStream(new List(), 0); + + Assert.Throws(() => stream.SetLength(0)); + } + + [Theory] + [InlineData(SeekOrigin.Current)] + [InlineData(SeekOrigin.End)] + public void Seek_NotBegin_Throws(SeekOrigin origin) + { + var stream = new SegmentReadStream(new List(), 0); + + Assert.Throws(() => stream.Seek(0, origin)); + } + + [Theory] + [InlineData(1)] + [InlineData(-1)] + [InlineData(100)] + [InlineData(long.MaxValue)] + [InlineData(long.MinValue)] + public void Seek_NotZero_Throws(long offset) + { + var stream = new SegmentReadStream(new List(), 0); + + Assert.Throws(() => stream.Seek(offset, SeekOrigin.Begin)); + } + + [Theory] + [MemberData(nameof(TestStreams))] + public void ReadByte_CanReadAllBytes(TestStreamInitInfo info) + { + var stream = new SegmentReadStream(info.Segments, info.Length); + + for (var i = 0; i < stream.Length; i++) + { + Assert.Equal(i, stream.Position); + Assert.Equal(i, stream.ReadByte()); + } + Assert.Equal(stream.Length, stream.Position); + Assert.Equal(-1, stream.ReadByte()); + Assert.Equal(stream.Length, stream.Position); + } + + [Theory] + [MemberData(nameof(TestStreams))] + public void Read_CountLessThanSegmentSize_CanReadAllBytes(TestStreamInitInfo info) + { + var stream = new SegmentReadStream(info.Segments, info.Length); + var count = info.SegmentSize - 1; + + for (var i = 0; i < stream.Length; i+=count) + { + var output = new byte[count]; + var expectedOutput = new byte[count]; + var expectedBytesRead = Math.Min(count, stream.Length - i); + for (var j = 0; j < expectedBytesRead; j++) + { + expectedOutput[j] = (byte)(i + j); + } + Assert.Equal(i, stream.Position); + Assert.Equal(expectedBytesRead, stream.Read(output, 0, count)); + Assert.True(expectedOutput.SequenceEqual(output)); + } + Assert.Equal(stream.Length, stream.Position); + Assert.Equal(0, stream.Read(new byte[count], 0, count)); + Assert.Equal(stream.Length, stream.Position); + } + + [Theory] + [MemberData(nameof(TestStreams))] + public void Read_CountEqualSegmentSize_CanReadAllBytes(TestStreamInitInfo info) + { + var stream = new SegmentReadStream(info.Segments, info.Length); + var count = info.SegmentSize; + + for (var i = 0; i < stream.Length; i += count) + { + var output = new byte[count]; + var expectedOutput = new byte[count]; + var expectedBytesRead = Math.Min(count, stream.Length - i); + for (var j = 0; j < expectedBytesRead; j++) + { + expectedOutput[j] = (byte)(i + j); + } + Assert.Equal(i, stream.Position); + Assert.Equal(expectedBytesRead, stream.Read(output, 0, count)); + Assert.True(expectedOutput.SequenceEqual(output)); + } + Assert.Equal(stream.Length, stream.Position); + Assert.Equal(0, stream.Read(new byte[count], 0, count)); + Assert.Equal(stream.Length, stream.Position); + } + + [Theory] + [MemberData(nameof(TestStreams))] + public void Read_CountGreaterThanSegmentSize_CanReadAllBytes(TestStreamInitInfo info) + { + var stream = new SegmentReadStream(info.Segments, info.Length); + var count = info.SegmentSize + 1; + + for (var i = 0; i < stream.Length; i += count) + { + var output = new byte[count]; + var expectedOutput = new byte[count]; + var expectedBytesRead = Math.Min(count, stream.Length - i); + for (var j = 0; j < expectedBytesRead; j++) + { + expectedOutput[j] = (byte)(i + j); + } + Assert.Equal(i, stream.Position); + Assert.Equal(expectedBytesRead, stream.Read(output, 0, count)); + Assert.True(expectedOutput.SequenceEqual(output)); + } + Assert.Equal(stream.Length, stream.Position); + Assert.Equal(0, stream.Read(new byte[count], 0, count)); + Assert.Equal(stream.Length, stream.Position); + } + + [Theory] + [MemberData(nameof(TestStreams))] + public void CopyToAsync_CopiesAllBytes(TestStreamInitInfo info) + { + var stream = new SegmentReadStream(info.Segments, info.Length); + var writeStream = new SegmentWriteStream(info.SegmentSize); + + stream.CopyTo(writeStream); + + Assert.Equal(stream.Length, stream.Position); + Assert.Equal(stream.Length, writeStream.Length); + var writeSegments = writeStream.GetSegments(); + for (var i = 0; i < info.Segments.Count; i++) + { + Assert.True(writeSegments[i].SequenceEqual(info.Segments[i])); + } + } + + [Theory] + [MemberData(nameof(TestStreams))] + public void CopyToAsync_CopiesFromCurrentPosition(TestStreamInitInfo info) + { + var skippedBytes = info.SegmentSize; + var writeStream = new SegmentWriteStream((int)info.Length); + var stream = new SegmentReadStream(info.Segments, info.Length); + stream.Read(new byte[skippedBytes], 0, skippedBytes); + + stream.CopyTo(writeStream); + + Assert.Equal(stream.Length, stream.Position); + Assert.Equal(stream.Length - skippedBytes, writeStream.Length); + var writeSegments = writeStream.GetSegments(); + + for (var i = skippedBytes; i < info.Length; i++) + { + Assert.Equal(info.Segments[i / info.SegmentSize][i % info.SegmentSize], writeSegments[0][i - skippedBytes]); + } + } + + [Theory] + [MemberData(nameof(TestStreams))] + public void CopyToAsync_CopiesFromStart_AfterReset(TestStreamInitInfo info) + { + var skippedBytes = info.SegmentSize; + var writeStream = new SegmentWriteStream(info.SegmentSize); + var stream = new SegmentReadStream(info.Segments, info.Length); + stream.Read(new byte[skippedBytes], 0, skippedBytes); + + stream.CopyTo(writeStream); + + // Assert bytes read from current location to the end + Assert.Equal(stream.Length, stream.Position); + Assert.Equal(stream.Length - skippedBytes, writeStream.Length); + + // Reset + stream.Position = 0; + writeStream = new SegmentWriteStream(info.SegmentSize); + + stream.CopyTo(writeStream); + + Assert.Equal(stream.Length, stream.Position); + Assert.Equal(stream.Length, writeStream.Length); + var writeSegments = writeStream.GetSegments(); + for (var i = 0; i < info.Segments.Count; i++) + { + Assert.True(writeSegments[i].SequenceEqual(info.Segments[i])); + } + } + } +} diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/SegmentWriteStreamTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/SegmentWriteStreamTests.cs new file mode 100644 index 0000000000..203b685b8d --- /dev/null +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/SegmentWriteStreamTests.cs @@ -0,0 +1,113 @@ +// 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 Microsoft.AspNetCore.ResponseCaching.Internal; +using Xunit; + +namespace Microsoft.AspNetCore.ResponseCaching.Tests +{ + public class SegmentWriteStreamTests + { + private static byte[] WriteData = new byte[] + { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14 + }; + + [Theory] + [InlineData(0)] + [InlineData(-1)] + public void SegmentWriteStream_InvalidSegmentSize_Throws(int segmentSize) + { + Assert.Throws(() => new SegmentWriteStream(segmentSize)); + } + + [Fact] + public void ReadAndSeekOperations_Throws() + { + var stream = new SegmentWriteStream(1); + + Assert.Throws(() => stream.Read(new byte[1], 0, 0)); + Assert.Throws(() => stream.Position = 0); + Assert.Throws(() => stream.Seek(0, SeekOrigin.Begin)); + } + + [Fact] + public void GetSegments_ExtractionDisablesWriting() + { + var stream = new SegmentWriteStream(1); + + Assert.True(stream.CanWrite); + Assert.Equal(0, stream.GetSegments().Count); + Assert.False(stream.CanWrite); + } + + [Theory] + [InlineData(4)] + [InlineData(5)] + [InlineData(6)] + public void WriteByte_CanWriteAllBytes(int segmentSize) + { + var stream = new SegmentWriteStream(segmentSize); + + foreach (var datum in WriteData) + { + stream.WriteByte(datum); + } + var segments = stream.GetSegments(); + + Assert.Equal(WriteData.Length, stream.Length); + Assert.Equal((WriteData.Length + segmentSize - 1)/ segmentSize, segments.Count); + + for (var i = 0; i < WriteData.Length; i += segmentSize) + { + var expectedSegmentSize = Math.Min(segmentSize, WriteData.Length - i); + var expectedSegment = new byte[expectedSegmentSize]; + for (int j = 0; j < expectedSegmentSize; j++) + { + expectedSegment[j] = (byte)(i + j); + } + var segment = segments[i / segmentSize]; + + Assert.Equal(expectedSegmentSize, segment.Length); + Assert.True(expectedSegment.SequenceEqual(segment)); + } + } + + [Theory] + [InlineData(4)] + [InlineData(5)] + [InlineData(6)] + public void Write_CanWriteAllBytes(int writeSize) + { + var segmentSize = 5; + var stream = new SegmentWriteStream(segmentSize); + + + for (var i = 0; i < WriteData.Length; i += writeSize) + { + stream.Write(WriteData, i, Math.Min(writeSize, WriteData.Length - i)); + } + var segments = stream.GetSegments(); + + Assert.Equal(WriteData.Length, stream.Length); + Assert.Equal((WriteData.Length + segmentSize - 1) / segmentSize, segments.Count); + + for (var i = 0; i < WriteData.Length; i += segmentSize) + { + var expectedSegmentSize = Math.Min(segmentSize, WriteData.Length - i); + var expectedSegment = new byte[expectedSegmentSize]; + for (int j = 0; j < expectedSegmentSize; j++) + { + expectedSegment[j] = (byte)(i + j); + } + var segment = segments[i / segmentSize]; + + Assert.Equal(expectedSegmentSize, segment.Length); + Assert.True(expectedSegment.SequenceEqual(segment)); + } + } + } +} diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs index d938065892..f6f0ed2db1 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.ResponseCaching.Internal; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Internal; using Microsoft.Extensions.ObjectPool; @@ -20,6 +21,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { internal class TestUtils { + static TestUtils() + { + // Force sharding in tests + StreamUtilities.BodySegmentSize = 10; + } + internal static RequestDelegate TestRequestDelegate = async (context) => { var uniqueId = Guid.NewGuid().ToString(); @@ -44,7 +51,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests return new ResponseCacheKeyProvider(new DefaultObjectPoolProvider(), Options.Create(options)); } - internal static IWebHostBuilder CreateBuilderWithResponseCache( + internal static IEnumerable CreateBuildersWithResponseCache( Action configureDelegate = null, ResponseCacheOptions options = null, RequestDelegate requestDelegate = null) @@ -62,10 +69,24 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests requestDelegate = TestRequestDelegate; } - return new WebHostBuilder() + // Test with MemoryResponseCacheStore + yield return new WebHostBuilder() .ConfigureServices(services => { - services.AddDistributedResponseCache(); + services.AddMemoryResponseCacheStore(); + }) + .Configure(app => + { + configureDelegate(app); + app.UseResponseCache(options); + app.Run(requestDelegate); + }); + + // Test with DistributedResponseCacheStore + yield return new WebHostBuilder() + .ConfigureServices(services => + { + services.AddDistributedResponseCacheStore(); }) .Configure(app => { @@ -167,11 +188,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests internal class TestResponseCacheStore : IResponseCacheStore { - private readonly IDictionary _storage = new Dictionary(); + private readonly IDictionary _storage = new Dictionary(); public int GetCount { get; private set; } public int SetCount { get; private set; } - public Task GetAsync(string key) + public Task GetAsync(string key) { GetCount++; try @@ -180,16 +201,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } catch { - return Task.FromResult(null); + return Task.FromResult(null); } } - public Task RemoveAsync(string key) - { - return TaskCache.CompletedTask; - } - - public Task SetAsync(string key, object entry, TimeSpan validFor) + public Task SetAsync(string key, IResponseCacheEntry entry, TimeSpan validFor) { SetCount++; _storage[key] = entry; From 10381b04561ff5970565008e71c0098d8e3b9c80 Mon Sep 17 00:00:00 2001 From: John Luo Date: Wed, 28 Sep 2016 18:22:57 -0700 Subject: [PATCH 040/188] Use HttpMethods --- .../ResponseCachePolicyProvider.cs | 3 +- .../ResponseCacheKeyProviderTests.cs | 8 +-- .../ResponseCachePolicyProviderTests.cs | 51 +++++++++++++------ 3 files changed, 41 insertions(+), 21 deletions(-) diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachePolicyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachePolicyProvider.cs index 351faa0df9..341c2e0d00 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachePolicyProvider.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachePolicyProvider.cs @@ -16,8 +16,7 @@ namespace Microsoft.AspNetCore.ResponseCaching { // Verify the method var request = context.HttpContext.Request; - if (!string.Equals("GET", request.Method, StringComparison.OrdinalIgnoreCase) && - !string.Equals("HEAD", request.Method, StringComparison.OrdinalIgnoreCase)) + if (!HttpMethods.IsGet(request.Method) && !HttpMethods.IsHead(request.Method)) { return false; } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheKeyProviderTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheKeyProviderTests.cs index 99d42298b2..87126b90e8 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheKeyProviderTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheKeyProviderTests.cs @@ -36,10 +36,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests UseCaseSensitivePaths = false }); var context = TestUtils.CreateTestContext(); - context.HttpContext.Request.Method = "GET"; + context.HttpContext.Request.Method = HttpMethods.Get; context.HttpContext.Request.Path = "/Path"; - Assert.Equal($"GET{KeyDelimiter}/PATH", cacheKeyProvider.CreateBaseKey(context)); + Assert.Equal($"{HttpMethods.Get}{KeyDelimiter}/PATH", cacheKeyProvider.CreateBaseKey(context)); } [Fact] @@ -50,10 +50,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests UseCaseSensitivePaths = true }); var context = TestUtils.CreateTestContext(); - context.HttpContext.Request.Method = "GET"; + context.HttpContext.Request.Method = HttpMethods.Get; context.HttpContext.Request.Path = "/Path"; - Assert.Equal($"GET{KeyDelimiter}/Path", cacheKeyProvider.CreateBaseKey(context)); + Assert.Equal($"{HttpMethods.Get}{KeyDelimiter}/Path", cacheKeyProvider.CreateBaseKey(context)); } [Fact] diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachePolicyProviderTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachePolicyProviderTests.cs index 071ab2fbb3..65bbf5434b 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachePolicyProviderTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachePolicyProviderTests.cs @@ -11,9 +11,20 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { public class ResponseCachePolicyProviderTests { + public static TheoryData CacheableMethods + { + get + { + return new TheoryData + { + HttpMethods.Get, + HttpMethods.Head + }; + } + } + [Theory] - [InlineData("GET")] - [InlineData("HEAD")] + [MemberData(nameof(CacheableMethods))] public void IsRequestCacheable_CacheableMethods_Allowed(string method) { var context = TestUtils.CreateTestContext(); @@ -21,16 +32,26 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Assert.True(new ResponseCachePolicyProvider().IsRequestCacheable(context)); } + public static TheoryData NonCacheableMethods + { + get + { + return new TheoryData + { + HttpMethods.Post, + HttpMethods.Put, + HttpMethods.Delete, + HttpMethods.Trace, + HttpMethods.Connect, + HttpMethods.Options, + "", + null + }; + } + } [Theory] - [InlineData("POST")] - [InlineData("OPTIONS")] - [InlineData("PUT")] - [InlineData("DELETE")] - [InlineData("TRACE")] - [InlineData("CONNECT")] - [InlineData("")] - [InlineData(null)] + [MemberData(nameof(NonCacheableMethods))] public void IsRequestCacheable_UncacheableMethods_NotAllowed(string method) { var context = TestUtils.CreateTestContext(); @@ -43,7 +64,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests public void IsRequestCacheable_AuthorizationHeaders_NotAllowed() { var context = TestUtils.CreateTestContext(); - context.HttpContext.Request.Method = "GET"; + context.HttpContext.Request.Method = HttpMethods.Get; context.HttpContext.Request.Headers[HeaderNames.Authorization] = "Basic plaintextUN:plaintextPW"; Assert.False(new ResponseCachePolicyProvider().IsRequestCacheable(context)); @@ -53,7 +74,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests public void IsRequestCacheable_NoCache_NotAllowed() { var context = TestUtils.CreateTestContext(); - context.HttpContext.Request.Method = "GET"; + context.HttpContext.Request.Method = HttpMethods.Get; context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() { NoCache = true @@ -66,7 +87,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests public void IsRequestCacheable_NoStore_Allowed() { var context = TestUtils.CreateTestContext(); - context.HttpContext.Request.Method = "GET"; + context.HttpContext.Request.Method = HttpMethods.Get; context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() { NoStore = true @@ -79,7 +100,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests public void IsRequestCacheable_LegacyDirectives_NotAllowed() { var context = TestUtils.CreateTestContext(); - context.HttpContext.Request.Method = "GET"; + context.HttpContext.Request.Method = HttpMethods.Get; context.HttpContext.Request.Headers[HeaderNames.Pragma] = "no-cache"; Assert.False(new ResponseCachePolicyProvider().IsRequestCacheable(context)); @@ -89,7 +110,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests public void IsRequestCacheable_LegacyDirectives_OverridenByCacheControl() { var context = TestUtils.CreateTestContext(); - context.HttpContext.Request.Method = "GET"; + context.HttpContext.Request.Method = HttpMethods.Get; context.HttpContext.Request.Headers[HeaderNames.Pragma] = "no-cache"; context.HttpContext.Request.Headers[HeaderNames.CacheControl] = "max-age=10"; From 24385e74c4a0a74f7b1693e1649c62d6348df330 Mon Sep 17 00:00:00 2001 From: John Luo Date: Thu, 29 Sep 2016 10:43:56 -0700 Subject: [PATCH 041/188] Internalize iresponsecachekeyprovider (#59) Make IResponseCacheKeyProvider internal --- .../ResponseCacheServiceCollectionExtensions.cs | 1 - .../IResponseCacheKeyProvider.cs | 4 ++-- .../{ => Internal}/ResponseCacheKeyProvider.cs | 12 ++++++------ .../ResponseCacheMiddleware.cs | 12 ++++++++++++ .../ResponseCacheMiddlewareTests.cs | 4 ++-- 5 files changed, 22 insertions(+), 11 deletions(-) rename src/Microsoft.AspNetCore.ResponseCaching/{Interfaces => Internal}/IResponseCacheKeyProvider.cs (92%) rename src/Microsoft.AspNetCore.ResponseCaching/{ => Internal}/ResponseCacheKeyProvider.cs (91%) diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCacheServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCacheServiceCollectionExtensions.cs index e57dc5856f..d85e45d3fc 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCacheServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCacheServiceCollectionExtensions.cs @@ -40,7 +40,6 @@ namespace Microsoft.Extensions.DependencyInjection private static IServiceCollection AddResponseCacheServices(this IServiceCollection services) { - services.TryAdd(ServiceDescriptor.Singleton()); services.TryAdd(ServiceDescriptor.Singleton()); return services; diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCacheKeyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/IResponseCacheKeyProvider.cs similarity index 92% rename from src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCacheKeyProvider.cs rename to src/Microsoft.AspNetCore.ResponseCaching/Internal/IResponseCacheKeyProvider.cs index 89ae2ffa04..199f1a837b 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCacheKeyProvider.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/IResponseCacheKeyProvider.cs @@ -3,9 +3,9 @@ using System.Collections.Generic; -namespace Microsoft.AspNetCore.ResponseCaching +namespace Microsoft.AspNetCore.ResponseCaching.Internal { - public interface IResponseCacheKeyProvider + internal interface IResponseCacheKeyProvider { /// /// Create a base key for a response cache entry. diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheKeyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCacheKeyProvider.cs similarity index 91% rename from src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheKeyProvider.cs rename to src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCacheKeyProvider.cs index a0db9a0a63..b7df0decc0 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheKeyProvider.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCacheKeyProvider.cs @@ -10,9 +10,9 @@ using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; -namespace Microsoft.AspNetCore.ResponseCaching +namespace Microsoft.AspNetCore.ResponseCaching.Internal { - public class ResponseCacheKeyProvider : IResponseCacheKeyProvider + internal class ResponseCacheKeyProvider : IResponseCacheKeyProvider { // Use the record separator for delimiting components of the cache key to avoid possible collisions private static readonly char KeyDelimiter = '\x1e'; @@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.ResponseCaching private readonly ObjectPool _builderPool; private readonly ResponseCacheOptions _options; - public ResponseCacheKeyProvider(ObjectPoolProvider poolProvider, IOptions options) + internal ResponseCacheKeyProvider(ObjectPoolProvider poolProvider, IOptions options) { if (poolProvider == null) { @@ -35,13 +35,13 @@ namespace Microsoft.AspNetCore.ResponseCaching _options = options.Value; } - public virtual IEnumerable CreateLookupVaryByKeys(ResponseCacheContext context) + public IEnumerable CreateLookupVaryByKeys(ResponseCacheContext context) { return new string[] { CreateStorageVaryByKey(context) }; } // GET/PATH - public virtual string CreateBaseKey(ResponseCacheContext context) + public string CreateBaseKey(ResponseCacheContext context) { if (context == null) { @@ -67,7 +67,7 @@ namespace Microsoft.AspNetCore.ResponseCaching } // BaseKeyHHeaderName=HeaderValueQQueryName=QueryValue - public virtual string CreateStorageVaryByKey(ResponseCacheContext context) + public string CreateStorageVaryByKey(ResponseCacheContext context) { if (context == null) { diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs index 72f6902751..c49ac77e0b 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs @@ -10,6 +10,7 @@ using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Headers; using Microsoft.AspNetCore.ResponseCaching.Internal; using Microsoft.Extensions.Internal; +using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; @@ -28,6 +29,17 @@ namespace Microsoft.AspNetCore.ResponseCaching private readonly Func _onStartingCallback; public ResponseCacheMiddleware( + RequestDelegate next, + IResponseCacheStore store, + IOptions options, + IResponseCachePolicyProvider policyProvider, + ObjectPoolProvider poolProvider) + :this (next, store, options, policyProvider, new ResponseCacheKeyProvider(poolProvider, options)) + { + } + + // Internal for testing + internal ResponseCacheMiddleware( RequestDelegate next, IResponseCacheStore store, IOptions options, diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheMiddlewareTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheMiddlewareTests.cs index 792c6630e5..138ed88af3 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheMiddlewareTests.cs @@ -64,7 +64,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests public async Task TryServeFromCacheAsync_VaryByRuleFound_CachedResponseNotFound_Fails() { var store = new TestResponseCacheStore(); - var middleware = TestUtils.CreateTestMiddleware(store: store, keyProvider: new TestResponseCacheKeyProvider("BaseKey")); + var middleware = TestUtils.CreateTestMiddleware(store: store, keyProvider: new TestResponseCacheKeyProvider("BaseKey", "VaryKey")); var context = TestUtils.CreateTestContext(); await store.SetAsync( @@ -73,7 +73,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests TimeSpan.Zero); Assert.False(await middleware.TryServeFromCacheAsync(context)); - Assert.Equal(1, store.GetCount); + Assert.Equal(2, store.GetCount); } [Fact] From aa52e66585acf4081793c9e5bfafb8385e92aac2 Mon Sep 17 00:00:00 2001 From: John Luo Date: Fri, 30 Sep 2016 11:24:44 -0700 Subject: [PATCH 042/188] Make DistributedResponseCacheStore internal (#61) Remove DistributedResponseCacheStore and move all interfaces to the Internal namespace --- samples/ResponseCachingSample/Startup.cs | 3 +- ...esponseCacheServiceCollectionExtensions.cs | 24 +- .../CacheEntry/CachedResponse.cs | 2 +- .../CacheEntry/CachedVaryByRules.cs | 2 +- .../Internal/DistributedResponseCacheStore.cs | 51 ---- .../Internal/FastGuid.cs | 1 - .../Interfaces/IResponseCacheEntry.cs | 2 +- .../IResponseCacheKeyProvider.cs | 2 +- .../IResponseCachePolicyProvider.cs | 2 +- .../Interfaces/IResponseCacheStore.cs | 2 +- .../{ => Internal}/ResponseCacheContext.cs | 3 +- .../Internal/ResponseCacheEntrySerializer.cs | 249 ------------------ .../Internal/ResponseCacheKeyProvider.cs | 4 +- .../ResponseCachePolicyProvider.cs | 2 +- .../ResponseCacheMiddleware.cs | 11 - .../CacheEntrySerializerTests.cs | 189 ------------- .../ResponseCachePolicyProviderTests.cs | 1 + .../TestUtils.cs | 15 +- 18 files changed, 15 insertions(+), 550 deletions(-) rename src/Microsoft.AspNetCore.ResponseCaching/{ => Internal}/CacheEntry/CachedResponse.cs (90%) rename src/Microsoft.AspNetCore.ResponseCaching/{ => Internal}/CacheEntry/CachedVaryByRules.cs (88%) delete mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Internal/DistributedResponseCacheStore.cs rename src/Microsoft.AspNetCore.ResponseCaching/{ => Internal}/Interfaces/IResponseCacheEntry.cs (80%) rename src/Microsoft.AspNetCore.ResponseCaching/Internal/{ => Interfaces}/IResponseCacheKeyProvider.cs (96%) rename src/Microsoft.AspNetCore.ResponseCaching/{ => Internal}/Interfaces/IResponseCachePolicyProvider.cs (96%) rename src/Microsoft.AspNetCore.ResponseCaching/{ => Internal}/Interfaces/IResponseCacheStore.cs (87%) rename src/Microsoft.AspNetCore.ResponseCaching/{ => Internal}/ResponseCacheContext.cs (97%) delete mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCacheEntrySerializer.cs rename src/Microsoft.AspNetCore.ResponseCaching/{ => Internal}/ResponseCachePolicyProvider.cs (99%) delete mode 100644 test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheEntrySerializerTests.cs diff --git a/samples/ResponseCachingSample/Startup.cs b/samples/ResponseCachingSample/Startup.cs index 66708440a9..376443ef0b 100644 --- a/samples/ResponseCachingSample/Startup.cs +++ b/samples/ResponseCachingSample/Startup.cs @@ -15,7 +15,7 @@ namespace ResponseCachingSample { public void ConfigureServices(IServiceCollection services) { - services.AddDistributedResponseCacheStore(); + services.AddMemoryResponseCacheStore(); } public void Configure(IApplicationBuilder app) @@ -23,7 +23,6 @@ namespace ResponseCachingSample app.UseResponseCache(); app.Run(async (context) => { - // These settings should be configured by context.Response.Cache.* context.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() { Public = true, diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCacheServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCacheServiceCollectionExtensions.cs index d85e45d3fc..b8020b56f9 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCacheServiceCollectionExtensions.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCacheServiceCollectionExtensions.cs @@ -18,29 +18,9 @@ namespace Microsoft.Extensions.DependencyInjection } services.AddMemoryCache(); - services.AddResponseCacheServices(); - services.TryAdd(ServiceDescriptor.Singleton()); - - return services; - } - - public static IServiceCollection AddDistributedResponseCacheStore(this IServiceCollection services) - { - if (services == null) - { - throw new ArgumentNullException(nameof(services)); - } - - services.AddDistributedMemoryCache(); - services.AddResponseCacheServices(); - services.TryAdd(ServiceDescriptor.Singleton()); - - return services; - } - - private static IServiceCollection AddResponseCacheServices(this IServiceCollection services) - { services.TryAdd(ServiceDescriptor.Singleton()); + services.TryAdd(ServiceDescriptor.Singleton()); + services.TryAdd(ServiceDescriptor.Singleton()); return services; } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedResponse.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CachedResponse.cs similarity index 90% rename from src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedResponse.cs rename to src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CachedResponse.cs index 1accf9a759..51083e40d2 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedResponse.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CachedResponse.cs @@ -5,7 +5,7 @@ using System; using System.IO; using Microsoft.AspNetCore.Http; -namespace Microsoft.AspNetCore.ResponseCaching +namespace Microsoft.AspNetCore.ResponseCaching.Internal { public class CachedResponse : IResponseCacheEntry { diff --git a/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedVaryByRules.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CachedVaryByRules.cs similarity index 88% rename from src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedVaryByRules.cs rename to src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CachedVaryByRules.cs index e0e82bd60a..d183724628 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/CacheEntry/CachedVaryByRules.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CachedVaryByRules.cs @@ -3,7 +3,7 @@ using Microsoft.Extensions.Primitives; -namespace Microsoft.AspNetCore.ResponseCaching +namespace Microsoft.AspNetCore.ResponseCaching.Internal { public class CachedVaryByRules : IResponseCacheEntry { diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/DistributedResponseCacheStore.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/DistributedResponseCacheStore.cs deleted file mode 100644 index ab708fc81b..0000000000 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/DistributedResponseCacheStore.cs +++ /dev/null @@ -1,51 +0,0 @@ -// 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.Extensions.Caching.Distributed; - -namespace Microsoft.AspNetCore.ResponseCaching.Internal -{ - public class DistributedResponseCacheStore : IResponseCacheStore - { - private readonly IDistributedCache _cache; - - public DistributedResponseCacheStore(IDistributedCache cache) - { - if (cache == null) - { - throw new ArgumentNullException(nameof(cache)); - } - - _cache = cache; - } - - public async Task GetAsync(string key) - { - try - { - return ResponseCacheEntrySerializer.Deserialize(await _cache.GetAsync(key)); - } - catch - { - return null; - } - } - - public async Task SetAsync(string key, IResponseCacheEntry entry, TimeSpan validFor) - { - try - { - await _cache.SetAsync( - key, - ResponseCacheEntrySerializer.Serialize(entry), - new DistributedCacheEntryOptions() - { - AbsoluteExpirationRelativeToNow = validFor - }); - } - catch { } - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/FastGuid.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/FastGuid.cs index 075403f35c..76cac184ae 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/FastGuid.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/FastGuid.cs @@ -54,7 +54,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal private static unsafe string GenerateGuidString(FastGuid guid) { - // stackalloc to allocate array on stack rather than heap char* charBuffer = stackalloc char[13]; diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCacheEntry.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCacheEntry.cs similarity index 80% rename from src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCacheEntry.cs rename to src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCacheEntry.cs index d09fd4da48..a8227fe243 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCacheEntry.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCacheEntry.cs @@ -1,7 +1,7 @@ // 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 +namespace Microsoft.AspNetCore.ResponseCaching.Internal { public interface IResponseCacheEntry { diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/IResponseCacheKeyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCacheKeyProvider.cs similarity index 96% rename from src/Microsoft.AspNetCore.ResponseCaching/Internal/IResponseCacheKeyProvider.cs rename to src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCacheKeyProvider.cs index 199f1a837b..eb9d626824 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/IResponseCacheKeyProvider.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCacheKeyProvider.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; namespace Microsoft.AspNetCore.ResponseCaching.Internal { - internal interface IResponseCacheKeyProvider + public interface IResponseCacheKeyProvider { /// /// Create a base key for a response cache entry. diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCachePolicyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCachePolicyProvider.cs similarity index 96% rename from src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCachePolicyProvider.cs rename to src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCachePolicyProvider.cs index 91ab9c5e14..0560212018 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCachePolicyProvider.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCachePolicyProvider.cs @@ -1,7 +1,7 @@ // 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 +namespace Microsoft.AspNetCore.ResponseCaching.Internal { public interface IResponseCachePolicyProvider { diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCacheStore.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCacheStore.cs similarity index 87% rename from src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCacheStore.cs rename to src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCacheStore.cs index 9a12b9b6df..2deaa41708 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Interfaces/IResponseCacheStore.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCacheStore.cs @@ -4,7 +4,7 @@ using System; using System.Threading.Tasks; -namespace Microsoft.AspNetCore.ResponseCaching +namespace Microsoft.AspNetCore.ResponseCaching.Internal { public interface IResponseCacheStore { diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheContext.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCacheContext.cs similarity index 97% rename from src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheContext.cs rename to src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCacheContext.cs index ac86b028ad..c11ff159f3 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheContext.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCacheContext.cs @@ -6,10 +6,9 @@ using System.IO; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Headers; -using Microsoft.AspNetCore.ResponseCaching.Internal; using Microsoft.Net.Http.Headers; -namespace Microsoft.AspNetCore.ResponseCaching +namespace Microsoft.AspNetCore.ResponseCaching.Internal { public class ResponseCacheContext { diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCacheEntrySerializer.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCacheEntrySerializer.cs deleted file mode 100644 index 46c679d241..0000000000 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCacheEntrySerializer.cs +++ /dev/null @@ -1,249 +0,0 @@ -// 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 ResponseCacheEntrySerializer - { - private const int FormatVersion = 1; - - internal static IResponseCacheEntry Deserialize(byte[] serializedEntry) - { - if (serializedEntry == null) - { - return null; - } - - using (var memory = new MemoryStream(serializedEntry)) - { - using (var reader = new BinaryReader(memory)) - { - return Read(reader); - } - } - } - - internal static byte[] Serialize(IResponseCacheEntry 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 (char: 'R' for CachedResponse, 'V' for CachedVaryByRules) - // Type-dependent data (see CachedResponse and CachedVaryByRules) - private static IResponseCacheEntry Read(BinaryReader reader) - { - if (reader == null) - { - throw new ArgumentNullException(nameof(reader)); - } - - if (reader.ReadInt32() != FormatVersion) - { - return null; - } - - var type = reader.ReadChar(); - - if (type == 'R') - { - return ReadCachedResponse(reader); - } - else if (type == 'V') - { - return ReadCachedVaryByRules(reader); - } - - // Unable to read as CachedResponse or CachedVaryByRules - return null; - } - - // Serialization Format - // Creation time - UtcTicks (long) - // Status code (int) - // Header count (int) - // Header(s) - // Key (string) - // ValueCount (int) - // Value(s) - // Value (string) - // BodyLength (int) - // Body (byte[]) - private static CachedResponse ReadCachedResponse(BinaryReader reader) - { - var created = new DateTimeOffset(reader.ReadInt64(), TimeSpan.Zero); - var statusCode = reader.ReadInt32(); - var headerCount = reader.ReadInt32(); - var headers = new HeaderDictionary(); - for (var index = 0; index < headerCount; index++) - { - var key = reader.ReadString(); - var headerValueCount = reader.ReadInt32(); - if (headerValueCount > 1) - { - var headerValues = new string[headerValueCount]; - for (var valueIndex = 0; valueIndex < headerValueCount; valueIndex++) - { - headerValues[valueIndex] = reader.ReadString(); - } - headers[key] = headerValues; - } - else if (headerValueCount == 1) - { - headers[key] = reader.ReadString(); - } - } - - var bodyLength = reader.ReadInt32(); - var bodyBytes = reader.ReadBytes(bodyLength); - - return new CachedResponse - { - Created = created, - StatusCode = statusCode, - Headers = headers, - Body = new MemoryStream(bodyBytes, writable: false) - }; - } - - // Serialization Format - // VaryKeyPrefix (string) - // Headers count - // Header(s) (comma separated string) - // QueryKey count - // QueryKey(s) (comma separated string) - private static CachedVaryByRules ReadCachedVaryByRules(BinaryReader reader) - { - var varyKeyPrefix = reader.ReadString(); - - var headerCount = reader.ReadInt32(); - var headers = new string[headerCount]; - for (var index = 0; index < headerCount; index++) - { - headers[index] = reader.ReadString(); - } - var queryKeysCount = reader.ReadInt32(); - var queryKeys = new string[queryKeysCount]; - for (var index = 0; index < queryKeysCount; index++) - { - queryKeys[index] = reader.ReadString(); - } - - return new CachedVaryByRules { VaryByKeyPrefix = varyKeyPrefix, Headers = headers, QueryKeys = queryKeys }; - } - - // See serialization format above - private static void Write(BinaryWriter writer, IResponseCacheEntry entry) - { - if (writer == null) - { - throw new ArgumentNullException(nameof(writer)); - } - - if (entry == null) - { - throw new ArgumentNullException(nameof(entry)); - } - - writer.Write(FormatVersion); - - if (entry is CachedResponse) - { - writer.Write('R'); - WriteCachedResponse(writer, (CachedResponse)entry); - } - else if (entry is CachedVaryByRules) - { - writer.Write('V'); - WriteCachedVaryByRules(writer, (CachedVaryByRules)entry); - } - else - { - throw new NotSupportedException($"Unrecognized entry type for {nameof(entry)}."); - } - } - - // See serialization format above - private static void WriteCachedResponse(BinaryWriter writer, CachedResponse entry) - { - writer.Write(entry.Created.UtcTicks); - writer.Write(entry.StatusCode); - writer.Write(entry.Headers.Count); - foreach (var header in entry.Headers) - { - writer.Write(header.Key); - writer.Write(header.Value.Count); - foreach (var headerValue in header.Value) - { - writer.Write(headerValue); - } - } - - if (entry.Body.CanSeek) - { - if (entry.Body.Length > int.MaxValue) - { - throw new NotSupportedException($"{nameof(entry.Body)} is too large to serialized."); - } - - var bodyLength = (int)entry.Body.Length; - var bodyBytes = new byte[bodyLength]; - var bytesRead = entry.Body.Read(bodyBytes, 0, bodyLength); - - if (bytesRead != bodyLength) - { - throw new InvalidOperationException($"Failed to fully read {nameof(entry.Body)}."); - } - - writer.Write(bodyLength); - writer.Write(bodyBytes); - } - else - { - var stream = new MemoryStream(); - entry.Body.CopyTo(stream); - - if (stream.Length > int.MaxValue) - { - throw new NotSupportedException($"{nameof(entry.Body)} is too large to serialized."); - } - - var bodyLength = (int)stream.Length; - writer.Write(bodyLength); - writer.Write(stream.ToArray()); - - } - } - - // See serialization format above - private static void WriteCachedVaryByRules(BinaryWriter writer, CachedVaryByRules varyByRules) - { - writer.Write(varyByRules.VaryByKeyPrefix); - - writer.Write(varyByRules.Headers.Count); - foreach (var header in varyByRules.Headers) - { - writer.Write(header); - } - - writer.Write(varyByRules.QueryKeys.Count); - foreach (var queryKey in varyByRules.QueryKeys) - { - writer.Write(queryKey); - } - } - } -} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCacheKeyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCacheKeyProvider.cs index b7df0decc0..ed586946d1 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCacheKeyProvider.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCacheKeyProvider.cs @@ -12,7 +12,7 @@ using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.ResponseCaching.Internal { - internal class ResponseCacheKeyProvider : IResponseCacheKeyProvider + public class ResponseCacheKeyProvider : IResponseCacheKeyProvider { // Use the record separator for delimiting components of the cache key to avoid possible collisions private static readonly char KeyDelimiter = '\x1e'; @@ -20,7 +20,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal private readonly ObjectPool _builderPool; private readonly ResponseCacheOptions _options; - internal ResponseCacheKeyProvider(ObjectPoolProvider poolProvider, IOptions options) + public ResponseCacheKeyProvider(ObjectPoolProvider poolProvider, IOptions options) { if (poolProvider == null) { diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachePolicyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachePolicyProvider.cs similarity index 99% rename from src/Microsoft.AspNetCore.ResponseCaching/ResponseCachePolicyProvider.cs rename to src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachePolicyProvider.cs index 341c2e0d00..ab331ff311 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachePolicyProvider.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachePolicyProvider.cs @@ -6,7 +6,7 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; -namespace Microsoft.AspNetCore.ResponseCaching +namespace Microsoft.AspNetCore.ResponseCaching.Internal { public class ResponseCachePolicyProvider : IResponseCachePolicyProvider { diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs index c49ac77e0b..da7251d108 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs @@ -30,20 +30,9 @@ namespace Microsoft.AspNetCore.ResponseCaching public ResponseCacheMiddleware( RequestDelegate next, - IResponseCacheStore store, IOptions options, IResponseCachePolicyProvider policyProvider, - ObjectPoolProvider poolProvider) - :this (next, store, options, policyProvider, new ResponseCacheKeyProvider(poolProvider, options)) - { - } - - // Internal for testing - internal ResponseCacheMiddleware( - RequestDelegate next, IResponseCacheStore store, - IOptions options, - IResponseCachePolicyProvider policyProvider, IResponseCacheKeyProvider keyProvider) { if (next == null) diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheEntrySerializerTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheEntrySerializerTests.cs deleted file mode 100644 index 7664e437cb..0000000000 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/CacheEntrySerializerTests.cs +++ /dev/null @@ -1,189 +0,0 @@ -// 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.Linq; -using System.Text; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.ResponseCaching.Internal; -using Microsoft.Extensions.Primitives; -using Xunit; - -namespace Microsoft.AspNetCore.ResponseCaching.Tests -{ - public class CacheEntrySerializerTests - { - [Fact] - public void Serialize_NullObject_Throws() - { - Assert.Throws(() => ResponseCacheEntrySerializer.Serialize(null)); - } - - private class UnknownResponseCacheEntry : IResponseCacheEntry - { - } - - [Fact] - public void Serialize_UnknownObject_Throws() - { - Assert.Throws(() => ResponseCacheEntrySerializer.Serialize(new UnknownResponseCacheEntry())); - } - - [Fact] - public void Deserialize_NullObject_ReturnsNull() - { - Assert.Null(ResponseCacheEntrySerializer.Deserialize(null)); - } - - [Fact] - public void RoundTrip_CachedResponseWithBody_Succeeds() - { - var headers = new HeaderDictionary(); - headers["keyA"] = "valueA"; - headers["keyB"] = "valueB"; - var body = Encoding.ASCII.GetBytes("Hello world"); - var cachedResponse = new CachedResponse() - { - Created = DateTimeOffset.UtcNow, - StatusCode = StatusCodes.Status200OK, - Body = new SegmentReadStream(new List(new[] { body }), body.Length), - Headers = headers - }; - - AssertCachedResponseEqual(cachedResponse, (CachedResponse)ResponseCacheEntrySerializer.Deserialize(ResponseCacheEntrySerializer.Serialize(cachedResponse))); - } - - [Fact] - public void RoundTrip_CachedResponseWithMultivalueHeaders_Succeeds() - { - var headers = new HeaderDictionary(); - headers["keyA"] = new StringValues(new[] { "ValueA", "ValueB" }); - var body = Encoding.ASCII.GetBytes("Hello world"); - var cachedResponse = new CachedResponse() - { - Created = DateTimeOffset.UtcNow, - StatusCode = StatusCodes.Status200OK, - Body = new SegmentReadStream(new List(new[] { body }), body.Length), - Headers = headers - }; - - AssertCachedResponseEqual(cachedResponse, (CachedResponse)ResponseCacheEntrySerializer.Deserialize(ResponseCacheEntrySerializer.Serialize(cachedResponse))); - } - - [Fact] - public void RoundTrip_CachedResponseWithEmptyHeaders_Succeeds() - { - var headers = new HeaderDictionary(); - headers["keyA"] = StringValues.Empty; - var body = Encoding.ASCII.GetBytes("Hello world"); - var cachedResponse = new CachedResponse() - { - Created = DateTimeOffset.UtcNow, - StatusCode = StatusCodes.Status200OK, - Body = new SegmentReadStream(new List(new[] { body }), body.Length), - Headers = headers - }; - - AssertCachedResponseEqual(cachedResponse, (CachedResponse)ResponseCacheEntrySerializer.Deserialize(ResponseCacheEntrySerializer.Serialize(cachedResponse))); - } - - [Fact] - public void RoundTrip_CachedVaryByRule_EmptyRules_Succeeds() - { - var cachedVaryByRule = new CachedVaryByRules() - { - VaryByKeyPrefix = FastGuid.NewGuid().IdString - }; - - AssertCachedVaryByRuleEqual(cachedVaryByRule, (CachedVaryByRules)ResponseCacheEntrySerializer.Deserialize(ResponseCacheEntrySerializer.Serialize(cachedVaryByRule))); - } - - [Fact] - public void RoundTrip_CachedVaryByRule_HeadersOnly_Succeeds() - { - var headers = new[] { "headerA", "headerB" }; - var cachedVaryByRule = new CachedVaryByRules() - { - VaryByKeyPrefix = FastGuid.NewGuid().IdString, - Headers = headers - }; - - AssertCachedVaryByRuleEqual(cachedVaryByRule, (CachedVaryByRules)ResponseCacheEntrySerializer.Deserialize(ResponseCacheEntrySerializer.Serialize(cachedVaryByRule))); - } - - [Fact] - public void RoundTrip_CachedVaryByRule_QueryKeysOnly_Succeeds() - { - var queryKeys = new[] { "queryA", "queryB" }; - var cachedVaryByRule = new CachedVaryByRules() - { - VaryByKeyPrefix = FastGuid.NewGuid().IdString, - QueryKeys = queryKeys - }; - - AssertCachedVaryByRuleEqual(cachedVaryByRule, (CachedVaryByRules)ResponseCacheEntrySerializer.Deserialize(ResponseCacheEntrySerializer.Serialize(cachedVaryByRule))); - } - - [Fact] - public void RoundTrip_CachedVaryByRule_HeadersAndQueryKeys_Succeeds() - { - var headers = new[] { "headerA", "headerB" }; - var queryKeys = new[] { "queryA", "queryB" }; - var cachedVaryByRule = new CachedVaryByRules() - { - VaryByKeyPrefix = FastGuid.NewGuid().IdString, - Headers = headers, - QueryKeys = queryKeys - }; - - AssertCachedVaryByRuleEqual(cachedVaryByRule, (CachedVaryByRules)ResponseCacheEntrySerializer.Deserialize(ResponseCacheEntrySerializer.Serialize(cachedVaryByRule))); - } - - [Fact] - public void Deserialize_InvalidEntries_ReturnsNull() - { - var headers = new[] { "headerA", "headerB" }; - var cachedVaryByRule = new CachedVaryByRules() - { - VaryByKeyPrefix = FastGuid.NewGuid().IdString, - Headers = headers - }; - var serializedEntry = ResponseCacheEntrySerializer.Serialize(cachedVaryByRule); - Array.Reverse(serializedEntry); - - Assert.Null(ResponseCacheEntrySerializer.Deserialize(serializedEntry)); - } - - private static void AssertCachedResponseEqual(CachedResponse expected, CachedResponse actual) - { - Assert.NotNull(actual); - Assert.NotNull(expected); - Assert.Equal(expected.Created, actual.Created); - Assert.Equal(expected.StatusCode, actual.StatusCode); - Assert.Equal(expected.Headers.Count, actual.Headers.Count); - foreach (var expectedHeader in expected.Headers) - { - Assert.Equal(expectedHeader.Value, actual.Headers[expectedHeader.Key]); - } - - Assert.Equal(expected.Body.Length, actual.Body.Length); - var bodyLength = (int)expected.Body.Length; - var expectedBytes = new byte[bodyLength]; - var actualBytes = new byte[bodyLength]; - expected.Body.Position = 0; // Rewind - Assert.Equal(bodyLength, expected.Body.Read(expectedBytes, 0, bodyLength)); - Assert.Equal(bodyLength, actual.Body.Read(actualBytes, 0, bodyLength)); - Assert.True(expectedBytes.SequenceEqual(actualBytes)); - } - - private static void AssertCachedVaryByRuleEqual(CachedVaryByRules expected, CachedVaryByRules actual) - { - Assert.NotNull(actual); - Assert.NotNull(expected); - Assert.Equal(expected.VaryByKeyPrefix, actual.VaryByKeyPrefix); - Assert.Equal(expected.Headers, actual.Headers); - Assert.Equal(expected.QueryKeys, actual.QueryKeys); - } - } -} diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachePolicyProviderTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachePolicyProviderTests.cs index 65bbf5434b..d9a914fda2 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachePolicyProviderTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachePolicyProviderTests.cs @@ -4,6 +4,7 @@ using System; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Headers; +using Microsoft.AspNetCore.ResponseCaching.Internal; using Microsoft.Net.Http.Headers; using Xunit; diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs index f6f0ed2db1..f21912ad77 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs @@ -81,19 +81,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests app.UseResponseCache(options); app.Run(requestDelegate); }); - - // Test with DistributedResponseCacheStore - yield return new WebHostBuilder() - .ConfigureServices(services => - { - services.AddDistributedResponseCacheStore(); - }) - .Configure(app => - { - configureDelegate(app); - app.UseResponseCache(options); - app.Run(requestDelegate); - }); } internal static ResponseCacheMiddleware CreateTestMiddleware( @@ -121,9 +108,9 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests return new ResponseCacheMiddleware( httpContext => TaskCache.CompletedTask, - store, Options.Create(options), policyProvider, + store, keyProvider); } From 4e10d191756c505d48a56bcaa499954c17f7f0f1 Mon Sep 17 00:00:00 2001 From: BrennanConroy Date: Mon, 3 Oct 2016 08:25:31 -0700 Subject: [PATCH 043/188] Update default Max Body Size --- .../ResponseCacheOptions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheOptions.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheOptions.cs index 795c80abcf..f530913fb9 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheOptions.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheOptions.cs @@ -9,9 +9,9 @@ namespace Microsoft.AspNetCore.Builder public class ResponseCacheOptions { /// - /// The largest cacheable size for the response body in bytes. The default is set to 1 MB. + /// The largest cacheable size for the response body in bytes. The default is set to 64 MB. /// - public long MaximumBodySize { get; set; } = 1024 * 1024; + public long MaximumBodySize { get; set; } = 64 * 1024 * 1024; /// /// true if request paths are case-sensitive; otherwise false. The default is to treat paths as case-insensitive. From aec2a7c2d20ec9aa70ef5bd51c4c14c0f8370d6d Mon Sep 17 00:00:00 2001 From: Pranav K Date: Wed, 28 Sep 2016 11:52:06 -0700 Subject: [PATCH 044/188] Updating partner package versions --- samples/ResponseCachingSample/project.json | 6 +++--- src/Microsoft.AspNetCore.ResponseCaching/project.json | 7 ++++--- .../project.json | 6 +++--- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/samples/ResponseCachingSample/project.json b/samples/ResponseCachingSample/project.json index a32ead3d3a..3ddb9bb33c 100644 --- a/samples/ResponseCachingSample/project.json +++ b/samples/ResponseCachingSample/project.json @@ -1,4 +1,4 @@ -{ +{ "version": "1.1.0-*", "dependencies": { "Microsoft.AspNetCore.ResponseCaching": "0.1.0-*", @@ -14,7 +14,7 @@ "netcoreapp1.0": { "dependencies": { "Microsoft.NETCore.App": { - "version": "1.0.0-*", + "version": "1.1.0-*", "type": "platform" } } @@ -31,4 +31,4 @@ "scripts": { "postpublish": "dotnet publish-iis --publish-folder %publish:OutputPath% --framework %publish:FullTargetFramework%" } -} +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.ResponseCaching/project.json b/src/Microsoft.AspNetCore.ResponseCaching/project.json index d2521465b9..82a82a25d2 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/project.json +++ b/src/Microsoft.AspNetCore.ResponseCaching/project.json @@ -1,4 +1,4 @@ -{ +{ "version": "0.1.0-*", "buildOptions": { "warningsAsErrors": true, @@ -28,10 +28,11 @@ "Microsoft.Extensions.TaskCache.Sources": { "version": "1.1.0-*", "type": "build" - } + }, + "NETStandard.Library": "1.6.1-*" }, "frameworks": { "net451": {}, "netstandard1.3": {} } -} +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json b/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json index 48dbeeae41..16096df8ea 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json @@ -1,4 +1,4 @@ -{ +{ "buildOptions": { "warningsAsErrors": true, "keyFile": "../../tools/Key.snk" @@ -13,7 +13,7 @@ "netcoreapp1.0": { "dependencies": { "Microsoft.NETCore.App": { - "version": "1.0.0-*", + "version": "1.1.0-*", "type": "platform" } } @@ -21,4 +21,4 @@ "net451": {} }, "testRunner": "xunit" -} +} \ No newline at end of file From 9e5dbee208161796b5e46a6a44bc26fa027a3a25 Mon Sep 17 00:00:00 2001 From: John Luo Date: Tue, 4 Oct 2016 16:03:25 -0700 Subject: [PATCH 045/188] Cache should use weak comparison for ETags --- .../ResponseCacheMiddleware.cs | 2 +- .../ResponseCacheMiddlewareTests.cs | 23 +++++++++++++++---- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs index da7251d108..425a291db5 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs @@ -336,7 +336,7 @@ namespace Microsoft.AspNetCore.ResponseCaching { foreach (var tag in ifNoneMatchHeader) { - if (cachedResponseHeaders.ETag.Compare(tag, useStrongComparison: true)) + if (cachedResponseHeaders.ETag.Compare(tag, useStrongComparison: false)) { return true; } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheMiddlewareTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheMiddlewareTests.cs index 138ed88af3..0f7eec6e82 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheMiddlewareTests.cs @@ -196,16 +196,31 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Assert.False(ResponseCacheMiddleware.ConditionalRequestSatisfied(context)); } - [Fact] - public void ConditionalRequestSatisfied_IfNoneMatch_ExplicitWithMatch_Passes() + public static TheoryData EquivalentWeakETags + { + get + { + return new TheoryData + { + { new EntityTagHeaderValue("\"tag\""), new EntityTagHeaderValue("\"tag\"") }, + { new EntityTagHeaderValue("\"tag\"", true), new EntityTagHeaderValue("\"tag\"") }, + { new EntityTagHeaderValue("\"tag\""), new EntityTagHeaderValue("\"tag\"", true) }, + { new EntityTagHeaderValue("\"tag\"", true), new EntityTagHeaderValue("\"tag\"", true) } + }; + } + } + + [Theory] + [MemberData(nameof(EquivalentWeakETags))] + public void ConditionalRequestSatisfied_IfNoneMatch_ExplicitWithMatch_Passes(EntityTagHeaderValue responseETag, EntityTagHeaderValue requestETag) { var context = TestUtils.CreateTestContext(); context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) { - ETag = new EntityTagHeaderValue("\"E1\"") + ETag = responseETag }; - context.TypedRequestHeaders.IfNoneMatch = new List(new[] { new EntityTagHeaderValue("\"E1\"") }); + context.TypedRequestHeaders.IfNoneMatch = new List(new[] { requestETag }); Assert.True(ResponseCacheMiddleware.ConditionalRequestSatisfied(context)); } From 3b2a2eb892c81db1c8ae666153bfaa0a15da5f7f Mon Sep 17 00:00:00 2001 From: John Luo Date: Thu, 29 Sep 2016 23:14:31 -0700 Subject: [PATCH 046/188] Add logging --- .../Internal/LoggerExtensions.cs | 300 ++++++++++++++++++ .../Internal/ResponseCacheContext.cs | 7 +- .../Internal/ResponseCachePolicyProvider.cs | 55 +++- .../ResponseCacheMiddleware.cs | 54 +++- .../project.json | 1 + .../ResponseCacheMiddlewareTests.cs | 265 +++++++++++++--- .../ResponseCachePolicyProviderTests.cs | 195 +++++++++--- .../TestUtils.cs | 66 +++- .../project.json | 1 + 9 files changed, 829 insertions(+), 115 deletions(-) create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Internal/LoggerExtensions.cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/LoggerExtensions.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/LoggerExtensions.cs new file mode 100644 index 0000000000..b90073179e --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/LoggerExtensions.cs @@ -0,0 +1,300 @@ +// 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.Logging; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.ResponseCaching.Internal +{ + /// + /// Defines *all* the logger messages produced by response caching + /// + internal static class LoggerExtensions + { + private static Action _logRequestMethodNotCacheable; + private static Action _logRequestWithAuthorizationNotCacheable; + private static Action _logRequestWithNoCacheNotCacheable; + private static Action _logRequestWithPragmaNoCacheNotCacheable; + private static Action _logExpirationMinFreshAdded; + private static Action _logExpirationSharedMaxAgeExceeded; + private static Action _logExpirationMustRevalidate; + private static Action _logExpirationMaxStaleSatisfied; + private static Action _logExpirationMaxAgeExceeded; + private static Action _logExpirationExpiresExceeded; + private static Action _logResponseWithoutPublicNotCacheable; + private static Action _logResponseWithNoStoreNotCacheable; + private static Action _logResponseWithNoCacheNotCacheable; + private static Action _logResponseWithSetCookieNotCacheable; + private static Action _logResponseWithVaryStarNotCacheable; + private static Action _logResponseWithPrivateNotCacheable; + private static Action _logResponseWithUnsuccessfulStatusCodeNotCacheable; + private static Action _logNotModifiedIfNoneMatchStar; + private static Action _logNotModifiedIfNoneMatchMatched; + private static Action _logNotModifiedIfUnmodifiedSinceSatisfied; + private static Action _logNotModifiedServed; + private static Action _logCachedResponseServed; + private static Action _logGatewayTimeoutServed; + private static Action _logNoResponseServed; + private static Action _logVaryByRulesUpdated; + private static Action _logResponseCached; + private static Action _logResponseNotCached; + private static Action _logResponseContentLengthMismatchNotCached; + + static LoggerExtensions() + { + _logRequestMethodNotCacheable = LoggerMessage.Define( + logLevel: LogLevel.Debug, + eventId: 1, + formatString: "The request cannot be served from cache because it uses the HTTP method: {Method}."); + _logRequestWithAuthorizationNotCacheable = LoggerMessage.Define( + logLevel: LogLevel.Debug, + eventId: 2, + formatString: $"The request cannot be served from cache because it contains an '{HeaderNames.Authorization}' header."); + _logRequestWithNoCacheNotCacheable = LoggerMessage.Define( + logLevel: LogLevel.Debug, + eventId: 3, + formatString: "The request cannot be served from cache because it contains a 'no-cache' cache directive."); + _logRequestWithPragmaNoCacheNotCacheable = LoggerMessage.Define( + logLevel: LogLevel.Debug, + eventId: 4, + formatString: "The request cannot be served from cache because it contains a 'no-cache' pragma directive."); + _logExpirationMinFreshAdded = LoggerMessage.Define( + logLevel: LogLevel.Debug, + eventId: 5, + formatString: "Adding a minimum freshness requirement of {Duration} specified by the 'min-fresh' cache directive."); + _logExpirationSharedMaxAgeExceeded = LoggerMessage.Define( + logLevel: LogLevel.Debug, + eventId: 6, + formatString: "The age of the entry is {Age} and has exceeded the maximum age for shared caches of {SharedMaxAge} specified by the 's-maxage' cache directive."); + _logExpirationMustRevalidate = LoggerMessage.Define( + logLevel: LogLevel.Debug, + eventId: 7, + formatString: "The age of the entry is {Age} and has exceeded the maximum age of {MaxAge} specified by the 'max-age' cache directive. It must be revalidated because the 'must-revalidate' cache directive is specified."); + _logExpirationMaxStaleSatisfied = LoggerMessage.Define( + logLevel: LogLevel.Debug, + eventId: 8, + formatString: "The age of the entry is {Age} and has exceeded the maximum age of {MaxAge} specified by the 'max-age' cache directive. However, it satisfied the maximum stale allowance of {MaxStale} specified by the 'max-stale' cache directive."); + _logExpirationMaxAgeExceeded = LoggerMessage.Define( + logLevel: LogLevel.Debug, + eventId: 9, + formatString: "The age of the entry is {Age} and has exceeded the maximum age of {MaxAge} specified by the 'max-age' cache directive."); + _logExpirationExpiresExceeded = LoggerMessage.Define( + logLevel: LogLevel.Debug, + eventId: 10, + formatString: $"The response time of the entry is {{ResponseTime}} and has exceeded the expiry date of {{Expired}} specified by the '{HeaderNames.Expires}' header."); + _logResponseWithoutPublicNotCacheable = LoggerMessage.Define( + logLevel: LogLevel.Debug, + eventId: 11, + formatString: "Response is not cacheable because it does not contain the 'public' cache directive."); + _logResponseWithNoStoreNotCacheable = LoggerMessage.Define( + logLevel: LogLevel.Debug, + eventId: 12, + formatString: "Response is not cacheable because it or its corresponding request contains a 'no-store' cache directive."); + _logResponseWithNoCacheNotCacheable = LoggerMessage.Define( + logLevel: LogLevel.Debug, + eventId: 13, + formatString: "Response is not cacheable because it contains a 'no-cache' cache directive."); + _logResponseWithSetCookieNotCacheable = LoggerMessage.Define( + logLevel: LogLevel.Debug, + eventId: 14, + formatString: $"Response is not cacheable because it contains a '{HeaderNames.SetCookie}' header."); + _logResponseWithVaryStarNotCacheable = LoggerMessage.Define( + logLevel: LogLevel.Debug, + eventId: 15, + formatString: $"Response is not cacheable because it contains a '{HeaderNames.Vary}' header with a value of *."); + _logResponseWithPrivateNotCacheable = LoggerMessage.Define( + logLevel: LogLevel.Debug, + eventId: 16, + formatString: "Response is not cacheable because it contains the 'private' cache directive."); + _logResponseWithUnsuccessfulStatusCodeNotCacheable = LoggerMessage.Define( + logLevel: LogLevel.Debug, + eventId: 17, + formatString: "Response is not cacheable because its status code {StatusCode} does not indicate success."); + _logNotModifiedIfNoneMatchStar = LoggerMessage.Define( + logLevel: LogLevel.Debug, + eventId: 18, + formatString: $"The '{HeaderNames.IfNoneMatch}' header of the request contains a value of *."); + _logNotModifiedIfNoneMatchMatched = LoggerMessage.Define( + logLevel: LogLevel.Debug, + eventId: 19, + formatString: $"The ETag {{ETag}} in the '{HeaderNames.IfNoneMatch}' header matched the ETag of a cached entry."); + _logNotModifiedIfUnmodifiedSinceSatisfied = LoggerMessage.Define( + logLevel: LogLevel.Debug, + eventId: 20, + formatString: $"The last modified date of {{LastModified}} is before the date {{IfUnmodifiedSince}} specified in the '{HeaderNames.IfUnmodifiedSince}' header."); + _logNotModifiedServed = LoggerMessage.Define( + logLevel: LogLevel.Information, + eventId: 21, + formatString: "The content requested has not been modified."); + _logCachedResponseServed = LoggerMessage.Define( + logLevel: LogLevel.Information, + eventId: 22, + formatString: "Serving response from cache."); + _logGatewayTimeoutServed = LoggerMessage.Define( + logLevel: LogLevel.Information, + eventId: 23, + formatString: "No cached response available for this request and the 'only-if-cached' cache directive was specified."); + _logNoResponseServed = LoggerMessage.Define( + logLevel: LogLevel.Information, + eventId: 24, + formatString: "No cached response available for this request."); + _logVaryByRulesUpdated = LoggerMessage.Define( + logLevel: LogLevel.Debug, + eventId: 25, + formatString: "Vary by rules were updated. Headers: {Headers}, Query keys: {QueryKeys}"); + _logResponseCached = LoggerMessage.Define( + logLevel: LogLevel.Information, + eventId: 26, + formatString: "The response has been cached."); + _logResponseNotCached = LoggerMessage.Define( + logLevel: LogLevel.Information, + eventId: 27, + formatString: "The response could not be cached for this request."); + _logResponseContentLengthMismatchNotCached = LoggerMessage.Define( + logLevel: LogLevel.Warning, + eventId: 28, + formatString: $"The response could not be cached for this request because the '{HeaderNames.ContentLength}' did not match the body length."); + } + + internal static void LogRequestMethodNotCacheable(this ILogger logger, string method) + { + _logRequestMethodNotCacheable(logger, method, null); + } + + internal static void LogRequestWithAuthorizationNotCacheable(this ILogger logger) + { + _logRequestWithAuthorizationNotCacheable(logger, null); + } + + internal static void LogRequestWithNoCacheNotCacheable(this ILogger logger) + { + _logRequestWithNoCacheNotCacheable(logger, null); + } + + internal static void LogRequestWithPragmaNoCacheNotCacheable(this ILogger logger) + { + _logRequestWithPragmaNoCacheNotCacheable(logger, null); + } + + internal static void LogExpirationMinFreshAdded(this ILogger logger, TimeSpan duration) + { + _logExpirationMinFreshAdded(logger, duration, null); + } + + internal static void LogExpirationSharedMaxAgeExceeded(this ILogger logger, TimeSpan age, TimeSpan sharedMaxAge) + { + _logExpirationSharedMaxAgeExceeded(logger, age, sharedMaxAge, null); + } + + internal static void LogExpirationMustRevalidate(this ILogger logger, TimeSpan age, TimeSpan maxAge) + { + _logExpirationMustRevalidate(logger, age, maxAge, null); + } + + internal static void LogExpirationMaxStaleSatisfied(this ILogger logger, TimeSpan age, TimeSpan maxAge, TimeSpan maxStale) + { + _logExpirationMaxStaleSatisfied(logger, age, maxAge, maxStale, null); + } + + internal static void LogExpirationMaxAgeExceeded(this ILogger logger, TimeSpan age, TimeSpan sharedMaxAge) + { + _logExpirationMaxAgeExceeded(logger, age, sharedMaxAge, null); + } + + internal static void LogExpirationExpiresExceeded(this ILogger logger, DateTimeOffset responseTime, DateTimeOffset expires) + { + _logExpirationExpiresExceeded(logger, responseTime, expires, null); + } + + internal static void LogResponseWithoutPublicNotCacheable(this ILogger logger) + { + _logResponseWithoutPublicNotCacheable(logger, null); + } + + internal static void LogResponseWithNoStoreNotCacheable(this ILogger logger) + { + _logResponseWithNoStoreNotCacheable(logger, null); + } + + internal static void LogResponseWithNoCacheNotCacheable(this ILogger logger) + { + _logResponseWithNoCacheNotCacheable(logger, null); + } + + internal static void LogResponseWithSetCookieNotCacheable(this ILogger logger) + { + _logResponseWithSetCookieNotCacheable(logger, null); + } + + internal static void LogResponseWithVaryStarNotCacheable(this ILogger logger) + { + _logResponseWithVaryStarNotCacheable(logger, null); + } + + internal static void LogResponseWithPrivateNotCacheable(this ILogger logger) + { + _logResponseWithPrivateNotCacheable(logger, null); + } + + internal static void LogResponseWithUnsuccessfulStatusCodeNotCacheable(this ILogger logger, int statusCode) + { + _logResponseWithUnsuccessfulStatusCodeNotCacheable(logger, statusCode, null); + } + + internal static void LogNotModifiedIfNoneMatchStar(this ILogger logger) + { + _logNotModifiedIfNoneMatchStar(logger, null); + } + + internal static void LogNotModifiedIfNoneMatchMatched(this ILogger logger, EntityTagHeaderValue etag) + { + _logNotModifiedIfNoneMatchMatched(logger, etag, null); + } + + internal static void LogNotModifiedIfUnmodifiedSinceSatisfied(this ILogger logger, DateTimeOffset lastModified, DateTimeOffset ifUnmodifiedSince) + { + _logNotModifiedIfUnmodifiedSinceSatisfied(logger, lastModified, ifUnmodifiedSince, null); + } + + internal static void LogNotModifiedServed(this ILogger logger) + { + _logNotModifiedServed(logger, null); + } + + internal static void LogCachedResponseServed(this ILogger logger) + { + _logCachedResponseServed(logger, null); + } + + internal static void LogGatewayTimeoutServed(this ILogger logger) + { + _logGatewayTimeoutServed(logger, null); + } + + internal static void LogNoResponseServed(this ILogger logger) + { + _logNoResponseServed(logger, null); + } + + internal static void LogVaryByRulesUpdated(this ILogger logger, string headers, string queryKeys) + { + _logVaryByRulesUpdated(logger, headers, queryKeys, null); + } + + internal static void LogResponseCached(this ILogger logger) + { + _logResponseCached(logger, null); + } + + internal static void LogResponseNotCached(this ILogger logger) + { + _logResponseNotCached(logger, null); + } + + internal static void LogResponseContentLengthMismatchNotCached(this ILogger logger) + { + _logResponseContentLengthMismatchNotCached(logger, null); + } + } +} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCacheContext.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCacheContext.cs index c11ff159f3..5d0f4cc17c 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCacheContext.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCacheContext.cs @@ -6,6 +6,7 @@ using System.IO; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Headers; +using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.ResponseCaching.Internal @@ -23,10 +24,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal private DateTimeOffset? _responseExpires; private bool _parsedResponseExpires; - internal ResponseCacheContext( - HttpContext httpContext) + internal ResponseCacheContext(HttpContext httpContext, ILogger logger) { HttpContext = httpContext; + Logger = logger; } public HttpContext HttpContext { get; } @@ -37,6 +38,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal public CachedVaryByRules CachedVaryByRules { get; internal set; } + internal ILogger Logger { get; } + internal bool ShouldCacheResponse { get; set; } internal string BaseKey { get; set; } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachePolicyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachePolicyProvider.cs index ab331ff311..cc7f174a07 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachePolicyProvider.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachePolicyProvider.cs @@ -18,12 +18,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal var request = context.HttpContext.Request; if (!HttpMethods.IsGet(request.Method) && !HttpMethods.IsHead(request.Method)) { + context.Logger.LogRequestMethodNotCacheable(request.Method); return false; } // Verify existence of authorization headers if (!StringValues.IsNullOrEmpty(request.Headers[HeaderNames.Authorization])) { + context.Logger.LogRequestWithAuthorizationNotCacheable(); return false; } @@ -32,6 +34,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal { if (context.RequestCacheControlHeaderValue.NoCache) { + context.Logger.LogRequestWithNoCacheNotCacheable(); return false; } } @@ -43,6 +46,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal { if (string.Equals("no-cache", directive, StringComparison.OrdinalIgnoreCase)) { + context.Logger.LogRequestWithPragmaNoCacheNotCacheable(); return false; } } @@ -56,18 +60,21 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal // Only cache pages explicitly marked with public if (!context.ResponseCacheControlHeaderValue.Public) { + context.Logger.LogResponseWithoutPublicNotCacheable(); return false; } // Check no-store if (context.RequestCacheControlHeaderValue.NoStore || context.ResponseCacheControlHeaderValue.NoStore) { + context.Logger.LogResponseWithNoStoreNotCacheable(); return false; } // Check no-cache if (context.ResponseCacheControlHeaderValue.NoCache) { + context.Logger.LogResponseWithNoCacheNotCacheable(); return false; } @@ -76,6 +83,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal // Do not cache responses with Set-Cookie headers if (!StringValues.IsNullOrEmpty(response.Headers[HeaderNames.SetCookie])) { + context.Logger.LogResponseWithSetCookieNotCacheable(); return false; } @@ -83,18 +91,21 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal var varyHeader = response.Headers[HeaderNames.Vary]; if (varyHeader.Count == 1 && string.Equals(varyHeader, "*", StringComparison.OrdinalIgnoreCase)) { + context.Logger.LogResponseWithVaryStarNotCacheable(); return false; } // Check private if (context.ResponseCacheControlHeaderValue.Private) { + context.Logger.LogResponseWithPrivateNotCacheable(); return false; } // Check response code if (response.StatusCode != StatusCodes.Status200OK) { + context.Logger.LogResponseWithUnsuccessfulStatusCodeNotCacheable(response.StatusCode); return false; } @@ -105,6 +116,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal !context.ResponseCacheControlHeaderValue.MaxAge.HasValue && context.ResponseTime.Value >= context.ResponseExpires) { + context.Logger.LogExpirationExpiresExceeded(context.ResponseTime.Value, context.ResponseExpires.Value); return false; } } @@ -113,22 +125,27 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal var age = context.ResponseTime.Value - context.ResponseDate.Value; // Validate shared max age - if (age >= context.ResponseCacheControlHeaderValue.SharedMaxAge) + var sharedMaxAge = context.ResponseCacheControlHeaderValue.SharedMaxAge; + if (age >= sharedMaxAge) { + context.Logger.LogExpirationSharedMaxAgeExceeded(age, sharedMaxAge.Value); return false; } - else if (!context.ResponseCacheControlHeaderValue.SharedMaxAge.HasValue) + else if (!sharedMaxAge.HasValue) { // Validate max age - if (age >= context.ResponseCacheControlHeaderValue.MaxAge) + var maxAge = context.ResponseCacheControlHeaderValue.MaxAge; + if (age >= maxAge) { + context.Logger.LogExpirationMaxAgeExceeded(age, maxAge.Value); return false; } - else if (!context.ResponseCacheControlHeaderValue.MaxAge.HasValue) + else if (!maxAge.HasValue) { // Validate expiration if (context.ResponseTime.Value >= context.ResponseExpires) { + context.Logger.LogExpirationExpiresExceeded(context.ResponseTime.Value, context.ResponseExpires.Value); return false; } } @@ -144,41 +161,55 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal var cachedControlHeaders = context.CachedResponseHeaders.CacheControl ?? EmptyCacheControl; // Add min-fresh requirements - if (context.RequestCacheControlHeaderValue.MinFresh.HasValue) + var minFresh = context.RequestCacheControlHeaderValue.MinFresh; + if (minFresh.HasValue) { - age += context.RequestCacheControlHeaderValue.MinFresh.Value; + age += minFresh.Value; + context.Logger.LogExpirationMinFreshAdded(minFresh.Value); } // Validate shared max age, this overrides any max age settings for shared caches - if (age >= cachedControlHeaders.SharedMaxAge) + var sharedMaxAge = cachedControlHeaders.SharedMaxAge; + if (age >= sharedMaxAge) { // shared max age implies must revalidate + context.Logger.LogExpirationSharedMaxAgeExceeded(age, sharedMaxAge.Value); return false; } - else if (!cachedControlHeaders.SharedMaxAge.HasValue) + else if (!sharedMaxAge.HasValue) { + var cachedMaxAge = cachedControlHeaders.MaxAge; + var requestMaxAge = context.RequestCacheControlHeaderValue.MaxAge; + var lowestMaxAge = cachedMaxAge < requestMaxAge ? cachedMaxAge : requestMaxAge ?? cachedMaxAge; // Validate max age - if (age >= cachedControlHeaders.MaxAge || age >= context.RequestCacheControlHeaderValue.MaxAge) + if (age >= lowestMaxAge) { // Must revalidate if (cachedControlHeaders.MustRevalidate) { + context.Logger.LogExpirationMustRevalidate(age, lowestMaxAge.Value); return false; } // Request allows stale values - if (age < context.RequestCacheControlHeaderValue.MaxStaleLimit) + var maxStaleLimit = context.RequestCacheControlHeaderValue.MaxStaleLimit; + if (maxStaleLimit.HasValue && age - lowestMaxAge < maxStaleLimit) { + context.Logger.LogExpirationMaxStaleSatisfied(age, lowestMaxAge.Value, maxStaleLimit.Value); return true; } + context.Logger.LogExpirationMaxAgeExceeded(age, lowestMaxAge.Value); return false; } - else if (!cachedControlHeaders.MaxAge.HasValue && !context.RequestCacheControlHeaderValue.MaxAge.HasValue) + else if (!cachedMaxAge.HasValue && !requestMaxAge.HasValue) { // Validate expiration - if (context.ResponseTime.Value >= context.CachedResponseHeaders.Expires) + var responseTime = context.ResponseTime.Value; + var expires = context.CachedResponseHeaders.Expires; + if (responseTime >= expires) { + context.Logger.LogExpirationExpiresExceeded(responseTime, expires.Value); return false; } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs index 425a291db5..c7939b82c8 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs @@ -10,7 +10,7 @@ using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Headers; using Microsoft.AspNetCore.ResponseCaching.Internal; using Microsoft.Extensions.Internal; -using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; @@ -22,15 +22,17 @@ namespace Microsoft.AspNetCore.ResponseCaching private static readonly TimeSpan DefaultExpirationTimeSpan = TimeSpan.FromSeconds(10); private readonly RequestDelegate _next; - private readonly IResponseCacheStore _store; private readonly ResponseCacheOptions _options; + private readonly ILogger _logger; private readonly IResponseCachePolicyProvider _policyProvider; + private readonly IResponseCacheStore _store; private readonly IResponseCacheKeyProvider _keyProvider; private readonly Func _onStartingCallback; public ResponseCacheMiddleware( RequestDelegate next, IOptions options, + ILoggerFactory loggerFactory, IResponseCachePolicyProvider policyProvider, IResponseCacheStore store, IResponseCacheKeyProvider keyProvider) @@ -39,34 +41,39 @@ namespace Microsoft.AspNetCore.ResponseCaching { throw new ArgumentNullException(nameof(next)); } - if (store == null) - { - throw new ArgumentNullException(nameof(store)); - } if (options == null) { throw new ArgumentNullException(nameof(options)); } + if (loggerFactory == null) + { + throw new ArgumentNullException(nameof(loggerFactory)); + } if (policyProvider == null) { throw new ArgumentNullException(nameof(policyProvider)); } + if (store == null) + { + throw new ArgumentNullException(nameof(store)); + } if (keyProvider == null) { throw new ArgumentNullException(nameof(keyProvider)); } _next = next; - _store = store; _options = options.Value; + _logger = loggerFactory.CreateLogger(); _policyProvider = policyProvider; + _store = store; _keyProvider = keyProvider; _onStartingCallback = state => OnResponseStartingAsync((ResponseCacheContext)state); } public async Task Invoke(HttpContext httpContext) { - var context = new ResponseCacheContext(httpContext); + var context = new ResponseCacheContext(httpContext, _logger); // Should we attempt any caching logic? if (_policyProvider.IsRequestCacheable(context)) @@ -115,8 +122,9 @@ namespace Microsoft.AspNetCore.ResponseCaching if (_policyProvider.IsCachedEntryFresh(context)) { // Check conditional request rules - if (ConditionalRequestSatisfied(context)) + if (ContentIsNotModified(context)) { + _logger.LogNotModifiedServed(); context.HttpContext.Response.StatusCode = StatusCodes.Status304NotModified; } else @@ -150,8 +158,8 @@ namespace Microsoft.AspNetCore.ResponseCaching context.HttpContext.Abort(); } } + _logger.LogCachedResponseServed(); } - return true; } @@ -183,13 +191,14 @@ namespace Microsoft.AspNetCore.ResponseCaching return true; } - if (context.RequestCacheControlHeaderValue.OnlyIfCached) { + _logger.LogGatewayTimeoutServed(); context.HttpContext.Response.StatusCode = StatusCodes.Status504GatewayTimeout; return true; } + _logger.LogNoResponseServed(); return false; } @@ -229,6 +238,7 @@ namespace Microsoft.AspNetCore.ResponseCaching } // Always overwrite the CachedVaryByRules to update the expiry information + _logger.LogVaryByRulesUpdated(normalizedVaryHeaders, normalizedVaryQueryKeys); await _store.SetAsync(context.BaseKey, context.CachedVaryByRules, context.CachedResponseValidFor); context.StorageVaryKey = _keyProvider.CreateStorageVaryByKey(context); @@ -272,8 +282,17 @@ namespace Microsoft.AspNetCore.ResponseCaching if (!contentLength.HasValue || contentLength == bufferStream.Length) { context.CachedResponse.Body = bufferStream; + _logger.LogResponseCached(); await _store.SetAsync(context.StorageVaryKey ?? context.BaseKey, context.CachedResponse, context.CachedResponseValidFor); } + else + { + _logger.LogResponseContentLengthMismatchNotCached(); + } + } + else + { + _logger.LogResponseNotCached(); } } @@ -320,7 +339,7 @@ namespace Microsoft.AspNetCore.ResponseCaching context.HttpContext.RemoveResponseCacheFeature(); } - internal static bool ConditionalRequestSatisfied(ResponseCacheContext context) + internal static bool ContentIsNotModified(ResponseCacheContext context) { var cachedResponseHeaders = context.CachedResponseHeaders; var ifNoneMatchHeader = context.TypedRequestHeaders.IfNoneMatch; @@ -329,6 +348,7 @@ namespace Microsoft.AspNetCore.ResponseCaching { if (ifNoneMatchHeader.Count == 1 && ifNoneMatchHeader[0].Equals(EntityTagHeaderValue.Any)) { + context.Logger.LogNotModifiedIfNoneMatchStar(); return true; } @@ -338,6 +358,7 @@ namespace Microsoft.AspNetCore.ResponseCaching { if (cachedResponseHeaders.ETag.Compare(tag, useStrongComparison: false)) { + context.Logger.LogNotModifiedIfNoneMatchMatched(tag); return true; } } @@ -346,9 +367,14 @@ namespace Microsoft.AspNetCore.ResponseCaching else { var ifUnmodifiedSince = context.TypedRequestHeaders.IfUnmodifiedSince; - if (ifUnmodifiedSince != null && (cachedResponseHeaders.LastModified ?? cachedResponseHeaders.Date) <= ifUnmodifiedSince) + if (ifUnmodifiedSince != null) { - return true; + var lastModified = cachedResponseHeaders.LastModified ?? cachedResponseHeaders.Date; + if (lastModified <= ifUnmodifiedSince) + { + context.Logger.LogNotModifiedIfUnmodifiedSinceSatisfied(lastModified.Value, ifUnmodifiedSince.Value); + return true; + } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/project.json b/src/Microsoft.AspNetCore.ResponseCaching/project.json index 82a82a25d2..5adf0b7360 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/project.json +++ b/src/Microsoft.AspNetCore.ResponseCaching/project.json @@ -25,6 +25,7 @@ "Microsoft.AspNetCore.Http": "1.1.0-*", "Microsoft.AspNetCore.Http.Extensions": "1.1.0-*", "Microsoft.Extensions.Caching.Memory": "1.1.0-*", + "Microsoft.Extensions.Logging.Abstractions": "1.1.0-*", "Microsoft.Extensions.TaskCache.Sources": { "version": "1.1.0-*", "type": "build" diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheMiddlewareTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheMiddlewareTests.cs index 0f7eec6e82..9e251dc9b1 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheMiddlewareTests.cs @@ -7,6 +7,9 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Headers; using Microsoft.AspNetCore.ResponseCaching.Internal; +using Microsoft.AspNetCore.WebUtilities; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; using Xunit; @@ -19,7 +22,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests public async Task TryServeFromCacheAsync_OnlyIfCached_Serves504() { var store = new TestResponseCacheStore(); - var middleware = TestUtils.CreateTestMiddleware(store: store, keyProvider: new TestResponseCacheKeyProvider()); + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, store: store, keyProvider: new TestResponseCacheKeyProvider()); var context = TestUtils.CreateTestContext(); context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() { @@ -28,24 +32,32 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Assert.True(await middleware.TryServeFromCacheAsync(context)); Assert.Equal(StatusCodes.Status504GatewayTimeout, context.HttpContext.Response.StatusCode); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.GatewayTimeoutServed); } [Fact] public async Task TryServeFromCacheAsync_CachedResponseNotFound_Fails() { var store = new TestResponseCacheStore(); - var middleware = TestUtils.CreateTestMiddleware(store: store, keyProvider: new TestResponseCacheKeyProvider("BaseKey")); + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, store: store, keyProvider: new TestResponseCacheKeyProvider("BaseKey")); var context = TestUtils.CreateTestContext(); Assert.False(await middleware.TryServeFromCacheAsync(context)); Assert.Equal(1, store.GetCount); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.NoResponseServed); } [Fact] public async Task TryServeFromCacheAsync_CachedResponseFound_Succeeds() { var store = new TestResponseCacheStore(); - var middleware = TestUtils.CreateTestMiddleware(store: store, keyProvider: new TestResponseCacheKeyProvider("BaseKey")); + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, store: store, keyProvider: new TestResponseCacheKeyProvider("BaseKey")); var context = TestUtils.CreateTestContext(); await store.SetAsync( @@ -58,13 +70,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Assert.True(await middleware.TryServeFromCacheAsync(context)); Assert.Equal(1, store.GetCount); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.CachedResponseServed); } [Fact] public async Task TryServeFromCacheAsync_VaryByRuleFound_CachedResponseNotFound_Fails() { var store = new TestResponseCacheStore(); - var middleware = TestUtils.CreateTestMiddleware(store: store, keyProvider: new TestResponseCacheKeyProvider("BaseKey", "VaryKey")); + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, store: store, keyProvider: new TestResponseCacheKeyProvider("BaseKey", "VaryKey")); var context = TestUtils.CreateTestContext(); await store.SetAsync( @@ -74,13 +90,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Assert.False(await middleware.TryServeFromCacheAsync(context)); Assert.Equal(2, store.GetCount); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.NoResponseServed); } [Fact] public async Task TryServeFromCacheAsync_VaryByRuleFound_CachedResponseFound_Succeeds() { var store = new TestResponseCacheStore(); - var middleware = TestUtils.CreateTestMiddleware(store: store, keyProvider: new TestResponseCacheKeyProvider("BaseKey", new[] { "VaryKey", "VaryKey2" })); + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, store: store, keyProvider: new TestResponseCacheKeyProvider("BaseKey", new[] { "VaryKey", "VaryKey2" })); var context = TestUtils.CreateTestContext(); await store.SetAsync( @@ -97,44 +117,83 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Assert.True(await middleware.TryServeFromCacheAsync(context)); Assert.Equal(3, store.GetCount); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.CachedResponseServed); } [Fact] - public void ConditionalRequestSatisfied_NotConditionalRequest_Fails() + public async Task TryServeFromCacheAsync_CachedResponseFound_Serves304IfPossible() { + var store = new TestResponseCacheStore(); + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, store: store, keyProvider: new TestResponseCacheKeyProvider("BaseKey")); var context = TestUtils.CreateTestContext(); + context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = "*"; + + await store.SetAsync( + "BaseKey", + new CachedResponse() + { + Body = new SegmentReadStream(new List(0), 0) + }, + TimeSpan.Zero); + + Assert.True(await middleware.TryServeFromCacheAsync(context)); + Assert.Equal(1, store.GetCount); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.NotModifiedServed); + } + + [Fact] + public void ContentIsNotModified_NotConditionalRequest_False() + { + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); - Assert.False(ResponseCacheMiddleware.ConditionalRequestSatisfied(context)); + Assert.False(ResponseCacheMiddleware.ContentIsNotModified(context)); + Assert.Empty(sink.Writes); } [Fact] - public void ConditionalRequestSatisfied_IfUnmodifiedSince_FallsbackToDateHeader() + public void ContentIsNotModified_IfUnmodifiedSince_FallsbackToDateHeader() { var utcNow = DateTimeOffset.UtcNow; - var context = TestUtils.CreateTestContext(); + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); context.TypedRequestHeaders.IfUnmodifiedSince = utcNow; // Verify modifications in the past succeeds context.CachedResponseHeaders.Date = utcNow - TimeSpan.FromSeconds(10); - Assert.True(ResponseCacheMiddleware.ConditionalRequestSatisfied(context)); + Assert.True(ResponseCacheMiddleware.ContentIsNotModified(context)); + Assert.Equal(1, sink.Writes.Count); // Verify modifications at present succeeds context.CachedResponseHeaders.Date = utcNow; - Assert.True(ResponseCacheMiddleware.ConditionalRequestSatisfied(context)); + Assert.True(ResponseCacheMiddleware.ContentIsNotModified(context)); + Assert.Equal(2, sink.Writes.Count); // Verify modifications in the future fails context.CachedResponseHeaders.Date = utcNow + TimeSpan.FromSeconds(10); - Assert.False(ResponseCacheMiddleware.ConditionalRequestSatisfied(context)); + Assert.False(ResponseCacheMiddleware.ContentIsNotModified(context)); + + // Verify logging + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.NotModifiedIfUnmodifiedSinceSatisfied, + LoggedMessage.NotModifiedIfUnmodifiedSinceSatisfied); } [Fact] - public void ConditionalRequestSatisfied_IfUnmodifiedSince_LastModifiedOverridesDateHeader() + public void ContentIsNotModified_IfUnmodifiedSince_LastModifiedOverridesDateHeader() { var utcNow = DateTimeOffset.UtcNow; - var context = TestUtils.CreateTestContext(); + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); context.TypedRequestHeaders.IfUnmodifiedSince = utcNow; @@ -142,24 +201,33 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests // Verify modifications in the past succeeds context.CachedResponseHeaders.Date = utcNow + TimeSpan.FromSeconds(10); context.CachedResponseHeaders.LastModified = utcNow - TimeSpan.FromSeconds(10); - Assert.True(ResponseCacheMiddleware.ConditionalRequestSatisfied(context)); + Assert.True(ResponseCacheMiddleware.ContentIsNotModified(context)); + Assert.Equal(1, sink.Writes.Count); // Verify modifications at present context.CachedResponseHeaders.Date = utcNow + TimeSpan.FromSeconds(10); context.CachedResponseHeaders.LastModified = utcNow; - Assert.True(ResponseCacheMiddleware.ConditionalRequestSatisfied(context)); + Assert.True(ResponseCacheMiddleware.ContentIsNotModified(context)); + Assert.Equal(2, sink.Writes.Count); // Verify modifications in the future fails context.CachedResponseHeaders.Date = utcNow - TimeSpan.FromSeconds(10); context.CachedResponseHeaders.LastModified = utcNow + TimeSpan.FromSeconds(10); - Assert.False(ResponseCacheMiddleware.ConditionalRequestSatisfied(context)); + Assert.False(ResponseCacheMiddleware.ContentIsNotModified(context)); + + // Verify logging + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.NotModifiedIfUnmodifiedSinceSatisfied, + LoggedMessage.NotModifiedIfUnmodifiedSinceSatisfied); } [Fact] - public void ConditionalRequestSatisfied_IfNoneMatch_Overrides_IfUnmodifiedSince_ToPass() + public void ContentIsNotModified_IfNoneMatch_Overrides_IfUnmodifiedSince_ToTrue() { var utcNow = DateTimeOffset.UtcNow; - var context = TestUtils.CreateTestContext(); + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); // This would fail the IfUnmodifiedSince checks @@ -167,14 +235,18 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.CachedResponseHeaders.LastModified = utcNow + TimeSpan.FromSeconds(10); context.TypedRequestHeaders.IfNoneMatch = new List(new[] { EntityTagHeaderValue.Any }); - Assert.True(ResponseCacheMiddleware.ConditionalRequestSatisfied(context)); + Assert.True(ResponseCacheMiddleware.ContentIsNotModified(context)); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.NotModifiedIfNoneMatchStar); } [Fact] - public void ConditionalRequestSatisfied_IfNoneMatch_Overrides_IfUnmodifiedSince_ToFail() + public void ContentIsNotModified_IfNoneMatch_Overrides_IfUnmodifiedSince_ToFalse() { var utcNow = DateTimeOffset.UtcNow; - var context = TestUtils.CreateTestContext(); + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); // This would pass the IfUnmodifiedSince checks @@ -182,18 +254,21 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.CachedResponseHeaders.LastModified = utcNow - TimeSpan.FromSeconds(10); context.TypedRequestHeaders.IfNoneMatch = new List(new[] { new EntityTagHeaderValue("\"E1\"") }); - Assert.False(ResponseCacheMiddleware.ConditionalRequestSatisfied(context)); + Assert.False(ResponseCacheMiddleware.ContentIsNotModified(context)); + Assert.Empty(sink.Writes); } [Fact] - public void ConditionalRequestSatisfied_IfNoneMatch_AnyWithoutETagInResponse_Passes() + public void ContentIsNotModified_IfNoneMatch_AnyWithoutETagInResponse_False() { - var context = TestUtils.CreateTestContext(); + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); context.TypedRequestHeaders.IfNoneMatch = new List(new[] { new EntityTagHeaderValue("\"E1\"") }); - Assert.False(ResponseCacheMiddleware.ConditionalRequestSatisfied(context)); + Assert.False(ResponseCacheMiddleware.ContentIsNotModified(context)); + Assert.Empty(sink.Writes); } public static TheoryData EquivalentWeakETags @@ -212,9 +287,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Theory] [MemberData(nameof(EquivalentWeakETags))] - public void ConditionalRequestSatisfied_IfNoneMatch_ExplicitWithMatch_Passes(EntityTagHeaderValue responseETag, EntityTagHeaderValue requestETag) + public void ContentIsNotModified_IfNoneMatch_ExplicitWithMatch_True(EntityTagHeaderValue responseETag, EntityTagHeaderValue requestETag) { - var context = TestUtils.CreateTestContext(); + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) { ETag = responseETag @@ -222,13 +298,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.TypedRequestHeaders.IfNoneMatch = new List(new[] { requestETag }); - Assert.True(ResponseCacheMiddleware.ConditionalRequestSatisfied(context)); + Assert.True(ResponseCacheMiddleware.ContentIsNotModified(context)); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.NotModifiedIfNoneMatchMatched); } [Fact] - public void ConditionalRequestSatisfied_IfNoneMatch_ExplicitWithoutMatch_Fails() + public void ContentIsNotModified_IfNoneMatch_ExplicitWithoutMatch_False() { - var context = TestUtils.CreateTestContext(); + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) { ETag = new EntityTagHeaderValue("\"E2\"") @@ -236,13 +316,15 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.TypedRequestHeaders.IfNoneMatch = new List(new[] { new EntityTagHeaderValue("\"E1\"") }); - Assert.False(ResponseCacheMiddleware.ConditionalRequestSatisfied(context)); + Assert.False(ResponseCacheMiddleware.ContentIsNotModified(context)); + Assert.Empty(sink.Writes); } [Fact] public async Task FinalizeCacheHeaders_DoNotUpdateShouldCacheResponse_IfResponseIsNotCacheable() { - var middleware = TestUtils.CreateTestMiddleware(policyProvider: new ResponseCachePolicyProvider()); + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, policyProvider: new ResponseCachePolicyProvider()); var context = TestUtils.CreateTestContext(); Assert.False(context.ShouldCacheResponse); @@ -251,12 +333,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests await middleware.FinalizeCacheHeadersAsync(context); Assert.False(context.ShouldCacheResponse); + Assert.Empty(sink.Writes); } [Fact] public async Task FinalizeCacheHeaders_UpdateShouldCacheResponse_IfResponseIsCacheable() { - var middleware = TestUtils.CreateTestMiddleware(policyProvider: new ResponseCachePolicyProvider()); + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, policyProvider: new ResponseCachePolicyProvider()); var context = TestUtils.CreateTestContext(); context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() { @@ -268,24 +352,28 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests await middleware.FinalizeCacheHeadersAsync(context); Assert.True(context.ShouldCacheResponse); + Assert.Empty(sink.Writes); } [Fact] public async Task FinalizeCacheHeaders_DefaultResponseValidity_Is10Seconds() { - var middleware = TestUtils.CreateTestMiddleware(); + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink); var context = TestUtils.CreateTestContext(); await middleware.FinalizeCacheHeadersAsync(context); Assert.Equal(TimeSpan.FromSeconds(10), context.CachedResponseValidFor); + Assert.Empty(sink.Writes); } [Fact] public async Task FinalizeCacheHeaders_ResponseValidity_UseExpiryIfAvailable() { var utcNow = DateTimeOffset.MinValue; - var middleware = TestUtils.CreateTestMiddleware(); + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink); var context = TestUtils.CreateTestContext(); context.ResponseTime = utcNow; @@ -294,12 +382,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests await middleware.FinalizeCacheHeadersAsync(context); Assert.Equal(TimeSpan.FromSeconds(11), context.CachedResponseValidFor); + Assert.Empty(sink.Writes); } [Fact] public async Task FinalizeCacheHeaders_ResponseValidity_UseMaxAgeIfAvailable() { - var middleware = TestUtils.CreateTestMiddleware(); + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink); var context = TestUtils.CreateTestContext(); context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() { @@ -311,12 +401,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests await middleware.FinalizeCacheHeadersAsync(context); Assert.Equal(TimeSpan.FromSeconds(12), context.CachedResponseValidFor); + Assert.Empty(sink.Writes); } [Fact] public async Task FinalizeCacheHeaders_ResponseValidity_UseSharedMaxAgeIfAvailable() { - var middleware = TestUtils.CreateTestMiddleware(); + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink); var context = TestUtils.CreateTestContext(); context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() { @@ -329,13 +421,15 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests await middleware.FinalizeCacheHeadersAsync(context); Assert.Equal(TimeSpan.FromSeconds(13), context.CachedResponseValidFor); + Assert.Empty(sink.Writes); } [Fact] public async Task FinalizeCacheHeaders_UpdateCachedVaryByRules_IfNotEquivalentToPrevious() { var store = new TestResponseCacheStore(); - var middleware = TestUtils.CreateTestMiddleware(store); + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, store: store); var context = TestUtils.CreateTestContext(); context.HttpContext.Response.Headers[HeaderNames.Vary] = new StringValues(new[] { "headerA", "HEADERB", "HEADERc" }); @@ -353,13 +447,18 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Assert.Equal(1, store.SetCount); Assert.NotSame(cachedVaryByRules, context.CachedVaryByRules); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.NoResponseServed, + LoggedMessage.VaryByRulesUpdated); } [Fact] - public async Task FinalizeCacheHeaders_DoNotUpdateCachedVaryByRules_IfEquivalentToPrevious() + public async Task FinalizeCacheHeaders_UpdateCachedVaryByRules_IfEquivalentToPrevious() { var store = new TestResponseCacheStore(); - var middleware = TestUtils.CreateTestMiddleware(store); + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, store: store); var context = TestUtils.CreateTestContext(); context.HttpContext.Response.Headers[HeaderNames.Vary] = new StringValues(new[] { "headerA", "HEADERB" }); @@ -379,13 +478,18 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests // An update to the cache is always made but the entry should be the same Assert.Equal(1, store.SetCount); Assert.Same(cachedVaryByRules, context.CachedVaryByRules); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.NoResponseServed, + LoggedMessage.VaryByRulesUpdated); } [Fact] public async Task FinalizeCacheHeaders_DoNotAddDate_IfSpecified() { var utcNow = DateTimeOffset.MinValue; - var middleware = TestUtils.CreateTestMiddleware(); + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink); var context = TestUtils.CreateTestContext(); context.ResponseTime = utcNow; @@ -394,13 +498,15 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests await middleware.FinalizeCacheHeadersAsync(context); Assert.Equal(utcNow, context.TypedResponseHeaders.Date); + Assert.Empty(sink.Writes); } [Fact] public async Task FinalizeCacheHeaders_AddsDate_IfNoneSpecified() { var utcNow = DateTimeOffset.MinValue; - var middleware = TestUtils.CreateTestMiddleware(); + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink); var context = TestUtils.CreateTestContext(); context.TypedResponseHeaders.Date = utcNow; context.ResponseTime = utcNow + TimeSpan.FromSeconds(10); @@ -410,12 +516,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests await middleware.FinalizeCacheHeadersAsync(context); Assert.Equal(utcNow, context.TypedResponseHeaders.Date); + Assert.Empty(sink.Writes); } [Fact] public async Task FinalizeCacheHeaders_StoresCachedResponse_InState() { - var middleware = TestUtils.CreateTestMiddleware(); + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink); var context = TestUtils.CreateTestContext(); Assert.Null(context.CachedResponse); @@ -423,12 +531,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests await middleware.FinalizeCacheHeadersAsync(context); Assert.NotNull(context.CachedResponse); + Assert.Empty(sink.Writes); } [Fact] public async Task FinalizeCacheHeaders_SplitsVaryHeaderByCommas() { - var middleware = TestUtils.CreateTestMiddleware(); + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink); var context = TestUtils.CreateTestContext(); context.HttpContext.Response.Headers[HeaderNames.Vary] = "HeaderB, heaDera"; @@ -436,13 +546,18 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests await middleware.FinalizeCacheHeadersAsync(context); Assert.Equal(new StringValues(new[] { "HEADERA", "HEADERB" }), context.CachedVaryByRules.Headers); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.NoResponseServed, + LoggedMessage.VaryByRulesUpdated); } [Fact] public async Task FinalizeCacheBody_Cache_IfContentLengthMatches() { var store = new TestResponseCacheStore(); - var middleware = TestUtils.CreateTestMiddleware(store); + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, store: store); var context = TestUtils.CreateTestContext(); middleware.ShimResponseStream(context); @@ -457,13 +572,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests await middleware.FinalizeCacheBodyAsync(context); Assert.Equal(1, store.SetCount); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.ResponseCached); } [Fact] public async Task FinalizeCacheBody_DoNotCache_IfContentLengthMismatches() { var store = new TestResponseCacheStore(); - var middleware = TestUtils.CreateTestMiddleware(store); + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, store: store); var context = TestUtils.CreateTestContext(); middleware.ShimResponseStream(context); @@ -478,13 +597,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests await middleware.FinalizeCacheBodyAsync(context); Assert.Equal(0, store.SetCount); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.ResponseContentLengthMismatchNotCached); } [Fact] public async Task FinalizeCacheBody_Cache_IfContentLengthAbsent() { var store = new TestResponseCacheStore(); - var middleware = TestUtils.CreateTestMiddleware(store); + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, store: store); var context = TestUtils.CreateTestContext(); middleware.ShimResponseStream(context); @@ -498,6 +621,52 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests await middleware.FinalizeCacheBodyAsync(context); Assert.Equal(1, store.SetCount); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.ResponseCached); + } + + [Fact] + public async Task FinalizeCacheBody_DoNotCache_IfShouldCacheResponseFalse() + { + var store = new TestResponseCacheStore(); + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, store: store); + var context = TestUtils.CreateTestContext(); + + middleware.ShimResponseStream(context); + await context.HttpContext.Response.WriteAsync(new string('0', 10)); + + context.ShouldCacheResponse = false; + + await middleware.FinalizeCacheBodyAsync(context); + + Assert.Equal(0, store.SetCount); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.ResponseNotCached); + } + + [Fact] + public async Task FinalizeCacheBody_DoNotCache_IfBufferingDisabled() + { + var store = new TestResponseCacheStore(); + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, store: store); + var context = TestUtils.CreateTestContext(); + + middleware.ShimResponseStream(context); + await context.HttpContext.Response.WriteAsync(new string('0', 10)); + + context.ShouldCacheResponse = true; + context.ResponseCacheStream.DisableBuffering(); + + await middleware.FinalizeCacheBodyAsync(context); + + Assert.Equal(0, store.SetCount); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.ResponseNotCached); } [Fact] diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachePolicyProviderTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachePolicyProviderTests.cs index d9a914fda2..2aefdb287e 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachePolicyProviderTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachePolicyProviderTests.cs @@ -5,8 +5,10 @@ using System; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Headers; using Microsoft.AspNetCore.ResponseCaching.Internal; +using Microsoft.Extensions.Logging.Testing; using Microsoft.Net.Http.Headers; using Xunit; +using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.ResponseCaching.Tests { @@ -28,10 +30,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [MemberData(nameof(CacheableMethods))] public void IsRequestCacheable_CacheableMethods_Allowed(string method) { - var context = TestUtils.CreateTestContext(); + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); context.HttpContext.Request.Method = method; Assert.True(new ResponseCachePolicyProvider().IsRequestCacheable(context)); + Assert.Empty(sink.Writes); } public static TheoryData NonCacheableMethods { @@ -55,26 +59,35 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [MemberData(nameof(NonCacheableMethods))] public void IsRequestCacheable_UncacheableMethods_NotAllowed(string method) { - var context = TestUtils.CreateTestContext(); + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); context.HttpContext.Request.Method = method; Assert.False(new ResponseCachePolicyProvider().IsRequestCacheable(context)); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.RequestMethodNotCacheable); } [Fact] public void IsRequestCacheable_AuthorizationHeaders_NotAllowed() { - var context = TestUtils.CreateTestContext(); + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); context.HttpContext.Request.Method = HttpMethods.Get; context.HttpContext.Request.Headers[HeaderNames.Authorization] = "Basic plaintextUN:plaintextPW"; Assert.False(new ResponseCachePolicyProvider().IsRequestCacheable(context)); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.RequestWithAuthorizationNotCacheable); } [Fact] public void IsRequestCacheable_NoCache_NotAllowed() { - var context = TestUtils.CreateTestContext(); + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); context.HttpContext.Request.Method = HttpMethods.Get; context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() { @@ -82,12 +95,16 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests }; Assert.False(new ResponseCachePolicyProvider().IsRequestCacheable(context)); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.RequestWithNoCacheNotCacheable); } [Fact] public void IsRequestCacheable_NoStore_Allowed() { - var context = TestUtils.CreateTestContext(); + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); context.HttpContext.Request.Method = HttpMethods.Get; context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() { @@ -95,53 +112,67 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests }; Assert.True(new ResponseCachePolicyProvider().IsRequestCacheable(context)); + Assert.Empty(sink.Writes); } [Fact] public void IsRequestCacheable_LegacyDirectives_NotAllowed() { - var context = TestUtils.CreateTestContext(); + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); context.HttpContext.Request.Method = HttpMethods.Get; context.HttpContext.Request.Headers[HeaderNames.Pragma] = "no-cache"; Assert.False(new ResponseCachePolicyProvider().IsRequestCacheable(context)); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.RequestWithPragmaNoCacheNotCacheable); } [Fact] public void IsRequestCacheable_LegacyDirectives_OverridenByCacheControl() { - var context = TestUtils.CreateTestContext(); + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); context.HttpContext.Request.Method = HttpMethods.Get; context.HttpContext.Request.Headers[HeaderNames.Pragma] = "no-cache"; context.HttpContext.Request.Headers[HeaderNames.CacheControl] = "max-age=10"; Assert.True(new ResponseCachePolicyProvider().IsRequestCacheable(context)); + Assert.Empty(sink.Writes); } [Fact] public void IsResponseCacheable_NoPublic_NotAllowed() { - var context = TestUtils.CreateTestContext(); + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.ResponseWithoutPublicNotCacheable); } [Fact] public void IsResponseCacheable_Public_Allowed() { - var context = TestUtils.CreateTestContext(); + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() { Public = true }; Assert.True(new ResponseCachePolicyProvider().IsResponseCacheable(context)); + Assert.Empty(sink.Writes); } [Fact] public void IsResponseCacheable_NoCache_NotAllowed() { - var context = TestUtils.CreateTestContext(); + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() { Public = true, @@ -149,12 +180,16 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests }; Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.ResponseWithNoCacheNotCacheable); } [Fact] public void IsResponseCacheable_RequestNoStore_NotAllowed() { - var context = TestUtils.CreateTestContext(); + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() { NoStore = true @@ -165,12 +200,16 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests }; Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.ResponseWithNoStoreNotCacheable); } [Fact] public void IsResponseCacheable_ResponseNoStore_NotAllowed() { - var context = TestUtils.CreateTestContext(); + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() { Public = true, @@ -178,12 +217,16 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests }; Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.ResponseWithNoStoreNotCacheable); } [Fact] public void IsResponseCacheable_SetCookieHeader_NotAllowed() { - var context = TestUtils.CreateTestContext(); + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() { Public = true @@ -191,12 +234,16 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.HttpContext.Response.Headers[HeaderNames.SetCookie] = "cookieName=cookieValue"; Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.ResponseWithSetCookieNotCacheable); } [Fact] public void IsResponseCacheable_VaryHeaderByStar_NotAllowed() { - var context = TestUtils.CreateTestContext(); + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() { Public = true @@ -204,12 +251,16 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.HttpContext.Response.Headers[HeaderNames.Vary] = "*"; Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.ResponseWithVaryStarNotCacheable); } [Fact] public void IsResponseCacheable_Private_NotAllowed() { - var context = TestUtils.CreateTestContext(); + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() { Public = true, @@ -217,13 +268,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests }; Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.ResponseWithPrivateNotCacheable); } [Theory] [InlineData(StatusCodes.Status200OK)] public void IsResponseCacheable_SuccessStatusCodes_Allowed(int statusCode) { - var context = TestUtils.CreateTestContext(); + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.StatusCode = statusCode; context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() { @@ -231,6 +286,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests }; Assert.True(new ResponseCachePolicyProvider().IsResponseCacheable(context)); + Assert.Empty(sink.Writes); } [Theory] @@ -284,7 +340,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [InlineData(StatusCodes.Status507InsufficientStorage)] public void IsResponseCacheable_NonSuccessStatusCodes_NotAllowed(int statusCode) { - var context = TestUtils.CreateTestContext(); + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.StatusCode = statusCode; context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() { @@ -292,12 +349,16 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests }; Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.ResponseWithUnsuccessfulStatusCodeNotCacheable); } [Fact] public void IsResponseCacheable_NoExpiryRequirements_IsAllowed() { - var context = TestUtils.CreateTestContext(); + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() { @@ -309,12 +370,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.ResponseTime = DateTimeOffset.MaxValue; Assert.True(new ResponseCachePolicyProvider().IsResponseCacheable(context)); + Assert.Empty(sink.Writes); } [Fact] public void IsResponseCacheable_AtExpiry_NotAllowed() { - var context = TestUtils.CreateTestContext(); + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() { @@ -327,13 +390,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.ResponseTime = utcNow; Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.ExpirationExpiresExceeded); } [Fact] public void IsResponseCacheable_MaxAgeOverridesExpiry_ToAllowed() { var utcNow = DateTimeOffset.UtcNow; - var context = TestUtils.CreateTestContext(); + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() { @@ -345,13 +412,15 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.ResponseTime = utcNow + TimeSpan.FromSeconds(9); Assert.True(new ResponseCachePolicyProvider().IsResponseCacheable(context)); + Assert.Empty(sink.Writes); } [Fact] public void IsResponseCacheable_MaxAgeOverridesExpiry_ToNotAllowed() { var utcNow = DateTimeOffset.UtcNow; - var context = TestUtils.CreateTestContext(); + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() { @@ -363,13 +432,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.ResponseTime = utcNow + TimeSpan.FromSeconds(10); Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.ExpirationMaxAgeExceeded); } [Fact] public void IsResponseCacheable_SharedMaxAgeOverridesMaxAge_ToAllowed() { var utcNow = DateTimeOffset.UtcNow; - var context = TestUtils.CreateTestContext(); + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() { @@ -381,13 +454,15 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.ResponseTime = utcNow + TimeSpan.FromSeconds(11); Assert.True(new ResponseCachePolicyProvider().IsResponseCacheable(context)); + Assert.Empty(sink.Writes); } [Fact] public void IsResponseCacheable_SharedMaxAgeOverridesMaxAge_ToNotAllowed() { var utcNow = DateTimeOffset.UtcNow; - var context = TestUtils.CreateTestContext(); + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() { @@ -399,25 +474,31 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.ResponseTime = utcNow + TimeSpan.FromSeconds(5); Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.ExpirationSharedMaxAgeExceeded); } [Fact] public void IsCachedEntryFresh_NoCachedCacheControl_FallsbackToEmptyCacheControl() { var utcNow = DateTimeOffset.UtcNow; - var context = TestUtils.CreateTestContext(); + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); context.ResponseTime = DateTimeOffset.MaxValue; context.CachedEntryAge = TimeSpan.MaxValue; context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); Assert.True(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); + Assert.Empty(sink.Writes); } [Fact] public void IsCachedEntryFresh_NoExpiryRequirements_IsFresh() { var utcNow = DateTimeOffset.UtcNow; - var context = TestUtils.CreateTestContext(); + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); context.ResponseTime = DateTimeOffset.MaxValue; context.CachedEntryAge = TimeSpan.MaxValue; context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) @@ -429,13 +510,15 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests }; Assert.True(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); + Assert.Empty(sink.Writes); } [Fact] public void IsCachedEntryFresh_AtExpiry_IsNotFresh() { var utcNow = DateTimeOffset.UtcNow; - var context = TestUtils.CreateTestContext(); + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); context.ResponseTime = utcNow; context.CachedEntryAge = TimeSpan.Zero; context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) @@ -448,13 +531,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests }; Assert.False(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.ExpirationExpiresExceeded); } [Fact] public void IsCachedEntryFresh_MaxAgeOverridesExpiry_ToFresh() { var utcNow = DateTimeOffset.UtcNow; - var context = TestUtils.CreateTestContext(); + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); context.CachedEntryAge = TimeSpan.FromSeconds(9); context.ResponseTime = utcNow + context.CachedEntryAge; context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) @@ -468,13 +555,15 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests }; Assert.True(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); + Assert.Empty(sink.Writes); } [Fact] public void IsCachedEntryFresh_MaxAgeOverridesExpiry_ToNotFresh() { var utcNow = DateTimeOffset.UtcNow; - var context = TestUtils.CreateTestContext(); + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); context.CachedEntryAge = TimeSpan.FromSeconds(10); context.ResponseTime = utcNow + context.CachedEntryAge; context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) @@ -488,13 +577,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests }; Assert.False(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.ExpirationMaxAgeExceeded); } [Fact] public void IsCachedEntryFresh_SharedMaxAgeOverridesMaxAge_ToFresh() { var utcNow = DateTimeOffset.UtcNow; - var context = TestUtils.CreateTestContext(); + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); context.CachedEntryAge = TimeSpan.FromSeconds(11); context.ResponseTime = utcNow + context.CachedEntryAge; context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) @@ -509,13 +602,15 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests }; Assert.True(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); + Assert.Empty(sink.Writes); } [Fact] public void IsCachedEntryFresh_SharedMaxAgeOverridesMaxAge_ToNotFresh() { var utcNow = DateTimeOffset.UtcNow; - var context = TestUtils.CreateTestContext(); + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); context.CachedEntryAge = TimeSpan.FromSeconds(5); context.ResponseTime = utcNow + context.CachedEntryAge; context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) @@ -530,12 +625,16 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests }; Assert.False(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.ExpirationSharedMaxAgeExceeded); } [Fact] public void IsCachedEntryFresh_MinFreshReducesFreshness_ToNotFresh() { - var context = TestUtils.CreateTestContext(); + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() { MinFresh = TimeSpan.FromSeconds(2) @@ -551,12 +650,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.CachedEntryAge = TimeSpan.FromSeconds(3); Assert.False(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.ExpirationMinFreshAdded, + LoggedMessage.ExpirationSharedMaxAgeExceeded); } [Fact] public void IsCachedEntryFresh_RequestMaxAgeRestrictAge_ToNotFresh() { - var context = TestUtils.CreateTestContext(); + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() { MaxAge = TimeSpan.FromSeconds(5) @@ -571,17 +675,21 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.CachedEntryAge = TimeSpan.FromSeconds(5); Assert.False(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.ExpirationMaxAgeExceeded); } [Fact] public void IsCachedEntryFresh_MaxStaleOverridesFreshness_ToFresh() { - var context = TestUtils.CreateTestContext(); + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() { MaxAge = TimeSpan.FromSeconds(5), MaxStale = true, // This value must be set to true in order to specify MaxStaleLimit - MaxStaleLimit = TimeSpan.FromSeconds(10) + MaxStaleLimit = TimeSpan.FromSeconds(2) }; context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) { @@ -593,17 +701,21 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.CachedEntryAge = TimeSpan.FromSeconds(6); Assert.True(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.ExpirationMaxStaleSatisfied); } [Fact] public void IsCachedEntryFresh_MaxStaleOverridesFreshness_ButStillNotFresh() { - var context = TestUtils.CreateTestContext(); + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() { MaxAge = TimeSpan.FromSeconds(5), MaxStale = true, // This value must be set to true in order to specify MaxStaleLimit - MaxStaleLimit = TimeSpan.FromSeconds(6) + MaxStaleLimit = TimeSpan.FromSeconds(1) }; context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) { @@ -615,17 +727,21 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.CachedEntryAge = TimeSpan.FromSeconds(6); Assert.False(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.ExpirationMaxAgeExceeded); } [Fact] public void IsCachedEntryFresh_MustRevalidateOverridesRequestMaxStale_ToNotFresh() { - var context = TestUtils.CreateTestContext(); + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() { MaxAge = TimeSpan.FromSeconds(5), MaxStale = true, // This value must be set to true in order to specify MaxStaleLimit - MaxStaleLimit = TimeSpan.FromSeconds(10) + MaxStaleLimit = TimeSpan.FromSeconds(2) }; context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) { @@ -638,6 +754,9 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.CachedEntryAge = TimeSpan.FromSeconds(6); Assert.False(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.ExpirationMustRevalidate); } } } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs index f21912ad77..9b59d61cb6 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs @@ -12,10 +12,13 @@ using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.ResponseCaching.Internal; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Internal; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; +using Xunit; namespace Microsoft.AspNetCore.ResponseCaching.Tests { @@ -86,6 +89,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests internal static ResponseCacheMiddleware CreateTestMiddleware( IResponseCacheStore store = null, ResponseCacheOptions options = null, + TestSink testSink = null, IResponseCacheKeyProvider keyProvider = null, IResponseCachePolicyProvider policyProvider = null) { @@ -109,6 +113,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests return new ResponseCacheMiddleware( httpContext => TaskCache.CompletedTask, Options.Create(options), + testSink == null ? (ILoggerFactory)NullLoggerFactory.Instance : new TestLoggerFactory(testSink, true), policyProvider, store, keyProvider); @@ -116,11 +121,70 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests internal static ResponseCacheContext CreateTestContext() { - return new ResponseCacheContext(new DefaultHttpContext()) + return new ResponseCacheContext(new DefaultHttpContext(), NullLogger.Instance) { ResponseTime = DateTimeOffset.UtcNow }; } + + internal static ResponseCacheContext CreateTestContext(ITestSink testSink) + { + return new ResponseCacheContext(new DefaultHttpContext(), new TestLogger("ResponseCachingTests", testSink, true)) + { + ResponseTime = DateTimeOffset.UtcNow + }; + } + + internal static void AssertLoggedMessages(List messages, params LoggedMessage[] expectedMessages) + { + Assert.Equal(messages.Count, expectedMessages.Length); + for (var i = 0; i < messages.Count; i++) + { + Assert.Equal(expectedMessages[i].EventId, messages[i].EventId); + Assert.Equal(expectedMessages[i].LogLevel, messages[i].LogLevel); + } + } + } + + internal class LoggedMessage + { + internal static LoggedMessage RequestMethodNotCacheable => new LoggedMessage(1, LogLevel.Debug); + internal static LoggedMessage RequestWithAuthorizationNotCacheable => new LoggedMessage(2, LogLevel.Debug); + internal static LoggedMessage RequestWithNoCacheNotCacheable => new LoggedMessage(3, LogLevel.Debug); + internal static LoggedMessage RequestWithPragmaNoCacheNotCacheable => new LoggedMessage(4, LogLevel.Debug); + internal static LoggedMessage ExpirationMinFreshAdded => new LoggedMessage(5, LogLevel.Debug); + internal static LoggedMessage ExpirationSharedMaxAgeExceeded => new LoggedMessage(6, LogLevel.Debug); + internal static LoggedMessage ExpirationMustRevalidate => new LoggedMessage(7, LogLevel.Debug); + internal static LoggedMessage ExpirationMaxStaleSatisfied => new LoggedMessage(8, LogLevel.Debug); + internal static LoggedMessage ExpirationMaxAgeExceeded => new LoggedMessage(9, LogLevel.Debug); + internal static LoggedMessage ExpirationExpiresExceeded => new LoggedMessage(10, LogLevel.Debug); + internal static LoggedMessage ResponseWithoutPublicNotCacheable => new LoggedMessage(11, LogLevel.Debug); + internal static LoggedMessage ResponseWithNoStoreNotCacheable => new LoggedMessage(12, LogLevel.Debug); + internal static LoggedMessage ResponseWithNoCacheNotCacheable => new LoggedMessage(13, LogLevel.Debug); + internal static LoggedMessage ResponseWithSetCookieNotCacheable => new LoggedMessage(14, LogLevel.Debug); + internal static LoggedMessage ResponseWithVaryStarNotCacheable => new LoggedMessage(15, LogLevel.Debug); + internal static LoggedMessage ResponseWithPrivateNotCacheable => new LoggedMessage(16, LogLevel.Debug); + internal static LoggedMessage ResponseWithUnsuccessfulStatusCodeNotCacheable => new LoggedMessage(17, LogLevel.Debug); + internal static LoggedMessage NotModifiedIfNoneMatchStar => new LoggedMessage(18, LogLevel.Debug); + internal static LoggedMessage NotModifiedIfNoneMatchMatched => new LoggedMessage(19, LogLevel.Debug); + internal static LoggedMessage NotModifiedIfUnmodifiedSinceSatisfied => new LoggedMessage(20, LogLevel.Debug); + internal static LoggedMessage NotModifiedServed => new LoggedMessage(21, LogLevel.Information); + internal static LoggedMessage CachedResponseServed => new LoggedMessage(22, LogLevel.Information); + internal static LoggedMessage GatewayTimeoutServed => new LoggedMessage(23, LogLevel.Information); + internal static LoggedMessage NoResponseServed => new LoggedMessage(24, LogLevel.Information); + internal static LoggedMessage VaryByRulesUpdated => new LoggedMessage(25, LogLevel.Debug); + internal static LoggedMessage ResponseCached => new LoggedMessage(26, LogLevel.Information); + internal static LoggedMessage ResponseNotCached => new LoggedMessage(27, LogLevel.Information); + internal static LoggedMessage ResponseContentLengthMismatchNotCached => new LoggedMessage(28, LogLevel.Warning); + + private LoggedMessage(int evenId, LogLevel logLevel) + { + EventId = evenId; + LogLevel = logLevel; + } + + internal int EventId { get; } + internal LogLevel LogLevel { get; } } internal class DummySendFileFeature : IHttpSendFileFeature diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json b/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json index 16096df8ea..7eefd0a959 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json @@ -7,6 +7,7 @@ "dotnet-test-xunit": "2.2.0-*", "Microsoft.AspNetCore.ResponseCaching": "0.1.0-*", "Microsoft.AspNetCore.TestHost": "1.1.0-*", + "Microsoft.Extensions.Logging.Testing": "1.1.0-*", "xunit": "2.2.0-*" }, "frameworks": { From 9d54cf1e8a848c8d12168680bb9d8fa59cb59ce0 Mon Sep 17 00:00:00 2001 From: John Luo Date: Thu, 6 Oct 2016 15:59:14 -0700 Subject: [PATCH 047/188] Move ResponseCacheFeature to new Abstractions package (#67) Move ResponseCacheFeature to new Abstractions package and update package version numbers --- ResponseCaching.sln | 7 +++++ samples/ResponseCachingSample/project.json | 2 +- .../IResponseCacheFeature.cs | 18 +++++++++++ ...NetCore.ResponseCaching.Abstractions.xproj | 21 +++++++++++++ .../Properties/AssemblyInfo.cs | 11 +++++++ .../project.json | 31 +++++++++++++++++++ .../ResponseCacheHttpContextExtensions.cs | 15 --------- .../Internal/InternalHttpContextExtensions.cs | 25 --------------- .../ResponseCacheExtensions.cs | 0 .../ResponseCacheFeature.cs | 2 +- .../ResponseCacheMiddleware.cs | 12 +++++-- ...esponseCacheServiceCollectionExtensions.cs | 0 .../project.json | 3 +- .../HttpContextInternalExtensionTests.cs | 25 --------------- .../ResponseCacheMiddlewareTests.cs | 25 ++++++++++++--- .../ResponseCacheTests.cs | 16 +++++----- .../project.json | 2 +- 17 files changed, 131 insertions(+), 84 deletions(-) create mode 100644 src/Microsoft.AspNetCore.ResponseCaching.Abstractions/IResponseCacheFeature.cs create mode 100644 src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Microsoft.AspNetCore.ResponseCaching.Abstractions.xproj create mode 100644 src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Properties/AssemblyInfo.cs create mode 100644 src/Microsoft.AspNetCore.ResponseCaching.Abstractions/project.json delete mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCacheHttpContextExtensions.cs delete mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Internal/InternalHttpContextExtensions.cs rename src/Microsoft.AspNetCore.ResponseCaching/{Extensions => }/ResponseCacheExtensions.cs (100%) rename src/Microsoft.AspNetCore.ResponseCaching/{Extensions => }/ResponseCacheServiceCollectionExtensions.cs (100%) delete mode 100644 test/Microsoft.AspNetCore.ResponseCaching.Tests/HttpContextInternalExtensionTests.cs diff --git a/ResponseCaching.sln b/ResponseCaching.sln index c052708fa0..43b8d0f0b8 100644 --- a/ResponseCaching.sln +++ b/ResponseCaching.sln @@ -20,6 +20,8 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.Respon EndProject Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.ResponseCaching", "src\Microsoft.AspNetCore.ResponseCaching\Microsoft.AspNetCore.ResponseCaching.xproj", "{D1031270-DBD3-4F02-A3DC-3E7DADE8EBE6}" EndProject +Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.ResponseCaching.Abstractions", "src\Microsoft.AspNetCore.ResponseCaching.Abstractions\Microsoft.AspNetCore.ResponseCaching.Abstractions.xproj", "{2D1022E8-CBB6-478D-A420-CB888D0EF7B7}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -38,6 +40,10 @@ Global {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 + {2D1022E8-CBB6-478D-A420-CB888D0EF7B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2D1022E8-CBB6-478D-A420-CB888D0EF7B7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2D1022E8-CBB6-478D-A420-CB888D0EF7B7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2D1022E8-CBB6-478D-A420-CB888D0EF7B7}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -46,5 +52,6 @@ Global {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} + {2D1022E8-CBB6-478D-A420-CB888D0EF7B7} = {367AABAF-E03C-4491-A9A7-BDDE8903D1B4} EndGlobalSection EndGlobal diff --git a/samples/ResponseCachingSample/project.json b/samples/ResponseCachingSample/project.json index 3ddb9bb33c..8fa43edb22 100644 --- a/samples/ResponseCachingSample/project.json +++ b/samples/ResponseCachingSample/project.json @@ -1,7 +1,7 @@ { "version": "1.1.0-*", "dependencies": { - "Microsoft.AspNetCore.ResponseCaching": "0.1.0-*", + "Microsoft.AspNetCore.ResponseCaching": "1.0.0-*", "Microsoft.AspNetCore.Server.IISIntegration": "1.1.0-*", "Microsoft.AspNetCore.Server.Kestrel": "1.1.0-*", "Microsoft.Extensions.Caching.Memory": "1.1.0-*" diff --git a/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/IResponseCacheFeature.cs b/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/IResponseCacheFeature.cs new file mode 100644 index 0000000000..2306c410f8 --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/IResponseCacheFeature.cs @@ -0,0 +1,18 @@ +// 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 +{ + /// + /// A feature for configuring additional response cache options on the HTTP response. + /// + public interface IResponseCacheFeature + { + /// + /// Gets or sets the query keys used by the response cache middleware for calculating secondary vary keys. + /// + StringValues VaryByQueryKeys { get; set; } + } +} diff --git a/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Microsoft.AspNetCore.ResponseCaching.Abstractions.xproj b/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Microsoft.AspNetCore.ResponseCaching.Abstractions.xproj new file mode 100644 index 0000000000..d06ce2e8f9 --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Microsoft.AspNetCore.ResponseCaching.Abstractions.xproj @@ -0,0 +1,21 @@ + + + + 14.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + + + + + 2d1022e8-cbb6-478d-a420-cb888d0ef7b7 + Microsoft.AspNetCore.ResponseCaching.Abstractions + .\obj + .\bin\ + v4.5.2 + + + + 2.0 + + + diff --git a/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..32dcddfc57 --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/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.Abstractions/project.json b/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/project.json new file mode 100644 index 0000000000..ee4622c44c --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/project.json @@ -0,0 +1,31 @@ +{ + "version": "1.0.0-*", + "buildOptions": { + "warningsAsErrors": true, + "keyFile": "../../tools/Key.snk", + "xmlDoc": true + }, + "description": "ASP.NET Core response caching middleware abstractions and feature interface definitions.", + "packOptions": { + "repository": { + "type": "git", + "url": "git://github.com/aspnet/ResponseCaching" + }, + "tags": [ + "aspnetcore", + "cache", + "caching" + ] + }, + "dependencies": { + "Microsoft.Extensions.Primitives": "1.1.0-*" + }, + "frameworks": { + "net451": {}, + "netstandard1.3": { + "dependencies": { + "NETStandard.Library": "1.6.1-*" + } + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCacheHttpContextExtensions.cs b/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCacheHttpContextExtensions.cs deleted file mode 100644 index d8349bf594..0000000000 --- a/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCacheHttpContextExtensions.cs +++ /dev/null @@ -1,15 +0,0 @@ -// 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 -{ - public static class ResponseCacheHttpContextExtensions - { - public static ResponseCacheFeature GetResponseCacheFeature(this HttpContext httpContext) - { - return httpContext.Features.Get(); - } - } -} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/InternalHttpContextExtensions.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/InternalHttpContextExtensions.cs deleted file mode 100644 index e76aa83a68..0000000000 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/InternalHttpContextExtensions.cs +++ /dev/null @@ -1,25 +0,0 @@ -// 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; - -namespace Microsoft.AspNetCore.ResponseCaching.Internal -{ - internal static class InternalHttpContextExtensions - { - internal static void AddResponseCacheFeature(this HttpContext httpContext) - { - if (httpContext.GetResponseCacheFeature() != null) - { - throw new InvalidOperationException($"Another instance of {nameof(ResponseCacheFeature)} already exists. Only one instance of {nameof(ResponseCacheMiddleware)} can be configured for an application."); - } - httpContext.Features.Set(new ResponseCacheFeature()); - } - - internal static void RemoveResponseCacheFeature(this HttpContext httpContext) - { - httpContext.Features.Set(null); - } - } -} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCacheExtensions.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheExtensions.cs similarity index 100% rename from src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCacheExtensions.cs rename to src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheExtensions.cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheFeature.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheFeature.cs index eec1a64887..374338a688 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheFeature.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheFeature.cs @@ -5,7 +5,7 @@ using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.ResponseCaching { - public class ResponseCacheFeature + public class ResponseCacheFeature : IResponseCacheFeature { public StringValues VaryByQueryKeys { get; set; } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs index c7939b82c8..6be81a9578 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs @@ -211,7 +211,7 @@ namespace Microsoft.AspNetCore.ResponseCaching // Create the cache entry now var response = context.HttpContext.Response; var varyHeaders = new StringValues(response.Headers.GetCommaSeparatedValues(HeaderNames.Vary)); - var varyQueryKeys = context.HttpContext.GetResponseCacheFeature()?.VaryByQueryKeys ?? StringValues.Empty; + var varyQueryKeys = context.HttpContext.Features.Get()?.VaryByQueryKeys ?? StringValues.Empty; context.CachedResponseValidFor = context.ResponseCacheControlHeaderValue.SharedMaxAge ?? context.ResponseCacheControlHeaderValue.MaxAge ?? (context.ResponseExpires - context.ResponseTime.Value) ?? @@ -325,7 +325,12 @@ namespace Microsoft.AspNetCore.ResponseCaching context.HttpContext.Features.Set(new SendFileFeatureWrapper(context.OriginalSendFileFeature, context.ResponseCacheStream)); } - context.HttpContext.AddResponseCacheFeature(); + // Add IResponseCacheFeature + if (context.HttpContext.Features.Get() != null) + { + throw new InvalidOperationException($"Another instance of {nameof(ResponseCacheFeature)} already exists. Only one instance of {nameof(ResponseCacheMiddleware)} can be configured for an application."); + } + context.HttpContext.Features.Set(new ResponseCacheFeature()); } internal static void UnshimResponseStream(ResponseCacheContext context) @@ -336,7 +341,8 @@ namespace Microsoft.AspNetCore.ResponseCaching // Unshim IHttpSendFileFeature context.HttpContext.Features.Set(context.OriginalSendFileFeature); - context.HttpContext.RemoveResponseCacheFeature(); + // Remove IResponseCacheFeature + context.HttpContext.Features.Set(null); } internal static bool ContentIsNotModified(ResponseCacheContext context) diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCacheServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheServiceCollectionExtensions.cs similarity index 100% rename from src/Microsoft.AspNetCore.ResponseCaching/Extensions/ResponseCacheServiceCollectionExtensions.cs rename to src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheServiceCollectionExtensions.cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/project.json b/src/Microsoft.AspNetCore.ResponseCaching/project.json index 5adf0b7360..04f2eae5c3 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/project.json +++ b/src/Microsoft.AspNetCore.ResponseCaching/project.json @@ -1,5 +1,5 @@ { - "version": "0.1.0-*", + "version": "1.0.0-*", "buildOptions": { "warningsAsErrors": true, "allowUnsafe": true, @@ -24,6 +24,7 @@ "dependencies": { "Microsoft.AspNetCore.Http": "1.1.0-*", "Microsoft.AspNetCore.Http.Extensions": "1.1.0-*", + "Microsoft.AspNetCore.ResponseCaching.Abstractions": "1.0.0-*", "Microsoft.Extensions.Caching.Memory": "1.1.0-*", "Microsoft.Extensions.Logging.Abstractions": "1.1.0-*", "Microsoft.Extensions.TaskCache.Sources": { diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/HttpContextInternalExtensionTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/HttpContextInternalExtensionTests.cs deleted file mode 100644 index 8efd416d16..0000000000 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/HttpContextInternalExtensionTests.cs +++ /dev/null @@ -1,25 +0,0 @@ -// 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.AspNetCore.ResponseCaching.Internal; -using Xunit; - -namespace Microsoft.AspNetCore.ResponseCaching.Tests -{ - public class HttpContextInternalExtensionTests - { - [Fact] - public void AddingSecondResponseCacheFeature_Throws() - { - var httpContext = new DefaultHttpContext(); - - // Should not throw - httpContext.AddResponseCacheFeature(); - - // Should throw - Assert.ThrowsAny(() => httpContext.AddResponseCacheFeature()); - } - } -} diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheMiddlewareTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheMiddlewareTests.cs index 9e251dc9b1..f2005ff54f 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheMiddlewareTests.cs @@ -433,8 +433,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var context = TestUtils.CreateTestContext(); context.HttpContext.Response.Headers[HeaderNames.Vary] = new StringValues(new[] { "headerA", "HEADERB", "HEADERc" }); - context.HttpContext.AddResponseCacheFeature(); - context.HttpContext.GetResponseCacheFeature().VaryByQueryKeys = new StringValues(new[] { "queryB", "QUERYA" }); + context.HttpContext.Features.Set(new ResponseCacheFeature() + { + VaryByQueryKeys = new StringValues(new[] { "queryB", "QUERYA" }) + }); var cachedVaryByRules = new CachedVaryByRules() { Headers = new StringValues(new[] { "HeaderA", "HeaderB" }), @@ -462,8 +464,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var context = TestUtils.CreateTestContext(); context.HttpContext.Response.Headers[HeaderNames.Vary] = new StringValues(new[] { "headerA", "HEADERB" }); - context.HttpContext.AddResponseCacheFeature(); - context.HttpContext.GetResponseCacheFeature().VaryByQueryKeys = new StringValues(new[] { "queryB", "QUERYA" }); + context.HttpContext.Features.Set(new ResponseCacheFeature() + { + VaryByQueryKeys = new StringValues(new[] { "queryB", "QUERYA" }) + }); var cachedVaryByRules = new CachedVaryByRules() { VaryByKeyPrefix = FastGuid.NewGuid().IdString, @@ -669,6 +673,19 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests LoggedMessage.ResponseNotCached); } + [Fact] + public void ShimResponseStream_SecondInvocation_Throws() + { + var middleware = TestUtils.CreateTestMiddleware(); + var context = TestUtils.CreateTestContext(); + + // Should not throw + middleware.ShimResponseStream(context); + + // Should throw + Assert.ThrowsAny(() => middleware.ShimResponseStream(context)); + } + [Fact] public void GetOrderCasingNormalizedStringValues_NormalizesCasingToUpper() { diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheTests.cs index bc473d12a5..68c7f2ab86 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheTests.cs @@ -104,7 +104,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var builders = TestUtils.CreateBuildersWithResponseCache(requestDelegate: async (context) => { - context.GetResponseCacheFeature().VaryByQueryKeys = "query"; + context.Features.Get().VaryByQueryKeys = "query"; await TestUtils.TestRequestDelegate(context); }); @@ -126,7 +126,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var builders = TestUtils.CreateBuildersWithResponseCache(requestDelegate: async (context) => { - context.GetResponseCacheFeature().VaryByQueryKeys = new[] { "QueryA", "queryb" }; + context.Features.Get().VaryByQueryKeys = new[] { "QueryA", "queryb" }; await TestUtils.TestRequestDelegate(context); }); @@ -148,7 +148,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var builders = TestUtils.CreateBuildersWithResponseCache(requestDelegate: async (context) => { - context.GetResponseCacheFeature().VaryByQueryKeys = new[] { "*" }; + context.Features.Get().VaryByQueryKeys = new[] { "*" }; await TestUtils.TestRequestDelegate(context); }); @@ -170,7 +170,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var builders = TestUtils.CreateBuildersWithResponseCache(requestDelegate: async (context) => { - context.GetResponseCacheFeature().VaryByQueryKeys = new[] { "QueryB", "QueryA" }; + context.Features.Get().VaryByQueryKeys = new[] { "QueryB", "QueryA" }; await TestUtils.TestRequestDelegate(context); }); @@ -192,7 +192,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var builders = TestUtils.CreateBuildersWithResponseCache(requestDelegate: async (context) => { - context.GetResponseCacheFeature().VaryByQueryKeys = new[] { "*" }; + context.Features.Get().VaryByQueryKeys = new[] { "*" }; await TestUtils.TestRequestDelegate(context); }); @@ -214,7 +214,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var builders = TestUtils.CreateBuildersWithResponseCache(requestDelegate: async (context) => { - context.GetResponseCacheFeature().VaryByQueryKeys = "query"; + context.Features.Get().VaryByQueryKeys = "query"; await TestUtils.TestRequestDelegate(context); }); @@ -236,7 +236,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var builders = TestUtils.CreateBuildersWithResponseCache(requestDelegate: async (context) => { - context.GetResponseCacheFeature().VaryByQueryKeys = new[] { "QueryA", "QueryB" }; + context.Features.Get().VaryByQueryKeys = new[] { "QueryA", "QueryB" }; await TestUtils.TestRequestDelegate(context); }); @@ -258,7 +258,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var builders = TestUtils.CreateBuildersWithResponseCache(requestDelegate: async (context) => { - context.GetResponseCacheFeature().VaryByQueryKeys = new[] { "*" }; + context.Features.Get().VaryByQueryKeys = new[] { "*" }; await TestUtils.TestRequestDelegate(context); }); diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json b/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json index 7eefd0a959..7932f53e10 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json @@ -5,7 +5,7 @@ }, "dependencies": { "dotnet-test-xunit": "2.2.0-*", - "Microsoft.AspNetCore.ResponseCaching": "0.1.0-*", + "Microsoft.AspNetCore.ResponseCaching": "1.0.0-*", "Microsoft.AspNetCore.TestHost": "1.1.0-*", "Microsoft.Extensions.Logging.Testing": "1.1.0-*", "xunit": "2.2.0-*" From 54610b8fe4cc988e9542227225d807be88efb82d Mon Sep 17 00:00:00 2001 From: John Luo Date: Thu, 6 Oct 2016 19:06:02 -0700 Subject: [PATCH 048/188] Handle null or empty vary rules --- .../ResponseCacheFeature.cs | 25 +++++++- .../ResponseCacheFeatureTests.cs | 64 +++++++++++++++++++ .../ResponseCacheMiddlewareTests.cs | 45 ++++++++++++- .../ResponseCachePolicyProviderTests.cs | 1 - 4 files changed, 131 insertions(+), 4 deletions(-) create mode 100644 test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheFeatureTests.cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheFeature.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheFeature.cs index 374338a688..4c0b129ff4 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheFeature.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheFeature.cs @@ -1,12 +1,35 @@ // 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.Primitives; namespace Microsoft.AspNetCore.ResponseCaching { public class ResponseCacheFeature : IResponseCacheFeature { - public StringValues VaryByQueryKeys { get; set; } + private StringValues _varyByQueryKeys; + + public StringValues VaryByQueryKeys + { + get + { + return _varyByQueryKeys; + } + set + { + if (value.Count > 1) + { + for (var i = 0; i < value.Count; i++) + { + if (string.IsNullOrEmpty(value[i])) + { + throw new ArgumentException($"When {nameof(value)} contains more than one value, it cannot contain a null or empty value.", nameof(value)); + } + } + } + _varyByQueryKeys = value; + } + } } } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheFeatureTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheFeatureTests.cs new file mode 100644 index 0000000000..5b2fa7016f --- /dev/null +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheFeatureTests.cs @@ -0,0 +1,64 @@ +// 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.Primitives; +using Xunit; + +namespace Microsoft.AspNetCore.ResponseCaching.Tests +{ + public class ResponseCacheFeatureTests + { + public static TheoryData ValidNullOrEmptyVaryRules + { + get + { + return new TheoryData + { + default(StringValues), + StringValues.Empty, + new StringValues((string)null), + new StringValues(string.Empty), + new StringValues((string[])null), + new StringValues(new string[0]), + new StringValues(new string[] { null }), + new StringValues(new string[] { string.Empty }) + }; + } + } + + [Theory] + [MemberData(nameof(ValidNullOrEmptyVaryRules))] + public void VaryByQueryKeys_Set_ValidEmptyValues_Succeeds(StringValues value) + { + // Does not throw + new ResponseCacheFeature().VaryByQueryKeys = value; + } + + public static TheoryData InvalidVaryRules + { + get + { + return new TheoryData + { + new StringValues(new string[] { null, null }), + new StringValues(new string[] { null, string.Empty }), + new StringValues(new string[] { string.Empty, null }), + new StringValues(new string[] { string.Empty, "Valid" }), + new StringValues(new string[] { "Valid", string.Empty }), + new StringValues(new string[] { null, "Valid" }), + new StringValues(new string[] { "Valid", null }) + }; + } + } + + + [Theory] + [MemberData(nameof(InvalidVaryRules))] + public void VaryByQueryKeys_Set_InValidEmptyValues_Throws(StringValues value) + { + // Throws + Assert.Throws(() => new ResponseCacheFeature().VaryByQueryKeys = value); + } + } +} diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheMiddlewareTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheMiddlewareTests.cs index f2005ff54f..402c3a722f 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheMiddlewareTests.cs @@ -7,8 +7,6 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Headers; using Microsoft.AspNetCore.ResponseCaching.Internal; -using Microsoft.AspNetCore.WebUtilities; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; @@ -488,6 +486,49 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests LoggedMessage.VaryByRulesUpdated); } + public static TheoryData NullOrEmptyVaryRules + { + get + { + return new TheoryData + { + default(StringValues), + StringValues.Empty, + new StringValues((string)null), + new StringValues(string.Empty), + new StringValues((string[])null), + new StringValues(new string[0]), + new StringValues(new string[] { null }), + new StringValues(new string[] { string.Empty }) + }; + } + } + + [Theory] + [MemberData(nameof(NullOrEmptyVaryRules))] + public async Task FinalizeCacheHeaders_UpdateCachedVaryByRules_NullOrEmptyRules(StringValues vary) + { + var store = new TestResponseCacheStore(); + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, store: store); + var context = TestUtils.CreateTestContext(); + + context.HttpContext.Response.Headers[HeaderNames.Vary] = vary; + context.HttpContext.Features.Set(new ResponseCacheFeature() + { + VaryByQueryKeys = vary + }); + + await middleware.TryServeFromCacheAsync(context); + await middleware.FinalizeCacheHeadersAsync(context); + + // Vary rules should not be updated + Assert.Equal(0, store.SetCount); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.NoResponseServed); + } + [Fact] public async Task FinalizeCacheHeaders_DoNotAddDate_IfSpecified() { diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachePolicyProviderTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachePolicyProviderTests.cs index 2aefdb287e..5aeda6b24a 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachePolicyProviderTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachePolicyProviderTests.cs @@ -8,7 +8,6 @@ using Microsoft.AspNetCore.ResponseCaching.Internal; using Microsoft.Extensions.Logging.Testing; using Microsoft.Net.Http.Headers; using Xunit; -using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.ResponseCaching.Tests { From 3e3746f56e714c40618bd390d5c67d714d50648f Mon Sep 17 00:00:00 2001 From: Brennan Conroy Date: Fri, 30 Sep 2016 16:36:28 -0700 Subject: [PATCH 049/188] StringBuilder extension to uppercase string --- .../Internal/ResponseCacheKeyProvider.cs | 19 +++++++++++----- .../Internal/StringBuilderExtensions.cs | 22 +++++++++++++++++++ 2 files changed, 36 insertions(+), 5 deletions(-) create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Internal/StringBuilderExtensions.cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCacheKeyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCacheKeyProvider.cs index ed586946d1..ed1e449ce0 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCacheKeyProvider.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCacheKeyProvider.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.ResponseCaching.Internal; using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; @@ -54,11 +55,19 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal try { builder - .Append(request.Method.ToUpperInvariant()) - .Append(KeyDelimiter) - .Append(_options.UseCaseSensitivePaths ? request.Path.Value : request.Path.Value.ToUpperInvariant()); + .AppendUpperInvariant(request.Method) + .Append(KeyDelimiter); - return builder.ToString();; + if (_options.UseCaseSensitivePaths) + { + builder.Append(request.Path.Value); + } + else + { + builder.AppendUpperInvariant(request.Path.Value); + } + + return builder.ToString(); } finally { @@ -123,7 +132,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal foreach (var query in context.HttpContext.Request.Query.OrderBy(q => q.Key, StringComparer.OrdinalIgnoreCase)) { builder.Append(KeyDelimiter) - .Append(query.Key.ToUpperInvariant()) + .AppendUpperInvariant(query.Key) .Append("=") .Append(query.Value); } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/StringBuilderExtensions.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/StringBuilderExtensions.cs new file mode 100644 index 0000000000..fc718ffeb0 --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/StringBuilderExtensions.cs @@ -0,0 +1,22 @@ +// 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.Text; + +namespace Microsoft.AspNetCore.ResponseCaching.Internal +{ + internal static class StringBuilderExtensions + { + internal static StringBuilder AppendUpperInvariant(this StringBuilder builder, string value) + { + builder.EnsureCapacity(builder.Length + value.Length); + for (var i = 0; i < value.Length; i++) + { + builder.Append(char.ToUpperInvariant(value[i])); + } + + return builder; + } + } +} \ No newline at end of file From 8656065f88b6de0bbe94772b95f877213ec2fdd2 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Wed, 12 Oct 2016 13:46:23 -0700 Subject: [PATCH 050/188] Updating to netcoreapp1.1 --- samples/ResponseCachingSample/project.json | 2 +- test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/ResponseCachingSample/project.json b/samples/ResponseCachingSample/project.json index 8fa43edb22..f9e5211e83 100644 --- a/samples/ResponseCachingSample/project.json +++ b/samples/ResponseCachingSample/project.json @@ -11,7 +11,7 @@ }, "frameworks": { "net451": {}, - "netcoreapp1.0": { + "netcoreapp1.1": { "dependencies": { "Microsoft.NETCore.App": { "version": "1.1.0-*", diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json b/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json index 7932f53e10..9f889a9479 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json @@ -11,7 +11,7 @@ "xunit": "2.2.0-*" }, "frameworks": { - "netcoreapp1.0": { + "netcoreapp1.1": { "dependencies": { "Microsoft.NETCore.App": { "version": "1.1.0-*", From 794cc5359b5d549571c4c20b8530b6601219034f Mon Sep 17 00:00:00 2001 From: Pranav K Date: Wed, 12 Oct 2016 16:09:33 -0700 Subject: [PATCH 051/188] Revert "Updating to netcoreapp1.1" This reverts commit 8656065f88b6de0bbe94772b95f877213ec2fdd2. --- samples/ResponseCachingSample/project.json | 2 +- test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/ResponseCachingSample/project.json b/samples/ResponseCachingSample/project.json index f9e5211e83..8fa43edb22 100644 --- a/samples/ResponseCachingSample/project.json +++ b/samples/ResponseCachingSample/project.json @@ -11,7 +11,7 @@ }, "frameworks": { "net451": {}, - "netcoreapp1.1": { + "netcoreapp1.0": { "dependencies": { "Microsoft.NETCore.App": { "version": "1.1.0-*", diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json b/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json index 9f889a9479..7932f53e10 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json @@ -11,7 +11,7 @@ "xunit": "2.2.0-*" }, "frameworks": { - "netcoreapp1.1": { + "netcoreapp1.0": { "dependencies": { "Microsoft.NETCore.App": { "version": "1.1.0-*", From bc6fff40e6e4f06df41e7bb00f65f156089c78a1 Mon Sep 17 00:00:00 2001 From: John Luo Date: Fri, 7 Oct 2016 16:12:50 -0700 Subject: [PATCH 052/188] Use 1.0.0 dependencies --- NuGet.Config | 3 ++- samples/ResponseCachingSample/project.json | 11 ++++----- .../project.json | 10 +++----- .../Internal/ResponseCachePolicyProvider.cs | 3 ++- .../Internal/TaskCache.cs | 23 +++++++++++++++++++ .../ResponseCacheMiddleware.cs | 2 +- .../project.json | 17 +++++--------- .../ResponseCachePolicyProviderTests.cs | 7 ------ .../TestUtils.cs | 15 +++++++++++- .../project.json | 18 +++++++++------ 10 files changed, 67 insertions(+), 42 deletions(-) create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Internal/TaskCache.cs diff --git a/NuGet.Config b/NuGet.Config index 0fd623ffdd..90ce61307f 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -1,7 +1,8 @@  - + + diff --git a/samples/ResponseCachingSample/project.json b/samples/ResponseCachingSample/project.json index 8fa43edb22..de721d72be 100644 --- a/samples/ResponseCachingSample/project.json +++ b/samples/ResponseCachingSample/project.json @@ -1,10 +1,9 @@ { - "version": "1.1.0-*", "dependencies": { - "Microsoft.AspNetCore.ResponseCaching": "1.0.0-*", - "Microsoft.AspNetCore.Server.IISIntegration": "1.1.0-*", - "Microsoft.AspNetCore.Server.Kestrel": "1.1.0-*", - "Microsoft.Extensions.Caching.Memory": "1.1.0-*" + "Microsoft.AspNetCore.ResponseCaching": "1.0.0", + "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0", + "Microsoft.AspNetCore.Server.Kestrel": "1.0.0", + "Microsoft.Extensions.Caching.Memory": "1.0.0" }, "buildOptions": { "emitEntryPoint": true @@ -14,7 +13,7 @@ "netcoreapp1.0": { "dependencies": { "Microsoft.NETCore.App": { - "version": "1.1.0-*", + "version": "1.0.0", "type": "platform" } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/project.json b/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/project.json index ee4622c44c..1cdc039a88 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/project.json +++ b/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/project.json @@ -1,5 +1,5 @@ { - "version": "1.0.0-*", + "version": "1.0.0", "buildOptions": { "warningsAsErrors": true, "keyFile": "../../tools/Key.snk", @@ -18,14 +18,10 @@ ] }, "dependencies": { - "Microsoft.Extensions.Primitives": "1.1.0-*" + "Microsoft.Extensions.Primitives": "1.0.0" }, "frameworks": { "net451": {}, - "netstandard1.3": { - "dependencies": { - "NETStandard.Library": "1.6.1-*" - } - } + "netstandard1.3": {} } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachePolicyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachePolicyProvider.cs index cc7f174a07..b614ddbba8 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachePolicyProvider.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachePolicyProvider.cs @@ -16,7 +16,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal { // Verify the method var request = context.HttpContext.Request; - if (!HttpMethods.IsGet(request.Method) && !HttpMethods.IsHead(request.Method)) + if (!string.Equals("GET", request.Method, StringComparison.OrdinalIgnoreCase) && + !string.Equals("HEAD", request.Method, StringComparison.OrdinalIgnoreCase)) { context.Logger.LogRequestMethodNotCacheable(request.Method); return false; diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/TaskCache.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/TaskCache.cs new file mode 100644 index 0000000000..541e2523e4 --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/TaskCache.cs @@ -0,0 +1,23 @@ +// 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; + +namespace Microsoft.Extensions.Internal +{ + internal static class TaskCache + { + /// + /// A that's already completed successfully. + /// + /// + /// We're caching this in a static readonly field to make it more inlinable and avoid the volatile lookup done + /// by Task.CompletedTask. + /// +#if NET451 + public static readonly Task CompletedTask = Task.FromResult(0); +#else + public static readonly Task CompletedTask = Task.CompletedTask; +#endif + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs index 6be81a9578..6e98dd9956 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs @@ -362,7 +362,7 @@ namespace Microsoft.AspNetCore.ResponseCaching { foreach (var tag in ifNoneMatchHeader) { - if (cachedResponseHeaders.ETag.Compare(tag, useStrongComparison: false)) + if (string.Equals(cachedResponseHeaders.ETag.Tag, tag.Tag, StringComparison.Ordinal)) { context.Logger.LogNotModifiedIfNoneMatchMatched(tag); return true; diff --git a/src/Microsoft.AspNetCore.ResponseCaching/project.json b/src/Microsoft.AspNetCore.ResponseCaching/project.json index 04f2eae5c3..e19977dba7 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/project.json +++ b/src/Microsoft.AspNetCore.ResponseCaching/project.json @@ -1,5 +1,5 @@ { - "version": "1.0.0-*", + "version": "1.0.0", "buildOptions": { "warningsAsErrors": true, "allowUnsafe": true, @@ -22,16 +22,11 @@ ] }, "dependencies": { - "Microsoft.AspNetCore.Http": "1.1.0-*", - "Microsoft.AspNetCore.Http.Extensions": "1.1.0-*", - "Microsoft.AspNetCore.ResponseCaching.Abstractions": "1.0.0-*", - "Microsoft.Extensions.Caching.Memory": "1.1.0-*", - "Microsoft.Extensions.Logging.Abstractions": "1.1.0-*", - "Microsoft.Extensions.TaskCache.Sources": { - "version": "1.1.0-*", - "type": "build" - }, - "NETStandard.Library": "1.6.1-*" + "Microsoft.AspNetCore.Http": "1.0.0", + "Microsoft.AspNetCore.Http.Extensions": "1.0.0", + "Microsoft.AspNetCore.ResponseCaching.Abstractions": "1.0.0", + "Microsoft.Extensions.Caching.Memory": "1.0.0", + "Microsoft.Extensions.Logging.Abstractions": "1.0.0" }, "frameworks": { "net451": {}, diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachePolicyProviderTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachePolicyProviderTests.cs index 5aeda6b24a..9057865c5b 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachePolicyProviderTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachePolicyProviderTests.cs @@ -295,7 +295,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [InlineData(StatusCodes.Status204NoContent)] [InlineData(StatusCodes.Status205ResetContent)] [InlineData(StatusCodes.Status206PartialContent)] - [InlineData(StatusCodes.Status207MultiStatus)] [InlineData(StatusCodes.Status300MultipleChoices)] [InlineData(StatusCodes.Status301MovedPermanently)] [InlineData(StatusCodes.Status302Found)] @@ -304,7 +303,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [InlineData(StatusCodes.Status305UseProxy)] [InlineData(StatusCodes.Status306SwitchProxy)] [InlineData(StatusCodes.Status307TemporaryRedirect)] - [InlineData(StatusCodes.Status308PermanentRedirect)] [InlineData(StatusCodes.Status400BadRequest)] [InlineData(StatusCodes.Status401Unauthorized)] [InlineData(StatusCodes.Status402PaymentRequired)] @@ -325,10 +323,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [InlineData(StatusCodes.Status417ExpectationFailed)] [InlineData(StatusCodes.Status418ImATeapot)] [InlineData(StatusCodes.Status419AuthenticationTimeout)] - [InlineData(StatusCodes.Status422UnprocessableEntity)] - [InlineData(StatusCodes.Status423Locked)] - [InlineData(StatusCodes.Status424FailedDependency)] - [InlineData(StatusCodes.Status451UnavailableForLegalReasons)] [InlineData(StatusCodes.Status500InternalServerError)] [InlineData(StatusCodes.Status501NotImplemented)] [InlineData(StatusCodes.Status502BadGateway)] @@ -336,7 +330,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [InlineData(StatusCodes.Status504GatewayTimeout)] [InlineData(StatusCodes.Status505HttpVersionNotsupported)] [InlineData(StatusCodes.Status506VariantAlsoNegotiates)] - [InlineData(StatusCodes.Status507InsufficientStorage)] public void IsResponseCacheable_NonSuccessStatusCodes_NotAllowed(int statusCode) { var sink = new TestSink(); diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs index 9b59d61cb6..250854d4c0 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs @@ -127,7 +127,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests }; } - internal static ResponseCacheContext CreateTestContext(ITestSink testSink) + internal static ResponseCacheContext CreateTestContext(TestSink testSink) { return new ResponseCacheContext(new DefaultHttpContext(), new TestLogger("ResponseCachingTests", testSink, true)) { @@ -146,6 +146,19 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } } + internal static class HttpMethods + { + public static readonly string Connect = "CONNECT"; + public static readonly string Delete = "DELETE"; + public static readonly string Get = "GET"; + public static readonly string Head = "HEAD"; + public static readonly string Options = "OPTIONS"; + public static readonly string Patch = "PATCH"; + public static readonly string Post = "POST"; + public static readonly string Put = "PUT"; + public static readonly string Trace = "TRACE"; + } + internal class LoggedMessage { internal static LoggedMessage RequestMethodNotCacheable => new LoggedMessage(1, LogLevel.Debug); diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json b/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json index 7932f53e10..ab35046c49 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json @@ -4,20 +4,24 @@ "keyFile": "../../tools/Key.snk" }, "dependencies": { - "dotnet-test-xunit": "2.2.0-*", - "Microsoft.AspNetCore.ResponseCaching": "1.0.0-*", - "Microsoft.AspNetCore.TestHost": "1.1.0-*", - "Microsoft.Extensions.Logging.Testing": "1.1.0-*", - "xunit": "2.2.0-*" + "dotnet-test-xunit": "1.0.0-rc3-000000-01", + "Microsoft.AspNetCore.ResponseCaching": "1.0.0", + "Microsoft.AspNetCore.TestHost": "1.0.0", + "Microsoft.Extensions.Logging.Testing": "1.0.0", + "xunit": "2.1.0" }, "frameworks": { "netcoreapp1.0": { "dependencies": { "Microsoft.NETCore.App": { - "version": "1.1.0-*", + "version": "1.0.0", "type": "platform" } - } + }, + "imports": [ + "dnxcore50", + "portable-net451+win8" + ] }, "net451": {} }, From cbbcab2a0d908a139b23f3dbdfa104bf681d6033 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Thu, 13 Oct 2016 11:24:00 -0700 Subject: [PATCH 053/188] Updating to netcoreapp1.1 --- samples/ResponseCachingSample/project.json | 2 +- test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/ResponseCachingSample/project.json b/samples/ResponseCachingSample/project.json index 8fa43edb22..f9e5211e83 100644 --- a/samples/ResponseCachingSample/project.json +++ b/samples/ResponseCachingSample/project.json @@ -11,7 +11,7 @@ }, "frameworks": { "net451": {}, - "netcoreapp1.0": { + "netcoreapp1.1": { "dependencies": { "Microsoft.NETCore.App": { "version": "1.1.0-*", diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json b/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json index 7932f53e10..9f889a9479 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json @@ -11,7 +11,7 @@ "xunit": "2.2.0-*" }, "frameworks": { - "netcoreapp1.0": { + "netcoreapp1.1": { "dependencies": { "Microsoft.NETCore.App": { "version": "1.1.0-*", From c46a019a6b8266aaa15487876b6c17b8a7f3c6b6 Mon Sep 17 00:00:00 2001 From: moozzyk Date: Thu, 13 Oct 2016 16:25:45 -0700 Subject: [PATCH 054/188] Updating dependency versions in the sample project --- samples/ResponseCachingSample/project.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/samples/ResponseCachingSample/project.json b/samples/ResponseCachingSample/project.json index de721d72be..e1a1d1740f 100644 --- a/samples/ResponseCachingSample/project.json +++ b/samples/ResponseCachingSample/project.json @@ -2,7 +2,7 @@ "dependencies": { "Microsoft.AspNetCore.ResponseCaching": "1.0.0", "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0", - "Microsoft.AspNetCore.Server.Kestrel": "1.0.0", + "Microsoft.AspNetCore.Server.Kestrel": "1.0.1", "Microsoft.Extensions.Caching.Memory": "1.0.0" }, "buildOptions": { @@ -13,7 +13,7 @@ "netcoreapp1.0": { "dependencies": { "Microsoft.NETCore.App": { - "version": "1.0.0", + "version": "1.0.1", "type": "platform" } } From 151ac2cdcba0af50ffedd83b90a13afe9abb920e Mon Sep 17 00:00:00 2001 From: John Luo Date: Thu, 13 Oct 2016 16:30:19 -0700 Subject: [PATCH 055/188] Add Microsoft.AspNetCore.ResponseCaching.Abstractions to NugetPackageVerifier.json --- NuGetPackageVerifier.json | 1 + 1 file changed, 1 insertion(+) diff --git a/NuGetPackageVerifier.json b/NuGetPackageVerifier.json index 27720e2307..65269108ed 100644 --- a/NuGetPackageVerifier.json +++ b/NuGetPackageVerifier.json @@ -5,6 +5,7 @@ ], "packages": { "Microsoft.AspNetCore.ResponseCaching": { }, + "Microsoft.AspNetCore.ResponseCaching.Abstractions": { }, } }, "Default": { // Rules to run for packages not listed in any other set. From 1571a2dbe98a1708132f6289d5d405e65e5319a3 Mon Sep 17 00:00:00 2001 From: moozzyk Date: Fri, 14 Oct 2016 13:14:23 -0700 Subject: [PATCH 056/188] Removing double casts --- .gitignore | 3 ++- .../Internal/MemoryResponseCacheStore.cs | 18 +++++++------- .../ResponseCacheMiddleware.cs | 24 ++++++++++++------- 3 files changed, 27 insertions(+), 18 deletions(-) diff --git a/.gitignore b/.gitignore index 718941ca08..e71f35b3a6 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,5 @@ nuget.exe project.lock.json /.vs/ .build/ -.testPublish/ \ No newline at end of file +.testPublish/ +launchSettings.json \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCacheStore.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCacheStore.cs index 1a49f1917e..02191c9603 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCacheStore.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCacheStore.cs @@ -24,11 +24,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal public Task GetAsync(string key) { var entry = _cache.Get(key); - - if (entry is MemoryCachedResponse) + + var memoryCachedResponse = entry as MemoryCachedResponse; + if (memoryCachedResponse != null) { - var memoryCachedResponse = (MemoryCachedResponse)entry; - return Task.FromResult(new CachedResponse() + return Task.FromResult(new CachedResponse { Created = memoryCachedResponse.Created, StatusCode = memoryCachedResponse.StatusCode, @@ -44,15 +44,15 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal public async Task SetAsync(string key, IResponseCacheEntry entry, TimeSpan validFor) { - if (entry is CachedResponse) + var cachedResponse = entry as CachedResponse; + if (cachedResponse != null) { - var cachedResponse = (CachedResponse)entry; var segmentStream = new SegmentWriteStream(StreamUtilities.BodySegmentSize); await cachedResponse.Body.CopyToAsync(segmentStream); _cache.Set( key, - new MemoryCachedResponse() + new MemoryCachedResponse { Created = cachedResponse.Created, StatusCode = cachedResponse.StatusCode, @@ -60,7 +60,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal BodySegments = segmentStream.GetSegments(), BodyLength = segmentStream.Length }, - new MemoryCacheEntryOptions() + new MemoryCacheEntryOptions { AbsoluteExpirationRelativeToNow = validFor }); @@ -70,7 +70,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal _cache.Set( key, entry, - new MemoryCacheEntryOptions() + new MemoryCacheEntryOptions { AbsoluteExpirationRelativeToNow = validFor }); diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs index 6be81a9578..000e03137a 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs @@ -111,8 +111,14 @@ namespace Microsoft.AspNetCore.ResponseCaching } } - internal async Task TryServeCachedResponseAsync(ResponseCacheContext context, CachedResponse cachedResponse) + internal async Task TryServeCachedResponseAsync(ResponseCacheContext context, IResponseCacheEntry cacheEntry) { + var cachedResponse = cacheEntry as CachedResponse; + if (cachedResponse == null) + { + return false; + } + context.CachedResponse = cachedResponse; context.CachedResponseHeaders = new ResponseHeaders(cachedResponse.Headers); context.ResponseTime = _options.SystemClock.UtcNow; @@ -171,24 +177,26 @@ namespace Microsoft.AspNetCore.ResponseCaching context.BaseKey = _keyProvider.CreateBaseKey(context); var cacheEntry = await _store.GetAsync(context.BaseKey); - if (cacheEntry is CachedVaryByRules) + var cachedVaryByRules = cacheEntry as CachedVaryByRules; + if (cachedVaryByRules != null) { // Request contains vary rules, recompute key(s) and try again - context.CachedVaryByRules = (CachedVaryByRules)cacheEntry; + context.CachedVaryByRules = cachedVaryByRules; foreach (var varyKey in _keyProvider.CreateLookupVaryByKeys(context)) { - cacheEntry = await _store.GetAsync(varyKey); - - if (cacheEntry is CachedResponse && await TryServeCachedResponseAsync(context, (CachedResponse)cacheEntry)) + if (await TryServeCachedResponseAsync(context, await _store.GetAsync(varyKey))) { return true; } } } - else if (cacheEntry is CachedResponse && await TryServeCachedResponseAsync(context, (CachedResponse)cacheEntry)) + else { - return true; + if (await TryServeCachedResponseAsync(context, cacheEntry)) + { + return true; + } } if (context.RequestCacheControlHeaderValue.OnlyIfCached) From cfa62bb440a875bbac178178df65f338ad888708 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Mon, 17 Oct 2016 09:49:40 -0700 Subject: [PATCH 057/188] Branching for 1.1.0-preview1 --- NuGet.Config | 4 ++-- build.ps1 | 2 +- build.sh | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/NuGet.Config b/NuGet.Config index 0fd623ffdd..ad973186eb 100644 --- a/NuGet.Config +++ b/NuGet.Config @@ -1,7 +1,7 @@ - + - + diff --git a/build.ps1 b/build.ps1 index 8f2f99691a..787f63ac02 100644 --- a/build.ps1 +++ b/build.ps1 @@ -33,7 +33,7 @@ cd $PSScriptRoot $repoFolder = $PSScriptRoot $env:REPO_FOLDER = $repoFolder -$koreBuildZip="https://github.com/aspnet/KoreBuild/archive/dev.zip" +$koreBuildZip="https://github.com/aspnet/KoreBuild/archive/rel/1.1.0-preview1.zip" if ($env:KOREBUILD_ZIP) { $koreBuildZip=$env:KOREBUILD_ZIP diff --git a/build.sh b/build.sh index f4208100eb..355c682856 100755 --- a/build.sh +++ b/build.sh @@ -2,7 +2,7 @@ repoFolder="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" cd $repoFolder -koreBuildZip="https://github.com/aspnet/KoreBuild/archive/dev.zip" +koreBuildZip="https://github.com/aspnet/KoreBuild/archive/rel/1.1.0-preview1.zip" if [ ! -z $KOREBUILD_ZIP ]; then koreBuildZip=$KOREBUILD_ZIP fi From 8acf71457ebe76bbe1409f0cb20e1e71e45b2d77 Mon Sep 17 00:00:00 2001 From: John Luo Date: Fri, 14 Oct 2016 15:44:21 -0700 Subject: [PATCH 058/188] API review feedback Rename middleware components ResponseCache => ResponseCaching Move ResponseCachingOptions to Microsoft.AspNetCore.ResponseCaching namespace Rename extension methods --- samples/ResponseCachingSample/Startup.cs | 4 +- ...eFeature.cs => IResponseCachingFeature.cs} | 6 +- .../project.json | 10 +- ...esponseCacheStore.cs => IResponseCache.cs} | 2 +- ...ider.cs => IResponseCachingKeyProvider.cs} | 14 +- ...r.cs => IResponseCachingPolicyProvider.cs} | 14 +- ...seCacheStore.cs => MemoryResponseCache.cs} | 4 +- ...heContext.cs => ResponseCachingContext.cs} | 6 +- ...vider.cs => ResponseCachingKeyProvider.cs} | 16 +- ...er.cs => ResponseCachingPolicyProvider.cs} | 8 +- .../Internal/SendFileFeatureWrapper.cs | 8 +- .../ResponseCacheExtensions.cs | 36 ----- ...esponseCacheServiceCollectionExtensions.cs | 28 ---- .../ResponseCachingExtensions.cs | 22 +++ ...heFeature.cs => ResponseCachingFeature.cs} | 11 +- ...leware.cs => ResponseCachingMiddleware.cs} | 83 +++++----- ...heOptions.cs => ResponseCachingOptions.cs} | 4 +- .../ResponseCachingServicesExtensions.cs | 59 +++++++ ...acheStream.cs => ResponseCachingStream.cs} | 4 +- .../ResponseCacheFeatureTests.cs | 64 -------- .../ResponseCachingFeatureTests.cs | 59 +++++++ ....cs => ResponseCachingKeyProviderTests.cs} | 27 ++-- ...s.cs => ResponseCachingMiddlewareTests.cs} | 136 ++++++++-------- ... => ResponseCachingPolicyProviderTests.cs} | 72 ++++----- ...eCacheTests.cs => ResponseCachingTests.cs} | 149 ++++++++++-------- .../TestUtils.cs | 82 +++++----- 26 files changed, 480 insertions(+), 448 deletions(-) rename src/Microsoft.AspNetCore.ResponseCaching.Abstractions/{IResponseCacheFeature.cs => IResponseCachingFeature.cs} (79%) rename src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/{IResponseCacheStore.cs => IResponseCache.cs} (91%) rename src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/{IResponseCacheKeyProvider.cs => IResponseCachingKeyProvider.cs} (61%) rename src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/{IResponseCachePolicyProvider.cs => IResponseCachingPolicyProvider.cs} (66%) rename src/Microsoft.AspNetCore.ResponseCaching/Internal/{MemoryResponseCacheStore.cs => MemoryResponseCache.cs} (95%) rename src/Microsoft.AspNetCore.ResponseCaching/Internal/{ResponseCacheContext.cs => ResponseCachingContext.cs} (95%) rename src/Microsoft.AspNetCore.ResponseCaching/Internal/{ResponseCacheKeyProvider.cs => ResponseCachingKeyProvider.cs} (90%) rename src/Microsoft.AspNetCore.ResponseCaching/Internal/{ResponseCachePolicyProvider.cs => ResponseCachingPolicyProvider.cs} (96%) delete mode 100644 src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheExtensions.cs delete mode 100644 src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheServiceCollectionExtensions.cs create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingExtensions.cs rename src/Microsoft.AspNetCore.ResponseCaching/{ResponseCacheFeature.cs => ResponseCachingFeature.cs} (73%) rename src/Microsoft.AspNetCore.ResponseCaching/{ResponseCacheMiddleware.cs => ResponseCachingMiddleware.cs} (84%) rename src/Microsoft.AspNetCore.ResponseCaching/{ResponseCacheOptions.cs => ResponseCachingOptions.cs} (91%) create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingServicesExtensions.cs rename src/Microsoft.AspNetCore.ResponseCaching/Streams/{ResponseCacheStream.cs => ResponseCachingStream.cs} (97%) delete mode 100644 test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheFeatureTests.cs create mode 100644 test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingFeatureTests.cs rename test/Microsoft.AspNetCore.ResponseCaching.Tests/{ResponseCacheKeyProviderTests.cs => ResponseCachingKeyProviderTests.cs} (84%) rename test/Microsoft.AspNetCore.ResponseCaching.Tests/{ResponseCacheMiddlewareTests.cs => ResponseCachingMiddlewareTests.cs} (86%) rename test/Microsoft.AspNetCore.ResponseCaching.Tests/{ResponseCachePolicyProviderTests.cs => ResponseCachingPolicyProviderTests.cs} (89%) rename test/Microsoft.AspNetCore.ResponseCaching.Tests/{ResponseCacheTests.cs => ResponseCachingTests.cs} (84%) diff --git a/samples/ResponseCachingSample/Startup.cs b/samples/ResponseCachingSample/Startup.cs index 376443ef0b..cd135b8451 100644 --- a/samples/ResponseCachingSample/Startup.cs +++ b/samples/ResponseCachingSample/Startup.cs @@ -15,12 +15,12 @@ namespace ResponseCachingSample { public void ConfigureServices(IServiceCollection services) { - services.AddMemoryResponseCacheStore(); + services.AddResponseCaching(); } public void Configure(IApplicationBuilder app) { - app.UseResponseCache(); + app.UseResponseCaching(); app.Run(async (context) => { context.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() diff --git a/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/IResponseCacheFeature.cs b/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/IResponseCachingFeature.cs similarity index 79% rename from src/Microsoft.AspNetCore.ResponseCaching.Abstractions/IResponseCacheFeature.cs rename to src/Microsoft.AspNetCore.ResponseCaching.Abstractions/IResponseCachingFeature.cs index 2306c410f8..c68c4c8c5c 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/IResponseCacheFeature.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/IResponseCachingFeature.cs @@ -1,18 +1,16 @@ // 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 { /// /// A feature for configuring additional response cache options on the HTTP response. /// - public interface IResponseCacheFeature + public interface IResponseCachingFeature { /// /// Gets or sets the query keys used by the response cache middleware for calculating secondary vary keys. /// - StringValues VaryByQueryKeys { get; set; } + string[] VaryByQueryKeys { get; set; } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/project.json b/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/project.json index 1cdc039a88..35e9e9ddec 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/project.json +++ b/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/project.json @@ -17,11 +17,13 @@ "caching" ] }, - "dependencies": { - "Microsoft.Extensions.Primitives": "1.0.0" - }, "frameworks": { "net451": {}, - "netstandard1.3": {} + "netstandard1.3": { + "dependencies": { + "System.Runtime": "4.1.0", + "System.Resources.ResourceManager": "4.0.1" + } + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCacheStore.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCache.cs similarity index 91% rename from src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCacheStore.cs rename to src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCache.cs index 2deaa41708..cd3b9da23f 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCacheStore.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCache.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; namespace Microsoft.AspNetCore.ResponseCaching.Internal { - public interface IResponseCacheStore + public interface IResponseCache { Task GetAsync(string key); Task SetAsync(string key, IResponseCacheEntry entry, TimeSpan validFor); diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCacheKeyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCachingKeyProvider.cs similarity index 61% rename from src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCacheKeyProvider.cs rename to src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCachingKeyProvider.cs index eb9d626824..ac6a20f005 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCacheKeyProvider.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCachingKeyProvider.cs @@ -5,27 +5,27 @@ using System.Collections.Generic; namespace Microsoft.AspNetCore.ResponseCaching.Internal { - public interface IResponseCacheKeyProvider + public interface IResponseCachingKeyProvider { /// /// Create a base key for a response cache entry. /// - /// The . + /// The . /// The created base key. - string CreateBaseKey(ResponseCacheContext context); + string CreateBaseKey(ResponseCachingContext context); /// /// Create a vary key for storing cached responses. /// - /// The . + /// The . /// The created vary key. - string CreateStorageVaryByKey(ResponseCacheContext context); + string CreateStorageVaryByKey(ResponseCachingContext context); /// /// Create one or more vary keys for looking up cached responses. /// - /// The . + /// The . /// An ordered containing the vary keys to try when looking up items. - IEnumerable CreateLookupVaryByKeys(ResponseCacheContext context); + IEnumerable CreateLookupVaryByKeys(ResponseCachingContext context); } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCachePolicyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCachingPolicyProvider.cs similarity index 66% rename from src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCachePolicyProvider.cs rename to src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCachingPolicyProvider.cs index 0560212018..77b3f28379 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCachePolicyProvider.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCachingPolicyProvider.cs @@ -3,27 +3,27 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal { - public interface IResponseCachePolicyProvider + public interface IResponseCachingPolicyProvider { /// /// Determine wehther the response cache middleware should be executed for the incoming HTTP request. /// - /// The . + /// The . /// true if the request is cacheable; otherwise false. - bool IsRequestCacheable(ResponseCacheContext context); + bool IsRequestCacheable(ResponseCachingContext context); /// /// Determine whether the response received by the middleware be cached for future requests. /// - /// The . + /// The . /// true if the response is cacheable; otherwise false. - bool IsResponseCacheable(ResponseCacheContext context); + bool IsResponseCacheable(ResponseCachingContext context); /// /// Determine whether the response retrieved from the response cache is fresh and be served. /// - /// The . + /// The . /// true if the cached entry is fresh; otherwise false. - bool IsCachedEntryFresh(ResponseCacheContext context); + bool IsCachedEntryFresh(ResponseCachingContext context); } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCacheStore.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs similarity index 95% rename from src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCacheStore.cs rename to src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs index 1a49f1917e..160730b907 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCacheStore.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs @@ -7,11 +7,11 @@ using Microsoft.Extensions.Caching.Memory; namespace Microsoft.AspNetCore.ResponseCaching.Internal { - public class MemoryResponseCacheStore : IResponseCacheStore + public class MemoryResponseCache : IResponseCache { private readonly IMemoryCache _cache; - public MemoryResponseCacheStore(IMemoryCache cache) + public MemoryResponseCache(IMemoryCache cache) { if (cache == null) { diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCacheContext.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingContext.cs similarity index 95% rename from src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCacheContext.cs rename to src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingContext.cs index 5d0f4cc17c..4fc7292894 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCacheContext.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingContext.cs @@ -11,7 +11,7 @@ using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.ResponseCaching.Internal { - public class ResponseCacheContext + public class ResponseCachingContext { private static readonly CacheControlHeaderValue EmptyCacheControl = new CacheControlHeaderValue(); @@ -24,7 +24,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal private DateTimeOffset? _responseExpires; private bool _parsedResponseExpires; - internal ResponseCacheContext(HttpContext httpContext, ILogger logger) + internal ResponseCachingContext(HttpContext httpContext, ILogger logger) { HttpContext = httpContext; Logger = logger; @@ -54,7 +54,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal internal Stream OriginalResponseStream { get; set; } - internal ResponseCacheStream ResponseCacheStream { get; set; } + internal ResponseCachingStream ResponseCachingStream { get; set; } internal IHttpSendFileFeature OriginalSendFileFeature { get; set; } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCacheKeyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingKeyProvider.cs similarity index 90% rename from src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCacheKeyProvider.cs rename to src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingKeyProvider.cs index ed1e449ce0..334022647a 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCacheKeyProvider.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingKeyProvider.cs @@ -5,23 +5,21 @@ using System; using System.Collections.Generic; using System.Linq; using System.Text; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.ResponseCaching.Internal; using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.ResponseCaching.Internal { - public class ResponseCacheKeyProvider : IResponseCacheKeyProvider + public class ResponseCachingKeyProvider : IResponseCachingKeyProvider { // Use the record separator for delimiting components of the cache key to avoid possible collisions private static readonly char KeyDelimiter = '\x1e'; private readonly ObjectPool _builderPool; - private readonly ResponseCacheOptions _options; + private readonly ResponseCachingOptions _options; - public ResponseCacheKeyProvider(ObjectPoolProvider poolProvider, IOptions options) + public ResponseCachingKeyProvider(ObjectPoolProvider poolProvider, IOptions options) { if (poolProvider == null) { @@ -36,13 +34,13 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal _options = options.Value; } - public IEnumerable CreateLookupVaryByKeys(ResponseCacheContext context) + public IEnumerable CreateLookupVaryByKeys(ResponseCachingContext context) { return new string[] { CreateStorageVaryByKey(context) }; } // GET/PATH - public string CreateBaseKey(ResponseCacheContext context) + public string CreateBaseKey(ResponseCachingContext context) { if (context == null) { @@ -76,7 +74,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal } // BaseKeyHHeaderName=HeaderValueQQueryName=QueryValue - public string CreateStorageVaryByKey(ResponseCacheContext context) + public string CreateStorageVaryByKey(ResponseCachingContext context) { if (context == null) { @@ -86,7 +84,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal var varyByRules = context.CachedVaryByRules; if (varyByRules == null) { - throw new InvalidOperationException($"{nameof(CachedVaryByRules)} must not be null on the {nameof(ResponseCacheContext)}"); + throw new InvalidOperationException($"{nameof(CachedVaryByRules)} must not be null on the {nameof(ResponseCachingContext)}"); } if ((StringValues.IsNullOrEmpty(varyByRules.Headers) && StringValues.IsNullOrEmpty(varyByRules.QueryKeys))) diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachePolicyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingPolicyProvider.cs similarity index 96% rename from src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachePolicyProvider.cs rename to src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingPolicyProvider.cs index b614ddbba8..8c5d283933 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachePolicyProvider.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingPolicyProvider.cs @@ -8,11 +8,11 @@ using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.ResponseCaching.Internal { - public class ResponseCachePolicyProvider : IResponseCachePolicyProvider + public class ResponseCachingPolicyProvider : IResponseCachingPolicyProvider { private static readonly CacheControlHeaderValue EmptyCacheControl = new CacheControlHeaderValue(); - public virtual bool IsRequestCacheable(ResponseCacheContext context) + public virtual bool IsRequestCacheable(ResponseCachingContext context) { // Verify the method var request = context.HttpContext.Request; @@ -56,7 +56,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal return true; } - public virtual bool IsResponseCacheable(ResponseCacheContext context) + public virtual bool IsResponseCacheable(ResponseCachingContext context) { // Only cache pages explicitly marked with public if (!context.ResponseCacheControlHeaderValue.Public) @@ -156,7 +156,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal return true; } - public virtual bool IsCachedEntryFresh(ResponseCacheContext context) + public virtual bool IsCachedEntryFresh(ResponseCachingContext context) { var age = context.CachedEntryAge.Value; var cachedControlHeaders = context.CachedResponseHeaders.CacheControl ?? EmptyCacheControl; diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/SendFileFeatureWrapper.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/SendFileFeatureWrapper.cs index 5d6264228e..2716e4cd37 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/SendFileFeatureWrapper.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/SendFileFeatureWrapper.cs @@ -10,18 +10,18 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal internal class SendFileFeatureWrapper : IHttpSendFileFeature { private readonly IHttpSendFileFeature _originalSendFileFeature; - private readonly ResponseCacheStream _responseCacheStream; + private readonly ResponseCachingStream _responseCachingStream; - public SendFileFeatureWrapper(IHttpSendFileFeature originalSendFileFeature, ResponseCacheStream responseCacheStream) + public SendFileFeatureWrapper(IHttpSendFileFeature originalSendFileFeature, ResponseCachingStream responseCachingStream) { _originalSendFileFeature = originalSendFileFeature; - _responseCacheStream = responseCacheStream; + _responseCachingStream = responseCachingStream; } // Flush and disable the buffer if anyone tries to call the SendFile feature. public Task SendFileAsync(string path, long offset, long? length, CancellationToken cancellation) { - _responseCacheStream.DisableBuffering(); + _responseCachingStream.DisableBuffering(); return _originalSendFileFeature.SendFileAsync(path, offset, length, cancellation); } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheExtensions.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheExtensions.cs deleted file mode 100644 index 8b96c3e8d8..0000000000 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheExtensions.cs +++ /dev/null @@ -1,36 +0,0 @@ -// 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.Extensions.Options; - -namespace Microsoft.AspNetCore.Builder -{ - public static class ResponseCacheExtensions - { - public static IApplicationBuilder UseResponseCache(this IApplicationBuilder app) - { - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } - - return app.UseMiddleware(); - } - - public static IApplicationBuilder UseResponseCache(this IApplicationBuilder app, ResponseCacheOptions options) - { - if (app == null) - { - throw new ArgumentNullException(nameof(app)); - } - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - - return app.UseMiddleware(Options.Create(options)); - } - } -} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheServiceCollectionExtensions.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheServiceCollectionExtensions.cs deleted file mode 100644 index b8020b56f9..0000000000 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheServiceCollectionExtensions.cs +++ /dev/null @@ -1,28 +0,0 @@ -// 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 ResponseCacheServiceCollectionExtensions - { - public static IServiceCollection AddMemoryResponseCacheStore(this IServiceCollection services) - { - if (services == null) - { - throw new ArgumentNullException(nameof(services)); - } - - services.AddMemoryCache(); - services.TryAdd(ServiceDescriptor.Singleton()); - services.TryAdd(ServiceDescriptor.Singleton()); - services.TryAdd(ServiceDescriptor.Singleton()); - - return services; - } - } -} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingExtensions.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingExtensions.cs new file mode 100644 index 0000000000..76b81dbccb --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingExtensions.cs @@ -0,0 +1,22 @@ +// 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.Extensions.Options; + +namespace Microsoft.AspNetCore.Builder +{ + public static class ResponseCachingExtensions + { + public static IApplicationBuilder UseResponseCaching(this IApplicationBuilder app) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + return app.UseMiddleware(); + } + } +} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheFeature.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingFeature.cs similarity index 73% rename from src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheFeature.cs rename to src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingFeature.cs index 4c0b129ff4..14232b97de 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheFeature.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingFeature.cs @@ -2,15 +2,14 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.ResponseCaching { - public class ResponseCacheFeature : IResponseCacheFeature + public class ResponseCachingFeature : IResponseCachingFeature { - private StringValues _varyByQueryKeys; + private string[] _varyByQueryKeys; - public StringValues VaryByQueryKeys + public string[] VaryByQueryKeys { get { @@ -18,9 +17,9 @@ namespace Microsoft.AspNetCore.ResponseCaching } set { - if (value.Count > 1) + if (value?.Length > 1) { - for (var i = 0; i < value.Count; i++) + for (var i = 0; i < value.Length; i++) { if (string.IsNullOrEmpty(value[i])) { diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs similarity index 84% rename from src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs rename to src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs index 6e98dd9956..72dbb5dc1c 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheMiddleware.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs @@ -4,7 +4,6 @@ using System; using System.Globalization; using System.Threading.Tasks; -using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Http.Headers; @@ -17,25 +16,25 @@ using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.ResponseCaching { - public class ResponseCacheMiddleware + public class ResponseCachingMiddleware { private static readonly TimeSpan DefaultExpirationTimeSpan = TimeSpan.FromSeconds(10); private readonly RequestDelegate _next; - private readonly ResponseCacheOptions _options; + private readonly ResponseCachingOptions _options; private readonly ILogger _logger; - private readonly IResponseCachePolicyProvider _policyProvider; - private readonly IResponseCacheStore _store; - private readonly IResponseCacheKeyProvider _keyProvider; + private readonly IResponseCachingPolicyProvider _policyProvider; + private readonly IResponseCache _cache; + private readonly IResponseCachingKeyProvider _keyProvider; private readonly Func _onStartingCallback; - public ResponseCacheMiddleware( + public ResponseCachingMiddleware( RequestDelegate next, - IOptions options, + IOptions options, ILoggerFactory loggerFactory, - IResponseCachePolicyProvider policyProvider, - IResponseCacheStore store, - IResponseCacheKeyProvider keyProvider) + IResponseCachingPolicyProvider policyProvider, + IResponseCache cache, + IResponseCachingKeyProvider keyProvider) { if (next == null) { @@ -53,9 +52,9 @@ namespace Microsoft.AspNetCore.ResponseCaching { throw new ArgumentNullException(nameof(policyProvider)); } - if (store == null) + if (cache == null) { - throw new ArgumentNullException(nameof(store)); + throw new ArgumentNullException(nameof(cache)); } if (keyProvider == null) { @@ -64,16 +63,16 @@ namespace Microsoft.AspNetCore.ResponseCaching _next = next; _options = options.Value; - _logger = loggerFactory.CreateLogger(); + _logger = loggerFactory.CreateLogger(); _policyProvider = policyProvider; - _store = store; + _cache = cache; _keyProvider = keyProvider; - _onStartingCallback = state => OnResponseStartingAsync((ResponseCacheContext)state); + _onStartingCallback = state => OnResponseStartingAsync((ResponseCachingContext)state); } public async Task Invoke(HttpContext httpContext) { - var context = new ResponseCacheContext(httpContext, _logger); + var context = new ResponseCachingContext(httpContext, _logger); // Should we attempt any caching logic? if (_policyProvider.IsRequestCacheable(context)) @@ -111,7 +110,7 @@ namespace Microsoft.AspNetCore.ResponseCaching } } - internal async Task TryServeCachedResponseAsync(ResponseCacheContext context, CachedResponse cachedResponse) + internal async Task TryServeCachedResponseAsync(ResponseCachingContext context, CachedResponse cachedResponse) { context.CachedResponse = cachedResponse; context.CachedResponseHeaders = new ResponseHeaders(cachedResponse.Headers); @@ -166,10 +165,10 @@ namespace Microsoft.AspNetCore.ResponseCaching return false; } - internal async Task TryServeFromCacheAsync(ResponseCacheContext context) + internal async Task TryServeFromCacheAsync(ResponseCachingContext context) { context.BaseKey = _keyProvider.CreateBaseKey(context); - var cacheEntry = await _store.GetAsync(context.BaseKey); + var cacheEntry = await _cache.GetAsync(context.BaseKey); if (cacheEntry is CachedVaryByRules) { @@ -178,7 +177,7 @@ namespace Microsoft.AspNetCore.ResponseCaching foreach (var varyKey in _keyProvider.CreateLookupVaryByKeys(context)) { - cacheEntry = await _store.GetAsync(varyKey); + cacheEntry = await _cache.GetAsync(varyKey); if (cacheEntry is CachedResponse && await TryServeCachedResponseAsync(context, (CachedResponse)cacheEntry)) { @@ -202,7 +201,7 @@ namespace Microsoft.AspNetCore.ResponseCaching return false; } - internal async Task FinalizeCacheHeadersAsync(ResponseCacheContext context) + internal async Task FinalizeCacheHeadersAsync(ResponseCachingContext context) { if (_policyProvider.IsResponseCacheable(context)) { @@ -211,7 +210,7 @@ namespace Microsoft.AspNetCore.ResponseCaching // Create the cache entry now var response = context.HttpContext.Response; var varyHeaders = new StringValues(response.Headers.GetCommaSeparatedValues(HeaderNames.Vary)); - var varyQueryKeys = context.HttpContext.Features.Get()?.VaryByQueryKeys ?? StringValues.Empty; + var varyQueryKeys = new StringValues(context.HttpContext.Features.Get()?.VaryByQueryKeys); context.CachedResponseValidFor = context.ResponseCacheControlHeaderValue.SharedMaxAge ?? context.ResponseCacheControlHeaderValue.MaxAge ?? (context.ResponseExpires - context.ResponseTime.Value) ?? @@ -239,7 +238,7 @@ namespace Microsoft.AspNetCore.ResponseCaching // Always overwrite the CachedVaryByRules to update the expiry information _logger.LogVaryByRulesUpdated(normalizedVaryHeaders, normalizedVaryQueryKeys); - await _store.SetAsync(context.BaseKey, context.CachedVaryByRules, context.CachedResponseValidFor); + await _cache.SetAsync(context.BaseKey, context.CachedVaryByRules, context.CachedResponseValidFor); context.StorageVaryKey = _keyProvider.CreateStorageVaryByKey(context); } @@ -269,21 +268,21 @@ namespace Microsoft.AspNetCore.ResponseCaching } else { - context.ResponseCacheStream.DisableBuffering(); + context.ResponseCachingStream.DisableBuffering(); } } - internal async Task FinalizeCacheBodyAsync(ResponseCacheContext context) + internal async Task FinalizeCacheBodyAsync(ResponseCachingContext context) { var contentLength = context.TypedResponseHeaders.ContentLength; - if (context.ShouldCacheResponse && context.ResponseCacheStream.BufferingEnabled) + if (context.ShouldCacheResponse && context.ResponseCachingStream.BufferingEnabled) { - var bufferStream = context.ResponseCacheStream.GetBufferStream(); + var bufferStream = context.ResponseCachingStream.GetBufferStream(); if (!contentLength.HasValue || contentLength == bufferStream.Length) { context.CachedResponse.Body = bufferStream; _logger.LogResponseCached(); - await _store.SetAsync(context.StorageVaryKey ?? context.BaseKey, context.CachedResponse, context.CachedResponseValidFor); + await _cache.SetAsync(context.StorageVaryKey ?? context.BaseKey, context.CachedResponse, context.CachedResponseValidFor); } else { @@ -296,7 +295,7 @@ namespace Microsoft.AspNetCore.ResponseCaching } } - internal Task OnResponseStartingAsync(ResponseCacheContext context) + internal Task OnResponseStartingAsync(ResponseCachingContext context) { if (!context.ResponseStarted) { @@ -311,29 +310,29 @@ namespace Microsoft.AspNetCore.ResponseCaching } } - internal void ShimResponseStream(ResponseCacheContext context) + internal void ShimResponseStream(ResponseCachingContext context) { // Shim response stream context.OriginalResponseStream = context.HttpContext.Response.Body; - context.ResponseCacheStream = new ResponseCacheStream(context.OriginalResponseStream, _options.MaximumBodySize, StreamUtilities.BodySegmentSize); - context.HttpContext.Response.Body = context.ResponseCacheStream; + context.ResponseCachingStream = new ResponseCachingStream(context.OriginalResponseStream, _options.MaximumBodySize, StreamUtilities.BodySegmentSize); + context.HttpContext.Response.Body = context.ResponseCachingStream; // Shim IHttpSendFileFeature context.OriginalSendFileFeature = context.HttpContext.Features.Get(); if (context.OriginalSendFileFeature != null) { - context.HttpContext.Features.Set(new SendFileFeatureWrapper(context.OriginalSendFileFeature, context.ResponseCacheStream)); + context.HttpContext.Features.Set(new SendFileFeatureWrapper(context.OriginalSendFileFeature, context.ResponseCachingStream)); } - // Add IResponseCacheFeature - if (context.HttpContext.Features.Get() != null) + // Add IResponseCachingFeature + if (context.HttpContext.Features.Get() != null) { - throw new InvalidOperationException($"Another instance of {nameof(ResponseCacheFeature)} already exists. Only one instance of {nameof(ResponseCacheMiddleware)} can be configured for an application."); + throw new InvalidOperationException($"Another instance of {nameof(ResponseCachingFeature)} already exists. Only one instance of {nameof(ResponseCachingMiddleware)} can be configured for an application."); } - context.HttpContext.Features.Set(new ResponseCacheFeature()); + context.HttpContext.Features.Set(new ResponseCachingFeature()); } - internal static void UnshimResponseStream(ResponseCacheContext context) + internal static void UnshimResponseStream(ResponseCachingContext context) { // Unshim response stream context.HttpContext.Response.Body = context.OriginalResponseStream; @@ -341,11 +340,11 @@ namespace Microsoft.AspNetCore.ResponseCaching // Unshim IHttpSendFileFeature context.HttpContext.Features.Set(context.OriginalSendFileFeature); - // Remove IResponseCacheFeature - context.HttpContext.Features.Set(null); + // Remove IResponseCachingFeature + context.HttpContext.Features.Set(null); } - internal static bool ContentIsNotModified(ResponseCacheContext context) + internal static bool ContentIsNotModified(ResponseCachingContext context) { var cachedResponseHeaders = context.CachedResponseHeaders; var ifNoneMatchHeader = context.TypedRequestHeaders.IfNoneMatch; diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheOptions.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingOptions.cs similarity index 91% rename from src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheOptions.cs rename to src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingOptions.cs index f530913fb9..89f9d3285d 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCacheOptions.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingOptions.cs @@ -4,9 +4,9 @@ using System.ComponentModel; using Microsoft.AspNetCore.ResponseCaching.Internal; -namespace Microsoft.AspNetCore.Builder +namespace Microsoft.AspNetCore.ResponseCaching { - public class ResponseCacheOptions + public class ResponseCachingOptions { /// /// The largest cacheable size for the response body in bytes. The default is set to 64 MB. diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingServicesExtensions.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingServicesExtensions.cs new file mode 100644 index 0000000000..99187dfd74 --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingServicesExtensions.cs @@ -0,0 +1,59 @@ +// 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 +{ + /// + /// Extension methods for the ResponseCaching middleware. + /// + public static class ResponseCachingServicesExtensions + { + /// + /// Add response caching services. + /// + /// The for adding services. + /// + public static IServiceCollection AddResponseCaching(this IServiceCollection services) + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + services.AddMemoryCache(); + services.TryAdd(ServiceDescriptor.Singleton()); + services.TryAdd(ServiceDescriptor.Singleton()); + services.TryAdd(ServiceDescriptor.Singleton()); + + return services; + } + + /// + /// Add response caching services and configure the related options. + /// + /// The for adding services. + /// A delegate to configure the . + /// + public static IServiceCollection AddResponseCaching(this IServiceCollection services, Action configureOptions) + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + if (configureOptions == null) + { + throw new ArgumentNullException(nameof(configureOptions)); + } + + services.Configure(configureOptions); + services.AddResponseCaching(); + + return services; + } + } +} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Streams/ResponseCacheStream.cs b/src/Microsoft.AspNetCore.ResponseCaching/Streams/ResponseCachingStream.cs similarity index 97% rename from src/Microsoft.AspNetCore.ResponseCaching/Streams/ResponseCacheStream.cs rename to src/Microsoft.AspNetCore.ResponseCaching/Streams/ResponseCachingStream.cs index 40fe217aec..6644063b88 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Streams/ResponseCacheStream.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Streams/ResponseCachingStream.cs @@ -8,14 +8,14 @@ using System.Threading.Tasks; namespace Microsoft.AspNetCore.ResponseCaching.Internal { - internal class ResponseCacheStream : Stream + internal class ResponseCachingStream : Stream { private readonly Stream _innerStream; private readonly long _maxBufferSize; private readonly int _segmentSize; private SegmentWriteStream _segmentWriteStream; - internal ResponseCacheStream(Stream innerStream, long maxBufferSize, int segmentSize) + internal ResponseCachingStream(Stream innerStream, long maxBufferSize, int segmentSize) { _innerStream = innerStream; _maxBufferSize = maxBufferSize; diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheFeatureTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheFeatureTests.cs deleted file mode 100644 index 5b2fa7016f..0000000000 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheFeatureTests.cs +++ /dev/null @@ -1,64 +0,0 @@ -// 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.Primitives; -using Xunit; - -namespace Microsoft.AspNetCore.ResponseCaching.Tests -{ - public class ResponseCacheFeatureTests - { - public static TheoryData ValidNullOrEmptyVaryRules - { - get - { - return new TheoryData - { - default(StringValues), - StringValues.Empty, - new StringValues((string)null), - new StringValues(string.Empty), - new StringValues((string[])null), - new StringValues(new string[0]), - new StringValues(new string[] { null }), - new StringValues(new string[] { string.Empty }) - }; - } - } - - [Theory] - [MemberData(nameof(ValidNullOrEmptyVaryRules))] - public void VaryByQueryKeys_Set_ValidEmptyValues_Succeeds(StringValues value) - { - // Does not throw - new ResponseCacheFeature().VaryByQueryKeys = value; - } - - public static TheoryData InvalidVaryRules - { - get - { - return new TheoryData - { - new StringValues(new string[] { null, null }), - new StringValues(new string[] { null, string.Empty }), - new StringValues(new string[] { string.Empty, null }), - new StringValues(new string[] { string.Empty, "Valid" }), - new StringValues(new string[] { "Valid", string.Empty }), - new StringValues(new string[] { null, "Valid" }), - new StringValues(new string[] { "Valid", null }) - }; - } - } - - - [Theory] - [MemberData(nameof(InvalidVaryRules))] - public void VaryByQueryKeys_Set_InValidEmptyValues_Throws(StringValues value) - { - // Throws - Assert.Throws(() => new ResponseCacheFeature().VaryByQueryKeys = value); - } - } -} diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingFeatureTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingFeatureTests.cs new file mode 100644 index 0000000000..3d5b57bf65 --- /dev/null +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingFeatureTests.cs @@ -0,0 +1,59 @@ +// 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 Xunit; + +namespace Microsoft.AspNetCore.ResponseCaching.Tests +{ + public class ResponseCachingFeatureTests + { + public static TheoryData ValidNullOrEmptyVaryRules + { + get + { + return new TheoryData + { + null, + new string[0], + new string[] { null }, + new string[] { string.Empty } + }; + } + } + + [Theory] + [MemberData(nameof(ValidNullOrEmptyVaryRules))] + public void VaryByQueryKeys_Set_ValidEmptyValues_Succeeds(string[] value) + { + // Does not throw + new ResponseCachingFeature().VaryByQueryKeys = value; + } + + public static TheoryData InvalidVaryRules + { + get + { + return new TheoryData + { + new string[] { null, null }, + new string[] { null, string.Empty }, + new string[] { string.Empty, null }, + new string[] { string.Empty, "Valid" }, + new string[] { "Valid", string.Empty }, + new string[] { null, "Valid" }, + new string[] { "Valid", null } + }; + } + } + + + [Theory] + [MemberData(nameof(InvalidVaryRules))] + public void VaryByQueryKeys_Set_InValidEmptyValues_Throws(string[] value) + { + // Throws + Assert.Throws(() => new ResponseCachingFeature().VaryByQueryKeys = value); + } + } +} diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheKeyProviderTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingKeyProviderTests.cs similarity index 84% rename from test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheKeyProviderTests.cs rename to test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingKeyProviderTests.cs index 87126b90e8..dbe9996e56 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheKeyProviderTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingKeyProviderTests.cs @@ -2,19 +2,18 @@ // 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.Http; using Microsoft.AspNetCore.ResponseCaching.Internal; using Xunit; namespace Microsoft.AspNetCore.ResponseCaching.Tests { - public class ResponseCacheKeyProviderTests + public class ResponseCachingKeyProviderTests { private static readonly char KeyDelimiter = '\x1e'; [Fact] - public void ResponseCacheKeyProvider_CreateStorageBaseKey_IncludesOnlyNormalizedMethodAndPath() + public void ResponseCachingKeyProvider_CreateStorageBaseKey_IncludesOnlyNormalizedMethodAndPath() { var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); var context = TestUtils.CreateTestContext(); @@ -29,9 +28,9 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public void ResponseCacheKeyProvider_CreateStorageBaseKey_CaseInsensitivePath_NormalizesPath() + public void ResponseCachingKeyProvider_CreateStorageBaseKey_CaseInsensitivePath_NormalizesPath() { - var cacheKeyProvider = TestUtils.CreateTestKeyProvider(new ResponseCacheOptions() + var cacheKeyProvider = TestUtils.CreateTestKeyProvider(new ResponseCachingOptions() { UseCaseSensitivePaths = false }); @@ -43,9 +42,9 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public void ResponseCacheKeyProvider_CreateStorageBaseKey_CaseSensitivePath_PreservesPathCase() + public void ResponseCachingKeyProvider_CreateStorageBaseKey_CaseSensitivePath_PreservesPathCase() { - var cacheKeyProvider = TestUtils.CreateTestKeyProvider(new ResponseCacheOptions() + var cacheKeyProvider = TestUtils.CreateTestKeyProvider(new ResponseCachingOptions() { UseCaseSensitivePaths = true }); @@ -57,7 +56,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public void ResponseCacheKeyProvider_CreateStorageVaryByKey_Throws_IfVaryByRulesIsNull() + public void ResponseCachingKeyProvider_CreateStorageVaryByKey_Throws_IfVaryByRulesIsNull() { var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); var context = TestUtils.CreateTestContext(); @@ -66,7 +65,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public void ResponseCacheKeyProvider_CreateStorageVaryKey_ReturnsCachedVaryByGuid_IfVaryByRulesIsEmpty() + public void ResponseCachingKeyProvider_CreateStorageVaryKey_ReturnsCachedVaryByGuid_IfVaryByRulesIsEmpty() { var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); var context = TestUtils.CreateTestContext(); @@ -79,7 +78,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public void ResponseCacheKeyProvider_CreateStorageVaryKey_IncludesListedHeadersOnly() + public void ResponseCachingKeyProvider_CreateStorageVaryKey_IncludesListedHeadersOnly() { var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); var context = TestUtils.CreateTestContext(); @@ -95,7 +94,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public void ResponseCacheKeyProvider_CreateStorageVaryKey_IncludesListedQueryKeysOnly() + public void ResponseCachingKeyProvider_CreateStorageVaryKey_IncludesListedQueryKeysOnly() { var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); var context = TestUtils.CreateTestContext(); @@ -111,7 +110,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public void ResponseCacheKeyProvider_CreateStorageVaryKey_IncludesQueryKeys_QueryKeyCaseInsensitive_UseQueryKeyCasing() + public void ResponseCachingKeyProvider_CreateStorageVaryKey_IncludesQueryKeys_QueryKeyCaseInsensitive_UseQueryKeyCasing() { var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); var context = TestUtils.CreateTestContext(); @@ -127,7 +126,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public void ResponseCacheKeyProvider_CreateStorageVaryKey_IncludesAllQueryKeysGivenAsterisk() + public void ResponseCachingKeyProvider_CreateStorageVaryKey_IncludesAllQueryKeysGivenAsterisk() { var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); var context = TestUtils.CreateTestContext(); @@ -145,7 +144,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public void ResponseCacheKeyProvider_CreateStorageVaryKey_IncludesListedHeadersAndQueryKeys() + public void ResponseCachingKeyProvider_CreateStorageVaryKey_IncludesListedHeadersAndQueryKeys() { var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); var context = TestUtils.CreateTestContext(); diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheMiddlewareTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs similarity index 86% rename from test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheMiddlewareTests.cs rename to test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs index 402c3a722f..fb742416b1 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs @@ -14,14 +14,14 @@ using Xunit; namespace Microsoft.AspNetCore.ResponseCaching.Tests { - public class ResponseCacheMiddlewareTests + public class ResponseCachingMiddlewareTests { [Fact] public async Task TryServeFromCacheAsync_OnlyIfCached_Serves504() { - var store = new TestResponseCacheStore(); + var cache = new TestResponseCache(); var sink = new TestSink(); - var middleware = TestUtils.CreateTestMiddleware(testSink: sink, store: store, keyProvider: new TestResponseCacheKeyProvider()); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider()); var context = TestUtils.CreateTestContext(); context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() { @@ -38,13 +38,13 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async Task TryServeFromCacheAsync_CachedResponseNotFound_Fails() { - var store = new TestResponseCacheStore(); + var cache = new TestResponseCache(); var sink = new TestSink(); - var middleware = TestUtils.CreateTestMiddleware(testSink: sink, store: store, keyProvider: new TestResponseCacheKeyProvider("BaseKey")); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider("BaseKey")); var context = TestUtils.CreateTestContext(); Assert.False(await middleware.TryServeFromCacheAsync(context)); - Assert.Equal(1, store.GetCount); + Assert.Equal(1, cache.GetCount); TestUtils.AssertLoggedMessages( sink.Writes, LoggedMessage.NoResponseServed); @@ -53,12 +53,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async Task TryServeFromCacheAsync_CachedResponseFound_Succeeds() { - var store = new TestResponseCacheStore(); + var cache = new TestResponseCache(); var sink = new TestSink(); - var middleware = TestUtils.CreateTestMiddleware(testSink: sink, store: store, keyProvider: new TestResponseCacheKeyProvider("BaseKey")); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider("BaseKey")); var context = TestUtils.CreateTestContext(); - await store.SetAsync( + await cache.SetAsync( "BaseKey", new CachedResponse() { @@ -67,7 +67,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests TimeSpan.Zero); Assert.True(await middleware.TryServeFromCacheAsync(context)); - Assert.Equal(1, store.GetCount); + Assert.Equal(1, cache.GetCount); TestUtils.AssertLoggedMessages( sink.Writes, LoggedMessage.CachedResponseServed); @@ -76,18 +76,18 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async Task TryServeFromCacheAsync_VaryByRuleFound_CachedResponseNotFound_Fails() { - var store = new TestResponseCacheStore(); + var cache = new TestResponseCache(); var sink = new TestSink(); - var middleware = TestUtils.CreateTestMiddleware(testSink: sink, store: store, keyProvider: new TestResponseCacheKeyProvider("BaseKey", "VaryKey")); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider("BaseKey", "VaryKey")); var context = TestUtils.CreateTestContext(); - await store.SetAsync( + await cache.SetAsync( "BaseKey", new CachedVaryByRules(), TimeSpan.Zero); Assert.False(await middleware.TryServeFromCacheAsync(context)); - Assert.Equal(2, store.GetCount); + Assert.Equal(2, cache.GetCount); TestUtils.AssertLoggedMessages( sink.Writes, LoggedMessage.NoResponseServed); @@ -96,16 +96,16 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async Task TryServeFromCacheAsync_VaryByRuleFound_CachedResponseFound_Succeeds() { - var store = new TestResponseCacheStore(); + var cache = new TestResponseCache(); var sink = new TestSink(); - var middleware = TestUtils.CreateTestMiddleware(testSink: sink, store: store, keyProvider: new TestResponseCacheKeyProvider("BaseKey", new[] { "VaryKey", "VaryKey2" })); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider("BaseKey", new[] { "VaryKey", "VaryKey2" })); var context = TestUtils.CreateTestContext(); - await store.SetAsync( + await cache.SetAsync( "BaseKey", new CachedVaryByRules(), TimeSpan.Zero); - await store.SetAsync( + await cache.SetAsync( "BaseKeyVaryKey2", new CachedResponse() { @@ -114,7 +114,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests TimeSpan.Zero); Assert.True(await middleware.TryServeFromCacheAsync(context)); - Assert.Equal(3, store.GetCount); + Assert.Equal(3, cache.GetCount); TestUtils.AssertLoggedMessages( sink.Writes, LoggedMessage.CachedResponseServed); @@ -123,13 +123,13 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async Task TryServeFromCacheAsync_CachedResponseFound_Serves304IfPossible() { - var store = new TestResponseCacheStore(); + var cache = new TestResponseCache(); var sink = new TestSink(); - var middleware = TestUtils.CreateTestMiddleware(testSink: sink, store: store, keyProvider: new TestResponseCacheKeyProvider("BaseKey")); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider("BaseKey")); var context = TestUtils.CreateTestContext(); context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = "*"; - await store.SetAsync( + await cache.SetAsync( "BaseKey", new CachedResponse() { @@ -138,7 +138,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests TimeSpan.Zero); Assert.True(await middleware.TryServeFromCacheAsync(context)); - Assert.Equal(1, store.GetCount); + Assert.Equal(1, cache.GetCount); TestUtils.AssertLoggedMessages( sink.Writes, LoggedMessage.NotModifiedServed); @@ -151,7 +151,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var context = TestUtils.CreateTestContext(sink); context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); - Assert.False(ResponseCacheMiddleware.ContentIsNotModified(context)); + Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context)); Assert.Empty(sink.Writes); } @@ -167,17 +167,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests // Verify modifications in the past succeeds context.CachedResponseHeaders.Date = utcNow - TimeSpan.FromSeconds(10); - Assert.True(ResponseCacheMiddleware.ContentIsNotModified(context)); + Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context)); Assert.Equal(1, sink.Writes.Count); // Verify modifications at present succeeds context.CachedResponseHeaders.Date = utcNow; - Assert.True(ResponseCacheMiddleware.ContentIsNotModified(context)); + Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context)); Assert.Equal(2, sink.Writes.Count); // Verify modifications in the future fails context.CachedResponseHeaders.Date = utcNow + TimeSpan.FromSeconds(10); - Assert.False(ResponseCacheMiddleware.ContentIsNotModified(context)); + Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context)); // Verify logging TestUtils.AssertLoggedMessages( @@ -199,19 +199,19 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests // Verify modifications in the past succeeds context.CachedResponseHeaders.Date = utcNow + TimeSpan.FromSeconds(10); context.CachedResponseHeaders.LastModified = utcNow - TimeSpan.FromSeconds(10); - Assert.True(ResponseCacheMiddleware.ContentIsNotModified(context)); + Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context)); Assert.Equal(1, sink.Writes.Count); // Verify modifications at present context.CachedResponseHeaders.Date = utcNow + TimeSpan.FromSeconds(10); context.CachedResponseHeaders.LastModified = utcNow; - Assert.True(ResponseCacheMiddleware.ContentIsNotModified(context)); + Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context)); Assert.Equal(2, sink.Writes.Count); // Verify modifications in the future fails context.CachedResponseHeaders.Date = utcNow - TimeSpan.FromSeconds(10); context.CachedResponseHeaders.LastModified = utcNow + TimeSpan.FromSeconds(10); - Assert.False(ResponseCacheMiddleware.ContentIsNotModified(context)); + Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context)); // Verify logging TestUtils.AssertLoggedMessages( @@ -233,7 +233,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.CachedResponseHeaders.LastModified = utcNow + TimeSpan.FromSeconds(10); context.TypedRequestHeaders.IfNoneMatch = new List(new[] { EntityTagHeaderValue.Any }); - Assert.True(ResponseCacheMiddleware.ContentIsNotModified(context)); + Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context)); TestUtils.AssertLoggedMessages( sink.Writes, LoggedMessage.NotModifiedIfNoneMatchStar); @@ -252,7 +252,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.CachedResponseHeaders.LastModified = utcNow - TimeSpan.FromSeconds(10); context.TypedRequestHeaders.IfNoneMatch = new List(new[] { new EntityTagHeaderValue("\"E1\"") }); - Assert.False(ResponseCacheMiddleware.ContentIsNotModified(context)); + Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context)); Assert.Empty(sink.Writes); } @@ -265,7 +265,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.TypedRequestHeaders.IfNoneMatch = new List(new[] { new EntityTagHeaderValue("\"E1\"") }); - Assert.False(ResponseCacheMiddleware.ContentIsNotModified(context)); + Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context)); Assert.Empty(sink.Writes); } @@ -296,7 +296,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.TypedRequestHeaders.IfNoneMatch = new List(new[] { requestETag }); - Assert.True(ResponseCacheMiddleware.ContentIsNotModified(context)); + Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context)); TestUtils.AssertLoggedMessages( sink.Writes, LoggedMessage.NotModifiedIfNoneMatchMatched); @@ -314,7 +314,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.TypedRequestHeaders.IfNoneMatch = new List(new[] { new EntityTagHeaderValue("\"E1\"") }); - Assert.False(ResponseCacheMiddleware.ContentIsNotModified(context)); + Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context)); Assert.Empty(sink.Writes); } @@ -322,7 +322,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests public async Task FinalizeCacheHeaders_DoNotUpdateShouldCacheResponse_IfResponseIsNotCacheable() { var sink = new TestSink(); - var middleware = TestUtils.CreateTestMiddleware(testSink: sink, policyProvider: new ResponseCachePolicyProvider()); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, policyProvider: new ResponseCachingPolicyProvider()); var context = TestUtils.CreateTestContext(); Assert.False(context.ShouldCacheResponse); @@ -338,7 +338,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests public async Task FinalizeCacheHeaders_UpdateShouldCacheResponse_IfResponseIsCacheable() { var sink = new TestSink(); - var middleware = TestUtils.CreateTestMiddleware(testSink: sink, policyProvider: new ResponseCachePolicyProvider()); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, policyProvider: new ResponseCachingPolicyProvider()); var context = TestUtils.CreateTestContext(); context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() { @@ -425,13 +425,13 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async Task FinalizeCacheHeaders_UpdateCachedVaryByRules_IfNotEquivalentToPrevious() { - var store = new TestResponseCacheStore(); + var cache = new TestResponseCache(); var sink = new TestSink(); - var middleware = TestUtils.CreateTestMiddleware(testSink: sink, store: store); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); var context = TestUtils.CreateTestContext(); context.HttpContext.Response.Headers[HeaderNames.Vary] = new StringValues(new[] { "headerA", "HEADERB", "HEADERc" }); - context.HttpContext.Features.Set(new ResponseCacheFeature() + context.HttpContext.Features.Set(new ResponseCachingFeature() { VaryByQueryKeys = new StringValues(new[] { "queryB", "QUERYA" }) }); @@ -445,7 +445,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests await middleware.TryServeFromCacheAsync(context); await middleware.FinalizeCacheHeadersAsync(context); - Assert.Equal(1, store.SetCount); + Assert.Equal(1, cache.SetCount); Assert.NotSame(cachedVaryByRules, context.CachedVaryByRules); TestUtils.AssertLoggedMessages( sink.Writes, @@ -456,13 +456,13 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async Task FinalizeCacheHeaders_UpdateCachedVaryByRules_IfEquivalentToPrevious() { - var store = new TestResponseCacheStore(); + var cache = new TestResponseCache(); var sink = new TestSink(); - var middleware = TestUtils.CreateTestMiddleware(testSink: sink, store: store); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); var context = TestUtils.CreateTestContext(); context.HttpContext.Response.Headers[HeaderNames.Vary] = new StringValues(new[] { "headerA", "HEADERB" }); - context.HttpContext.Features.Set(new ResponseCacheFeature() + context.HttpContext.Features.Set(new ResponseCachingFeature() { VaryByQueryKeys = new StringValues(new[] { "queryB", "QUERYA" }) }); @@ -478,7 +478,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests await middleware.FinalizeCacheHeadersAsync(context); // An update to the cache is always made but the entry should be the same - Assert.Equal(1, store.SetCount); + Assert.Equal(1, cache.SetCount); Assert.Same(cachedVaryByRules, context.CachedVaryByRules); TestUtils.AssertLoggedMessages( sink.Writes, @@ -508,13 +508,13 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [MemberData(nameof(NullOrEmptyVaryRules))] public async Task FinalizeCacheHeaders_UpdateCachedVaryByRules_NullOrEmptyRules(StringValues vary) { - var store = new TestResponseCacheStore(); + var cache = new TestResponseCache(); var sink = new TestSink(); - var middleware = TestUtils.CreateTestMiddleware(testSink: sink, store: store); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); var context = TestUtils.CreateTestContext(); context.HttpContext.Response.Headers[HeaderNames.Vary] = vary; - context.HttpContext.Features.Set(new ResponseCacheFeature() + context.HttpContext.Features.Set(new ResponseCachingFeature() { VaryByQueryKeys = vary }); @@ -523,7 +523,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests await middleware.FinalizeCacheHeadersAsync(context); // Vary rules should not be updated - Assert.Equal(0, store.SetCount); + Assert.Equal(0, cache.SetCount); TestUtils.AssertLoggedMessages( sink.Writes, LoggedMessage.NoResponseServed); @@ -600,9 +600,9 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async Task FinalizeCacheBody_Cache_IfContentLengthMatches() { - var store = new TestResponseCacheStore(); + var cache = new TestResponseCache(); var sink = new TestSink(); - var middleware = TestUtils.CreateTestMiddleware(testSink: sink, store: store); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); var context = TestUtils.CreateTestContext(); middleware.ShimResponseStream(context); @@ -616,7 +616,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests await middleware.FinalizeCacheBodyAsync(context); - Assert.Equal(1, store.SetCount); + Assert.Equal(1, cache.SetCount); TestUtils.AssertLoggedMessages( sink.Writes, LoggedMessage.ResponseCached); @@ -625,9 +625,9 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async Task FinalizeCacheBody_DoNotCache_IfContentLengthMismatches() { - var store = new TestResponseCacheStore(); + var cache = new TestResponseCache(); var sink = new TestSink(); - var middleware = TestUtils.CreateTestMiddleware(testSink: sink, store: store); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); var context = TestUtils.CreateTestContext(); middleware.ShimResponseStream(context); @@ -641,7 +641,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests await middleware.FinalizeCacheBodyAsync(context); - Assert.Equal(0, store.SetCount); + Assert.Equal(0, cache.SetCount); TestUtils.AssertLoggedMessages( sink.Writes, LoggedMessage.ResponseContentLengthMismatchNotCached); @@ -650,9 +650,9 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async Task FinalizeCacheBody_Cache_IfContentLengthAbsent() { - var store = new TestResponseCacheStore(); + var cache = new TestResponseCache(); var sink = new TestSink(); - var middleware = TestUtils.CreateTestMiddleware(testSink: sink, store: store); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); var context = TestUtils.CreateTestContext(); middleware.ShimResponseStream(context); @@ -665,7 +665,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests await middleware.FinalizeCacheBodyAsync(context); - Assert.Equal(1, store.SetCount); + Assert.Equal(1, cache.SetCount); TestUtils.AssertLoggedMessages( sink.Writes, LoggedMessage.ResponseCached); @@ -674,9 +674,9 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async Task FinalizeCacheBody_DoNotCache_IfShouldCacheResponseFalse() { - var store = new TestResponseCacheStore(); + var cache = new TestResponseCache(); var sink = new TestSink(); - var middleware = TestUtils.CreateTestMiddleware(testSink: sink, store: store); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); var context = TestUtils.CreateTestContext(); middleware.ShimResponseStream(context); @@ -686,7 +686,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests await middleware.FinalizeCacheBodyAsync(context); - Assert.Equal(0, store.SetCount); + Assert.Equal(0, cache.SetCount); TestUtils.AssertLoggedMessages( sink.Writes, LoggedMessage.ResponseNotCached); @@ -695,20 +695,20 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async Task FinalizeCacheBody_DoNotCache_IfBufferingDisabled() { - var store = new TestResponseCacheStore(); + var cache = new TestResponseCache(); var sink = new TestSink(); - var middleware = TestUtils.CreateTestMiddleware(testSink: sink, store: store); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); var context = TestUtils.CreateTestContext(); middleware.ShimResponseStream(context); await context.HttpContext.Response.WriteAsync(new string('0', 10)); context.ShouldCacheResponse = true; - context.ResponseCacheStream.DisableBuffering(); + context.ResponseCachingStream.DisableBuffering(); await middleware.FinalizeCacheBodyAsync(context); - Assert.Equal(0, store.SetCount); + Assert.Equal(0, cache.SetCount); TestUtils.AssertLoggedMessages( sink.Writes, LoggedMessage.ResponseNotCached); @@ -733,7 +733,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var uppercaseStrings = new StringValues(new[] { "STRINGA", "STRINGB" }); var lowercaseStrings = new StringValues(new[] { "stringA", "stringB" }); - var normalizedStrings = ResponseCacheMiddleware.GetOrderCasingNormalizedStringValues(lowercaseStrings); + var normalizedStrings = ResponseCachingMiddleware.GetOrderCasingNormalizedStringValues(lowercaseStrings); Assert.Equal(uppercaseStrings, normalizedStrings); } @@ -744,7 +744,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var orderedStrings = new StringValues(new[] { "STRINGA", "STRINGB" }); var reverseOrderStrings = new StringValues(new[] { "STRINGB", "STRINGA" }); - var normalizedStrings = ResponseCacheMiddleware.GetOrderCasingNormalizedStringValues(reverseOrderStrings); + var normalizedStrings = ResponseCachingMiddleware.GetOrderCasingNormalizedStringValues(reverseOrderStrings); Assert.Equal(orderedStrings, normalizedStrings); } @@ -754,7 +754,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var originalStrings = new StringValues(new[] { "STRINGA, STRINGB" }); - var normalizedStrings = ResponseCacheMiddleware.GetOrderCasingNormalizedStringValues(originalStrings); + var normalizedStrings = ResponseCachingMiddleware.GetOrderCasingNormalizedStringValues(originalStrings); Assert.Equal(originalStrings, normalizedStrings); } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachePolicyProviderTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingPolicyProviderTests.cs similarity index 89% rename from test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachePolicyProviderTests.cs rename to test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingPolicyProviderTests.cs index 9057865c5b..f6269d3c8e 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachePolicyProviderTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingPolicyProviderTests.cs @@ -11,7 +11,7 @@ using Xunit; namespace Microsoft.AspNetCore.ResponseCaching.Tests { - public class ResponseCachePolicyProviderTests + public class ResponseCachingPolicyProviderTests { public static TheoryData CacheableMethods { @@ -33,7 +33,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var context = TestUtils.CreateTestContext(sink); context.HttpContext.Request.Method = method; - Assert.True(new ResponseCachePolicyProvider().IsRequestCacheable(context)); + Assert.True(new ResponseCachingPolicyProvider().IsRequestCacheable(context)); Assert.Empty(sink.Writes); } public static TheoryData NonCacheableMethods @@ -62,7 +62,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var context = TestUtils.CreateTestContext(sink); context.HttpContext.Request.Method = method; - Assert.False(new ResponseCachePolicyProvider().IsRequestCacheable(context)); + Assert.False(new ResponseCachingPolicyProvider().IsRequestCacheable(context)); TestUtils.AssertLoggedMessages( sink.Writes, LoggedMessage.RequestMethodNotCacheable); @@ -76,7 +76,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.HttpContext.Request.Method = HttpMethods.Get; context.HttpContext.Request.Headers[HeaderNames.Authorization] = "Basic plaintextUN:plaintextPW"; - Assert.False(new ResponseCachePolicyProvider().IsRequestCacheable(context)); + Assert.False(new ResponseCachingPolicyProvider().IsRequestCacheable(context)); TestUtils.AssertLoggedMessages( sink.Writes, LoggedMessage.RequestWithAuthorizationNotCacheable); @@ -93,7 +93,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests NoCache = true }; - Assert.False(new ResponseCachePolicyProvider().IsRequestCacheable(context)); + Assert.False(new ResponseCachingPolicyProvider().IsRequestCacheable(context)); TestUtils.AssertLoggedMessages( sink.Writes, LoggedMessage.RequestWithNoCacheNotCacheable); @@ -110,7 +110,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests NoStore = true }; - Assert.True(new ResponseCachePolicyProvider().IsRequestCacheable(context)); + Assert.True(new ResponseCachingPolicyProvider().IsRequestCacheable(context)); Assert.Empty(sink.Writes); } @@ -122,7 +122,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.HttpContext.Request.Method = HttpMethods.Get; context.HttpContext.Request.Headers[HeaderNames.Pragma] = "no-cache"; - Assert.False(new ResponseCachePolicyProvider().IsRequestCacheable(context)); + Assert.False(new ResponseCachingPolicyProvider().IsRequestCacheable(context)); TestUtils.AssertLoggedMessages( sink.Writes, LoggedMessage.RequestWithPragmaNoCacheNotCacheable); @@ -137,7 +137,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.HttpContext.Request.Headers[HeaderNames.Pragma] = "no-cache"; context.HttpContext.Request.Headers[HeaderNames.CacheControl] = "max-age=10"; - Assert.True(new ResponseCachePolicyProvider().IsRequestCacheable(context)); + Assert.True(new ResponseCachingPolicyProvider().IsRequestCacheable(context)); Assert.Empty(sink.Writes); } @@ -147,7 +147,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); + Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); TestUtils.AssertLoggedMessages( sink.Writes, LoggedMessage.ResponseWithoutPublicNotCacheable); @@ -163,7 +163,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Public = true }; - Assert.True(new ResponseCachePolicyProvider().IsResponseCacheable(context)); + Assert.True(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); Assert.Empty(sink.Writes); } @@ -178,7 +178,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests NoCache = true }; - Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); + Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); TestUtils.AssertLoggedMessages( sink.Writes, LoggedMessage.ResponseWithNoCacheNotCacheable); @@ -198,7 +198,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Public = true }; - Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); + Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); TestUtils.AssertLoggedMessages( sink.Writes, LoggedMessage.ResponseWithNoStoreNotCacheable); @@ -215,7 +215,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests NoStore = true }; - Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); + Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); TestUtils.AssertLoggedMessages( sink.Writes, LoggedMessage.ResponseWithNoStoreNotCacheable); @@ -232,7 +232,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests }; context.HttpContext.Response.Headers[HeaderNames.SetCookie] = "cookieName=cookieValue"; - Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); + Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); TestUtils.AssertLoggedMessages( sink.Writes, LoggedMessage.ResponseWithSetCookieNotCacheable); @@ -249,7 +249,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests }; context.HttpContext.Response.Headers[HeaderNames.Vary] = "*"; - Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); + Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); TestUtils.AssertLoggedMessages( sink.Writes, LoggedMessage.ResponseWithVaryStarNotCacheable); @@ -266,7 +266,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Private = true }; - Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); + Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); TestUtils.AssertLoggedMessages( sink.Writes, LoggedMessage.ResponseWithPrivateNotCacheable); @@ -284,7 +284,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Public = true }; - Assert.True(new ResponseCachePolicyProvider().IsResponseCacheable(context)); + Assert.True(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); Assert.Empty(sink.Writes); } @@ -340,7 +340,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Public = true }; - Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); + Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); TestUtils.AssertLoggedMessages( sink.Writes, LoggedMessage.ResponseWithUnsuccessfulStatusCodeNotCacheable); @@ -361,7 +361,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.TypedResponseHeaders.Date = utcNow; context.ResponseTime = DateTimeOffset.MaxValue; - Assert.True(new ResponseCachePolicyProvider().IsResponseCacheable(context)); + Assert.True(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); Assert.Empty(sink.Writes); } @@ -381,7 +381,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.TypedResponseHeaders.Date = utcNow; context.ResponseTime = utcNow; - Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); + Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); TestUtils.AssertLoggedMessages( sink.Writes, LoggedMessage.ExpirationExpiresExceeded); @@ -403,7 +403,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.TypedResponseHeaders.Date = utcNow; context.ResponseTime = utcNow + TimeSpan.FromSeconds(9); - Assert.True(new ResponseCachePolicyProvider().IsResponseCacheable(context)); + Assert.True(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); Assert.Empty(sink.Writes); } @@ -423,7 +423,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.TypedResponseHeaders.Date = utcNow; context.ResponseTime = utcNow + TimeSpan.FromSeconds(10); - Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); + Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); TestUtils.AssertLoggedMessages( sink.Writes, LoggedMessage.ExpirationMaxAgeExceeded); @@ -445,7 +445,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.TypedResponseHeaders.Date = utcNow; context.ResponseTime = utcNow + TimeSpan.FromSeconds(11); - Assert.True(new ResponseCachePolicyProvider().IsResponseCacheable(context)); + Assert.True(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); Assert.Empty(sink.Writes); } @@ -465,7 +465,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.TypedResponseHeaders.Date = utcNow; context.ResponseTime = utcNow + TimeSpan.FromSeconds(5); - Assert.False(new ResponseCachePolicyProvider().IsResponseCacheable(context)); + Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); TestUtils.AssertLoggedMessages( sink.Writes, LoggedMessage.ExpirationSharedMaxAgeExceeded); @@ -481,7 +481,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.CachedEntryAge = TimeSpan.MaxValue; context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); - Assert.True(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); + Assert.True(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); Assert.Empty(sink.Writes); } @@ -501,7 +501,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } }; - Assert.True(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); + Assert.True(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); Assert.Empty(sink.Writes); } @@ -522,7 +522,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Expires = utcNow }; - Assert.False(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); + Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); TestUtils.AssertLoggedMessages( sink.Writes, LoggedMessage.ExpirationExpiresExceeded); @@ -546,7 +546,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Expires = utcNow }; - Assert.True(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); + Assert.True(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); Assert.Empty(sink.Writes); } @@ -568,7 +568,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Expires = utcNow }; - Assert.False(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); + Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); TestUtils.AssertLoggedMessages( sink.Writes, LoggedMessage.ExpirationMaxAgeExceeded); @@ -593,7 +593,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Expires = utcNow }; - Assert.True(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); + Assert.True(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); Assert.Empty(sink.Writes); } @@ -616,7 +616,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Expires = utcNow }; - Assert.False(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); + Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); TestUtils.AssertLoggedMessages( sink.Writes, LoggedMessage.ExpirationSharedMaxAgeExceeded); @@ -641,7 +641,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests }; context.CachedEntryAge = TimeSpan.FromSeconds(3); - Assert.False(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); + Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); TestUtils.AssertLoggedMessages( sink.Writes, LoggedMessage.ExpirationMinFreshAdded, @@ -666,7 +666,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests }; context.CachedEntryAge = TimeSpan.FromSeconds(5); - Assert.False(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); + Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); TestUtils.AssertLoggedMessages( sink.Writes, LoggedMessage.ExpirationMaxAgeExceeded); @@ -692,7 +692,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests }; context.CachedEntryAge = TimeSpan.FromSeconds(6); - Assert.True(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); + Assert.True(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); TestUtils.AssertLoggedMessages( sink.Writes, LoggedMessage.ExpirationMaxStaleSatisfied); @@ -718,7 +718,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests }; context.CachedEntryAge = TimeSpan.FromSeconds(6); - Assert.False(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); + Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); TestUtils.AssertLoggedMessages( sink.Writes, LoggedMessage.ExpirationMaxAgeExceeded); @@ -745,7 +745,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests }; context.CachedEntryAge = TimeSpan.FromSeconds(6); - Assert.False(new ResponseCachePolicyProvider().IsCachedEntryFresh(context)); + Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); TestUtils.AssertLoggedMessages( sink.Writes, LoggedMessage.ExpirationMustRevalidate); diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs similarity index 84% rename from test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheTests.cs rename to test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs index 68c7f2ab86..c867115dc4 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCacheTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs @@ -14,12 +14,12 @@ using Xunit; namespace Microsoft.AspNetCore.ResponseCaching.Tests { - public class ResponseCacheTests + public class ResponseCachingTests { [Fact] public async void ServesCachedContent_IfAvailable() { - var builders = TestUtils.CreateBuildersWithResponseCache(); + var builders = TestUtils.CreateBuildersWithResponseCaching(); foreach (var builder in builders) { @@ -29,7 +29,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var initialResponse = await client.GetAsync(""); var subsequentResponse = await client.GetAsync(""); - await AssertResponseCachedAsync(initialResponse, subsequentResponse); + await AssertCachedResponseAsync(initialResponse, subsequentResponse); } } } @@ -37,7 +37,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfNotAvailable() { - var builders = TestUtils.CreateBuildersWithResponseCache(); + var builders = TestUtils.CreateBuildersWithResponseCaching(); foreach (var builder in builders) { @@ -47,7 +47,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var initialResponse = await client.GetAsync(""); var subsequentResponse = await client.GetAsync("/different"); - await AssertResponseNotCachedAsync(initialResponse, subsequentResponse); + await AssertFreshResponseAsync(initialResponse, subsequentResponse); } } } @@ -55,7 +55,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfVaryHeader_Matches() { - var builders = TestUtils.CreateBuildersWithResponseCache(requestDelegate: async (context) => + var builders = TestUtils.CreateBuildersWithResponseCaching(requestDelegate: async (context) => { context.Response.Headers[HeaderNames.Vary] = HeaderNames.From; await TestUtils.TestRequestDelegate(context); @@ -70,7 +70,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var initialResponse = await client.GetAsync(""); var subsequentResponse = await client.GetAsync(""); - await AssertResponseCachedAsync(initialResponse, subsequentResponse); + await AssertCachedResponseAsync(initialResponse, subsequentResponse); } } } @@ -78,7 +78,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfVaryHeader_Mismatches() { - var builders = TestUtils.CreateBuildersWithResponseCache(requestDelegate: async (context) => + var builders = TestUtils.CreateBuildersWithResponseCaching(requestDelegate: async (context) => { context.Response.Headers[HeaderNames.Vary] = HeaderNames.From; await TestUtils.TestRequestDelegate(context); @@ -94,7 +94,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests client.DefaultRequestHeaders.From = "user2@example.com"; var subsequentResponse = await client.GetAsync(""); - await AssertResponseNotCachedAsync(initialResponse, subsequentResponse); + await AssertFreshResponseAsync(initialResponse, subsequentResponse); } } } @@ -102,9 +102,9 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfVaryQueryKeys_Matches() { - var builders = TestUtils.CreateBuildersWithResponseCache(requestDelegate: async (context) => + var builders = TestUtils.CreateBuildersWithResponseCaching(requestDelegate: async (context) => { - context.Features.Get().VaryByQueryKeys = "query"; + context.Features.Get().VaryByQueryKeys = new[] { "query" }; await TestUtils.TestRequestDelegate(context); }); @@ -116,7 +116,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var initialResponse = await client.GetAsync("?query=value"); var subsequentResponse = await client.GetAsync("?query=value"); - await AssertResponseCachedAsync(initialResponse, subsequentResponse); + await AssertCachedResponseAsync(initialResponse, subsequentResponse); } } } @@ -124,9 +124,9 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfVaryQueryKeysExplicit_Matches_QueryKeyCaseInsensitive() { - var builders = TestUtils.CreateBuildersWithResponseCache(requestDelegate: async (context) => + var builders = TestUtils.CreateBuildersWithResponseCaching(requestDelegate: async (context) => { - context.Features.Get().VaryByQueryKeys = new[] { "QueryA", "queryb" }; + context.Features.Get().VaryByQueryKeys = new[] { "QueryA", "queryb" }; await TestUtils.TestRequestDelegate(context); }); @@ -138,7 +138,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var initialResponse = await client.GetAsync("?querya=valuea&queryb=valueb"); var subsequentResponse = await client.GetAsync("?QueryA=valuea&QueryB=valueb"); - await AssertResponseCachedAsync(initialResponse, subsequentResponse); + await AssertCachedResponseAsync(initialResponse, subsequentResponse); } } } @@ -146,9 +146,9 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfVaryQueryKeyStar_Matches_QueryKeyCaseInsensitive() { - var builders = TestUtils.CreateBuildersWithResponseCache(requestDelegate: async (context) => + var builders = TestUtils.CreateBuildersWithResponseCaching(requestDelegate: async (context) => { - context.Features.Get().VaryByQueryKeys = new[] { "*" }; + context.Features.Get().VaryByQueryKeys = new[] { "*" }; await TestUtils.TestRequestDelegate(context); }); @@ -160,7 +160,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var initialResponse = await client.GetAsync("?querya=valuea&queryb=valueb"); var subsequentResponse = await client.GetAsync("?QueryA=valuea&QueryB=valueb"); - await AssertResponseCachedAsync(initialResponse, subsequentResponse); + await AssertCachedResponseAsync(initialResponse, subsequentResponse); } } } @@ -168,9 +168,9 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfVaryQueryKeyExplicit_Matches_OrderInsensitive() { - var builders = TestUtils.CreateBuildersWithResponseCache(requestDelegate: async (context) => + var builders = TestUtils.CreateBuildersWithResponseCaching(requestDelegate: async (context) => { - context.Features.Get().VaryByQueryKeys = new[] { "QueryB", "QueryA" }; + context.Features.Get().VaryByQueryKeys = new[] { "QueryB", "QueryA" }; await TestUtils.TestRequestDelegate(context); }); @@ -182,7 +182,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var initialResponse = await client.GetAsync("?QueryA=ValueA&QueryB=ValueB"); var subsequentResponse = await client.GetAsync("?QueryB=ValueB&QueryA=ValueA"); - await AssertResponseCachedAsync(initialResponse, subsequentResponse); + await AssertCachedResponseAsync(initialResponse, subsequentResponse); } } } @@ -190,9 +190,9 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfVaryQueryKeyStar_Matches_OrderInsensitive() { - var builders = TestUtils.CreateBuildersWithResponseCache(requestDelegate: async (context) => + var builders = TestUtils.CreateBuildersWithResponseCaching(requestDelegate: async (context) => { - context.Features.Get().VaryByQueryKeys = new[] { "*" }; + context.Features.Get().VaryByQueryKeys = new[] { "*" }; await TestUtils.TestRequestDelegate(context); }); @@ -204,7 +204,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var initialResponse = await client.GetAsync("?QueryA=ValueA&QueryB=ValueB"); var subsequentResponse = await client.GetAsync("?QueryB=ValueB&QueryA=ValueA"); - await AssertResponseCachedAsync(initialResponse, subsequentResponse); + await AssertCachedResponseAsync(initialResponse, subsequentResponse); } } } @@ -212,9 +212,9 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfVaryQueryKey_Mismatches() { - var builders = TestUtils.CreateBuildersWithResponseCache(requestDelegate: async (context) => + var builders = TestUtils.CreateBuildersWithResponseCaching(requestDelegate: async (context) => { - context.Features.Get().VaryByQueryKeys = "query"; + context.Features.Get().VaryByQueryKeys = new[] { "query" }; await TestUtils.TestRequestDelegate(context); }); @@ -226,7 +226,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var initialResponse = await client.GetAsync("?query=value"); var subsequentResponse = await client.GetAsync("?query=value2"); - await AssertResponseNotCachedAsync(initialResponse, subsequentResponse); + await AssertFreshResponseAsync(initialResponse, subsequentResponse); } } } @@ -234,9 +234,9 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfVaryQueryKeyExplicit_Mismatch_QueryKeyCaseSensitive() { - var builders = TestUtils.CreateBuildersWithResponseCache(requestDelegate: async (context) => + var builders = TestUtils.CreateBuildersWithResponseCaching(requestDelegate: async (context) => { - context.Features.Get().VaryByQueryKeys = new[] { "QueryA", "QueryB" }; + context.Features.Get().VaryByQueryKeys = new[] { "QueryA", "QueryB" }; await TestUtils.TestRequestDelegate(context); }); @@ -248,7 +248,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var initialResponse = await client.GetAsync("?querya=valuea&queryb=valueb"); var subsequentResponse = await client.GetAsync("?querya=ValueA&queryb=ValueB"); - await AssertResponseNotCachedAsync(initialResponse, subsequentResponse); + await AssertFreshResponseAsync(initialResponse, subsequentResponse); } } } @@ -256,9 +256,9 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfVaryQueryKeyStar_Mismatch_QueryKeyValueCaseSensitive() { - var builders = TestUtils.CreateBuildersWithResponseCache(requestDelegate: async (context) => + var builders = TestUtils.CreateBuildersWithResponseCaching(requestDelegate: async (context) => { - context.Features.Get().VaryByQueryKeys = new[] { "*" }; + context.Features.Get().VaryByQueryKeys = new[] { "*" }; await TestUtils.TestRequestDelegate(context); }); @@ -270,7 +270,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var initialResponse = await client.GetAsync("?querya=valuea&queryb=valueb"); var subsequentResponse = await client.GetAsync("?querya=ValueA&queryb=ValueB"); - await AssertResponseNotCachedAsync(initialResponse, subsequentResponse); + await AssertFreshResponseAsync(initialResponse, subsequentResponse); } } } @@ -278,7 +278,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfRequestRequirements_NotMet() { - var builders = TestUtils.CreateBuildersWithResponseCache(); + var builders = TestUtils.CreateBuildersWithResponseCaching(); foreach (var builder in builders) { @@ -292,7 +292,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests }; var subsequentResponse = await client.GetAsync(""); - await AssertResponseNotCachedAsync(initialResponse, subsequentResponse); + await AssertFreshResponseAsync(initialResponse, subsequentResponse); } } } @@ -300,7 +300,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void Serves504_IfOnlyIfCachedHeader_IsSpecified() { - var builders = TestUtils.CreateBuildersWithResponseCache(); + var builders = TestUtils.CreateBuildersWithResponseCaching(); foreach (var builder in builders) { @@ -323,7 +323,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfSetCookie_IsSpecified() { - var builders = TestUtils.CreateBuildersWithResponseCache(requestDelegate: async (context) => + var builders = TestUtils.CreateBuildersWithResponseCaching(requestDelegate: async (context) => { var headers = context.Response.Headers[HeaderNames.SetCookie] = "cookieName=cookieValue"; await TestUtils.TestRequestDelegate(context); @@ -337,7 +337,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var initialResponse = await client.GetAsync(""); var subsequentResponse = await client.GetAsync(""); - await AssertResponseNotCachedAsync(initialResponse, subsequentResponse); + await AssertFreshResponseAsync(initialResponse, subsequentResponse); } } } @@ -345,7 +345,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfIHttpSendFileFeature_NotUsed() { - var builders = TestUtils.CreateBuildersWithResponseCache(app => + var builders = TestUtils.CreateBuildersWithResponseCaching(app => { app.Use(async (context, next) => { @@ -362,7 +362,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var initialResponse = await client.GetAsync(""); var subsequentResponse = await client.GetAsync(""); - await AssertResponseCachedAsync(initialResponse, subsequentResponse); + await AssertCachedResponseAsync(initialResponse, subsequentResponse); } } } @@ -370,7 +370,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfIHttpSendFileFeature_Used() { - var builders = TestUtils.CreateBuildersWithResponseCache( + var builders = TestUtils.CreateBuildersWithResponseCaching( app => { app.Use(async (context, next) => @@ -393,7 +393,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var initialResponse = await client.GetAsync(""); var subsequentResponse = await client.GetAsync(""); - await AssertResponseNotCachedAsync(initialResponse, subsequentResponse); + await AssertFreshResponseAsync(initialResponse, subsequentResponse); } } } @@ -401,7 +401,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfSubsequentRequest_ContainsNoStore() { - var builders = TestUtils.CreateBuildersWithResponseCache(); + var builders = TestUtils.CreateBuildersWithResponseCaching(); foreach (var builder in builders) { @@ -415,7 +415,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests }; var subsequentResponse = await client.GetAsync(""); - await AssertResponseCachedAsync(initialResponse, subsequentResponse); + await AssertCachedResponseAsync(initialResponse, subsequentResponse); } } } @@ -423,7 +423,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfInitialRequestContains_NoStore() { - var builders = TestUtils.CreateBuildersWithResponseCache(); + var builders = TestUtils.CreateBuildersWithResponseCaching(); foreach (var builder in builders) { @@ -437,7 +437,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var initialResponse = await client.GetAsync(""); var subsequentResponse = await client.GetAsync(""); - await AssertResponseNotCachedAsync(initialResponse, subsequentResponse); + await AssertFreshResponseAsync(initialResponse, subsequentResponse); } } } @@ -445,7 +445,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void Serves304_IfIfModifiedSince_Satisfied() { - var builders = TestUtils.CreateBuildersWithResponseCache(); + var builders = TestUtils.CreateBuildersWithResponseCaching(); foreach (var builder in builders) { @@ -465,7 +465,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfIfModifiedSince_NotSatisfied() { - var builders = TestUtils.CreateBuildersWithResponseCache(); + var builders = TestUtils.CreateBuildersWithResponseCaching(); foreach (var builder in builders) { @@ -476,7 +476,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests client.DefaultRequestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue; var subsequentResponse = await client.GetAsync(""); - await AssertResponseCachedAsync(initialResponse, subsequentResponse); + await AssertCachedResponseAsync(initialResponse, subsequentResponse); } } } @@ -484,7 +484,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void Serves304_IfIfNoneMatch_Satisfied() { - var builders = TestUtils.CreateBuildersWithResponseCache(requestDelegate: async (context) => + var builders = TestUtils.CreateBuildersWithResponseCaching(requestDelegate: async (context) => { var headers = context.Response.GetTypedHeaders().ETag = new EntityTagHeaderValue("\"E1\""); await TestUtils.TestRequestDelegate(context); @@ -508,7 +508,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfIfNoneMatch_NotSatisfied() { - var builders = TestUtils.CreateBuildersWithResponseCache(requestDelegate: async (context) => + var builders = TestUtils.CreateBuildersWithResponseCaching(requestDelegate: async (context) => { var headers = context.Response.GetTypedHeaders().ETag = new EntityTagHeaderValue("\"E1\""); await TestUtils.TestRequestDelegate(context); @@ -523,7 +523,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests client.DefaultRequestHeaders.IfNoneMatch.Add(new System.Net.Http.Headers.EntityTagHeaderValue("\"E2\"")); var subsequentResponse = await client.GetAsync(""); - await AssertResponseCachedAsync(initialResponse, subsequentResponse); + await AssertCachedResponseAsync(initialResponse, subsequentResponse); } } } @@ -531,7 +531,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfBodySize_IsCacheable() { - var builders = TestUtils.CreateBuildersWithResponseCache(options: new ResponseCacheOptions() + var builders = TestUtils.CreateBuildersWithResponseCaching(options: new ResponseCachingOptions() { MaximumBodySize = 100 }); @@ -544,7 +544,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var initialResponse = await client.GetAsync(""); var subsequentResponse = await client.GetAsync(""); - await AssertResponseCachedAsync(initialResponse, subsequentResponse); + await AssertCachedResponseAsync(initialResponse, subsequentResponse); } } } @@ -552,7 +552,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfBodySize_IsNotCacheable() { - var builders = TestUtils.CreateBuildersWithResponseCache(options: new ResponseCacheOptions() + var builders = TestUtils.CreateBuildersWithResponseCaching(options: new ResponseCachingOptions() { MaximumBodySize = 1 }); @@ -565,7 +565,28 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var initialResponse = await client.GetAsync(""); var subsequentResponse = await client.GetAsync("/different"); - await AssertResponseNotCachedAsync(initialResponse, subsequentResponse); + await AssertFreshResponseAsync(initialResponse, subsequentResponse); + } + } + } + + [Fact] + public async void ServesFreshContent_CaseSensitivePaths_IsNotCacheable() + { + var builders = TestUtils.CreateBuildersWithResponseCaching(options: new ResponseCachingOptions() + { + UseCaseSensitivePaths = true + }); + + foreach (var builder in builders) + { + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync("/path"); + var subsequentResponse = await client.GetAsync("/Path"); + + await AssertFreshResponseAsync(initialResponse, subsequentResponse); } } } @@ -573,7 +594,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_WithoutReplacingCachedVaryBy_OnCacheMiss() { - var builders = TestUtils.CreateBuildersWithResponseCache(requestDelegate: async (context) => + var builders = TestUtils.CreateBuildersWithResponseCaching(requestDelegate: async (context) => { context.Response.Headers[HeaderNames.Vary] = HeaderNames.From; await TestUtils.TestRequestDelegate(context); @@ -591,7 +612,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests client.DefaultRequestHeaders.From = "user@example.com"; var subsequentResponse = await client.GetAsync(""); - await AssertResponseCachedAsync(initialResponse, subsequentResponse); + await AssertCachedResponseAsync(initialResponse, subsequentResponse); } } } @@ -599,7 +620,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfCachedVaryByUpdated_OnCacheMiss() { - var builders = TestUtils.CreateBuildersWithResponseCache(requestDelegate: async (context) => + var builders = TestUtils.CreateBuildersWithResponseCaching(requestDelegate: async (context) => { context.Response.Headers[HeaderNames.Vary] = context.Request.Headers[HeaderNames.Pragma]; await TestUtils.TestRequestDelegate(context); @@ -626,7 +647,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests client.DefaultRequestHeaders.MaxForwards = 1; var subsequentResponse = await client.GetAsync(""); - await AssertResponseNotCachedAsync(initialResponse, subsequentResponse); + await AssertFreshResponseAsync(initialResponse, subsequentResponse); } } } @@ -634,7 +655,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfCachedVaryByNotUpdated_OnCacheMiss() { - var builders = TestUtils.CreateBuildersWithResponseCache(requestDelegate: async (context) => + var builders = TestUtils.CreateBuildersWithResponseCaching(requestDelegate: async (context) => { context.Response.Headers[HeaderNames.Vary] = context.Request.Headers[HeaderNames.Pragma]; await TestUtils.TestRequestDelegate(context); @@ -661,12 +682,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests client.DefaultRequestHeaders.MaxForwards = 1; var subsequentResponse = await client.GetAsync(""); - await AssertResponseCachedAsync(initialResponse, subsequentResponse); + await AssertCachedResponseAsync(initialResponse, subsequentResponse); } } } - private static async Task AssertResponseCachedAsync(HttpResponseMessage initialResponse, HttpResponseMessage subsequentResponse) + private static async Task AssertCachedResponseAsync(HttpResponseMessage initialResponse, HttpResponseMessage subsequentResponse) { initialResponse.EnsureSuccessStatusCode(); subsequentResponse.EnsureSuccessStatusCode(); @@ -679,7 +700,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Assert.Equal(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync()); } - private static async Task AssertResponseNotCachedAsync(HttpResponseMessage initialResponse, HttpResponseMessage subsequentResponse) + private static async Task AssertFreshResponseAsync(HttpResponseMessage initialResponse, HttpResponseMessage subsequentResponse) { initialResponse.EnsureSuccessStatusCode(); subsequentResponse.EnsureSuccessStatusCode(); diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs index 250854d4c0..76f659774a 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs @@ -44,92 +44,96 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests await context.Response.WriteAsync(uniqueId); }; - internal static IResponseCacheKeyProvider CreateTestKeyProvider() + internal static IResponseCachingKeyProvider CreateTestKeyProvider() { - return CreateTestKeyProvider(new ResponseCacheOptions()); + return CreateTestKeyProvider(new ResponseCachingOptions()); } - internal static IResponseCacheKeyProvider CreateTestKeyProvider(ResponseCacheOptions options) + internal static IResponseCachingKeyProvider CreateTestKeyProvider(ResponseCachingOptions options) { - return new ResponseCacheKeyProvider(new DefaultObjectPoolProvider(), Options.Create(options)); + return new ResponseCachingKeyProvider(new DefaultObjectPoolProvider(), Options.Create(options)); } - internal static IEnumerable CreateBuildersWithResponseCache( + internal static IEnumerable CreateBuildersWithResponseCaching( Action configureDelegate = null, - ResponseCacheOptions options = null, + ResponseCachingOptions options = null, RequestDelegate requestDelegate = null) { if (configureDelegate == null) { configureDelegate = app => { }; } - if (options == null) - { - options = new ResponseCacheOptions(); - } if (requestDelegate == null) { requestDelegate = TestRequestDelegate; } - // Test with MemoryResponseCacheStore + // Test with in memory ResponseCache yield return new WebHostBuilder() .ConfigureServices(services => { - services.AddMemoryResponseCacheStore(); + services.AddResponseCaching(responseCachingOptions => + { + if (options != null) + { + responseCachingOptions.MaximumBodySize = options.MaximumBodySize; + responseCachingOptions.UseCaseSensitivePaths = options.UseCaseSensitivePaths; + responseCachingOptions.SystemClock = options.SystemClock; + } + }); }) .Configure(app => { configureDelegate(app); - app.UseResponseCache(options); + app.UseResponseCaching(); app.Run(requestDelegate); }); } - internal static ResponseCacheMiddleware CreateTestMiddleware( - IResponseCacheStore store = null, - ResponseCacheOptions options = null, + internal static ResponseCachingMiddleware CreateTestMiddleware( + IResponseCache cache = null, + ResponseCachingOptions options = null, TestSink testSink = null, - IResponseCacheKeyProvider keyProvider = null, - IResponseCachePolicyProvider policyProvider = null) + IResponseCachingKeyProvider keyProvider = null, + IResponseCachingPolicyProvider policyProvider = null) { - if (store == null) + if (cache == null) { - store = new TestResponseCacheStore(); + cache = new TestResponseCache(); } if (options == null) { - options = new ResponseCacheOptions(); + options = new ResponseCachingOptions(); } if (keyProvider == null) { - keyProvider = new ResponseCacheKeyProvider(new DefaultObjectPoolProvider(), Options.Create(options)); + keyProvider = new ResponseCachingKeyProvider(new DefaultObjectPoolProvider(), Options.Create(options)); } if (policyProvider == null) { - policyProvider = new TestResponseCachePolicyProvider(); + policyProvider = new TestResponseCachingPolicyProvider(); } - return new ResponseCacheMiddleware( + return new ResponseCachingMiddleware( httpContext => TaskCache.CompletedTask, Options.Create(options), testSink == null ? (ILoggerFactory)NullLoggerFactory.Instance : new TestLoggerFactory(testSink, true), policyProvider, - store, + cache, keyProvider); } - internal static ResponseCacheContext CreateTestContext() + internal static ResponseCachingContext CreateTestContext() { - return new ResponseCacheContext(new DefaultHttpContext(), NullLogger.Instance) + return new ResponseCachingContext(new DefaultHttpContext(), NullLogger.Instance) { ResponseTime = DateTimeOffset.UtcNow }; } - internal static ResponseCacheContext CreateTestContext(TestSink testSink) + internal static ResponseCachingContext CreateTestContext(TestSink testSink) { - return new ResponseCacheContext(new DefaultHttpContext(), new TestLogger("ResponseCachingTests", testSink, true)) + return new ResponseCachingContext(new DefaultHttpContext(), new TestLogger("ResponseCachingTests", testSink, true)) { ResponseTime = DateTimeOffset.UtcNow }; @@ -208,21 +212,21 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } } - internal class TestResponseCachePolicyProvider : IResponseCachePolicyProvider + internal class TestResponseCachingPolicyProvider : IResponseCachingPolicyProvider { - public bool IsCachedEntryFresh(ResponseCacheContext context) => true; + public bool IsCachedEntryFresh(ResponseCachingContext context) => true; - public bool IsRequestCacheable(ResponseCacheContext context) => true; + public bool IsRequestCacheable(ResponseCachingContext context) => true; - public bool IsResponseCacheable(ResponseCacheContext context) => true; + public bool IsResponseCacheable(ResponseCachingContext context) => true; } - internal class TestResponseCacheKeyProvider : IResponseCacheKeyProvider + internal class TestResponseCachingKeyProvider : IResponseCachingKeyProvider { private readonly string _baseKey; private readonly StringValues _varyKey; - public TestResponseCacheKeyProvider(string lookupBaseKey = null, StringValues? lookupVaryKey = null) + public TestResponseCachingKeyProvider(string lookupBaseKey = null, StringValues? lookupVaryKey = null) { _baseKey = lookupBaseKey; if (lookupVaryKey.HasValue) @@ -231,7 +235,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } } - public IEnumerable CreateLookupVaryByKeys(ResponseCacheContext context) + public IEnumerable CreateLookupVaryByKeys(ResponseCachingContext context) { foreach (var varyKey in _varyKey) { @@ -239,18 +243,18 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } } - public string CreateBaseKey(ResponseCacheContext context) + public string CreateBaseKey(ResponseCachingContext context) { return _baseKey; } - public string CreateStorageVaryByKey(ResponseCacheContext context) + public string CreateStorageVaryByKey(ResponseCachingContext context) { throw new NotImplementedException(); } } - internal class TestResponseCacheStore : IResponseCacheStore + internal class TestResponseCache : IResponseCache { private readonly IDictionary _storage = new Dictionary(); public int GetCount { get; private set; } From 8403184752a1969bb13737f4902a70facf232c94 Mon Sep 17 00:00:00 2001 From: John Luo Date: Mon, 17 Oct 2016 12:24:04 -0700 Subject: [PATCH 059/188] Update version number to 1.0.0-preview1 --- samples/ResponseCachingSample/project.json | 2 +- .../project.json | 2 +- src/Microsoft.AspNetCore.ResponseCaching/project.json | 4 ++-- test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/samples/ResponseCachingSample/project.json b/samples/ResponseCachingSample/project.json index e1a1d1740f..75bf3448bc 100644 --- a/samples/ResponseCachingSample/project.json +++ b/samples/ResponseCachingSample/project.json @@ -1,6 +1,6 @@ { "dependencies": { - "Microsoft.AspNetCore.ResponseCaching": "1.0.0", + "Microsoft.AspNetCore.ResponseCaching": "1.0.0-preview1", "Microsoft.AspNetCore.Server.IISIntegration": "1.0.0", "Microsoft.AspNetCore.Server.Kestrel": "1.0.1", "Microsoft.Extensions.Caching.Memory": "1.0.0" diff --git a/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/project.json b/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/project.json index 35e9e9ddec..1c24353d75 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/project.json +++ b/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/project.json @@ -1,5 +1,5 @@ { - "version": "1.0.0", + "version": "1.0.0-preview1", "buildOptions": { "warningsAsErrors": true, "keyFile": "../../tools/Key.snk", diff --git a/src/Microsoft.AspNetCore.ResponseCaching/project.json b/src/Microsoft.AspNetCore.ResponseCaching/project.json index e19977dba7..a0c4a337f1 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/project.json +++ b/src/Microsoft.AspNetCore.ResponseCaching/project.json @@ -1,5 +1,5 @@ { - "version": "1.0.0", + "version": "1.0.0-preview1", "buildOptions": { "warningsAsErrors": true, "allowUnsafe": true, @@ -24,7 +24,7 @@ "dependencies": { "Microsoft.AspNetCore.Http": "1.0.0", "Microsoft.AspNetCore.Http.Extensions": "1.0.0", - "Microsoft.AspNetCore.ResponseCaching.Abstractions": "1.0.0", + "Microsoft.AspNetCore.ResponseCaching.Abstractions": "1.0.0-preview1", "Microsoft.Extensions.Caching.Memory": "1.0.0", "Microsoft.Extensions.Logging.Abstractions": "1.0.0" }, diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json b/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json index ab35046c49..a842a18f86 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json @@ -5,7 +5,7 @@ }, "dependencies": { "dotnet-test-xunit": "1.0.0-rc3-000000-01", - "Microsoft.AspNetCore.ResponseCaching": "1.0.0", + "Microsoft.AspNetCore.ResponseCaching": "1.0.0-preview1", "Microsoft.AspNetCore.TestHost": "1.0.0", "Microsoft.Extensions.Logging.Testing": "1.0.0", "xunit": "2.1.0" From 87e4b9df39fafd300458e0a7b9a09220fd6e937a Mon Sep 17 00:00:00 2001 From: John Luo Date: Mon, 17 Oct 2016 12:26:54 -0700 Subject: [PATCH 060/188] Rename NuGet.Config to NuGet.config --- NuGet.Config => NuGet.config | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename NuGet.Config => NuGet.config (100%) diff --git a/NuGet.Config b/NuGet.config similarity index 100% rename from NuGet.Config rename to NuGet.config From 3eb7edd3649b1ebe1abed3efd6ff66973c87478c Mon Sep 17 00:00:00 2001 From: John Luo Date: Tue, 18 Oct 2016 16:46:05 -0700 Subject: [PATCH 061/188] Update Microsoft.Extensions.Logging.Testing version --- test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json b/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json index a842a18f86..9db3081fd4 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json @@ -7,7 +7,7 @@ "dotnet-test-xunit": "1.0.0-rc3-000000-01", "Microsoft.AspNetCore.ResponseCaching": "1.0.0-preview1", "Microsoft.AspNetCore.TestHost": "1.0.0", - "Microsoft.Extensions.Logging.Testing": "1.0.0", + "Microsoft.Extensions.Logging.Testing": "1.0.0-rtm-21431", "xunit": "2.1.0" }, "frameworks": { From 70fce81d7d175b101c1a9a5ab6c1cd9f40d12968 Mon Sep 17 00:00:00 2001 From: moozzyk Date: Mon, 24 Oct 2016 14:24:09 -0700 Subject: [PATCH 062/188] Adding additional test coverage --- .../ResponseCachingTests.cs | 194 +++++++++++++++++- .../TestUtils.cs | 27 ++- 2 files changed, 207 insertions(+), 14 deletions(-) diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs index c867115dc4..f2a014b707 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -16,8 +17,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { public class ResponseCachingTests { - [Fact] - public async void ServesCachedContent_IfAvailable() + [Theory] + [InlineData("GET")] + [InlineData("HEAD")] + public async void ServesCachedContent_IfAvailable(string method) { var builders = TestUtils.CreateBuildersWithResponseCaching(); @@ -26,16 +29,18 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests using (var server = new TestServer(builder)) { var client = server.CreateClient(); - var initialResponse = await client.GetAsync(""); - var subsequentResponse = await client.GetAsync(""); + var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); + var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); await AssertCachedResponseAsync(initialResponse, subsequentResponse); } } } - [Fact] - public async void ServesFreshContent_IfNotAvailable() + [Theory] + [InlineData("GET")] + [InlineData("HEAD")] + public async void ServesFreshContent_IfNotAvailable(string method) { var builders = TestUtils.CreateBuildersWithResponseCaching(); @@ -44,8 +49,169 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests using (var server = new TestServer(builder)) { var client = server.CreateClient(); - var initialResponse = await client.GetAsync(""); - var subsequentResponse = await client.GetAsync("/different"); + var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); + var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, "different")); + + await AssertFreshResponseAsync(initialResponse, subsequentResponse); + } + } + } + + [Fact] + public async void ServesFreshContent_Post() + { + var builders = TestUtils.CreateBuildersWithResponseCaching(); + + foreach (var builder in builders) + { + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.PostAsync("", new StringContent(string.Empty)); + var subsequentResponse = await client.PostAsync("", new StringContent(string.Empty)); + + await AssertFreshResponseAsync(initialResponse, subsequentResponse); + } + } + } + + [Fact] + public async void ServesFreshContent_Head_Get() + { + var builders = TestUtils.CreateBuildersWithResponseCaching(); + + foreach (var builder in builders) + { + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var subsequentResponse = await client.SendAsync(new HttpRequestMessage(HttpMethod.Head, "")); + var initialResponse = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "")); + + await AssertFreshResponseAsync(initialResponse, subsequentResponse); + } + } + } + + [Fact] + public async void ServesFreshContent_Get_Head() + { + var builders = TestUtils.CreateBuildersWithResponseCaching(); + + foreach (var builder in builders) + { + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "")); + var subsequentResponse = await client.SendAsync(new HttpRequestMessage(HttpMethod.Head, "")); + + await AssertFreshResponseAsync(initialResponse, subsequentResponse); + } + } + } + + [Theory] + [InlineData("GET")] + [InlineData("HEAD")] + public async void ServesFreshContent_If_CacheControlNoCache(string method) + { + var builders = TestUtils.CreateBuildersWithResponseCaching(); + + foreach (var builder in builders) + { + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + client.DefaultRequestHeaders.CacheControl = + new System.Net.Http.Headers.CacheControlHeaderValue { NoCache = true }; + + var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); + var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); + + await AssertFreshResponseAsync(initialResponse, subsequentResponse); + } + } + } + + [Theory] + [InlineData("GET")] + [InlineData("HEAD")] + public async void ServesFreshContent_If_PragmaNoCache(string method) + { + var builders = TestUtils.CreateBuildersWithResponseCaching(); + + foreach (var builder in builders) + { + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + client.DefaultRequestHeaders.Pragma.Clear(); + client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("no-cache")); + + var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); + var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); + + await AssertFreshResponseAsync(initialResponse, subsequentResponse); + } + } + } + + [Theory] + [InlineData("GET")] + [InlineData("HEAD")] + public async void ServesCachedContent_If_PathCasingDiffers(string method) + { + var builders = TestUtils.CreateBuildersWithResponseCaching(); + + foreach (var builder in builders) + { + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, "path")); + var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, "PATH")); + + await AssertCachedResponseAsync(initialResponse, subsequentResponse); + } + } + } + + [Theory] + [InlineData("GET")] + [InlineData("HEAD")] + public async void ServesFreshContent_If_ResponseExpired(string method) + { + var builders = TestUtils.CreateBuildersWithResponseCaching(); + + foreach (var builder in builders) + { + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, "?Expires=0")); + var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); + + await AssertFreshResponseAsync(initialResponse, subsequentResponse); + } + } + } + + [Theory] + [InlineData("GET")] + [InlineData("HEAD")] + public async void ServesFreshContent_If_Authorization_HeaderExists(string method) + { + var builders = TestUtils.CreateBuildersWithResponseCaching(); + + foreach (var builder in builders) + { + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("abc"); + var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); + var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); await AssertFreshResponseAsync(initialResponse, subsequentResponse); } @@ -706,7 +872,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests subsequentResponse.EnsureSuccessStatusCode(); Assert.False(subsequentResponse.Headers.Contains(HeaderNames.Age)); - Assert.NotEqual(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync()); + + if (initialResponse.RequestMessage.Method == HttpMethod.Head && + subsequentResponse.RequestMessage.Method == HttpMethod.Head) + { + Assert.True(initialResponse.Headers.Contains("X-Value")); + Assert.NotEqual(initialResponse.Headers.GetValues("X-Value"), subsequentResponse.Headers.GetValues("X-Value")); + } + else + { + Assert.NotEqual(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync()); + } } } } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs index f4f9125e82..b50be5b175 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Net.Http; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; @@ -30,18 +31,29 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests StreamUtilities.BodySegmentSize = 10; } - internal static RequestDelegate TestRequestDelegate = async (context) => + internal static RequestDelegate TestRequestDelegate = async context => { - var uniqueId = Guid.NewGuid().ToString(); var headers = context.Response.GetTypedHeaders(); - headers.CacheControl = new CacheControlHeaderValue() + + var expires = context.Request.Query["Expires"]; + if (!string.IsNullOrEmpty(expires)) + { + headers.Expires = DateTimeOffset.Now.AddSeconds(int.Parse(expires)); + } + + var uniqueId = Guid.NewGuid().ToString(); + headers.CacheControl = new CacheControlHeaderValue { Public = true, - MaxAge = TimeSpan.FromSeconds(10) + MaxAge = string.IsNullOrEmpty(expires) ? TimeSpan.FromSeconds(10) : (TimeSpan?)null }; headers.Date = DateTimeOffset.UtcNow; headers.Headers["X-Value"] = uniqueId; - await context.Response.WriteAsync(uniqueId); + + if (context.Request.Method != "HEAD") + { + await context.Response.WriteAsync(uniqueId); + } }; internal static IResponseCachingKeyProvider CreateTestKeyProvider() @@ -148,6 +160,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Assert.Equal(expectedMessages[i].LogLevel, messages[i].LogLevel); } } + + public static HttpRequestMessage CreateRequest(string method, string requestUri) + { + return new HttpRequestMessage(new HttpMethod(method), requestUri); + } } internal class LoggedMessage From 63c0bec743103ebd715f64426400a26f5fd91ea8 Mon Sep 17 00:00:00 2001 From: John Luo Date: Fri, 28 Oct 2016 14:34:52 -0700 Subject: [PATCH 063/188] Update version to 1.1.0-* --- samples/ResponseCachingSample/project.json | 3 +-- .../project.json | 2 +- src/Microsoft.AspNetCore.ResponseCaching/project.json | 4 ++-- test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/samples/ResponseCachingSample/project.json b/samples/ResponseCachingSample/project.json index f9e5211e83..df0dbb37e3 100644 --- a/samples/ResponseCachingSample/project.json +++ b/samples/ResponseCachingSample/project.json @@ -1,7 +1,6 @@ { - "version": "1.1.0-*", "dependencies": { - "Microsoft.AspNetCore.ResponseCaching": "1.0.0-*", + "Microsoft.AspNetCore.ResponseCaching": "1.1.0-*", "Microsoft.AspNetCore.Server.IISIntegration": "1.1.0-*", "Microsoft.AspNetCore.Server.Kestrel": "1.1.0-*", "Microsoft.Extensions.Caching.Memory": "1.1.0-*" diff --git a/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/project.json b/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/project.json index ee4622c44c..2250db4823 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/project.json +++ b/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/project.json @@ -1,5 +1,5 @@ { - "version": "1.0.0-*", + "version": "1.1.0-*", "buildOptions": { "warningsAsErrors": true, "keyFile": "../../tools/Key.snk", diff --git a/src/Microsoft.AspNetCore.ResponseCaching/project.json b/src/Microsoft.AspNetCore.ResponseCaching/project.json index 04f2eae5c3..a23ec89529 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/project.json +++ b/src/Microsoft.AspNetCore.ResponseCaching/project.json @@ -1,5 +1,5 @@ { - "version": "1.0.0-*", + "version": "1.1.0-*", "buildOptions": { "warningsAsErrors": true, "allowUnsafe": true, @@ -24,7 +24,7 @@ "dependencies": { "Microsoft.AspNetCore.Http": "1.1.0-*", "Microsoft.AspNetCore.Http.Extensions": "1.1.0-*", - "Microsoft.AspNetCore.ResponseCaching.Abstractions": "1.0.0-*", + "Microsoft.AspNetCore.ResponseCaching.Abstractions": "1.1.0-*", "Microsoft.Extensions.Caching.Memory": "1.1.0-*", "Microsoft.Extensions.Logging.Abstractions": "1.1.0-*", "Microsoft.Extensions.TaskCache.Sources": { diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json b/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json index 9f889a9479..8d45842e98 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json @@ -5,7 +5,7 @@ }, "dependencies": { "dotnet-test-xunit": "2.2.0-*", - "Microsoft.AspNetCore.ResponseCaching": "1.0.0-*", + "Microsoft.AspNetCore.ResponseCaching": "1.1.0-*", "Microsoft.AspNetCore.TestHost": "1.1.0-*", "Microsoft.Extensions.Logging.Testing": "1.1.0-*", "xunit": "2.2.0-*" From f2b882504091f83d9a3b45fccfe58f62a2cdba40 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Wed, 9 Nov 2016 11:32:59 -0800 Subject: [PATCH 064/188] Branching for 1.1.0 --- NuGet.config | 4 ++-- build.ps1 | 2 +- build.sh | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/NuGet.config b/NuGet.config index 0fd623ffdd..ad973186eb 100644 --- a/NuGet.config +++ b/NuGet.config @@ -1,7 +1,7 @@ - + - + diff --git a/build.ps1 b/build.ps1 index 8f2f99691a..24ca167cf6 100644 --- a/build.ps1 +++ b/build.ps1 @@ -33,7 +33,7 @@ cd $PSScriptRoot $repoFolder = $PSScriptRoot $env:REPO_FOLDER = $repoFolder -$koreBuildZip="https://github.com/aspnet/KoreBuild/archive/dev.zip" +$koreBuildZip="https://github.com/aspnet/KoreBuild/archive/rel/1.1.0.zip" if ($env:KOREBUILD_ZIP) { $koreBuildZip=$env:KOREBUILD_ZIP diff --git a/build.sh b/build.sh index f4208100eb..fea9ac64ad 100755 --- a/build.sh +++ b/build.sh @@ -2,7 +2,7 @@ repoFolder="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" cd $repoFolder -koreBuildZip="https://github.com/aspnet/KoreBuild/archive/dev.zip" +koreBuildZip="https://github.com/aspnet/KoreBuild/archive/rel/1.1.0.zip" if [ ! -z $KOREBUILD_ZIP ]; then koreBuildZip=$KOREBUILD_ZIP fi From f26ef2e50c2a3b47123b78172df2e43a30dc62de Mon Sep 17 00:00:00 2001 From: Pranav K Date: Wed, 9 Nov 2016 14:19:20 -0800 Subject: [PATCH 065/188] Updating versions to 1.2.0-* --- samples/ResponseCachingSample/project.json | 8 ++++---- .../project.json | 6 +++--- .../project.json | 16 +++++++++------- .../project.json | 6 +++--- 4 files changed, 19 insertions(+), 17 deletions(-) diff --git a/samples/ResponseCachingSample/project.json b/samples/ResponseCachingSample/project.json index df0dbb37e3..009f6ed7f9 100644 --- a/samples/ResponseCachingSample/project.json +++ b/samples/ResponseCachingSample/project.json @@ -1,9 +1,9 @@ { "dependencies": { - "Microsoft.AspNetCore.ResponseCaching": "1.1.0-*", - "Microsoft.AspNetCore.Server.IISIntegration": "1.1.0-*", - "Microsoft.AspNetCore.Server.Kestrel": "1.1.0-*", - "Microsoft.Extensions.Caching.Memory": "1.1.0-*" + "Microsoft.AspNetCore.ResponseCaching": "1.2.0-*", + "Microsoft.AspNetCore.Server.IISIntegration": "1.2.0-*", + "Microsoft.AspNetCore.Server.Kestrel": "1.2.0-*", + "Microsoft.Extensions.Caching.Memory": "1.2.0-*" }, "buildOptions": { "emitEntryPoint": true diff --git a/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/project.json b/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/project.json index 2250db4823..33d56f6e66 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/project.json +++ b/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/project.json @@ -1,5 +1,5 @@ -{ - "version": "1.1.0-*", +{ + "version": "1.2.0-*", "buildOptions": { "warningsAsErrors": true, "keyFile": "../../tools/Key.snk", @@ -18,7 +18,7 @@ ] }, "dependencies": { - "Microsoft.Extensions.Primitives": "1.1.0-*" + "Microsoft.Extensions.Primitives": "1.2.0-*" }, "frameworks": { "net451": {}, diff --git a/src/Microsoft.AspNetCore.ResponseCaching/project.json b/src/Microsoft.AspNetCore.ResponseCaching/project.json index a23ec89529..e09ec49d34 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/project.json +++ b/src/Microsoft.AspNetCore.ResponseCaching/project.json @@ -1,5 +1,5 @@ { - "version": "1.1.0-*", + "version": "1.2.0-*", "buildOptions": { "warningsAsErrors": true, "allowUnsafe": true, @@ -22,13 +22,15 @@ ] }, "dependencies": { - "Microsoft.AspNetCore.Http": "1.1.0-*", - "Microsoft.AspNetCore.Http.Extensions": "1.1.0-*", - "Microsoft.AspNetCore.ResponseCaching.Abstractions": "1.1.0-*", - "Microsoft.Extensions.Caching.Memory": "1.1.0-*", - "Microsoft.Extensions.Logging.Abstractions": "1.1.0-*", + "Microsoft.AspNetCore.Http": "1.2.0-*", + "Microsoft.AspNetCore.Http.Extensions": "1.2.0-*", + "Microsoft.AspNetCore.ResponseCaching.Abstractions": { + "target": "project" + }, + "Microsoft.Extensions.Caching.Memory": "1.2.0-*", + "Microsoft.Extensions.Logging.Abstractions": "1.2.0-*", "Microsoft.Extensions.TaskCache.Sources": { - "version": "1.1.0-*", + "version": "1.2.0-*", "type": "build" }, "NETStandard.Library": "1.6.1-*" diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json b/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json index 8d45842e98..e5ec8165bb 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json @@ -5,9 +5,9 @@ }, "dependencies": { "dotnet-test-xunit": "2.2.0-*", - "Microsoft.AspNetCore.ResponseCaching": "1.1.0-*", - "Microsoft.AspNetCore.TestHost": "1.1.0-*", - "Microsoft.Extensions.Logging.Testing": "1.1.0-*", + "Microsoft.AspNetCore.ResponseCaching": "1.2.0-*", + "Microsoft.AspNetCore.TestHost": "1.2.0-*", + "Microsoft.Extensions.Logging.Testing": "1.2.0-*", "xunit": "2.2.0-*" }, "frameworks": { From 41f58191bfe120b8ed74f2369f50ee68bda9bf30 Mon Sep 17 00:00:00 2001 From: John Luo Date: Tue, 15 Nov 2016 15:57:56 -0800 Subject: [PATCH 066/188] Update sample to add a README.md file --- samples/ResponseCachingSample/README.md | 6 ++++++ samples/ResponseCachingSample/Startup.cs | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 samples/ResponseCachingSample/README.md diff --git a/samples/ResponseCachingSample/README.md b/samples/ResponseCachingSample/README.md new file mode 100644 index 0000000000..08583fda40 --- /dev/null +++ b/samples/ResponseCachingSample/README.md @@ -0,0 +1,6 @@ +ASP.NET Core Response Caching Sample +=================================== + +This sample illustrates the usage of ASP.NET Core response caching middleware. The application sends a `Hello World!` message and the current time along with a `Cache-Control` header to configure caching behavior. The application also sends a `Vary` header to configure the cache to serve the response only if the `Accept-Encoding` header of subsequent requests matches that from the original request. + +When running the sample, a response will be served from cache when possible and will be stored for up to 10 seconds. diff --git a/samples/ResponseCachingSample/Startup.cs b/samples/ResponseCachingSample/Startup.cs index cd135b8451..ca2e7fbcf3 100644 --- a/samples/ResponseCachingSample/Startup.cs +++ b/samples/ResponseCachingSample/Startup.cs @@ -28,7 +28,7 @@ namespace ResponseCachingSample Public = true, MaxAge = TimeSpan.FromSeconds(10) }; - context.Response.Headers[HeaderNames.Vary] = new string[] { "Accept-Encoding", "Non-Existent" }; + context.Response.Headers[HeaderNames.Vary] = new string[] { "Accept-Encoding" }; await context.Response.WriteAsync("Hello World! " + DateTime.UtcNow); }); From 2c54091236f239c03c56b486dbcfb56a94cc4a81 Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Fri, 18 Nov 2016 10:56:58 -0800 Subject: [PATCH 067/188] Clean tmp folder after unzipping KoreBuild --- build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.sh b/build.sh index f4208100eb..4fd7ede788 100755 --- a/build.sh +++ b/build.sh @@ -38,7 +38,7 @@ if test ! -d $buildFolder; then chmod +x $buildFile # Cleanup - if test ! -d $tempFolder; then + if test -d $tempFolder; then rm -rf $tempFolder fi fi From dbbd38b3347e6eb7add8970e324838bd908b433e Mon Sep 17 00:00:00 2001 From: "N. Taylor Mullen" Date: Wed, 23 Nov 2016 15:59:40 -0800 Subject: [PATCH 068/188] Pin global.json SDK to 1.0.0-preview2-1-003177. --- global.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/global.json b/global.json index 829daadd5b..f45e8cc925 100644 --- a/global.json +++ b/global.json @@ -1,3 +1,8 @@ { - "projects": ["src"] + "projects": [ + "src" + ], + "sdk": { + "version": "1.0.0-preview2-1-003177" + } } \ No newline at end of file From e823118e51c9e5d426315026266b99899a29c30b Mon Sep 17 00:00:00 2001 From: "N. Taylor Mullen" Date: Thu, 8 Dec 2016 10:03:47 -0800 Subject: [PATCH 069/188] Update .travis.yml osx image to xcode7.3. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index d7636fa329..a0be886892 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,7 +19,7 @@ mono: os: - linux - osx -osx_image: xcode7.1 +osx_image: xcode7.3 branches: only: - master From 9c94a7764bdb1f7194709ec7d5bc011f21f01a37 Mon Sep 17 00:00:00 2001 From: BrennanConroy Date: Mon, 3 Oct 2016 10:25:06 -0700 Subject: [PATCH 070/188] Improve header parsing performance --- .../CacheControlValues.cs | 21 ++ .../Internal/HttpHeaderParsingHelpers.cs | 118 +++++++++ .../Internal/ResponseCachingContext.cs | 107 ++++---- .../Internal/ResponseCachingPolicyProvider.cs | 90 ++++--- .../ResponseCachingMiddleware.cs | 54 ++-- .../ParsingHelpersTests.cs | 39 +++ .../ResponseCachingMiddlewareTests.cs | 96 ++++--- .../ResponseCachingPolicyProviderTests.cs | 248 ++++++++---------- 8 files changed, 470 insertions(+), 303 deletions(-) create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/CacheControlValues.cs create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Internal/HttpHeaderParsingHelpers.cs create mode 100644 test/Microsoft.AspNetCore.ResponseCaching.Tests/ParsingHelpersTests.cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/CacheControlValues.cs b/src/Microsoft.AspNetCore.ResponseCaching/CacheControlValues.cs new file mode 100644 index 0000000000..1dd0db8167 --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/CacheControlValues.cs @@ -0,0 +1,21 @@ +// 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.Internal +{ + internal class CacheControlValues + { + public const string MaxAgeString = "max-age"; + public const string MaxStaleString = "max-stale"; + public const string MinFreshString = "min-fresh"; + public const string MustRevalidateString = "must-revalidate"; + public const string NoCacheString = "no-cache"; + public const string NoStoreString = "no-store"; + public const string NoTransformString = "no-transform"; + public const string OnlyIfCachedString = "only-if-cached"; + public const string PrivateString = "private"; + public const string ProxyRevalidateString = "proxy-revalidate"; + public const string PublicString = "public"; + public const string SharedMaxAgeString = "s-maxage"; + } +} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/HttpHeaderParsingHelpers.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/HttpHeaderParsingHelpers.cs new file mode 100644 index 0000000000..94d452198b --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/HttpHeaderParsingHelpers.cs @@ -0,0 +1,118 @@ +// 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.Globalization; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.ResponseCaching.Internal +{ + internal static class HttpHeaderParsingHelpers + { + private static readonly string[] DateFormats = new string[] { + // "r", // RFC 1123, required output format but too strict for input + "ddd, d MMM yyyy H:m:s 'GMT'", // RFC 1123 (r, except it allows both 1 and 01 for date and time) + "ddd, d MMM yyyy H:m:s", // RFC 1123, no zone - assume GMT + "d MMM yyyy H:m:s 'GMT'", // RFC 1123, no day-of-week + "d MMM yyyy H:m:s", // RFC 1123, no day-of-week, no zone + "ddd, d MMM yy H:m:s 'GMT'", // RFC 1123, short year + "ddd, d MMM yy H:m:s", // RFC 1123, short year, no zone + "d MMM yy H:m:s 'GMT'", // RFC 1123, no day-of-week, short year + "d MMM yy H:m:s", // RFC 1123, no day-of-week, short year, no zone + + "dddd, d'-'MMM'-'yy H:m:s 'GMT'", // RFC 850 + "dddd, d'-'MMM'-'yy H:m:s", // RFC 850 no zone + "ddd MMM d H:m:s yyyy", // ANSI C's asctime() format + + "ddd, d MMM yyyy H:m:s zzz", // RFC 5322 + "ddd, d MMM yyyy H:m:s", // RFC 5322 no zone + "d MMM yyyy H:m:s zzz", // RFC 5322 no day-of-week + "d MMM yyyy H:m:s", // RFC 5322 no day-of-week, no zone + }; + + // Try the various date formats in the order listed above. + // We should accept a wide verity of common formats, but only output RFC 1123 style dates. + internal static bool TryParseHeaderDate(string input, out DateTimeOffset result) => DateTimeOffset.TryParseExact(input, DateFormats, DateTimeFormatInfo.InvariantInfo, + DateTimeStyles.AllowWhiteSpaces | DateTimeStyles.AssumeUniversal, out result); + + // Try to get the value of a specific header from a list of headers + // e.g. "header1=10, header2=30" + internal static bool TryParseHeaderTimeSpan(StringValues headers, string headerName, out TimeSpan? value) + { + foreach (var header in headers) + { + var index = header.IndexOf(headerName, StringComparison.OrdinalIgnoreCase); + if (index != -1) + { + index += headerName.Length; + int seconds; + if (!TryParseHeaderInt(index, header, out seconds)) + { + break; + } + value = TimeSpan.FromSeconds(seconds); + return true; + } + } + value = null; + return false; + } + + internal static bool HeaderContains(StringValues headers, string headerName) + { + foreach (var header in headers) + { + var index = header.IndexOf(headerName, StringComparison.OrdinalIgnoreCase); + if (index != -1) + { + return true; + } + } + + return false; + } + + private static bool TryParseHeaderInt(int startIndex, string header, out int value) + { + var found = false; + while (startIndex != header.Length) + { + var c = header[startIndex]; + if (c == '=') + { + found = true; + } + else if (c != ' ') + { + --startIndex; + break; + } + ++startIndex; + } + if (found && startIndex != header.Length) + { + var endIndex = startIndex + 1; + while (endIndex < header.Length) + { + var c = header[endIndex]; + if ((c >= '0') && (c <= '9')) + { + endIndex++; + } + else + { + break; + } + } + var length = endIndex - (startIndex + 1); + if (length > 0) + { + value = int.Parse(header.Substring(startIndex + 1, length), NumberStyles.None, NumberFormatInfo.InvariantInfo); + return true; + } + } + value = 0; + return false; + } + } +} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingContext.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingContext.cs index 4fc7292894..4d03434f6a 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingContext.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingContext.cs @@ -5,7 +5,6 @@ using System; using System.IO; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; -using Microsoft.AspNetCore.Http.Headers; using Microsoft.Extensions.Logging; using Microsoft.Net.Http.Headers; @@ -13,16 +12,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal { public class ResponseCachingContext { - private static readonly CacheControlHeaderValue EmptyCacheControl = new CacheControlHeaderValue(); - - private RequestHeaders _requestHeaders; - private ResponseHeaders _responseHeaders; - private CacheControlHeaderValue _requestCacheControl; - private CacheControlHeaderValue _responseCacheControl; private DateTimeOffset? _responseDate; private bool _parsedResponseDate; private DateTimeOffset? _responseExpires; private bool _parsedResponseExpires; + private TimeSpan? _responseSharedMaxAge; + private bool _parsedResponseSharedMaxAge; + private TimeSpan? _responseMaxAge; + private bool _parsedResponseMaxAge; internal ResponseCachingContext(HttpContext httpContext, ILogger logger) { @@ -58,55 +55,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal internal IHttpSendFileFeature OriginalSendFileFeature { get; set; } - internal ResponseHeaders CachedResponseHeaders { get; set; } - - internal RequestHeaders TypedRequestHeaders - { - get - { - if (_requestHeaders == null) - { - _requestHeaders = HttpContext.Request.GetTypedHeaders(); - } - return _requestHeaders; - } - } - - internal ResponseHeaders TypedResponseHeaders - { - get - { - if (_responseHeaders == null) - { - _responseHeaders = HttpContext.Response.GetTypedHeaders(); - } - return _responseHeaders; - } - } - - internal CacheControlHeaderValue RequestCacheControlHeaderValue - { - get - { - if (_requestCacheControl == null) - { - _requestCacheControl = TypedRequestHeaders.CacheControl ?? EmptyCacheControl; - } - return _requestCacheControl; - } - } - - internal CacheControlHeaderValue ResponseCacheControlHeaderValue - { - get - { - if (_responseCacheControl == null) - { - _responseCacheControl = TypedResponseHeaders.CacheControl ?? EmptyCacheControl; - } - return _responseCacheControl; - } - } + internal IHeaderDictionary CachedResponseHeaders { get; set; } internal DateTimeOffset? ResponseDate { @@ -115,7 +64,15 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal if (!_parsedResponseDate) { _parsedResponseDate = true; - _responseDate = TypedResponseHeaders.Date; + DateTimeOffset date; + if (HttpHeaderParsingHelpers.TryParseHeaderDate(HttpContext.Response.Headers[HeaderNames.Date], out date)) + { + _responseDate = date; + } + else + { + _responseDate = null; + } } return _responseDate; } @@ -134,10 +91,44 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal if (!_parsedResponseExpires) { _parsedResponseExpires = true; - _responseExpires = TypedResponseHeaders.Expires; + DateTimeOffset expires; + if (HttpHeaderParsingHelpers.TryParseHeaderDate(HttpContext.Response.Headers[HeaderNames.Expires], out expires)) + { + _responseExpires = expires; + } + else + { + _responseExpires = null; + } } return _responseExpires; } } + + internal TimeSpan? ResponseSharedMaxAge + { + get + { + if (!_parsedResponseSharedMaxAge) + { + _parsedResponseSharedMaxAge = true; + HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(HttpContext.Response.Headers[HeaderNames.CacheControl], CacheControlValues.SharedMaxAgeString, out _responseSharedMaxAge); + } + return _responseSharedMaxAge; + } + } + + internal TimeSpan? ResponseMaxAge + { + get + { + if (!_parsedResponseMaxAge) + { + _parsedResponseMaxAge = true; + HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(HttpContext.Response.Headers[HeaderNames.CacheControl], CacheControlValues.MaxAgeString, out _responseMaxAge); + } + return _responseMaxAge; + } + } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingPolicyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingPolicyProvider.cs index 0072546ef7..87e37915ab 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingPolicyProvider.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingPolicyProvider.cs @@ -10,8 +10,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal { public class ResponseCachingPolicyProvider : IResponseCachingPolicyProvider { - private static readonly CacheControlHeaderValue EmptyCacheControl = new CacheControlHeaderValue(); - public virtual bool IsRequestCacheable(ResponseCachingContext context) { // Verify the method @@ -32,7 +30,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal // Verify request cache-control parameters if (!StringValues.IsNullOrEmpty(request.Headers[HeaderNames.CacheControl])) { - if (context.RequestCacheControlHeaderValue.NoCache) + if (HttpHeaderParsingHelpers.HeaderContains(request.Headers[HeaderNames.CacheControl], CacheControlValues.NoCacheString)) { context.Logger.LogRequestWithNoCacheNotCacheable(); return false; @@ -42,13 +40,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal { // Support for legacy HTTP 1.0 cache directive var pragmaHeaderValues = request.Headers[HeaderNames.Pragma]; - foreach (var directive in pragmaHeaderValues) + if (HttpHeaderParsingHelpers.HeaderContains(request.Headers[HeaderNames.Pragma], CacheControlValues.NoCacheString)) { - if (string.Equals("no-cache", directive, StringComparison.OrdinalIgnoreCase)) - { - context.Logger.LogRequestWithPragmaNoCacheNotCacheable(); - return false; - } + context.Logger.LogRequestWithPragmaNoCacheNotCacheable(); + return false; } } @@ -57,22 +52,30 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal public virtual bool IsResponseCacheable(ResponseCachingContext context) { + var responseCacheControlHeader = context.HttpContext.Response.Headers[HeaderNames.CacheControl]; + // Only cache pages explicitly marked with public - if (!context.ResponseCacheControlHeaderValue.Public) + if (!HttpHeaderParsingHelpers.HeaderContains(responseCacheControlHeader, CacheControlValues.PublicString)) { context.Logger.LogResponseWithoutPublicNotCacheable(); return false; } // Check no-store - if (context.RequestCacheControlHeaderValue.NoStore || context.ResponseCacheControlHeaderValue.NoStore) + if (HttpHeaderParsingHelpers.HeaderContains(context.HttpContext.Request.Headers[HeaderNames.CacheControl], CacheControlValues.NoStoreString)) + { + context.Logger.LogResponseWithNoStoreNotCacheable(); + return false; + } + + if (HttpHeaderParsingHelpers.HeaderContains(responseCacheControlHeader, CacheControlValues.NoStoreString)) { context.Logger.LogResponseWithNoStoreNotCacheable(); return false; } // Check no-cache - if (context.ResponseCacheControlHeaderValue.NoCache) + if (HttpHeaderParsingHelpers.HeaderContains(responseCacheControlHeader, CacheControlValues.NoCacheString)) { context.Logger.LogResponseWithNoCacheNotCacheable(); return false; @@ -96,7 +99,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal } // Check private - if (context.ResponseCacheControlHeaderValue.Private) + if (HttpHeaderParsingHelpers.HeaderContains(responseCacheControlHeader, CacheControlValues.PrivateString)) { context.Logger.LogResponseWithPrivateNotCacheable(); return false; @@ -112,8 +115,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal // Check response freshness if (!context.ResponseDate.HasValue) { - if (!context.ResponseCacheControlHeaderValue.SharedMaxAge.HasValue && - !context.ResponseCacheControlHeaderValue.MaxAge.HasValue && + if (!context.ResponseSharedMaxAge.HasValue && + !context.ResponseMaxAge.HasValue && context.ResponseTime.Value >= context.ResponseExpires) { context.Logger.LogExpirationExpiresExceeded(context.ResponseTime.Value, context.ResponseExpires.Value); @@ -125,22 +128,20 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal var age = context.ResponseTime.Value - context.ResponseDate.Value; // Validate shared max age - var sharedMaxAge = context.ResponseCacheControlHeaderValue.SharedMaxAge; - if (age >= sharedMaxAge) + if (age >= context.ResponseSharedMaxAge) { - context.Logger.LogExpirationSharedMaxAgeExceeded(age, sharedMaxAge.Value); + context.Logger.LogExpirationSharedMaxAgeExceeded(age, context.ResponseSharedMaxAge.Value); return false; } - else if (!sharedMaxAge.HasValue) + else if (!context.ResponseSharedMaxAge.HasValue) { // Validate max age - var maxAge = context.ResponseCacheControlHeaderValue.MaxAge; - if (age >= maxAge) + if (age >= context.ResponseMaxAge) { - context.Logger.LogExpirationMaxAgeExceeded(age, maxAge.Value); + context.Logger.LogExpirationMaxAgeExceeded(age, context.ResponseMaxAge.Value); return false; } - else if (!maxAge.HasValue) + else if (!context.ResponseMaxAge.HasValue) { // Validate expiration if (context.ResponseTime.Value >= context.ResponseExpires) @@ -158,44 +159,53 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal public virtual bool IsCachedEntryFresh(ResponseCachingContext context) { var age = context.CachedEntryAge.Value; - var cachedControlHeaders = context.CachedResponseHeaders.CacheControl ?? EmptyCacheControl; + var cachedControlHeaders = context.CachedResponseHeaders[HeaderNames.CacheControl]; + var requestCacheControlHeaders = context.HttpContext.Request.Headers[HeaderNames.CacheControl]; // Add min-fresh requirements - var minFresh = context.RequestCacheControlHeaderValue.MinFresh; - if (minFresh.HasValue) + TimeSpan? minFresh; + if (HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(requestCacheControlHeaders, CacheControlValues.MinFreshString, out minFresh)) { age += minFresh.Value; context.Logger.LogExpirationMinFreshAdded(minFresh.Value); } // Validate shared max age, this overrides any max age settings for shared caches - var sharedMaxAge = cachedControlHeaders.SharedMaxAge; - if (age >= sharedMaxAge) + TimeSpan? cachedSharedMaxAge; + HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(cachedControlHeaders, CacheControlValues.SharedMaxAgeString, out cachedSharedMaxAge); + + if (age >= cachedSharedMaxAge) { // shared max age implies must revalidate - context.Logger.LogExpirationSharedMaxAgeExceeded(age, sharedMaxAge.Value); + context.Logger.LogExpirationSharedMaxAgeExceeded(age, cachedSharedMaxAge.Value); return false; } - else if (!sharedMaxAge.HasValue) + else if (!cachedSharedMaxAge.HasValue) { - var cachedMaxAge = cachedControlHeaders.MaxAge; - var requestMaxAge = context.RequestCacheControlHeaderValue.MaxAge; + TimeSpan? requestMaxAge; + HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(requestCacheControlHeaders, CacheControlValues.MaxAgeString, out requestMaxAge); + + TimeSpan? cachedMaxAge; + HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(cachedControlHeaders, CacheControlValues.MaxAgeString, out cachedMaxAge); + var lowestMaxAge = cachedMaxAge < requestMaxAge ? cachedMaxAge : requestMaxAge ?? cachedMaxAge; // Validate max age if (age >= lowestMaxAge) { // Must revalidate - if (cachedControlHeaders.MustRevalidate) + if (HttpHeaderParsingHelpers.HeaderContains(cachedControlHeaders, CacheControlValues.MustRevalidateString)) { context.Logger.LogExpirationMustRevalidate(age, lowestMaxAge.Value); return false; } + TimeSpan? requestMaxStale; + HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(requestCacheControlHeaders, CacheControlValues.MaxStaleString, out requestMaxStale); + // Request allows stale values - var maxStaleLimit = context.RequestCacheControlHeaderValue.MaxStaleLimit; - if (maxStaleLimit.HasValue && age - lowestMaxAge < maxStaleLimit) + if (requestMaxStale.HasValue && age - lowestMaxAge < requestMaxStale) { - context.Logger.LogExpirationMaxStaleSatisfied(age, lowestMaxAge.Value, maxStaleLimit.Value); + context.Logger.LogExpirationMaxStaleSatisfied(age, lowestMaxAge.Value, requestMaxStale.Value); return true; } @@ -205,11 +215,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal else if (!cachedMaxAge.HasValue && !requestMaxAge.HasValue) { // Validate expiration - var responseTime = context.ResponseTime.Value; - var expires = context.CachedResponseHeaders.Expires; - if (responseTime >= expires) + DateTimeOffset expires; + if (HttpHeaderParsingHelpers.TryParseHeaderDate(context.CachedResponseHeaders[HeaderNames.Expires], out expires) && + context.ResponseTime.Value >= expires) { - context.Logger.LogExpirationExpiresExceeded(responseTime, expires.Value); + context.Logger.LogExpirationExpiresExceeded(context.ResponseTime.Value, expires); return false; } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs index dc01df4fba..a87f2deee9 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs @@ -119,7 +119,7 @@ namespace Microsoft.AspNetCore.ResponseCaching } context.CachedResponse = cachedResponse; - context.CachedResponseHeaders = new ResponseHeaders(cachedResponse.Headers); + context.CachedResponseHeaders = cachedResponse.Headers; context.ResponseTime = _options.SystemClock.UtcNow; var cachedEntryAge = context.ResponseTime.Value - context.CachedResponse.Created; context.CachedEntryAge = cachedEntryAge > TimeSpan.Zero ? cachedEntryAge : TimeSpan.Zero; @@ -198,7 +198,7 @@ namespace Microsoft.AspNetCore.ResponseCaching } } - if (context.RequestCacheControlHeaderValue.OnlyIfCached) + if (HttpHeaderParsingHelpers.HeaderContains(context.HttpContext.Request.Headers[HeaderNames.CacheControl], CacheControlValues.OnlyIfCachedString)) { _logger.LogGatewayTimeoutServed(); context.HttpContext.Response.StatusCode = StatusCodes.Status504GatewayTimeout; @@ -219,8 +219,8 @@ namespace Microsoft.AspNetCore.ResponseCaching var response = context.HttpContext.Response; var varyHeaders = new StringValues(response.Headers.GetCommaSeparatedValues(HeaderNames.Vary)); var varyQueryKeys = new StringValues(context.HttpContext.Features.Get()?.VaryByQueryKeys); - context.CachedResponseValidFor = context.ResponseCacheControlHeaderValue.SharedMaxAge ?? - context.ResponseCacheControlHeaderValue.MaxAge ?? + context.CachedResponseValidFor = context.ResponseSharedMaxAge ?? + context.ResponseMaxAge ?? (context.ResponseExpires - context.ResponseTime.Value) ?? DefaultExpirationTimeSpan; @@ -256,7 +256,7 @@ namespace Microsoft.AspNetCore.ResponseCaching { context.ResponseDate = context.ResponseTime.Value; // Setting the date on the raw response headers. - context.TypedResponseHeaders.Date = context.ResponseDate; + context.HttpContext.Response.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(context.ResponseDate.Value); } // Store the response on the state @@ -266,7 +266,7 @@ namespace Microsoft.AspNetCore.ResponseCaching StatusCode = context.HttpContext.Response.StatusCode }; - foreach (var header in context.TypedResponseHeaders.Headers) + foreach (var header in context.HttpContext.Response.Headers) { if (!string.Equals(header.Key, HeaderNames.Age, StringComparison.OrdinalIgnoreCase)) { @@ -282,7 +282,7 @@ namespace Microsoft.AspNetCore.ResponseCaching internal async Task FinalizeCacheBodyAsync(ResponseCachingContext context) { - var contentLength = context.TypedResponseHeaders.ContentLength; + var contentLength = context.HttpContext.Response.ContentLength; if (context.ShouldCacheResponse && context.ResponseCachingStream.BufferingEnabled) { var bufferStream = context.ResponseCachingStream.GetBufferStream(); @@ -355,37 +355,51 @@ namespace Microsoft.AspNetCore.ResponseCaching internal static bool ContentIsNotModified(ResponseCachingContext context) { var cachedResponseHeaders = context.CachedResponseHeaders; - var ifNoneMatchHeader = context.TypedRequestHeaders.IfNoneMatch; + var ifNoneMatchHeader = context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch]; - if (ifNoneMatchHeader != null) + if (!StringValues.IsNullOrEmpty(ifNoneMatchHeader)) { - if (ifNoneMatchHeader.Count == 1 && ifNoneMatchHeader[0].Equals(EntityTagHeaderValue.Any)) + if (ifNoneMatchHeader.Count == 1 && ifNoneMatchHeader[0].Equals(EntityTagHeaderValue.Any.Tag)) { context.Logger.LogNotModifiedIfNoneMatchStar(); return true; } - if (cachedResponseHeaders.ETag != null) + if (!StringValues.IsNullOrEmpty(cachedResponseHeaders[HeaderNames.ETag])) { - foreach (var tag in ifNoneMatchHeader) + EntityTagHeaderValue eTag; + if (EntityTagHeaderValue.TryParse(cachedResponseHeaders[HeaderNames.ETag], out eTag)) { - if (cachedResponseHeaders.ETag.Compare(tag, useStrongComparison: false)) + foreach (var tag in ifNoneMatchHeader) { - context.Logger.LogNotModifiedIfNoneMatchMatched(tag); - return true; + EntityTagHeaderValue requestETag; + if (EntityTagHeaderValue.TryParse(tag, out requestETag) && + eTag.Compare(requestETag, useStrongComparison: false)) + { + context.Logger.LogNotModifiedIfNoneMatchMatched(requestETag); + return true; + } } } } } else { - var ifUnmodifiedSince = context.TypedRequestHeaders.IfUnmodifiedSince; - if (ifUnmodifiedSince != null) + var ifUnmodifiedSince = context.HttpContext.Request.Headers[HeaderNames.IfUnmodifiedSince]; + if (!StringValues.IsNullOrEmpty(ifUnmodifiedSince)) { - var lastModified = cachedResponseHeaders.LastModified ?? cachedResponseHeaders.Date; - if (lastModified <= ifUnmodifiedSince) + DateTimeOffset modified; + if (!HttpHeaderParsingHelpers.TryParseHeaderDate(cachedResponseHeaders[HeaderNames.LastModified], out modified) && + !HttpHeaderParsingHelpers.TryParseHeaderDate(cachedResponseHeaders[HeaderNames.Date], out modified)) { - context.Logger.LogNotModifiedIfUnmodifiedSinceSatisfied(lastModified.Value, ifUnmodifiedSince.Value); + return false; + } + + DateTimeOffset unmodifiedSince; + if (HttpHeaderParsingHelpers.TryParseHeaderDate(ifUnmodifiedSince, out unmodifiedSince) && + modified <= unmodifiedSince) + { + context.Logger.LogNotModifiedIfUnmodifiedSinceSatisfied(modified, unmodifiedSince); return true; } } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ParsingHelpersTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ParsingHelpersTests.cs new file mode 100644 index 0000000000..fb495bb535 --- /dev/null +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ParsingHelpersTests.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.Internal; +using Microsoft.Extensions.Primitives; +using Xunit; + +namespace Microsoft.AspNetCore.ResponseCaching.Tests +{ + public class ParsingHelpersTests + { + [Theory] + [InlineData("h=1", "h", 1)] + [InlineData("header1=3, header2=10", "header1", 3)] + [InlineData("header1 =45, header2=80", "header1", 45)] + [InlineData("header1= 89 , header2=22", "header1", 89)] + [InlineData("header1= 89 , header2= 42", "header2", 42)] + void TryGetHeaderValue_Succeeds(string headerValue, string headerName, int expectedValue) + { + TimeSpan? value; + Assert.True(HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(new StringValues(headerValue), headerName, out value)); + Assert.Equal(TimeSpan.FromSeconds(expectedValue), value); + } + + [Theory] + [InlineData("h=", "h")] + [InlineData("header1=, header2=10", "header1")] + [InlineData("header1 , header2=80", "header1")] + [InlineData("h=10", "header")] + [InlineData("", "")] + [InlineData(null, null)] + void TryGetHeaderValue_Fails(string headerValue, string headerName) + { + TimeSpan? value; + Assert.False(HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(new StringValues(headerValue), headerName, out value)); + } + } +} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs index fb742416b1..ad607e9a1c 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs @@ -23,10 +23,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider()); var context = TestUtils.CreateTestContext(); - context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { OnlyIfCached = true - }; + }.ToString(); Assert.True(await middleware.TryServeFromCacheAsync(context)); Assert.Equal(StatusCodes.Status504GatewayTimeout, context.HttpContext.Response.StatusCode); @@ -149,7 +149,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); + context.CachedResponseHeaders = new HeaderDictionary(); Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context)); Assert.Empty(sink.Writes); @@ -161,22 +161,22 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var utcNow = DateTimeOffset.UtcNow; var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); + context.CachedResponseHeaders = new HeaderDictionary(); - context.TypedRequestHeaders.IfUnmodifiedSince = utcNow; + context.HttpContext.Request.Headers[HeaderNames.IfUnmodifiedSince] = HeaderUtilities.FormatDate(utcNow); // Verify modifications in the past succeeds - context.CachedResponseHeaders.Date = utcNow - TimeSpan.FromSeconds(10); + context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10)); Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context)); Assert.Equal(1, sink.Writes.Count); // Verify modifications at present succeeds - context.CachedResponseHeaders.Date = utcNow; + context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow); Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context)); Assert.Equal(2, sink.Writes.Count); // Verify modifications in the future fails - context.CachedResponseHeaders.Date = utcNow + TimeSpan.FromSeconds(10); + context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context)); // Verify logging @@ -192,25 +192,25 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var utcNow = DateTimeOffset.UtcNow; var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); + context.CachedResponseHeaders = new HeaderDictionary(); - context.TypedRequestHeaders.IfUnmodifiedSince = utcNow; + context.HttpContext.Request.Headers[HeaderNames.IfUnmodifiedSince] = HeaderUtilities.FormatDate(utcNow); // Verify modifications in the past succeeds - context.CachedResponseHeaders.Date = utcNow + TimeSpan.FromSeconds(10); - context.CachedResponseHeaders.LastModified = utcNow - TimeSpan.FromSeconds(10); + context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); + context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10)); Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context)); Assert.Equal(1, sink.Writes.Count); // Verify modifications at present - context.CachedResponseHeaders.Date = utcNow + TimeSpan.FromSeconds(10); - context.CachedResponseHeaders.LastModified = utcNow; + context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); + context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow); Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context)); Assert.Equal(2, sink.Writes.Count); // Verify modifications in the future fails - context.CachedResponseHeaders.Date = utcNow - TimeSpan.FromSeconds(10); - context.CachedResponseHeaders.LastModified = utcNow + TimeSpan.FromSeconds(10); + context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10)); + context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context)); // Verify logging @@ -226,13 +226,13 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var utcNow = DateTimeOffset.UtcNow; var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); + context.CachedResponseHeaders = new HeaderDictionary(); // This would fail the IfUnmodifiedSince checks - context.TypedRequestHeaders.IfUnmodifiedSince = utcNow; - context.CachedResponseHeaders.LastModified = utcNow + TimeSpan.FromSeconds(10); + context.HttpContext.Request.Headers[HeaderNames.IfUnmodifiedSince] = HeaderUtilities.FormatDate(utcNow); + context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); - context.TypedRequestHeaders.IfNoneMatch = new List(new[] { EntityTagHeaderValue.Any }); + context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = EntityTagHeaderValue.Any.ToString(); Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context)); TestUtils.AssertLoggedMessages( sink.Writes, @@ -245,13 +245,13 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var utcNow = DateTimeOffset.UtcNow; var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); + context.CachedResponseHeaders = new HeaderDictionary(); // This would pass the IfUnmodifiedSince checks - context.TypedRequestHeaders.IfUnmodifiedSince = utcNow; - context.CachedResponseHeaders.LastModified = utcNow - TimeSpan.FromSeconds(10); + context.HttpContext.Request.Headers[HeaderNames.IfUnmodifiedSince] = HeaderUtilities.FormatDate(utcNow); + context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10)); - context.TypedRequestHeaders.IfNoneMatch = new List(new[] { new EntityTagHeaderValue("\"E1\"") }); + context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = new List(new[] { new EntityTagHeaderValue("\"E1\"") }).ToString(); Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context)); Assert.Empty(sink.Writes); } @@ -261,9 +261,9 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); + context.CachedResponseHeaders = new HeaderDictionary(); - context.TypedRequestHeaders.IfNoneMatch = new List(new[] { new EntityTagHeaderValue("\"E1\"") }); + context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = new List(new[] { new EntityTagHeaderValue("\"E1\"") }).ToString(); Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context)); Assert.Empty(sink.Writes); @@ -289,12 +289,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) - { - ETag = responseETag - }; + context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponseHeaders[HeaderNames.ETag] = responseETag.ToString(); - context.TypedRequestHeaders.IfNoneMatch = new List(new[] { requestETag }); + context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = requestETag.ToString(); Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context)); TestUtils.AssertLoggedMessages( @@ -307,12 +305,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) - { - ETag = new EntityTagHeaderValue("\"E2\"") - }; + context.CachedResponseHeaders = new HeaderDictionary(); + context.HttpContext.Response.Headers[HeaderNames.ETag] = new EntityTagHeaderValue("\"E2\"").ToString(); - context.TypedRequestHeaders.IfNoneMatch = new List(new[] { new EntityTagHeaderValue("\"E1\"") }); + context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = new List(new[] { new EntityTagHeaderValue("\"E1\"") }).ToString(); Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context)); Assert.Empty(sink.Writes); @@ -340,10 +336,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink, policyProvider: new ResponseCachingPolicyProvider()); var context = TestUtils.CreateTestContext(); - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { Public = true - }; + }.ToString(); Assert.False(context.ShouldCacheResponse); @@ -375,7 +371,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var context = TestUtils.CreateTestContext(); context.ResponseTime = utcNow; - context.TypedResponseHeaders.Expires = utcNow + TimeSpan.FromSeconds(11); + context.HttpContext.Response.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(11)); await middleware.FinalizeCacheHeadersAsync(context); @@ -389,12 +385,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink); var context = TestUtils.CreateTestContext(); - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { MaxAge = TimeSpan.FromSeconds(12) - }; + }.ToString(); - context.TypedResponseHeaders.Expires = context.ResponseTime + TimeSpan.FromSeconds(11); + context.HttpContext.Response.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(context.ResponseTime.Value + TimeSpan.FromSeconds(11)); await middleware.FinalizeCacheHeadersAsync(context); @@ -408,13 +404,13 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink); var context = TestUtils.CreateTestContext(); - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { MaxAge = TimeSpan.FromSeconds(12), SharedMaxAge = TimeSpan.FromSeconds(13) - }; + }.ToString(); - context.TypedResponseHeaders.Expires = context.ResponseTime + TimeSpan.FromSeconds(11); + context.HttpContext.Response.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(context.ResponseTime.Value + TimeSpan.FromSeconds(11)); await middleware.FinalizeCacheHeadersAsync(context); @@ -538,11 +534,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var context = TestUtils.CreateTestContext(); context.ResponseTime = utcNow; - Assert.Null(context.TypedResponseHeaders.Date); + Assert.True(StringValues.IsNullOrEmpty(context.HttpContext.Response.Headers[HeaderNames.Date])); await middleware.FinalizeCacheHeadersAsync(context); - Assert.Equal(utcNow, context.TypedResponseHeaders.Date); + Assert.Equal(HeaderUtilities.FormatDate(utcNow), context.HttpContext.Response.Headers[HeaderNames.Date]); Assert.Empty(sink.Writes); } @@ -553,14 +549,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink); var context = TestUtils.CreateTestContext(); - context.TypedResponseHeaders.Date = utcNow; + context.HttpContext.Response.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow); context.ResponseTime = utcNow + TimeSpan.FromSeconds(10); - Assert.Equal(utcNow, context.TypedResponseHeaders.Date); + Assert.Equal(HeaderUtilities.FormatDate(utcNow), context.HttpContext.Response.Headers[HeaderNames.Date]); await middleware.FinalizeCacheHeadersAsync(context); - Assert.Equal(utcNow, context.TypedResponseHeaders.Date); + Assert.Equal(HeaderUtilities.FormatDate(utcNow), context.HttpContext.Response.Headers[HeaderNames.Date]); Assert.Empty(sink.Writes); } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingPolicyProviderTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingPolicyProviderTests.cs index 36fbb6a2d6..02a068e6e4 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingPolicyProviderTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingPolicyProviderTests.cs @@ -88,10 +88,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); context.HttpContext.Request.Method = HttpMethods.Get; - context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { NoCache = true - }; + }.ToString(); Assert.False(new ResponseCachingPolicyProvider().IsRequestCacheable(context)); TestUtils.AssertLoggedMessages( @@ -105,10 +105,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); context.HttpContext.Request.Method = HttpMethods.Get; - context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { NoStore = true - }; + }.ToString(); Assert.True(new ResponseCachingPolicyProvider().IsRequestCacheable(context)); Assert.Empty(sink.Writes); @@ -158,10 +158,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { Public = true - }; + }.ToString(); Assert.True(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); Assert.Empty(sink.Writes); @@ -172,11 +172,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { Public = true, NoCache = true - }; + }.ToString(); Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); TestUtils.AssertLoggedMessages( @@ -189,14 +189,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { NoStore = true - }; - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + }.ToString(); + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { Public = true - }; + }.ToString(); Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); TestUtils.AssertLoggedMessages( @@ -209,11 +209,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { Public = true, NoStore = true - }; + }.ToString(); Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); TestUtils.AssertLoggedMessages( @@ -226,10 +226,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { Public = true - }; + }.ToString(); context.HttpContext.Response.Headers[HeaderNames.SetCookie] = "cookieName=cookieValue"; Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); @@ -243,10 +243,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { Public = true - }; + }.ToString(); context.HttpContext.Response.Headers[HeaderNames.Vary] = "*"; Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); @@ -260,11 +260,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { Public = true, Private = true - }; + }.ToString(); Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); TestUtils.AssertLoggedMessages( @@ -279,10 +279,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.StatusCode = statusCode; - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { Public = true - }; + }.ToString(); Assert.True(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); Assert.Empty(sink.Writes); @@ -342,10 +342,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.StatusCode = statusCode; - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { Public = true - }; + }.ToString(); Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); TestUtils.AssertLoggedMessages( @@ -359,13 +359,13 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { Public = true - }; + }.ToString(); var utcNow = DateTimeOffset.UtcNow; - context.TypedResponseHeaders.Date = utcNow; + context.HttpContext.Response.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow); context.ResponseTime = DateTimeOffset.MaxValue; Assert.True(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); @@ -378,14 +378,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { Public = true - }; + }.ToString(); var utcNow = DateTimeOffset.UtcNow; - context.TypedResponseHeaders.Expires = utcNow; + context.HttpContext.Response.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow); - context.TypedResponseHeaders.Date = utcNow; + context.HttpContext.Response.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow); context.ResponseTime = utcNow; Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); @@ -401,13 +401,13 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { Public = true, MaxAge = TimeSpan.FromSeconds(10) - }; - context.TypedResponseHeaders.Expires = utcNow; - context.TypedResponseHeaders.Date = utcNow; + }.ToString(); + context.HttpContext.Response.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow); + context.HttpContext.Response.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow); context.ResponseTime = utcNow + TimeSpan.FromSeconds(9); Assert.True(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); @@ -421,13 +421,13 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { Public = true, MaxAge = TimeSpan.FromSeconds(10) - }; - context.TypedResponseHeaders.Expires = utcNow; - context.TypedResponseHeaders.Date = utcNow; + }.ToString(); + context.HttpContext.Response.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow); + context.HttpContext.Response.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow); context.ResponseTime = utcNow + TimeSpan.FromSeconds(10); Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); @@ -443,13 +443,13 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { Public = true, MaxAge = TimeSpan.FromSeconds(10), SharedMaxAge = TimeSpan.FromSeconds(15) - }; - context.TypedResponseHeaders.Date = utcNow; + }.ToString(); + context.HttpContext.Response.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow); context.ResponseTime = utcNow + TimeSpan.FromSeconds(11); Assert.True(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); @@ -463,13 +463,13 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; - context.TypedResponseHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { Public = true, MaxAge = TimeSpan.FromSeconds(10), SharedMaxAge = TimeSpan.FromSeconds(5) - }; - context.TypedResponseHeaders.Date = utcNow; + }.ToString(); + context.HttpContext.Response.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow); context.ResponseTime = utcNow + TimeSpan.FromSeconds(5); Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); @@ -486,7 +486,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var context = TestUtils.CreateTestContext(sink); context.ResponseTime = DateTimeOffset.MaxValue; context.CachedEntryAge = TimeSpan.MaxValue; - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()); + context.CachedResponseHeaders = new HeaderDictionary(); Assert.True(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); Assert.Empty(sink.Writes); @@ -500,13 +500,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var context = TestUtils.CreateTestContext(sink); context.ResponseTime = DateTimeOffset.MaxValue; context.CachedEntryAge = TimeSpan.MaxValue; - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) + context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() { - CacheControl = new CacheControlHeaderValue() - { - Public = true - } - }; + Public = true + }.ToString(); Assert.True(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); Assert.Empty(sink.Writes); @@ -520,14 +518,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var context = TestUtils.CreateTestContext(sink); context.ResponseTime = utcNow; context.CachedEntryAge = TimeSpan.Zero; - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) + context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() { - CacheControl = new CacheControlHeaderValue() - { - Public = true - }, - Expires = utcNow - }; + Public = true + }.ToString(); + context.CachedResponseHeaders[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow); Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); TestUtils.AssertLoggedMessages( @@ -543,15 +539,13 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var context = TestUtils.CreateTestContext(sink); context.CachedEntryAge = TimeSpan.FromSeconds(9); context.ResponseTime = utcNow + context.CachedEntryAge; - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) + context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() { - CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }, - Expires = utcNow - }; + Public = true, + MaxAge = TimeSpan.FromSeconds(10) + }.ToString(); + context.HttpContext.Response.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow); Assert.True(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); Assert.Empty(sink.Writes); @@ -565,15 +559,13 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var context = TestUtils.CreateTestContext(sink); context.CachedEntryAge = TimeSpan.FromSeconds(10); context.ResponseTime = utcNow + context.CachedEntryAge; - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) + context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() { - CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10) - }, - Expires = utcNow - }; + Public = true, + MaxAge = TimeSpan.FromSeconds(10) + }.ToString(); + context.HttpContext.Response.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow); Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); TestUtils.AssertLoggedMessages( @@ -589,16 +581,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var context = TestUtils.CreateTestContext(sink); context.CachedEntryAge = TimeSpan.FromSeconds(11); context.ResponseTime = utcNow + context.CachedEntryAge; - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) + context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() { - CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10), - SharedMaxAge = TimeSpan.FromSeconds(15) - }, - Expires = utcNow - }; + Public = true, + MaxAge = TimeSpan.FromSeconds(10), + SharedMaxAge = TimeSpan.FromSeconds(15) + }.ToString(); + context.CachedResponseHeaders[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow); Assert.True(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); Assert.Empty(sink.Writes); @@ -612,16 +602,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var context = TestUtils.CreateTestContext(sink); context.CachedEntryAge = TimeSpan.FromSeconds(5); context.ResponseTime = utcNow + context.CachedEntryAge; - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) + context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() { - CacheControl = new CacheControlHeaderValue() - { - Public = true, - MaxAge = TimeSpan.FromSeconds(10), - SharedMaxAge = TimeSpan.FromSeconds(5) - }, - Expires = utcNow - }; + Public = true, + MaxAge = TimeSpan.FromSeconds(10), + SharedMaxAge = TimeSpan.FromSeconds(5) + }.ToString(); + context.CachedResponseHeaders[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow); Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); TestUtils.AssertLoggedMessages( @@ -634,18 +622,16 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { MinFresh = TimeSpan.FromSeconds(2) - }; - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) + }.ToString(); + context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() { - CacheControl = new CacheControlHeaderValue() - { - MaxAge = TimeSpan.FromSeconds(10), - SharedMaxAge = TimeSpan.FromSeconds(5) - } - }; + MaxAge = TimeSpan.FromSeconds(10), + SharedMaxAge = TimeSpan.FromSeconds(5) + }.ToString(); context.CachedEntryAge = TimeSpan.FromSeconds(3); Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); @@ -660,17 +646,15 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { MaxAge = TimeSpan.FromSeconds(5) - }; - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) + }.ToString(); + context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() { - CacheControl = new CacheControlHeaderValue() - { - MaxAge = TimeSpan.FromSeconds(10), - } - }; + MaxAge = TimeSpan.FromSeconds(10), + }.ToString(); context.CachedEntryAge = TimeSpan.FromSeconds(5); Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); @@ -684,19 +668,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { MaxAge = TimeSpan.FromSeconds(5), MaxStale = true, // This value must be set to true in order to specify MaxStaleLimit MaxStaleLimit = TimeSpan.FromSeconds(2) - }; - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) + }.ToString(); + context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() { - CacheControl = new CacheControlHeaderValue() - { - MaxAge = TimeSpan.FromSeconds(5), - } - }; + MaxAge = TimeSpan.FromSeconds(5), + }.ToString(); context.CachedEntryAge = TimeSpan.FromSeconds(6); Assert.True(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); @@ -710,19 +692,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { MaxAge = TimeSpan.FromSeconds(5), MaxStale = true, // This value must be set to true in order to specify MaxStaleLimit MaxStaleLimit = TimeSpan.FromSeconds(1) - }; - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) + }.ToString(); + context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() { - CacheControl = new CacheControlHeaderValue() - { - MaxAge = TimeSpan.FromSeconds(5), - } - }; + MaxAge = TimeSpan.FromSeconds(5), + }.ToString(); context.CachedEntryAge = TimeSpan.FromSeconds(6); Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); @@ -736,20 +716,18 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.TypedRequestHeaders.CacheControl = new CacheControlHeaderValue() + context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { MaxAge = TimeSpan.FromSeconds(5), MaxStale = true, // This value must be set to true in order to specify MaxStaleLimit MaxStaleLimit = TimeSpan.FromSeconds(2) - }; - context.CachedResponseHeaders = new ResponseHeaders(new HeaderDictionary()) + }.ToString(); + context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() { - CacheControl = new CacheControlHeaderValue() - { - MaxAge = TimeSpan.FromSeconds(5), - MustRevalidate = true - } - }; + MaxAge = TimeSpan.FromSeconds(5), + MustRevalidate = true + }.ToString(); context.CachedEntryAge = TimeSpan.FromSeconds(6); Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); From e01431f33c07151c2129f4d92e2b7ceebbb8653a Mon Sep 17 00:00:00 2001 From: John Luo Date: Fri, 9 Dec 2016 14:55:53 -0800 Subject: [PATCH 071/188] Relocate improvements to HttpAbstractions --- .../CacheControlValues.cs | 21 ---- .../Internal/HttpHeaderParsingHelpers.cs | 118 ------------------ .../Internal/MemoryResponseCache.cs | 2 +- .../Internal/ResponseCachingContext.cs | 8 +- .../Internal/ResponseCachingPolicyProvider.cs | 35 +++--- .../ResponseCachingMiddleware.cs | 32 +++-- .../ParsingHelpersTests.cs | 39 ------ .../ResponseCachingMiddlewareTests.cs | 27 ++-- 8 files changed, 54 insertions(+), 228 deletions(-) delete mode 100644 src/Microsoft.AspNetCore.ResponseCaching/CacheControlValues.cs delete mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Internal/HttpHeaderParsingHelpers.cs delete mode 100644 test/Microsoft.AspNetCore.ResponseCaching.Tests/ParsingHelpersTests.cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/CacheControlValues.cs b/src/Microsoft.AspNetCore.ResponseCaching/CacheControlValues.cs deleted file mode 100644 index 1dd0db8167..0000000000 --- a/src/Microsoft.AspNetCore.ResponseCaching/CacheControlValues.cs +++ /dev/null @@ -1,21 +0,0 @@ -// 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.Internal -{ - internal class CacheControlValues - { - public const string MaxAgeString = "max-age"; - public const string MaxStaleString = "max-stale"; - public const string MinFreshString = "min-fresh"; - public const string MustRevalidateString = "must-revalidate"; - public const string NoCacheString = "no-cache"; - public const string NoStoreString = "no-store"; - public const string NoTransformString = "no-transform"; - public const string OnlyIfCachedString = "only-if-cached"; - public const string PrivateString = "private"; - public const string ProxyRevalidateString = "proxy-revalidate"; - public const string PublicString = "public"; - public const string SharedMaxAgeString = "s-maxage"; - } -} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/HttpHeaderParsingHelpers.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/HttpHeaderParsingHelpers.cs deleted file mode 100644 index 94d452198b..0000000000 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/HttpHeaderParsingHelpers.cs +++ /dev/null @@ -1,118 +0,0 @@ -// 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.Globalization; -using Microsoft.Extensions.Primitives; - -namespace Microsoft.AspNetCore.ResponseCaching.Internal -{ - internal static class HttpHeaderParsingHelpers - { - private static readonly string[] DateFormats = new string[] { - // "r", // RFC 1123, required output format but too strict for input - "ddd, d MMM yyyy H:m:s 'GMT'", // RFC 1123 (r, except it allows both 1 and 01 for date and time) - "ddd, d MMM yyyy H:m:s", // RFC 1123, no zone - assume GMT - "d MMM yyyy H:m:s 'GMT'", // RFC 1123, no day-of-week - "d MMM yyyy H:m:s", // RFC 1123, no day-of-week, no zone - "ddd, d MMM yy H:m:s 'GMT'", // RFC 1123, short year - "ddd, d MMM yy H:m:s", // RFC 1123, short year, no zone - "d MMM yy H:m:s 'GMT'", // RFC 1123, no day-of-week, short year - "d MMM yy H:m:s", // RFC 1123, no day-of-week, short year, no zone - - "dddd, d'-'MMM'-'yy H:m:s 'GMT'", // RFC 850 - "dddd, d'-'MMM'-'yy H:m:s", // RFC 850 no zone - "ddd MMM d H:m:s yyyy", // ANSI C's asctime() format - - "ddd, d MMM yyyy H:m:s zzz", // RFC 5322 - "ddd, d MMM yyyy H:m:s", // RFC 5322 no zone - "d MMM yyyy H:m:s zzz", // RFC 5322 no day-of-week - "d MMM yyyy H:m:s", // RFC 5322 no day-of-week, no zone - }; - - // Try the various date formats in the order listed above. - // We should accept a wide verity of common formats, but only output RFC 1123 style dates. - internal static bool TryParseHeaderDate(string input, out DateTimeOffset result) => DateTimeOffset.TryParseExact(input, DateFormats, DateTimeFormatInfo.InvariantInfo, - DateTimeStyles.AllowWhiteSpaces | DateTimeStyles.AssumeUniversal, out result); - - // Try to get the value of a specific header from a list of headers - // e.g. "header1=10, header2=30" - internal static bool TryParseHeaderTimeSpan(StringValues headers, string headerName, out TimeSpan? value) - { - foreach (var header in headers) - { - var index = header.IndexOf(headerName, StringComparison.OrdinalIgnoreCase); - if (index != -1) - { - index += headerName.Length; - int seconds; - if (!TryParseHeaderInt(index, header, out seconds)) - { - break; - } - value = TimeSpan.FromSeconds(seconds); - return true; - } - } - value = null; - return false; - } - - internal static bool HeaderContains(StringValues headers, string headerName) - { - foreach (var header in headers) - { - var index = header.IndexOf(headerName, StringComparison.OrdinalIgnoreCase); - if (index != -1) - { - return true; - } - } - - return false; - } - - private static bool TryParseHeaderInt(int startIndex, string header, out int value) - { - var found = false; - while (startIndex != header.Length) - { - var c = header[startIndex]; - if (c == '=') - { - found = true; - } - else if (c != ' ') - { - --startIndex; - break; - } - ++startIndex; - } - if (found && startIndex != header.Length) - { - var endIndex = startIndex + 1; - while (endIndex < header.Length) - { - var c = header[endIndex]; - if ((c >= '0') && (c <= '9')) - { - endIndex++; - } - else - { - break; - } - } - var length = endIndex - (startIndex + 1); - if (length > 0) - { - value = int.Parse(header.Substring(startIndex + 1, length), NumberStyles.None, NumberFormatInfo.InvariantInfo); - return true; - } - } - value = 0; - return false; - } - } -} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs index 4a855e2d24..f2509c8ce3 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs @@ -24,7 +24,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal public Task GetAsync(string key) { var entry = _cache.Get(key); - + var memoryCachedResponse = entry as MemoryCachedResponse; if (memoryCachedResponse != null) { diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingContext.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingContext.cs index 4d03434f6a..eeed0d9a09 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingContext.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingContext.cs @@ -65,7 +65,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal { _parsedResponseDate = true; DateTimeOffset date; - if (HttpHeaderParsingHelpers.TryParseHeaderDate(HttpContext.Response.Headers[HeaderNames.Date], out date)) + if (HeaderUtilities.TryParseDate(HttpContext.Response.Headers[HeaderNames.Date], out date)) { _responseDate = date; } @@ -92,7 +92,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal { _parsedResponseExpires = true; DateTimeOffset expires; - if (HttpHeaderParsingHelpers.TryParseHeaderDate(HttpContext.Response.Headers[HeaderNames.Expires], out expires)) + if (HeaderUtilities.TryParseDate(HttpContext.Response.Headers[HeaderNames.Expires], out expires)) { _responseExpires = expires; } @@ -112,7 +112,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal if (!_parsedResponseSharedMaxAge) { _parsedResponseSharedMaxAge = true; - HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(HttpContext.Response.Headers[HeaderNames.CacheControl], CacheControlValues.SharedMaxAgeString, out _responseSharedMaxAge); + HeaderUtilities.TryParseSeconds(HttpContext.Response.Headers[HeaderNames.CacheControl], CacheControlHeaderValue.SharedMaxAgeString, out _responseSharedMaxAge); } return _responseSharedMaxAge; } @@ -125,7 +125,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal if (!_parsedResponseMaxAge) { _parsedResponseMaxAge = true; - HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(HttpContext.Response.Headers[HeaderNames.CacheControl], CacheControlValues.MaxAgeString, out _responseMaxAge); + HeaderUtilities.TryParseSeconds(HttpContext.Response.Headers[HeaderNames.CacheControl], CacheControlHeaderValue.MaxAgeString, out _responseMaxAge); } return _responseMaxAge; } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingPolicyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingPolicyProvider.cs index 87e37915ab..c37fcacc70 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingPolicyProvider.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingPolicyProvider.cs @@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal // Verify request cache-control parameters if (!StringValues.IsNullOrEmpty(request.Headers[HeaderNames.CacheControl])) { - if (HttpHeaderParsingHelpers.HeaderContains(request.Headers[HeaderNames.CacheControl], CacheControlValues.NoCacheString)) + if (HeaderUtilities.ContainsCacheDirective(request.Headers[HeaderNames.CacheControl], CacheControlHeaderValue.NoCacheString)) { context.Logger.LogRequestWithNoCacheNotCacheable(); return false; @@ -40,7 +40,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal { // Support for legacy HTTP 1.0 cache directive var pragmaHeaderValues = request.Headers[HeaderNames.Pragma]; - if (HttpHeaderParsingHelpers.HeaderContains(request.Headers[HeaderNames.Pragma], CacheControlValues.NoCacheString)) + if (HeaderUtilities.ContainsCacheDirective(request.Headers[HeaderNames.Pragma], CacheControlHeaderValue.NoCacheString)) { context.Logger.LogRequestWithPragmaNoCacheNotCacheable(); return false; @@ -55,27 +55,22 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal var responseCacheControlHeader = context.HttpContext.Response.Headers[HeaderNames.CacheControl]; // Only cache pages explicitly marked with public - if (!HttpHeaderParsingHelpers.HeaderContains(responseCacheControlHeader, CacheControlValues.PublicString)) + if (!HeaderUtilities.ContainsCacheDirective(responseCacheControlHeader, CacheControlHeaderValue.PublicString)) { context.Logger.LogResponseWithoutPublicNotCacheable(); return false; } // Check no-store - if (HttpHeaderParsingHelpers.HeaderContains(context.HttpContext.Request.Headers[HeaderNames.CacheControl], CacheControlValues.NoStoreString)) - { - context.Logger.LogResponseWithNoStoreNotCacheable(); - return false; - } - - if (HttpHeaderParsingHelpers.HeaderContains(responseCacheControlHeader, CacheControlValues.NoStoreString)) + if (HeaderUtilities.ContainsCacheDirective(context.HttpContext.Request.Headers[HeaderNames.CacheControl], CacheControlHeaderValue.NoStoreString) + || HeaderUtilities.ContainsCacheDirective(responseCacheControlHeader, CacheControlHeaderValue.NoStoreString)) { context.Logger.LogResponseWithNoStoreNotCacheable(); return false; } // Check no-cache - if (HttpHeaderParsingHelpers.HeaderContains(responseCacheControlHeader, CacheControlValues.NoCacheString)) + if (HeaderUtilities.ContainsCacheDirective(responseCacheControlHeader, CacheControlHeaderValue.NoCacheString)) { context.Logger.LogResponseWithNoCacheNotCacheable(); return false; @@ -99,7 +94,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal } // Check private - if (HttpHeaderParsingHelpers.HeaderContains(responseCacheControlHeader, CacheControlValues.PrivateString)) + if (HeaderUtilities.ContainsCacheDirective(responseCacheControlHeader, CacheControlHeaderValue.PrivateString)) { context.Logger.LogResponseWithPrivateNotCacheable(); return false; @@ -159,12 +154,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal public virtual bool IsCachedEntryFresh(ResponseCachingContext context) { var age = context.CachedEntryAge.Value; - var cachedControlHeaders = context.CachedResponseHeaders[HeaderNames.CacheControl]; + var cachedCacheControlHeaders = context.CachedResponseHeaders[HeaderNames.CacheControl]; var requestCacheControlHeaders = context.HttpContext.Request.Headers[HeaderNames.CacheControl]; // Add min-fresh requirements TimeSpan? minFresh; - if (HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(requestCacheControlHeaders, CacheControlValues.MinFreshString, out minFresh)) + if (HeaderUtilities.TryParseSeconds(requestCacheControlHeaders, CacheControlHeaderValue.MinFreshString, out minFresh)) { age += minFresh.Value; context.Logger.LogExpirationMinFreshAdded(minFresh.Value); @@ -172,7 +167,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal // Validate shared max age, this overrides any max age settings for shared caches TimeSpan? cachedSharedMaxAge; - HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(cachedControlHeaders, CacheControlValues.SharedMaxAgeString, out cachedSharedMaxAge); + HeaderUtilities.TryParseSeconds(cachedCacheControlHeaders, CacheControlHeaderValue.SharedMaxAgeString, out cachedSharedMaxAge); if (age >= cachedSharedMaxAge) { @@ -183,24 +178,24 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal else if (!cachedSharedMaxAge.HasValue) { TimeSpan? requestMaxAge; - HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(requestCacheControlHeaders, CacheControlValues.MaxAgeString, out requestMaxAge); + HeaderUtilities.TryParseSeconds(requestCacheControlHeaders, CacheControlHeaderValue.MaxAgeString, out requestMaxAge); TimeSpan? cachedMaxAge; - HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(cachedControlHeaders, CacheControlValues.MaxAgeString, out cachedMaxAge); + HeaderUtilities.TryParseSeconds(cachedCacheControlHeaders, CacheControlHeaderValue.MaxAgeString, out cachedMaxAge); var lowestMaxAge = cachedMaxAge < requestMaxAge ? cachedMaxAge : requestMaxAge ?? cachedMaxAge; // Validate max age if (age >= lowestMaxAge) { // Must revalidate - if (HttpHeaderParsingHelpers.HeaderContains(cachedControlHeaders, CacheControlValues.MustRevalidateString)) + if (HeaderUtilities.ContainsCacheDirective(cachedCacheControlHeaders, CacheControlHeaderValue.MustRevalidateString)) { context.Logger.LogExpirationMustRevalidate(age, lowestMaxAge.Value); return false; } TimeSpan? requestMaxStale; - HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(requestCacheControlHeaders, CacheControlValues.MaxStaleString, out requestMaxStale); + HeaderUtilities.TryParseSeconds(requestCacheControlHeaders, CacheControlHeaderValue.MaxStaleString, out requestMaxStale); // Request allows stale values if (requestMaxStale.HasValue && age - lowestMaxAge < requestMaxStale) @@ -216,7 +211,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal { // Validate expiration DateTimeOffset expires; - if (HttpHeaderParsingHelpers.TryParseHeaderDate(context.CachedResponseHeaders[HeaderNames.Expires], out expires) && + if (HeaderUtilities.TryParseDate(context.CachedResponseHeaders[HeaderNames.Expires], out expires) && context.ResponseTime.Value >= expires) { context.Logger.LogExpirationExpiresExceeded(context.ResponseTime.Value, expires); diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs index a87f2deee9..a15353a9d9 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs @@ -2,11 +2,11 @@ // 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.Globalization; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; -using Microsoft.AspNetCore.Http.Headers; using Microsoft.AspNetCore.ResponseCaching.Internal; using Microsoft.Extensions.Internal; using Microsoft.Extensions.Logging; @@ -198,7 +198,7 @@ namespace Microsoft.AspNetCore.ResponseCaching } } - if (HttpHeaderParsingHelpers.HeaderContains(context.HttpContext.Request.Headers[HeaderNames.CacheControl], CacheControlValues.OnlyIfCachedString)) + if (HeaderUtilities.ContainsCacheDirective(context.HttpContext.Request.Headers[HeaderNames.CacheControl], CacheControlHeaderValue.OnlyIfCachedString)) { _logger.LogGatewayTimeoutServed(); context.HttpContext.Response.StatusCode = StatusCodes.Status504GatewayTimeout; @@ -359,26 +359,24 @@ namespace Microsoft.AspNetCore.ResponseCaching if (!StringValues.IsNullOrEmpty(ifNoneMatchHeader)) { - if (ifNoneMatchHeader.Count == 1 && ifNoneMatchHeader[0].Equals(EntityTagHeaderValue.Any.Tag)) + if (ifNoneMatchHeader.Count == 1 && string.Equals(ifNoneMatchHeader[0], EntityTagHeaderValue.Any.Tag, StringComparison.OrdinalIgnoreCase)) { context.Logger.LogNotModifiedIfNoneMatchStar(); return true; } - if (!StringValues.IsNullOrEmpty(cachedResponseHeaders[HeaderNames.ETag])) + EntityTagHeaderValue eTag; + IList ifNoneMatchEtags; + if (!StringValues.IsNullOrEmpty(cachedResponseHeaders[HeaderNames.ETag]) + && EntityTagHeaderValue.TryParse(cachedResponseHeaders[HeaderNames.ETag], out eTag) + && EntityTagHeaderValue.TryParseList(ifNoneMatchHeader, out ifNoneMatchEtags)) { - EntityTagHeaderValue eTag; - if (EntityTagHeaderValue.TryParse(cachedResponseHeaders[HeaderNames.ETag], out eTag)) + foreach (var requestETag in ifNoneMatchEtags) { - foreach (var tag in ifNoneMatchHeader) + if (eTag.Compare(requestETag, useStrongComparison: false)) { - EntityTagHeaderValue requestETag; - if (EntityTagHeaderValue.TryParse(tag, out requestETag) && - eTag.Compare(requestETag, useStrongComparison: false)) - { - context.Logger.LogNotModifiedIfNoneMatchMatched(requestETag); - return true; - } + context.Logger.LogNotModifiedIfNoneMatchMatched(requestETag); + return true; } } } @@ -389,14 +387,14 @@ namespace Microsoft.AspNetCore.ResponseCaching if (!StringValues.IsNullOrEmpty(ifUnmodifiedSince)) { DateTimeOffset modified; - if (!HttpHeaderParsingHelpers.TryParseHeaderDate(cachedResponseHeaders[HeaderNames.LastModified], out modified) && - !HttpHeaderParsingHelpers.TryParseHeaderDate(cachedResponseHeaders[HeaderNames.Date], out modified)) + if (!HeaderUtilities.TryParseDate(cachedResponseHeaders[HeaderNames.LastModified], out modified) && + !HeaderUtilities.TryParseDate(cachedResponseHeaders[HeaderNames.Date], out modified)) { return false; } DateTimeOffset unmodifiedSince; - if (HttpHeaderParsingHelpers.TryParseHeaderDate(ifUnmodifiedSince, out unmodifiedSince) && + if (HeaderUtilities.TryParseDate(ifUnmodifiedSince, out unmodifiedSince) && modified <= unmodifiedSince) { context.Logger.LogNotModifiedIfUnmodifiedSinceSatisfied(modified, unmodifiedSince); diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ParsingHelpersTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ParsingHelpersTests.cs deleted file mode 100644 index fb495bb535..0000000000 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ParsingHelpersTests.cs +++ /dev/null @@ -1,39 +0,0 @@ -// 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.Internal; -using Microsoft.Extensions.Primitives; -using Xunit; - -namespace Microsoft.AspNetCore.ResponseCaching.Tests -{ - public class ParsingHelpersTests - { - [Theory] - [InlineData("h=1", "h", 1)] - [InlineData("header1=3, header2=10", "header1", 3)] - [InlineData("header1 =45, header2=80", "header1", 45)] - [InlineData("header1= 89 , header2=22", "header1", 89)] - [InlineData("header1= 89 , header2= 42", "header2", 42)] - void TryGetHeaderValue_Succeeds(string headerValue, string headerName, int expectedValue) - { - TimeSpan? value; - Assert.True(HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(new StringValues(headerValue), headerName, out value)); - Assert.Equal(TimeSpan.FromSeconds(expectedValue), value); - } - - [Theory] - [InlineData("h=", "h")] - [InlineData("header1=, header2=10", "header1")] - [InlineData("header1 , header2=80", "header1")] - [InlineData("h=10", "header")] - [InlineData("", "")] - [InlineData(null, null)] - void TryGetHeaderValue_Fails(string headerValue, string headerName) - { - TimeSpan? value; - Assert.False(HttpHeaderParsingHelpers.TryParseHeaderTimeSpan(new StringValues(headerValue), headerName, out value)); - } - } -} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs index ad607e9a1c..bced86fbc8 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Headers; using Microsoft.AspNetCore.ResponseCaching.Internal; using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.Primitives; @@ -251,7 +250,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.HttpContext.Request.Headers[HeaderNames.IfUnmodifiedSince] = HeaderUtilities.FormatDate(utcNow); context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10)); - context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = new List(new[] { new EntityTagHeaderValue("\"E1\"") }).ToString(); + context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = "\"E1\""; Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context)); Assert.Empty(sink.Writes); } @@ -262,8 +261,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); context.CachedResponseHeaders = new HeaderDictionary(); - - context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = new List(new[] { new EntityTagHeaderValue("\"E1\"") }).ToString(); + context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = "\"E1\""; Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context)); Assert.Empty(sink.Writes); @@ -291,7 +289,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var context = TestUtils.CreateTestContext(sink); context.CachedResponseHeaders = new HeaderDictionary(); context.CachedResponseHeaders[HeaderNames.ETag] = responseETag.ToString(); - context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = requestETag.ToString(); Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context)); @@ -306,14 +303,28 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); context.CachedResponseHeaders = new HeaderDictionary(); - context.HttpContext.Response.Headers[HeaderNames.ETag] = new EntityTagHeaderValue("\"E2\"").ToString(); - - context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = new List(new[] { new EntityTagHeaderValue("\"E1\"") }).ToString(); + context.CachedResponseHeaders[HeaderNames.ETag] = "\"E2\""; + context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = "\"E1\""; Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context)); Assert.Empty(sink.Writes); } + [Fact] + public void ContentIsNotModified_IfNoneMatch_MatchesAtLeastOneValue_True() + { + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); + context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponseHeaders[HeaderNames.ETag] = "\"E2\""; + context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = new string[] { "\"E0\", \"E1\"", "\"E1\", \"E2\"" }; + + Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context)); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.NotModifiedIfNoneMatchMatched); + } + [Fact] public async Task FinalizeCacheHeaders_DoNotUpdateShouldCacheResponse_IfResponseIsNotCacheable() { From 184c0f3c44542a09d5ad9749d8f8139364c30a2f Mon Sep 17 00:00:00 2001 From: Ajay Bhargav Baaskaran Date: Mon, 12 Dec 2016 00:41:46 -0800 Subject: [PATCH 072/188] Removed packages list in NuGetPackageVerifier.json --- NuGetPackageVerifier.json | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/NuGetPackageVerifier.json b/NuGetPackageVerifier.json index 65269108ed..b153ab1515 100644 --- a/NuGetPackageVerifier.json +++ b/NuGetPackageVerifier.json @@ -1,14 +1,5 @@ { - "adx": { // Packages written by the ADX team and that ship on NuGet.org - "rules": [ - "AdxVerificationCompositeRule" - ], - "packages": { - "Microsoft.AspNetCore.ResponseCaching": { }, - "Microsoft.AspNetCore.ResponseCaching.Abstractions": { }, - } - }, - "Default": { // Rules to run for packages not listed in any other set. + "Default": { "rules": [ "DefaultCompositeRule" ] From 8e8525512dc2f519afb00d66d48678b814102759 Mon Sep 17 00:00:00 2001 From: John Luo Date: Fri, 9 Dec 2016 16:08:09 -0800 Subject: [PATCH 073/188] Performance optimizations - Calculate age using operations on long - Compute content length of resposne on store - Format age using the new HeaderUtility - Lazily create HeaderDictionary - Use for instead of foreach to reduce allocations from enumerators --- .../Internal/CacheEntry/CachedResponse.cs | 2 +- .../Internal/MemoryResponseCache.cs | 1 + .../Internal/ResponseCachingKeyProvider.cs | 34 +++++++++++++------ .../ResponseCachingMiddleware.cs | 25 ++++++++------ .../ResponseCachingMiddlewareTests.cs | 7 +++- 5 files changed, 47 insertions(+), 22 deletions(-) diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CachedResponse.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CachedResponse.cs index 51083e40d2..62734f8039 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CachedResponse.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CachedResponse.cs @@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal public int StatusCode { get; set; } - public IHeaderDictionary Headers { get; set; } = new HeaderDictionary(); + public IHeaderDictionary Headers { get; set; } public Stream Body { get; set; } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs index f2509c8ce3..4023345682 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs @@ -4,6 +4,7 @@ using System; using System.Threading.Tasks; using Microsoft.Extensions.Caching.Memory; +using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.ResponseCaching.Internal { diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingKeyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingKeyProvider.cs index 334022647a..4cb9fcf604 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingKeyProvider.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingKeyProvider.cs @@ -107,13 +107,18 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal builder.Append(KeyDelimiter) .Append('H'); - foreach (var header in varyByRules.Headers) + for (var i = 0; i < varyByRules.Headers.Count; i++) { + var header = varyByRules.Headers[i]; + var headerValues = context.HttpContext.Request.Headers[header]; builder.Append(KeyDelimiter) .Append(header) - .Append("=") - // TODO: Perf - iterate the string values instead? - .Append(context.HttpContext.Request.Headers[header]); + .Append("="); + + for (var j = 0; j < headerValues.Count; j++) + { + builder.Append(headerValues[j]); + } } } @@ -131,19 +136,28 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal { builder.Append(KeyDelimiter) .AppendUpperInvariant(query.Key) - .Append("=") - .Append(query.Value); + .Append("="); + + for (var i = 0; i < query.Value.Count; i++) + { + builder.Append(query.Value[i]); + } } } else { - foreach (var queryKey in varyByRules.QueryKeys) + for (var i = 0; i < varyByRules.QueryKeys.Count; i++) { + var queryKey = varyByRules.QueryKeys[i]; + var queryKeyValues = context.HttpContext.Request.Query[queryKey]; builder.Append(KeyDelimiter) .Append(queryKey) - .Append("=") - // TODO: Perf - iterate the string values instead? - .Append(context.HttpContext.Request.Query[queryKey]); + .Append("="); + + for (var j = 0; j < queryKeyValues.Count; j++) + { + builder.Append(queryKeyValues[j]); + } } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs index a15353a9d9..2ac2000f21 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; @@ -142,18 +141,15 @@ namespace Microsoft.AspNetCore.ResponseCaching response.Headers.Add(header); } - response.Headers[HeaderNames.Age] = context.CachedEntryAge.Value.TotalSeconds.ToString("F0", CultureInfo.InvariantCulture); + // Note: int64 division truncates result and errors may be up to 1 second. This reduction in + // accuracy of age calculation is considered appropriate since it is small compared to clock + // skews and the "Age" header is an estimate of the real age of cached content. + response.Headers[HeaderNames.Age] = HeaderUtilities.FormatInt64(context.CachedEntryAge.Value.Ticks / TimeSpan.TicksPerSecond); // Copy the cached response body var body = context.CachedResponse.Body; if (body.Length > 0) { - // Add a content-length if required - if (!response.ContentLength.HasValue && StringValues.IsNullOrEmpty(response.Headers[HeaderNames.TransferEncoding])) - { - response.ContentLength = body.Length; - } - try { await body.CopyToAsync(response.Body, StreamUtilities.BodySegmentSize, context.HttpContext.RequestAborted); @@ -263,7 +259,8 @@ namespace Microsoft.AspNetCore.ResponseCaching context.CachedResponse = new CachedResponse { Created = context.ResponseDate.Value, - StatusCode = context.HttpContext.Response.StatusCode + StatusCode = context.HttpContext.Response.StatusCode, + Headers = new HeaderDictionary() }; foreach (var header in context.HttpContext.Response.Headers) @@ -288,6 +285,13 @@ namespace Microsoft.AspNetCore.ResponseCaching var bufferStream = context.ResponseCachingStream.GetBufferStream(); if (!contentLength.HasValue || contentLength == bufferStream.Length) { + var response = context.HttpContext.Response; + // Add a content-length if required + if (!response.ContentLength.HasValue && StringValues.IsNullOrEmpty(response.Headers[HeaderNames.TransferEncoding])) + { + context.CachedResponse.Headers[HeaderNames.ContentLength] = HeaderUtilities.FormatInt64(bufferStream.Length); + } + context.CachedResponse.Body = bufferStream; _logger.LogResponseCached(); await _cache.SetAsync(context.StorageVaryKey ?? context.BaseKey, context.CachedResponse, context.CachedResponseValidFor); @@ -371,8 +375,9 @@ namespace Microsoft.AspNetCore.ResponseCaching && EntityTagHeaderValue.TryParse(cachedResponseHeaders[HeaderNames.ETag], out eTag) && EntityTagHeaderValue.TryParseList(ifNoneMatchHeader, out ifNoneMatchEtags)) { - foreach (var requestETag in ifNoneMatchEtags) + for (var i = 0; i < ifNoneMatchEtags.Count; i++) { + var requestETag = ifNoneMatchEtags[i]; if (eTag.Compare(requestETag, useStrongComparison: false)) { context.Logger.LogNotModifiedIfNoneMatchMatched(requestETag); diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs index bced86fbc8..a31d7c308f 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs @@ -61,6 +61,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests "BaseKey", new CachedResponse() { + Headers = new HeaderDictionary(), Body = new SegmentReadStream(new List(0), 0) }, TimeSpan.Zero); @@ -108,6 +109,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests "BaseKeyVaryKey2", new CachedResponse() { + Headers = new HeaderDictionary(), Body = new SegmentReadStream(new List(0), 0) }, TimeSpan.Zero); @@ -666,7 +668,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests await context.HttpContext.Response.WriteAsync(new string('0', 10)); context.ShouldCacheResponse = true; - context.CachedResponse = new CachedResponse(); + context.CachedResponse = new CachedResponse() + { + Headers = new HeaderDictionary() + }; context.BaseKey = "BaseKey"; context.CachedResponseValidFor = TimeSpan.FromSeconds(10); From a5717aa583c4defc5b2b3b72d7d2c427aac5538d Mon Sep 17 00:00:00 2001 From: Pranav K Date: Mon, 5 Dec 2016 09:03:39 -0800 Subject: [PATCH 074/188] Updating to 4.4 CoreFx packages --- global.json | 2 +- samples/ResponseCachingSample/project.json | 2 +- .../project.json | 2 +- src/Microsoft.AspNetCore.ResponseCaching/project.json | 2 +- test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/global.json b/global.json index f45e8cc925..0ad1995dd2 100644 --- a/global.json +++ b/global.json @@ -3,6 +3,6 @@ "src" ], "sdk": { - "version": "1.0.0-preview2-1-003177" + "version": "1.0.0-preview2-1-003180" } } \ No newline at end of file diff --git a/samples/ResponseCachingSample/project.json b/samples/ResponseCachingSample/project.json index 009f6ed7f9..fde231f6d4 100644 --- a/samples/ResponseCachingSample/project.json +++ b/samples/ResponseCachingSample/project.json @@ -13,7 +13,7 @@ "netcoreapp1.1": { "dependencies": { "Microsoft.NETCore.App": { - "version": "1.1.0-*", + "version": "1.2.0-*", "type": "platform" } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/project.json b/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/project.json index 33d56f6e66..db41492279 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/project.json +++ b/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/project.json @@ -24,7 +24,7 @@ "net451": {}, "netstandard1.3": { "dependencies": { - "NETStandard.Library": "1.6.1-*" + "NETStandard.Library": "1.6.2-*" } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/project.json b/src/Microsoft.AspNetCore.ResponseCaching/project.json index e09ec49d34..c4a0254f95 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/project.json +++ b/src/Microsoft.AspNetCore.ResponseCaching/project.json @@ -33,7 +33,7 @@ "version": "1.2.0-*", "type": "build" }, - "NETStandard.Library": "1.6.1-*" + "NETStandard.Library": "1.6.2-*" }, "frameworks": { "net451": {}, diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json b/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json index e5ec8165bb..909d049885 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json @@ -14,7 +14,7 @@ "netcoreapp1.1": { "dependencies": { "Microsoft.NETCore.App": { - "version": "1.1.0-*", + "version": "1.2.0-*", "type": "platform" } } From 3bf5f6a1ce69b65c998d6f5c739822a9bed4a67e Mon Sep 17 00:00:00 2001 From: John Luo Date: Tue, 10 Jan 2017 14:48:39 -0800 Subject: [PATCH 075/188] Restructure response caching middleware flow - Always add IresponseCachingFeatu8re before calling the next middleware #81 - Use If-Modified-Since instead of the incorrect If-Unmodified-Since header #83 - Handle proxy-revalidate in the same way as must-revalidate #83 - Handle max-stale with no specified limit #83 - Bypass cache lookup for no-cache but store the response #83 - Bypass response capturing and buffering when no-store is specified #83 - Replace IsRequestCacheable cache policy with three new independent policy checks to reflect these changes - Modify middleware flow to accommodate cache policy updates --- .../IResponseCachingPolicyProvider.cs | 24 +- .../Internal/LoggerExtensions.cs | 22 +- .../Internal/ResponseCachingContext.cs | 2 +- .../Internal/ResponseCachingPolicyProvider.cs | 38 ++- .../ResponseCachingMiddleware.cs | 90 ++++--- .../ResponseCachingMiddlewareTests.cs | 242 +++++++++++++----- .../ResponseCachingPolicyProviderTests.cs | 155 +++++++---- .../ResponseCachingTests.cs | 53 +++- .../TestUtils.cs | 48 +++- 9 files changed, 488 insertions(+), 186 deletions(-) diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCachingPolicyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCachingPolicyProvider.cs index 77b3f28379..51a040098b 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCachingPolicyProvider.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCachingPolicyProvider.cs @@ -6,21 +6,35 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal public interface IResponseCachingPolicyProvider { /// - /// Determine wehther the response cache middleware should be executed for the incoming HTTP request. + /// Determine whether the response caching logic should be attempted for the incoming HTTP request. /// /// The . - /// true if the request is cacheable; otherwise false. - bool IsRequestCacheable(ResponseCachingContext context); + /// true if response caching logic should be attempted; otherwise false. + bool AttemptResponseCaching(ResponseCachingContext context); /// - /// Determine whether the response received by the middleware be cached for future requests. + /// Determine whether a cache lookup is allowed for the incoming HTTP request. + /// + /// The . + /// true if cache lookup for this request is allowed; otherwise false. + bool AllowCacheLookup(ResponseCachingContext context); + + /// + /// Determine whether storage of the response is allowed for the incoming HTTP request. + /// + /// The . + /// true if storage of the response for this request is allowed; otherwise false. + bool AllowCacheStorage(ResponseCachingContext context); + + /// + /// Determine whether the response received by the middleware can be cached for future requests. /// /// The . /// true if the response is cacheable; otherwise false. bool IsResponseCacheable(ResponseCachingContext context); /// - /// Determine whether the response retrieved from the response cache is fresh and be served. + /// Determine whether the response retrieved from the response cache is fresh and can be served. /// /// The . /// true if the cached entry is fresh; otherwise false. diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/LoggerExtensions.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/LoggerExtensions.cs index b90073179e..f8a0bf3151 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/LoggerExtensions.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/LoggerExtensions.cs @@ -31,7 +31,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal private static Action _logResponseWithUnsuccessfulStatusCodeNotCacheable; private static Action _logNotModifiedIfNoneMatchStar; private static Action _logNotModifiedIfNoneMatchMatched; - private static Action _logNotModifiedIfUnmodifiedSinceSatisfied; + private static Action _logNotModifiedIfModifiedSinceSatisfied; private static Action _logNotModifiedServed; private static Action _logCachedResponseServed; private static Action _logGatewayTimeoutServed; @@ -40,6 +40,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal private static Action _logResponseCached; private static Action _logResponseNotCached; private static Action _logResponseContentLengthMismatchNotCached; + private static Action _logExpirationInfiniteMaxStaleSatisfied; static LoggerExtensions() { @@ -70,7 +71,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal _logExpirationMustRevalidate = LoggerMessage.Define( logLevel: LogLevel.Debug, eventId: 7, - formatString: "The age of the entry is {Age} and has exceeded the maximum age of {MaxAge} specified by the 'max-age' cache directive. It must be revalidated because the 'must-revalidate' cache directive is specified."); + formatString: "The age of the entry is {Age} and has exceeded the maximum age of {MaxAge} specified by the 'max-age' cache directive. It must be revalidated because the 'must-revalidate' or 'proxy-revalidate' cache directive is specified."); _logExpirationMaxStaleSatisfied = LoggerMessage.Define( logLevel: LogLevel.Debug, eventId: 8, @@ -119,10 +120,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal logLevel: LogLevel.Debug, eventId: 19, formatString: $"The ETag {{ETag}} in the '{HeaderNames.IfNoneMatch}' header matched the ETag of a cached entry."); - _logNotModifiedIfUnmodifiedSinceSatisfied = LoggerMessage.Define( + _logNotModifiedIfModifiedSinceSatisfied = LoggerMessage.Define( logLevel: LogLevel.Debug, eventId: 20, - formatString: $"The last modified date of {{LastModified}} is before the date {{IfUnmodifiedSince}} specified in the '{HeaderNames.IfUnmodifiedSince}' header."); + formatString: $"The last modified date of {{LastModified}} is before the date {{IfModifiedSince}} specified in the '{HeaderNames.IfModifiedSince}' header."); _logNotModifiedServed = LoggerMessage.Define( logLevel: LogLevel.Information, eventId: 21, @@ -155,6 +156,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal logLevel: LogLevel.Warning, eventId: 28, formatString: $"The response could not be cached for this request because the '{HeaderNames.ContentLength}' did not match the body length."); + _logExpirationInfiniteMaxStaleSatisfied = LoggerMessage.Define( + logLevel: LogLevel.Debug, + eventId: 29, + formatString: "The age of the entry is {Age} and has exceeded the maximum age of {MaxAge} specified by the 'max-age' cache directive. However, the 'max-stale' cache directive was specified without an assigned value and a stale response of any age is accepted."); } internal static void LogRequestMethodNotCacheable(this ILogger logger, string method) @@ -252,9 +257,9 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal _logNotModifiedIfNoneMatchMatched(logger, etag, null); } - internal static void LogNotModifiedIfUnmodifiedSinceSatisfied(this ILogger logger, DateTimeOffset lastModified, DateTimeOffset ifUnmodifiedSince) + internal static void LogNotModifiedIfModifiedSinceSatisfied(this ILogger logger, DateTimeOffset lastModified, DateTimeOffset ifModifiedSince) { - _logNotModifiedIfUnmodifiedSinceSatisfied(logger, lastModified, ifUnmodifiedSince, null); + _logNotModifiedIfModifiedSinceSatisfied(logger, lastModified, ifModifiedSince, null); } internal static void LogNotModifiedServed(this ILogger logger) @@ -296,5 +301,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal { _logResponseContentLengthMismatchNotCached(logger, null); } + + internal static void LogExpirationInfiniteMaxStaleSatisfied(this ILogger logger, TimeSpan age, TimeSpan maxAge) + { + _logExpirationInfiniteMaxStaleSatisfied(logger, age, maxAge, null); + } } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingContext.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingContext.cs index eeed0d9a09..f9f8e8657e 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingContext.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingContext.cs @@ -37,7 +37,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal internal ILogger Logger { get; } - internal bool ShouldCacheResponse { get; set; } + internal bool ShouldCacheResponse { get; set; } internal string BaseKey { get; set; } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingPolicyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingPolicyProvider.cs index c37fcacc70..2108ff3a2b 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingPolicyProvider.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingPolicyProvider.cs @@ -10,10 +10,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal { public class ResponseCachingPolicyProvider : IResponseCachingPolicyProvider { - public virtual bool IsRequestCacheable(ResponseCachingContext context) + public virtual bool AttemptResponseCaching(ResponseCachingContext context) { - // Verify the method var request = context.HttpContext.Request; + + // Verify the method if (!HttpMethods.IsGet(request.Method) && !HttpMethods.IsHead(request.Method)) { context.Logger.LogRequestMethodNotCacheable(request.Method); @@ -27,6 +28,13 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal return false; } + return true; + } + + public virtual bool AllowCacheLookup(ResponseCachingContext context) + { + var request = context.HttpContext.Request; + // Verify request cache-control parameters if (!StringValues.IsNullOrEmpty(request.Headers[HeaderNames.CacheControl])) { @@ -50,6 +58,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal return true; } + public virtual bool AllowCacheStorage(ResponseCachingContext context) + { + // Check request no-store + return !HeaderUtilities.ContainsCacheDirective(context.HttpContext.Request.Headers[HeaderNames.CacheControl], CacheControlHeaderValue.NoStoreString); + } + public virtual bool IsResponseCacheable(ResponseCachingContext context) { var responseCacheControlHeader = context.HttpContext.Response.Headers[HeaderNames.CacheControl]; @@ -61,9 +75,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal return false; } - // Check no-store - if (HeaderUtilities.ContainsCacheDirective(context.HttpContext.Request.Headers[HeaderNames.CacheControl], CacheControlHeaderValue.NoStoreString) - || HeaderUtilities.ContainsCacheDirective(responseCacheControlHeader, CacheControlHeaderValue.NoStoreString)) + // Check response no-store + if (HeaderUtilities.ContainsCacheDirective(responseCacheControlHeader, CacheControlHeaderValue.NoStoreString)) { context.Logger.LogResponseWithNoStoreNotCacheable(); return false; @@ -187,17 +200,26 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal // Validate max age if (age >= lowestMaxAge) { - // Must revalidate - if (HeaderUtilities.ContainsCacheDirective(cachedCacheControlHeaders, CacheControlHeaderValue.MustRevalidateString)) + // Must revalidate or proxy revalidate + if (HeaderUtilities.ContainsCacheDirective(cachedCacheControlHeaders, CacheControlHeaderValue.MustRevalidateString) + || HeaderUtilities.ContainsCacheDirective(cachedCacheControlHeaders, CacheControlHeaderValue.ProxyRevalidateString)) { context.Logger.LogExpirationMustRevalidate(age, lowestMaxAge.Value); return false; } TimeSpan? requestMaxStale; + var maxStaleExist = HeaderUtilities.ContainsCacheDirective(requestCacheControlHeaders, CacheControlHeaderValue.MaxStaleString); HeaderUtilities.TryParseSeconds(requestCacheControlHeaders, CacheControlHeaderValue.MaxStaleString, out requestMaxStale); - // Request allows stale values + // Request allows stale values with no age limit + if (maxStaleExist && !requestMaxStale.HasValue) + { + context.Logger.LogExpirationInfiniteMaxStaleSatisfied(age, lowestMaxAge.Value); + return true; + } + + // Request allows stale values with age limit if (requestMaxStale.HasValue && age - lowestMaxAge < requestMaxStale) { context.Logger.LogExpirationMaxStaleSatisfied(age, lowestMaxAge.Value, requestMaxStale.Value); diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs index 2ac2000f21..ed41f3cdaa 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs @@ -74,39 +74,53 @@ namespace Microsoft.AspNetCore.ResponseCaching var context = new ResponseCachingContext(httpContext, _logger); // Should we attempt any caching logic? - if (_policyProvider.IsRequestCacheable(context)) + if (_policyProvider.AttemptResponseCaching(context)) { // Can this request be served from cache? - if (await TryServeFromCacheAsync(context)) + if (_policyProvider.AllowCacheLookup(context) && await TryServeFromCacheAsync(context)) { return; } - // Hook up to listen to the response stream - ShimResponseStream(context); - - try + // Should we store the response to this request? + if (_policyProvider.AllowCacheStorage(context)) { - // Subscribe to OnStarting event - httpContext.Response.OnStarting(_onStartingCallback, context); + // Hook up to listen to the response stream + ShimResponseStream(context); - await _next(httpContext); + try + { + // Subscribe to OnStarting event + httpContext.Response.OnStarting(_onStartingCallback, context); - // If there was no response body, check the response headers now. We can cache things like redirects. - await OnResponseStartingAsync(context); + await _next(httpContext); - // Finalize the cache entry - await FinalizeCacheBodyAsync(context); - } - finally - { - UnshimResponseStream(context); + // If there was no response body, check the response headers now. We can cache things like redirects. + await OnResponseStartingAsync(context); + + // Finalize the cache entry + await FinalizeCacheBodyAsync(context); + } + finally + { + UnshimResponseStream(context); + } + + return; } } - else + + // Response should not be captured but add IResponseCachingFeature which may be required when the response is generated + AddResponseCachingFeature(httpContext); + + try { await _next(httpContext); } + finally + { + RemoveResponseCachingFeature(httpContext); + } } internal async Task TryServeCachedResponseAsync(ResponseCachingContext context, IResponseCacheEntry cacheEntry) @@ -220,6 +234,12 @@ namespace Microsoft.AspNetCore.ResponseCaching (context.ResponseExpires - context.ResponseTime.Value) ?? DefaultExpirationTimeSpan; + // Generate a base key if none exist + if (string.IsNullOrEmpty(context.BaseKey)) + { + context.BaseKey = _keyProvider.CreateBaseKey(context); + } + // Check if any vary rules exist if (!StringValues.IsNullOrEmpty(varyHeaders) || !StringValues.IsNullOrEmpty(varyQueryKeys)) { @@ -279,9 +299,9 @@ namespace Microsoft.AspNetCore.ResponseCaching internal async Task FinalizeCacheBodyAsync(ResponseCachingContext context) { - var contentLength = context.HttpContext.Response.ContentLength; if (context.ShouldCacheResponse && context.ResponseCachingStream.BufferingEnabled) { + var contentLength = context.HttpContext.Response.ContentLength; var bufferStream = context.ResponseCachingStream.GetBufferStream(); if (!contentLength.HasValue || contentLength == bufferStream.Length) { @@ -322,6 +342,15 @@ namespace Microsoft.AspNetCore.ResponseCaching } } + internal static void AddResponseCachingFeature(HttpContext context) + { + if (context.Features.Get() != null) + { + throw new InvalidOperationException($"Another instance of {nameof(ResponseCachingFeature)} already exists. Only one instance of {nameof(ResponseCachingMiddleware)} can be configured for an application."); + } + context.Features.Set(new ResponseCachingFeature()); + } + internal void ShimResponseStream(ResponseCachingContext context) { // Shim response stream @@ -337,13 +366,12 @@ namespace Microsoft.AspNetCore.ResponseCaching } // Add IResponseCachingFeature - if (context.HttpContext.Features.Get() != null) - { - throw new InvalidOperationException($"Another instance of {nameof(ResponseCachingFeature)} already exists. Only one instance of {nameof(ResponseCachingMiddleware)} can be configured for an application."); - } - context.HttpContext.Features.Set(new ResponseCachingFeature()); + AddResponseCachingFeature(context.HttpContext); } + internal static void RemoveResponseCachingFeature(HttpContext context) => + context.Features.Set(null); + internal static void UnshimResponseStream(ResponseCachingContext context) { // Unshim response stream @@ -353,7 +381,7 @@ namespace Microsoft.AspNetCore.ResponseCaching context.HttpContext.Features.Set(context.OriginalSendFileFeature); // Remove IResponseCachingFeature - context.HttpContext.Features.Set(null); + RemoveResponseCachingFeature(context.HttpContext); } internal static bool ContentIsNotModified(ResponseCachingContext context) @@ -388,8 +416,8 @@ namespace Microsoft.AspNetCore.ResponseCaching } else { - var ifUnmodifiedSince = context.HttpContext.Request.Headers[HeaderNames.IfUnmodifiedSince]; - if (!StringValues.IsNullOrEmpty(ifUnmodifiedSince)) + var ifModifiedSince = context.HttpContext.Request.Headers[HeaderNames.IfModifiedSince]; + if (!StringValues.IsNullOrEmpty(ifModifiedSince)) { DateTimeOffset modified; if (!HeaderUtilities.TryParseDate(cachedResponseHeaders[HeaderNames.LastModified], out modified) && @@ -398,11 +426,11 @@ namespace Microsoft.AspNetCore.ResponseCaching return false; } - DateTimeOffset unmodifiedSince; - if (HeaderUtilities.TryParseDate(ifUnmodifiedSince, out unmodifiedSince) && - modified <= unmodifiedSince) + DateTimeOffset modifiedSince; + if (HeaderUtilities.TryParseDate(ifModifiedSince, out modifiedSince) && + modified <= modifiedSince) { - context.Logger.LogNotModifiedIfUnmodifiedSinceSatisfied(modified, unmodifiedSince); + context.Logger.LogNotModifiedIfModifiedSinceSatisfied(modified, modifiedSince); return true; } } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs index a31d7c308f..fc66045bdb 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs @@ -5,7 +5,9 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.ResponseCaching.Internal; +using Microsoft.Extensions.Internal; using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; @@ -157,14 +159,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public void ContentIsNotModified_IfUnmodifiedSince_FallsbackToDateHeader() + public void ContentIsNotModified_IfModifiedSince_FallsbackToDateHeader() { var utcNow = DateTimeOffset.UtcNow; var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); context.CachedResponseHeaders = new HeaderDictionary(); - context.HttpContext.Request.Headers[HeaderNames.IfUnmodifiedSince] = HeaderUtilities.FormatDate(utcNow); + context.HttpContext.Request.Headers[HeaderNames.IfModifiedSince] = HeaderUtilities.FormatDate(utcNow); // Verify modifications in the past succeeds context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10)); @@ -183,19 +185,19 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests // Verify logging TestUtils.AssertLoggedMessages( sink.Writes, - LoggedMessage.NotModifiedIfUnmodifiedSinceSatisfied, - LoggedMessage.NotModifiedIfUnmodifiedSinceSatisfied); + LoggedMessage.NotModifiedIfModifiedSinceSatisfied, + LoggedMessage.NotModifiedIfModifiedSinceSatisfied); } [Fact] - public void ContentIsNotModified_IfUnmodifiedSince_LastModifiedOverridesDateHeader() + public void ContentIsNotModified_IfModifiedSince_LastModifiedOverridesDateHeader() { var utcNow = DateTimeOffset.UtcNow; var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); context.CachedResponseHeaders = new HeaderDictionary(); - context.HttpContext.Request.Headers[HeaderNames.IfUnmodifiedSince] = HeaderUtilities.FormatDate(utcNow); + context.HttpContext.Request.Headers[HeaderNames.IfModifiedSince] = HeaderUtilities.FormatDate(utcNow); // Verify modifications in the past succeeds context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); @@ -217,20 +219,20 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests // Verify logging TestUtils.AssertLoggedMessages( sink.Writes, - LoggedMessage.NotModifiedIfUnmodifiedSinceSatisfied, - LoggedMessage.NotModifiedIfUnmodifiedSinceSatisfied); + LoggedMessage.NotModifiedIfModifiedSinceSatisfied, + LoggedMessage.NotModifiedIfModifiedSinceSatisfied); } [Fact] - public void ContentIsNotModified_IfNoneMatch_Overrides_IfUnmodifiedSince_ToTrue() + public void ContentIsNotModified_IfNoneMatch_Overrides_IfModifiedSince_ToTrue() { var utcNow = DateTimeOffset.UtcNow; var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); context.CachedResponseHeaders = new HeaderDictionary(); - // This would fail the IfUnmodifiedSince checks - context.HttpContext.Request.Headers[HeaderNames.IfUnmodifiedSince] = HeaderUtilities.FormatDate(utcNow); + // This would fail the IfModifiedSince checks + context.HttpContext.Request.Headers[HeaderNames.IfModifiedSince] = HeaderUtilities.FormatDate(utcNow); context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = EntityTagHeaderValue.Any.ToString(); @@ -241,15 +243,15 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public void ContentIsNotModified_IfNoneMatch_Overrides_IfUnmodifiedSince_ToFalse() + public void ContentIsNotModified_IfNoneMatch_Overrides_IfModifiedSince_ToFalse() { var utcNow = DateTimeOffset.UtcNow; var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); context.CachedResponseHeaders = new HeaderDictionary(); - // This would pass the IfUnmodifiedSince checks - context.HttpContext.Request.Headers[HeaderNames.IfUnmodifiedSince] = HeaderUtilities.FormatDate(utcNow); + // This would pass the IfModifiedSince checks + context.HttpContext.Request.Headers[HeaderNames.IfModifiedSince] = HeaderUtilities.FormatDate(utcNow); context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10)); context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = "\"E1\""; @@ -328,27 +330,56 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public async Task FinalizeCacheHeaders_DoNotUpdateShouldCacheResponse_IfResponseIsNotCacheable() + public async Task OnResponseStartingAsync_IfAllowResponseCaptureIsTrue_SetsResponseTime() { - var sink = new TestSink(); - var middleware = TestUtils.CreateTestMiddleware(testSink: sink, policyProvider: new ResponseCachingPolicyProvider()); + var clock = new TestClock + { + UtcNow = DateTimeOffset.UtcNow + }; + var middleware = TestUtils.CreateTestMiddleware(options: new ResponseCachingOptions + { + SystemClock = clock + }); var context = TestUtils.CreateTestContext(); + context.ResponseTime = null; - Assert.False(context.ShouldCacheResponse); + await middleware.OnResponseStartingAsync(context); - middleware.ShimResponseStream(context); - await middleware.FinalizeCacheHeadersAsync(context); - - Assert.False(context.ShouldCacheResponse); - Assert.Empty(sink.Writes); + Assert.Equal(clock.UtcNow, context.ResponseTime); } [Fact] - public async Task FinalizeCacheHeaders_UpdateShouldCacheResponse_IfResponseIsCacheable() + public async Task OnResponseStartingAsync_IfAllowResponseCaptureIsTrue_SetsResponseTimeOnlyOnce() + { + var clock = new TestClock + { + UtcNow = DateTimeOffset.UtcNow + }; + var middleware = TestUtils.CreateTestMiddleware(options: new ResponseCachingOptions + { + SystemClock = clock + }); + var context = TestUtils.CreateTestContext(); + var initialTime = clock.UtcNow; + context.ResponseTime = null; + + await middleware.OnResponseStartingAsync(context); + Assert.Equal(initialTime, context.ResponseTime); + + clock.UtcNow += TimeSpan.FromSeconds(10); + + await middleware.OnResponseStartingAsync(context); + Assert.NotEqual(clock.UtcNow, context.ResponseTime); + Assert.Equal(initialTime, context.ResponseTime); + } + + [Fact] + public async Task FinalizeCacheHeadersAsync_UpdateShouldCacheResponse_IfResponseCacheable() { var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink, policyProvider: new ResponseCachingPolicyProvider()); var context = TestUtils.CreateTestContext(); + context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { Public = true @@ -363,7 +394,22 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public async Task FinalizeCacheHeaders_DefaultResponseValidity_Is10Seconds() + public async Task FinalizeCacheHeadersAsync_DoNotUpdateShouldCacheResponse_IfResponseIsNotCacheable() + { + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, policyProvider: new ResponseCachingPolicyProvider()); + var context = TestUtils.CreateTestContext(); + + middleware.ShimResponseStream(context); + + await middleware.FinalizeCacheHeadersAsync(context); + + Assert.False(context.ShouldCacheResponse); + Assert.Empty(sink.Writes); + } + + [Fact] + public async Task FinalizeCacheHeadersAsync_DefaultResponseValidity_Is10Seconds() { var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink); @@ -376,15 +422,21 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public async Task FinalizeCacheHeaders_ResponseValidity_UseExpiryIfAvailable() + public async Task FinalizeCacheHeadersAsync_ResponseValidity_UseExpiryIfAvailable() { - var utcNow = DateTimeOffset.MinValue; + var clock = new TestClock + { + UtcNow = DateTimeOffset.MinValue + }; var sink = new TestSink(); - var middleware = TestUtils.CreateTestMiddleware(testSink: sink); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, options: new ResponseCachingOptions + { + SystemClock = clock + }); var context = TestUtils.CreateTestContext(); - context.ResponseTime = utcNow; - context.HttpContext.Response.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(11)); + context.ResponseTime = clock.UtcNow; + context.HttpContext.Response.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(clock.UtcNow + TimeSpan.FromSeconds(11)); await middleware.FinalizeCacheHeadersAsync(context); @@ -393,17 +445,26 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public async Task FinalizeCacheHeaders_ResponseValidity_UseMaxAgeIfAvailable() + public async Task FinalizeCacheHeadersAsync_ResponseValidity_UseMaxAgeIfAvailable() { + var clock = new TestClock + { + UtcNow = DateTimeOffset.UtcNow + }; var sink = new TestSink(); - var middleware = TestUtils.CreateTestMiddleware(testSink: sink); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, options: new ResponseCachingOptions + { + SystemClock = clock + }); var context = TestUtils.CreateTestContext(); + + context.ResponseTime = clock.UtcNow; context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { MaxAge = TimeSpan.FromSeconds(12) }.ToString(); - context.HttpContext.Response.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(context.ResponseTime.Value + TimeSpan.FromSeconds(11)); + context.HttpContext.Response.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(clock.UtcNow + TimeSpan.FromSeconds(11)); await middleware.FinalizeCacheHeadersAsync(context); @@ -412,18 +473,26 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public async Task FinalizeCacheHeaders_ResponseValidity_UseSharedMaxAgeIfAvailable() + public async Task FinalizeCacheHeadersAsync_ResponseValidity_UseSharedMaxAgeIfAvailable() { + var clock = new TestClock + { + UtcNow = DateTimeOffset.UtcNow + }; var sink = new TestSink(); - var middleware = TestUtils.CreateTestMiddleware(testSink: sink); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, options: new ResponseCachingOptions + { + SystemClock = clock + }); var context = TestUtils.CreateTestContext(); + + context.ResponseTime = clock.UtcNow; context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { MaxAge = TimeSpan.FromSeconds(12), SharedMaxAge = TimeSpan.FromSeconds(13) }.ToString(); - - context.HttpContext.Response.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(context.ResponseTime.Value + TimeSpan.FromSeconds(11)); + context.HttpContext.Response.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(clock.UtcNow + TimeSpan.FromSeconds(11)); await middleware.FinalizeCacheHeadersAsync(context); @@ -432,7 +501,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public async Task FinalizeCacheHeaders_UpdateCachedVaryByRules_IfNotEquivalentToPrevious() + public async Task FinalizeCacheHeadersAsync_UpdateCachedVaryByRules_IfNotEquivalentToPrevious() { var cache = new TestResponseCache(); var sink = new TestSink(); @@ -451,19 +520,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests }; context.CachedVaryByRules = cachedVaryByRules; - await middleware.TryServeFromCacheAsync(context); await middleware.FinalizeCacheHeadersAsync(context); Assert.Equal(1, cache.SetCount); Assert.NotSame(cachedVaryByRules, context.CachedVaryByRules); TestUtils.AssertLoggedMessages( sink.Writes, - LoggedMessage.NoResponseServed, LoggedMessage.VaryByRulesUpdated); } [Fact] - public async Task FinalizeCacheHeaders_UpdateCachedVaryByRules_IfEquivalentToPrevious() + public async Task FinalizeCacheHeadersAsync_UpdateCachedVaryByRules_IfEquivalentToPrevious() { var cache = new TestResponseCache(); var sink = new TestSink(); @@ -483,7 +550,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests }; context.CachedVaryByRules = cachedVaryByRules; - await middleware.TryServeFromCacheAsync(context); await middleware.FinalizeCacheHeadersAsync(context); // An update to the cache is always made but the entry should be the same @@ -491,7 +557,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests Assert.Same(cachedVaryByRules, context.CachedVaryByRules); TestUtils.AssertLoggedMessages( sink.Writes, - LoggedMessage.NoResponseServed, LoggedMessage.VaryByRulesUpdated); } @@ -515,7 +580,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Theory] [MemberData(nameof(NullOrEmptyVaryRules))] - public async Task FinalizeCacheHeaders_UpdateCachedVaryByRules_NullOrEmptyRules(StringValues vary) + public async Task FinalizeCacheHeadersAsync_UpdateCachedVaryByRules_NullOrEmptyRules(StringValues vary) { var cache = new TestResponseCache(); var sink = new TestSink(); @@ -528,40 +593,43 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests VaryByQueryKeys = vary }); - await middleware.TryServeFromCacheAsync(context); await middleware.FinalizeCacheHeadersAsync(context); // Vary rules should not be updated Assert.Equal(0, cache.SetCount); - TestUtils.AssertLoggedMessages( - sink.Writes, - LoggedMessage.NoResponseServed); + Assert.Empty(sink.Writes); } [Fact] - public async Task FinalizeCacheHeaders_DoNotAddDate_IfSpecified() + public async Task FinalizeCacheHeadersAsync_AddsDate_IfNoneSpecified() { - var utcNow = DateTimeOffset.MinValue; + var clock = new TestClock + { + UtcNow = DateTimeOffset.UtcNow + }; var sink = new TestSink(); - var middleware = TestUtils.CreateTestMiddleware(testSink: sink); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, options: new ResponseCachingOptions + { + SystemClock = clock + }); var context = TestUtils.CreateTestContext(); - context.ResponseTime = utcNow; Assert.True(StringValues.IsNullOrEmpty(context.HttpContext.Response.Headers[HeaderNames.Date])); await middleware.FinalizeCacheHeadersAsync(context); - Assert.Equal(HeaderUtilities.FormatDate(utcNow), context.HttpContext.Response.Headers[HeaderNames.Date]); + Assert.Equal(HeaderUtilities.FormatDate(clock.UtcNow), context.HttpContext.Response.Headers[HeaderNames.Date]); Assert.Empty(sink.Writes); } [Fact] - public async Task FinalizeCacheHeaders_AddsDate_IfNoneSpecified() + public async Task FinalizeCacheHeadersAsync_DoNotAddDate_IfSpecified() { var utcNow = DateTimeOffset.MinValue; var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink); var context = TestUtils.CreateTestContext(); + context.HttpContext.Response.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow); context.ResponseTime = utcNow + TimeSpan.FromSeconds(10); @@ -574,7 +642,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public async Task FinalizeCacheHeaders_StoresCachedResponse_InState() + public async Task FinalizeCacheHeadersAsync_StoresCachedResponse_InState() { var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink); @@ -589,20 +657,19 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public async Task FinalizeCacheHeaders_SplitsVaryHeaderByCommas() + public async Task FinalizeCacheHeadersAsync_SplitsVaryHeaderByCommas() { var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink); var context = TestUtils.CreateTestContext(); + context.HttpContext.Response.Headers[HeaderNames.Vary] = "HeaderB, heaDera"; - await middleware.TryServeFromCacheAsync(context); await middleware.FinalizeCacheHeadersAsync(context); Assert.Equal(new StringValues(new[] { "HEADERA", "HEADERB" }), context.CachedVaryByRules.Headers); TestUtils.AssertLoggedMessages( sink.Writes, - LoggedMessage.NoResponseServed, LoggedMessage.VaryByRulesUpdated); } @@ -614,11 +681,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); var context = TestUtils.CreateTestContext(); + context.ShouldCacheResponse = true; middleware.ShimResponseStream(context); context.HttpContext.Response.ContentLength = 20; + await context.HttpContext.Response.WriteAsync(new string('0', 20)); - context.ShouldCacheResponse = true; context.CachedResponse = new CachedResponse(); context.BaseKey = "BaseKey"; context.CachedResponseValidFor = TimeSpan.FromSeconds(10); @@ -639,11 +707,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); var context = TestUtils.CreateTestContext(); + context.ShouldCacheResponse = true; middleware.ShimResponseStream(context); context.HttpContext.Response.ContentLength = 9; + await context.HttpContext.Response.WriteAsync(new string('0', 10)); - context.ShouldCacheResponse = true; context.CachedResponse = new CachedResponse(); context.BaseKey = "BaseKey"; context.CachedResponseValidFor = TimeSpan.FromSeconds(10); @@ -664,10 +733,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); var context = TestUtils.CreateTestContext(); + context.ShouldCacheResponse = true; middleware.ShimResponseStream(context); + await context.HttpContext.Response.WriteAsync(new string('0', 10)); - context.ShouldCacheResponse = true; context.CachedResponse = new CachedResponse() { Headers = new HeaderDictionary() @@ -691,10 +761,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); var context = TestUtils.CreateTestContext(); + context.ShouldCacheResponse = false; middleware.ShimResponseStream(context); await context.HttpContext.Response.WriteAsync(new string('0', 10)); - context.ShouldCacheResponse = false; await middleware.FinalizeCacheBodyAsync(context); @@ -712,10 +782,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); var context = TestUtils.CreateTestContext(); + context.ShouldCacheResponse = true; middleware.ShimResponseStream(context); await context.HttpContext.Response.WriteAsync(new string('0', 10)); - context.ShouldCacheResponse = true; context.ResponseCachingStream.DisableBuffering(); await middleware.FinalizeCacheBodyAsync(context); @@ -727,16 +797,52 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public void ShimResponseStream_SecondInvocation_Throws() + public void AddResponseCachingFeature_SecondInvocation_Throws() { - var middleware = TestUtils.CreateTestMiddleware(); - var context = TestUtils.CreateTestContext(); + var httpContext = new DefaultHttpContext(); // Should not throw - middleware.ShimResponseStream(context); + ResponseCachingMiddleware.AddResponseCachingFeature(httpContext); // Should throw - Assert.ThrowsAny(() => middleware.ShimResponseStream(context)); + Assert.ThrowsAny(() => ResponseCachingMiddleware.AddResponseCachingFeature(httpContext)); + } + + private class FakeResponseFeature : HttpResponseFeature + { + public override void OnStarting(Func callback, object state) { } + } + + [Theory] + // If allowResponseCaching is false, other settings will not matter but are included for completeness + [InlineData(false, false, false)] + [InlineData(false, false, true)] + [InlineData(false, true, false)] + [InlineData(false, true, true)] + [InlineData(true, false, false)] + [InlineData(true, false, true)] + [InlineData(true, true, false)] + [InlineData(true, true, true)] + public async Task Invoke_AddsResponseCachingFeature_Always(bool allowResponseCaching, bool allowCacheLookup, bool allowCacheStorage) + { + var responseCachingFeatureAdded = false; + var middleware = TestUtils.CreateTestMiddleware(next: httpContext => + { + responseCachingFeatureAdded = httpContext.Features.Get() != null; + return TaskCache.CompletedTask; + }, + policyProvider: new TestResponseCachingPolicyProvider + { + AttemptResponseCachingValue = allowResponseCaching, + AllowCacheLookupValue = allowCacheLookup, + AllowCacheStorageValue = allowCacheStorage + }); + + var context = new DefaultHttpContext(); + context.Features.Set(new FakeResponseFeature()); + await middleware.Invoke(context); + + Assert.True(responseCachingFeatureAdded); } [Fact] diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingPolicyProviderTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingPolicyProviderTests.cs index 02a068e6e4..4f1307b4bc 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingPolicyProviderTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingPolicyProviderTests.cs @@ -3,7 +3,6 @@ using System; using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Headers; using Microsoft.AspNetCore.ResponseCaching.Internal; using Microsoft.Extensions.Logging.Testing; using Microsoft.Net.Http.Headers; @@ -27,13 +26,13 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Theory] [MemberData(nameof(CacheableMethods))] - public void IsRequestCacheable_CacheableMethods_Allowed(string method) + public void AttemptResponseCaching_CacheableMethods_Allowed(string method) { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); context.HttpContext.Request.Method = method; - Assert.True(new ResponseCachingPolicyProvider().IsRequestCacheable(context)); + Assert.True(new ResponseCachingPolicyProvider().AttemptResponseCaching(context)); Assert.Empty(sink.Writes); } public static TheoryData NonCacheableMethods @@ -56,51 +55,34 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Theory] [MemberData(nameof(NonCacheableMethods))] - public void IsRequestCacheable_UncacheableMethods_NotAllowed(string method) + public void AttemptResponseCaching_UncacheableMethods_NotAllowed(string method) { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); context.HttpContext.Request.Method = method; - Assert.False(new ResponseCachingPolicyProvider().IsRequestCacheable(context)); + Assert.False(new ResponseCachingPolicyProvider().AttemptResponseCaching(context)); TestUtils.AssertLoggedMessages( sink.Writes, LoggedMessage.RequestMethodNotCacheable); } [Fact] - public void IsRequestCacheable_AuthorizationHeaders_NotAllowed() + public void AttemptResponseCaching_AuthorizationHeaders_NotAllowed() { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); context.HttpContext.Request.Method = HttpMethods.Get; context.HttpContext.Request.Headers[HeaderNames.Authorization] = "Basic plaintextUN:plaintextPW"; - Assert.False(new ResponseCachingPolicyProvider().IsRequestCacheable(context)); + Assert.False(new ResponseCachingPolicyProvider().AttemptResponseCaching(context)); TestUtils.AssertLoggedMessages( sink.Writes, LoggedMessage.RequestWithAuthorizationNotCacheable); } [Fact] - public void IsRequestCacheable_NoCache_NotAllowed() - { - var sink = new TestSink(); - var context = TestUtils.CreateTestContext(sink); - context.HttpContext.Request.Method = HttpMethods.Get; - context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() - { - NoCache = true - }.ToString(); - - Assert.False(new ResponseCachingPolicyProvider().IsRequestCacheable(context)); - TestUtils.AssertLoggedMessages( - sink.Writes, - LoggedMessage.RequestWithNoCacheNotCacheable); - } - - [Fact] - public void IsRequestCacheable_NoStore_Allowed() + public void AllowCacheStorage_NoStore_Allowed() { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); @@ -110,26 +92,43 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests NoStore = true }.ToString(); - Assert.True(new ResponseCachingPolicyProvider().IsRequestCacheable(context)); + Assert.True(new ResponseCachingPolicyProvider().AllowCacheLookup(context)); Assert.Empty(sink.Writes); } [Fact] - public void IsRequestCacheable_LegacyDirectives_NotAllowed() + public void AllowCacheLookup_NoCache_NotAllowed() + { + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); + context.HttpContext.Request.Method = HttpMethods.Get; + context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() + { + NoCache = true + }.ToString(); + + Assert.False(new ResponseCachingPolicyProvider().AllowCacheLookup(context)); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.RequestWithNoCacheNotCacheable); + } + + [Fact] + public void AllowCacheLookup_LegacyDirectives_NotAllowed() { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); context.HttpContext.Request.Method = HttpMethods.Get; context.HttpContext.Request.Headers[HeaderNames.Pragma] = "no-cache"; - Assert.False(new ResponseCachingPolicyProvider().IsRequestCacheable(context)); + Assert.False(new ResponseCachingPolicyProvider().AllowCacheLookup(context)); TestUtils.AssertLoggedMessages( sink.Writes, LoggedMessage.RequestWithPragmaNoCacheNotCacheable); } [Fact] - public void IsRequestCacheable_LegacyDirectives_OverridenByCacheControl() + public void AllowCacheLookup_LegacyDirectives_OverridenByCacheControl() { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); @@ -137,7 +136,22 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.HttpContext.Request.Headers[HeaderNames.Pragma] = "no-cache"; context.HttpContext.Request.Headers[HeaderNames.CacheControl] = "max-age=10"; - Assert.True(new ResponseCachingPolicyProvider().IsRequestCacheable(context)); + Assert.True(new ResponseCachingPolicyProvider().AllowCacheLookup(context)); + Assert.Empty(sink.Writes); + } + + [Fact] + public void AllowCacheStorage_NoStore_NotAllowed() + { + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); + context.HttpContext.Request.Method = HttpMethods.Get; + context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() + { + NoStore = true + }.ToString(); + + Assert.False(new ResponseCachingPolicyProvider().AllowCacheStorage(context)); Assert.Empty(sink.Writes); } @@ -184,26 +198,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests LoggedMessage.ResponseWithNoCacheNotCacheable); } - [Fact] - public void IsResponseCacheable_RequestNoStore_NotAllowed() - { - var sink = new TestSink(); - var context = TestUtils.CreateTestContext(sink); - context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() - { - NoStore = true - }.ToString(); - context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() - { - Public = true - }.ToString(); - - Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); - TestUtils.AssertLoggedMessages( - sink.Writes, - LoggedMessage.ResponseWithNoStoreNotCacheable); - } - [Fact] public void IsResponseCacheable_ResponseNoStore_NotAllowed() { @@ -289,6 +283,9 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Theory] + [InlineData(StatusCodes.Status100Continue)] + [InlineData(StatusCodes.Status101SwitchingProtocols)] + [InlineData(StatusCodes.Status102Processing)] [InlineData(StatusCodes.Status201Created)] [InlineData(StatusCodes.Status202Accepted)] [InlineData(StatusCodes.Status203NonAuthoritative)] @@ -296,6 +293,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [InlineData(StatusCodes.Status205ResetContent)] [InlineData(StatusCodes.Status206PartialContent)] [InlineData(StatusCodes.Status207MultiStatus)] + [InlineData(StatusCodes.Status208AlreadyReported)] + [InlineData(StatusCodes.Status226IMUsed)] [InlineData(StatusCodes.Status300MultipleChoices)] [InlineData(StatusCodes.Status301MovedPermanently)] [InlineData(StatusCodes.Status302Found)] @@ -325,9 +324,14 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [InlineData(StatusCodes.Status417ExpectationFailed)] [InlineData(StatusCodes.Status418ImATeapot)] [InlineData(StatusCodes.Status419AuthenticationTimeout)] + [InlineData(StatusCodes.Status421MisdirectedRequest)] [InlineData(StatusCodes.Status422UnprocessableEntity)] [InlineData(StatusCodes.Status423Locked)] [InlineData(StatusCodes.Status424FailedDependency)] + [InlineData(StatusCodes.Status426UpgradeRequired)] + [InlineData(StatusCodes.Status428PreconditionRequired)] + [InlineData(StatusCodes.Status429TooManyRequests)] + [InlineData(StatusCodes.Status431RequestHeaderFieldsTooLarge)] [InlineData(StatusCodes.Status451UnavailableForLegalReasons)] [InlineData(StatusCodes.Status500InternalServerError)] [InlineData(StatusCodes.Status501NotImplemented)] @@ -337,6 +341,9 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [InlineData(StatusCodes.Status505HttpVersionNotsupported)] [InlineData(StatusCodes.Status506VariantAlsoNegotiates)] [InlineData(StatusCodes.Status507InsufficientStorage)] + [InlineData(StatusCodes.Status508LoopDetected)] + [InlineData(StatusCodes.Status510NotExtended)] + [InlineData(StatusCodes.Status511NetworkAuthenticationRequired)] public void IsResponseCacheable_NonSuccessStatusCodes_NotAllowed(int statusCode) { var sink = new TestSink(); @@ -687,6 +694,29 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests LoggedMessage.ExpirationMaxStaleSatisfied); } + [Fact] + public void IsCachedEntryFresh_MaxStaleInfiniteOverridesFreshness_ToFresh() + { + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); + context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() + { + MaxAge = TimeSpan.FromSeconds(5), + MaxStale = true // No value specified means a MaxStaleLimit of infinity + }.ToString(); + context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() + { + MaxAge = TimeSpan.FromSeconds(5), + }.ToString(); + context.CachedEntryAge = TimeSpan.FromSeconds(6); + + Assert.True(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.ExpirationInfiniteMaxStaleSatisfied); + } + [Fact] public void IsCachedEntryFresh_MaxStaleOverridesFreshness_ButStillNotFresh() { @@ -735,5 +765,30 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests sink.Writes, LoggedMessage.ExpirationMustRevalidate); } + + [Fact] + public void IsCachedEntryFresh_ProxyRevalidateOverridesRequestMaxStale_ToNotFresh() + { + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); + context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() + { + MaxAge = TimeSpan.FromSeconds(5), + MaxStale = true, // This value must be set to true in order to specify MaxStaleLimit + MaxStaleLimit = TimeSpan.FromSeconds(2) + }.ToString(); + context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() + { + MaxAge = TimeSpan.FromSeconds(5), + MustRevalidate = true + }.ToString(); + context.CachedEntryAge = TimeSpan.FromSeconds(6); + + Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.ExpirationMustRevalidate); + } } } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs index f2a014b707..e8347bf11a 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs @@ -123,10 +123,16 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests using (var server = new TestServer(builder)) { var client = server.CreateClient(); - client.DefaultRequestHeaders.CacheControl = - new System.Net.Http.Headers.CacheControlHeaderValue { NoCache = true }; var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); + + // verify the response is cached + var cachedResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); + await AssertCachedResponseAsync(initialResponse, cachedResponse); + + // assert cached response no longer served + client.DefaultRequestHeaders.CacheControl = + new System.Net.Http.Headers.CacheControlHeaderValue { NoCache = true }; var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); await AssertFreshResponseAsync(initialResponse, subsequentResponse); @@ -146,10 +152,16 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests using (var server = new TestServer(builder)) { var client = server.CreateClient(); - client.DefaultRequestHeaders.Pragma.Clear(); - client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("no-cache")); var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); + + // verify the response is cached + var cachedResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); + await AssertCachedResponseAsync(initialResponse, cachedResponse); + + // assert cached response no longer served + client.DefaultRequestHeaders.Pragma.Clear(); + client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("no-cache")); var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); await AssertFreshResponseAsync(initialResponse, subsequentResponse); @@ -565,7 +577,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public async void ServesCachedContent_IfSubsequentRequest_ContainsNoStore() + public async void ServesCachedContent_IfSubsequentRequestContainsNoStore() { var builders = TestUtils.CreateBuildersWithResponseCaching(); @@ -587,7 +599,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public async void ServesFreshContent_IfInitialRequestContains_NoStore() + public async void ServesFreshContent_IfInitialRequestContainsNoStore() { var builders = TestUtils.CreateBuildersWithResponseCaching(); @@ -608,6 +620,31 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } } + [Fact] + public async void ServesFreshContent_IfInitialResponseContainsNoStore() + { + var builders = TestUtils.CreateBuildersWithResponseCaching(requestDelegate: async (context) => + { + var headers = context.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() + { + NoStore = true + }; + await TestUtils.TestRequestDelegate(context); + }); + + foreach (var builder in builders) + { + using (var server = new TestServer(builder)) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + var subsequentResponse = await client.GetAsync(""); + + await AssertFreshResponseAsync(initialResponse, subsequentResponse); + } + } + } + [Fact] public async void Serves304_IfIfModifiedSince_Satisfied() { @@ -619,7 +656,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var client = server.CreateClient(); var initialResponse = await client.GetAsync(""); - client.DefaultRequestHeaders.IfUnmodifiedSince = DateTimeOffset.MaxValue; + client.DefaultRequestHeaders.IfModifiedSince = DateTimeOffset.MaxValue; var subsequentResponse = await client.GetAsync(""); initialResponse.EnsureSuccessStatusCode(); @@ -639,7 +676,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { var client = server.CreateClient(); var initialResponse = await client.GetAsync(""); - client.DefaultRequestHeaders.IfUnmodifiedSince = DateTimeOffset.MinValue; + client.DefaultRequestHeaders.IfModifiedSince = DateTimeOffset.MinValue; var subsequentResponse = await client.GetAsync(""); await AssertCachedResponseAsync(initialResponse, subsequentResponse); diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs index b50be5b175..88d1511375 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs @@ -20,6 +20,7 @@ using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; using Xunit; +using ISystemClock = Microsoft.AspNetCore.ResponseCaching.Internal.ISystemClock; namespace Microsoft.AspNetCore.ResponseCaching.Tests { @@ -42,11 +43,19 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } var uniqueId = Guid.NewGuid().ToString(); - headers.CacheControl = new CacheControlHeaderValue + if (headers.CacheControl == null) { - Public = true, - MaxAge = string.IsNullOrEmpty(expires) ? TimeSpan.FromSeconds(10) : (TimeSpan?)null - }; + headers.CacheControl = new CacheControlHeaderValue + { + Public = true, + MaxAge = string.IsNullOrEmpty(expires) ? TimeSpan.FromSeconds(10) : (TimeSpan?)null + }; + } + else + { + headers.CacheControl.Public = true; + headers.CacheControl.MaxAge = string.IsNullOrEmpty(expires) ? TimeSpan.FromSeconds(10) : (TimeSpan?)null; + } headers.Date = DateTimeOffset.UtcNow; headers.Headers["X-Value"] = uniqueId; @@ -103,12 +112,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } internal static ResponseCachingMiddleware CreateTestMiddleware( + RequestDelegate next = null, IResponseCache cache = null, ResponseCachingOptions options = null, TestSink testSink = null, IResponseCachingKeyProvider keyProvider = null, IResponseCachingPolicyProvider policyProvider = null) { + if (next == null) + { + next = httpContext => TaskCache.CompletedTask; + } if (cache == null) { cache = new TestResponseCache(); @@ -127,7 +141,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } return new ResponseCachingMiddleware( - httpContext => TaskCache.CompletedTask, + next, Options.Create(options), testSink == null ? (ILoggerFactory)NullLoggerFactory.Instance : new TestLoggerFactory(testSink, true), policyProvider, @@ -188,7 +202,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests internal static LoggedMessage ResponseWithUnsuccessfulStatusCodeNotCacheable => new LoggedMessage(17, LogLevel.Debug); internal static LoggedMessage NotModifiedIfNoneMatchStar => new LoggedMessage(18, LogLevel.Debug); internal static LoggedMessage NotModifiedIfNoneMatchMatched => new LoggedMessage(19, LogLevel.Debug); - internal static LoggedMessage NotModifiedIfUnmodifiedSinceSatisfied => new LoggedMessage(20, LogLevel.Debug); + internal static LoggedMessage NotModifiedIfModifiedSinceSatisfied => new LoggedMessage(20, LogLevel.Debug); internal static LoggedMessage NotModifiedServed => new LoggedMessage(21, LogLevel.Information); internal static LoggedMessage CachedResponseServed => new LoggedMessage(22, LogLevel.Information); internal static LoggedMessage GatewayTimeoutServed => new LoggedMessage(23, LogLevel.Information); @@ -197,6 +211,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests internal static LoggedMessage ResponseCached => new LoggedMessage(26, LogLevel.Information); internal static LoggedMessage ResponseNotCached => new LoggedMessage(27, LogLevel.Information); internal static LoggedMessage ResponseContentLengthMismatchNotCached => new LoggedMessage(28, LogLevel.Warning); + internal static LoggedMessage ExpirationInfiniteMaxStaleSatisfied => new LoggedMessage(29, LogLevel.Debug); private LoggedMessage(int evenId, LogLevel logLevel) { @@ -218,11 +233,21 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests internal class TestResponseCachingPolicyProvider : IResponseCachingPolicyProvider { - public bool IsCachedEntryFresh(ResponseCachingContext context) => true; + public bool AllowCacheLookupValue { get; set; } = false; + public bool AllowCacheStorageValue { get; set; } = false; + public bool AttemptResponseCachingValue { get; set; } = false; + public bool IsCachedEntryFreshValue { get; set; } = true; + public bool IsResponseCacheableValue { get; set; } = true; - public bool IsRequestCacheable(ResponseCachingContext context) => true; + public bool AllowCacheLookup(ResponseCachingContext context) => AllowCacheLookupValue; - public bool IsResponseCacheable(ResponseCachingContext context) => true; + public bool AllowCacheStorage(ResponseCachingContext context) => AllowCacheStorageValue; + + public bool AttemptResponseCaching(ResponseCachingContext context) => AttemptResponseCachingValue; + + public bool IsCachedEntryFresh(ResponseCachingContext context) => IsCachedEntryFreshValue; + + public bool IsResponseCacheable(ResponseCachingContext context) => IsResponseCacheableValue; } internal class TestResponseCachingKeyProvider : IResponseCachingKeyProvider @@ -284,4 +309,9 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests return TaskCache.CompletedTask; } } + + internal class TestClock : ISystemClock + { + public DateTimeOffset UtcNow { get; set; } + } } From 267f5134c872c90c6d80732823e117646ade6e79 Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Wed, 1 Feb 2017 10:48:39 -0800 Subject: [PATCH 076/188] Upgrade to VS 2017 --- NuGet.config | 3 +- ResponseCaching.sln | 19 +++----- appveyor.yml | 3 +- build.ps1 | 2 +- build.sh | 2 +- {tools => build}/Key.snk | Bin build/common.props | 24 ++++++++++ global.json | 8 ---- .../ResponseCachingSample.csproj | 17 +++++++ .../ResponseCachingSample.xproj | 19 -------- samples/ResponseCachingSample/project.json | 33 -------------- ...etCore.ResponseCaching.Abstractions.csproj | 16 +++++++ ...NetCore.ResponseCaching.Abstractions.xproj | 21 --------- .../Properties/AssemblyInfo.cs | 11 ----- .../project.json | 31 ------------- ...icrosoft.AspNetCore.ResponseCaching.csproj | 23 ++++++++++ ...Microsoft.AspNetCore.ResponseCaching.xproj | 19 -------- .../Properties/AssemblyInfo.cs | 7 --- .../project.json | 42 ------------------ ...ft.AspNetCore.ResponseCaching.Tests.csproj | 19 ++++++++ ...oft.AspNetCore.ResponseCaching.Tests.xproj | 21 --------- .../project.json | 25 ----------- version.props | 7 +++ 23 files changed, 119 insertions(+), 253 deletions(-) rename {tools => build}/Key.snk (100%) create mode 100644 build/common.props delete mode 100644 global.json create mode 100644 samples/ResponseCachingSample/ResponseCachingSample.csproj delete mode 100644 samples/ResponseCachingSample/ResponseCachingSample.xproj delete mode 100644 samples/ResponseCachingSample/project.json create mode 100644 src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj delete mode 100644 src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Microsoft.AspNetCore.ResponseCaching.Abstractions.xproj delete mode 100644 src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Properties/AssemblyInfo.cs delete mode 100644 src/Microsoft.AspNetCore.ResponseCaching.Abstractions/project.json create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj delete mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.xproj delete mode 100644 src/Microsoft.AspNetCore.ResponseCaching/project.json create mode 100644 test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj delete mode 100644 test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.xproj delete mode 100644 test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json create mode 100644 version.props diff --git a/NuGet.config b/NuGet.config index 0fd623ffdd..8e65695611 100644 --- a/NuGet.config +++ b/NuGet.config @@ -1,7 +1,8 @@ - + + diff --git a/ResponseCaching.sln b/ResponseCaching.sln index 43b8d0f0b8..23eb129413 100644 --- a/ResponseCaching.sln +++ b/ResponseCaching.sln @@ -1,7 +1,7 @@ - + Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 14 -VisualStudioVersion = 14.0.25420.1 +# Visual Studio 15 +VisualStudioVersion = 15.0.26127.0 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{367AABAF-E03C-4491-A9A7-BDDE8903D1B4}" EndProject @@ -9,18 +9,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{C51D 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}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ResponseCachingSample", "samples\ResponseCachingSample\ResponseCachingSample.csproj", "{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 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.ResponseCaching.Tests", "test\Microsoft.AspNetCore.ResponseCaching.Tests\Microsoft.AspNetCore.ResponseCaching.Tests.csproj", "{151B2027-3936-44B9-A4A0-E1E5902125AB}" 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}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.ResponseCaching", "src\Microsoft.AspNetCore.ResponseCaching\Microsoft.AspNetCore.ResponseCaching.csproj", "{D1031270-DBD3-4F02-A3DC-3E7DADE8EBE6}" EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.ResponseCaching", "src\Microsoft.AspNetCore.ResponseCaching\Microsoft.AspNetCore.ResponseCaching.xproj", "{D1031270-DBD3-4F02-A3DC-3E7DADE8EBE6}" -EndProject -Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.AspNetCore.ResponseCaching.Abstractions", "src\Microsoft.AspNetCore.ResponseCaching.Abstractions\Microsoft.AspNetCore.ResponseCaching.Abstractions.xproj", "{2D1022E8-CBB6-478D-A420-CB888D0EF7B7}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.ResponseCaching.Abstractions", "src\Microsoft.AspNetCore.ResponseCaching.Abstractions\Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj", "{2D1022E8-CBB6-478D-A420-CB888D0EF7B7}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/appveyor.yml b/appveyor.yml index b9a9bcd1e6..df67923781 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -10,4 +10,5 @@ build_script: - build.cmd verify clone_depth: 1 test: off -deploy: off \ No newline at end of file +deploy: off +os: Visual Studio 2017 RC diff --git a/build.ps1 b/build.ps1 index 8f2f99691a..0605b59c01 100644 --- a/build.ps1 +++ b/build.ps1 @@ -33,7 +33,7 @@ cd $PSScriptRoot $repoFolder = $PSScriptRoot $env:REPO_FOLDER = $repoFolder -$koreBuildZip="https://github.com/aspnet/KoreBuild/archive/dev.zip" +$koreBuildZip="https://github.com/aspnet/KoreBuild/archive/feature/msbuild.zip" if ($env:KOREBUILD_ZIP) { $koreBuildZip=$env:KOREBUILD_ZIP diff --git a/build.sh b/build.sh index 4fd7ede788..07997d6c83 100755 --- a/build.sh +++ b/build.sh @@ -2,7 +2,7 @@ repoFolder="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" cd $repoFolder -koreBuildZip="https://github.com/aspnet/KoreBuild/archive/dev.zip" +koreBuildZip="https://github.com/aspnet/KoreBuild/archive/feature/msbuild.zip" if [ ! -z $KOREBUILD_ZIP ]; then koreBuildZip=$KOREBUILD_ZIP fi diff --git a/tools/Key.snk b/build/Key.snk similarity index 100% rename from tools/Key.snk rename to build/Key.snk diff --git a/build/common.props b/build/common.props new file mode 100644 index 0000000000..8c19f09a32 --- /dev/null +++ b/build/common.props @@ -0,0 +1,24 @@ + + + + + Microsoft ASP.NET Core + https://github.com/aspnet/ResponseCaching + git + $(MSBuildThisFileDirectory)Key.snk + true + true + 1.2.0-* + 1.6.2-* + $(VersionSuffix)-$(BuildNumber) + + + + + + + + + + + diff --git a/global.json b/global.json deleted file mode 100644 index 0ad1995dd2..0000000000 --- a/global.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "projects": [ - "src" - ], - "sdk": { - "version": "1.0.0-preview2-1-003180" - } -} \ No newline at end of file diff --git a/samples/ResponseCachingSample/ResponseCachingSample.csproj b/samples/ResponseCachingSample/ResponseCachingSample.csproj new file mode 100644 index 0000000000..a86b0e11b3 --- /dev/null +++ b/samples/ResponseCachingSample/ResponseCachingSample.csproj @@ -0,0 +1,17 @@ + + + + net451;netcoreapp1.1 + + win7-x64 + Exe + + + + + + + + + + diff --git a/samples/ResponseCachingSample/ResponseCachingSample.xproj b/samples/ResponseCachingSample/ResponseCachingSample.xproj deleted file mode 100644 index 43167a3606..0000000000 --- a/samples/ResponseCachingSample/ResponseCachingSample.xproj +++ /dev/null @@ -1,19 +0,0 @@ - - - - 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/project.json b/samples/ResponseCachingSample/project.json deleted file mode 100644 index fde231f6d4..0000000000 --- a/samples/ResponseCachingSample/project.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "dependencies": { - "Microsoft.AspNetCore.ResponseCaching": "1.2.0-*", - "Microsoft.AspNetCore.Server.IISIntegration": "1.2.0-*", - "Microsoft.AspNetCore.Server.Kestrel": "1.2.0-*", - "Microsoft.Extensions.Caching.Memory": "1.2.0-*" - }, - "buildOptions": { - "emitEntryPoint": true - }, - "frameworks": { - "net451": {}, - "netcoreapp1.1": { - "dependencies": { - "Microsoft.NETCore.App": { - "version": "1.2.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%" - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj b/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj new file mode 100644 index 0000000000..2cdfa4d58a --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj @@ -0,0 +1,16 @@ + + + + + + ASP.NET Core response caching middleware abstractions and feature interface definitions. + net451;netstandard1.3 + true + aspnetcore;cache;caching + + + + + + + diff --git a/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Microsoft.AspNetCore.ResponseCaching.Abstractions.xproj b/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Microsoft.AspNetCore.ResponseCaching.Abstractions.xproj deleted file mode 100644 index d06ce2e8f9..0000000000 --- a/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Microsoft.AspNetCore.ResponseCaching.Abstractions.xproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - - 2d1022e8-cbb6-478d-a420-cb888d0ef7b7 - Microsoft.AspNetCore.ResponseCaching.Abstractions - .\obj - .\bin\ - v4.5.2 - - - - 2.0 - - - diff --git a/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Properties/AssemblyInfo.cs deleted file mode 100644 index 32dcddfc57..0000000000 --- a/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,11 +0,0 @@ -// 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.Abstractions/project.json b/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/project.json deleted file mode 100644 index db41492279..0000000000 --- a/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/project.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "version": "1.2.0-*", - "buildOptions": { - "warningsAsErrors": true, - "keyFile": "../../tools/Key.snk", - "xmlDoc": true - }, - "description": "ASP.NET Core response caching middleware abstractions and feature interface definitions.", - "packOptions": { - "repository": { - "type": "git", - "url": "git://github.com/aspnet/ResponseCaching" - }, - "tags": [ - "aspnetcore", - "cache", - "caching" - ] - }, - "dependencies": { - "Microsoft.Extensions.Primitives": "1.2.0-*" - }, - "frameworks": { - "net451": {}, - "netstandard1.3": { - "dependencies": { - "NETStandard.Library": "1.6.2-*" - } - } - } -} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj b/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj new file mode 100644 index 0000000000..aa20232fa8 --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj @@ -0,0 +1,23 @@ + + + + + + ASP.NET Core middleware for caching HTTP responses on the server. + net451;netstandard1.3 + $(NoWarn);CS1591 + true + true + aspnetcore;cache;caching + + + + + + + + + + + + diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.xproj b/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.xproj deleted file mode 100644 index a4d4533df7..0000000000 --- a/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.xproj +++ /dev/null @@ -1,19 +0,0 @@ - - - - 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 index f6017986e9..7a0fd0e4de 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Properties/AssemblyInfo.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Properties/AssemblyInfo.cs @@ -1,13 +1,6 @@ // 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; using System.Runtime.CompilerServices; -[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")] [assembly: InternalsVisibleTo("Microsoft.AspNetCore.ResponseCaching.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.ResponseCaching/project.json b/src/Microsoft.AspNetCore.ResponseCaching/project.json deleted file mode 100644 index c4a0254f95..0000000000 --- a/src/Microsoft.AspNetCore.ResponseCaching/project.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "version": "1.2.0-*", - "buildOptions": { - "warningsAsErrors": true, - "allowUnsafe": 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.2.0-*", - "Microsoft.AspNetCore.Http.Extensions": "1.2.0-*", - "Microsoft.AspNetCore.ResponseCaching.Abstractions": { - "target": "project" - }, - "Microsoft.Extensions.Caching.Memory": "1.2.0-*", - "Microsoft.Extensions.Logging.Abstractions": "1.2.0-*", - "Microsoft.Extensions.TaskCache.Sources": { - "version": "1.2.0-*", - "type": "build" - }, - "NETStandard.Library": "1.6.2-*" - }, - "frameworks": { - "net451": {}, - "netstandard1.3": {} - } -} \ No newline at end of file diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj b/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj new file mode 100644 index 0000000000..666a99690e --- /dev/null +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj @@ -0,0 +1,19 @@ + + + + + + netcoreapp1.1;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 deleted file mode 100644 index ea971b7729..0000000000 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.xproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - 14.0 - $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) - - - - 151b2027-3936-44b9-a4a0-e1e5902125ab - Microsoft.AspNetCore.ResponseCaching.Tests - .\obj - .\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 deleted file mode 100644 index 909d049885..0000000000 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/project.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "buildOptions": { - "warningsAsErrors": true, - "keyFile": "../../tools/Key.snk" - }, - "dependencies": { - "dotnet-test-xunit": "2.2.0-*", - "Microsoft.AspNetCore.ResponseCaching": "1.2.0-*", - "Microsoft.AspNetCore.TestHost": "1.2.0-*", - "Microsoft.Extensions.Logging.Testing": "1.2.0-*", - "xunit": "2.2.0-*" - }, - "frameworks": { - "netcoreapp1.1": { - "dependencies": { - "Microsoft.NETCore.App": { - "version": "1.2.0-*", - "type": "platform" - } - } - }, - "net451": {} - }, - "testRunner": "xunit" -} \ No newline at end of file diff --git a/version.props b/version.props new file mode 100644 index 0000000000..17fd5ac36d --- /dev/null +++ b/version.props @@ -0,0 +1,7 @@ + + + + 1.2.0 + preview1 + + From d43f05189a4da3f25c56b6c88823064e8933f1d5 Mon Sep 17 00:00:00 2001 From: John Luo Date: Fri, 3 Feb 2017 14:25:59 -0800 Subject: [PATCH 077/188] Overwrite headers when serving response from cache #101 --- .../ResponseCachingMiddleware.cs | 4 +-- .../ResponseCachingMiddlewareTests.cs | 29 +++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs index ed41f3cdaa..798679eebc 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs @@ -152,7 +152,7 @@ namespace Microsoft.AspNetCore.ResponseCaching response.StatusCode = context.CachedResponse.StatusCode; foreach (var header in context.CachedResponse.Headers) { - response.Headers.Add(header); + response.Headers[header.Key] = header.Value; } // Note: int64 division truncates result and errors may be up to 1 second. This reduction in @@ -287,7 +287,7 @@ namespace Microsoft.AspNetCore.ResponseCaching { if (!string.Equals(header.Key, HeaderNames.Age, StringComparison.OrdinalIgnoreCase)) { - context.CachedResponse.Headers.Add(header); + context.CachedResponse.Headers[header.Key] = header.Value; } } } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs index fc66045bdb..3cda27ef28 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs @@ -75,6 +75,35 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests LoggedMessage.CachedResponseServed); } + [Fact] + public async Task TryServeFromCacheAsync_CachedResponseFound_OverwritesExistingHeaders() + { + var cache = new TestResponseCache(); + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider("BaseKey")); + var context = TestUtils.CreateTestContext(); + + context.HttpContext.Response.Headers["MyHeader"] = "OldValue"; + await cache.SetAsync( + "BaseKey", + new CachedResponse() + { + Headers = new HeaderDictionary() + { + { "MyHeader", "NewValue" } + }, + Body = new SegmentReadStream(new List(0), 0) + }, + TimeSpan.Zero); + + Assert.True(await middleware.TryServeFromCacheAsync(context)); + Assert.Equal("NewValue", context.HttpContext.Response.Headers["MyHeader"]); + Assert.Equal(1, cache.GetCount); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.CachedResponseServed); + } + [Fact] public async Task TryServeFromCacheAsync_VaryByRuleFound_CachedResponseNotFound_Fails() { From 719041297963278f5e0dd44ce5519bdca44e1184 Mon Sep 17 00:00:00 2001 From: John Luo Date: Wed, 8 Feb 2017 16:06:20 -0800 Subject: [PATCH 078/188] Tigger caching header finalization on write #92 --- .../Internal/Interfaces/IResponseCache.cs | 3 + .../Internal/MemoryResponseCache.cs | 25 +++- ...icrosoft.AspNetCore.ResponseCaching.csproj | 2 +- .../ResponseCachingMiddleware.cs | 76 +++++++--- .../Streams/ResponseCachingStream.cs | 36 ++++- .../ResponseCachingMiddlewareTests.cs | 13 +- .../ResponseCachingTests.cs | 112 +++------------ .../TestUtils.cs | 134 ++++++++++++++---- 8 files changed, 243 insertions(+), 158 deletions(-) diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCache.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCache.cs index cd3b9da23f..41c85b277a 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCache.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCache.cs @@ -8,7 +8,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal { public interface IResponseCache { + IResponseCacheEntry Get(string key); Task GetAsync(string key); + + void Set(string key, IResponseCacheEntry entry, TimeSpan validFor); Task SetAsync(string key, IResponseCacheEntry entry, TimeSpan validFor); } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs index 4023345682..42c613bc2c 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs @@ -4,7 +4,7 @@ using System; using System.Threading.Tasks; using Microsoft.Extensions.Caching.Memory; -using Microsoft.Net.Http.Headers; +using Microsoft.Extensions.Internal; namespace Microsoft.AspNetCore.ResponseCaching.Internal { @@ -22,34 +22,39 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal _cache = cache; } - public Task GetAsync(string key) + public IResponseCacheEntry Get(string key) { var entry = _cache.Get(key); var memoryCachedResponse = entry as MemoryCachedResponse; if (memoryCachedResponse != null) { - return Task.FromResult(new CachedResponse + return new CachedResponse { Created = memoryCachedResponse.Created, StatusCode = memoryCachedResponse.StatusCode, Headers = memoryCachedResponse.Headers, Body = new SegmentReadStream(memoryCachedResponse.BodySegments, memoryCachedResponse.BodyLength) - }); + }; } else { - return Task.FromResult(entry as IResponseCacheEntry); + return entry as IResponseCacheEntry; } } - public async Task SetAsync(string key, IResponseCacheEntry entry, TimeSpan validFor) + public Task GetAsync(string key) + { + return Task.FromResult(Get(key)); + } + + public void Set(string key, IResponseCacheEntry entry, TimeSpan validFor) { var cachedResponse = entry as CachedResponse; if (cachedResponse != null) { var segmentStream = new SegmentWriteStream(StreamUtilities.BodySegmentSize); - await cachedResponse.Body.CopyToAsync(segmentStream); + cachedResponse.Body.CopyTo(segmentStream); _cache.Set( key, @@ -77,5 +82,11 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal }); } } + + public Task SetAsync(string key, IResponseCacheEntry entry, TimeSpan validFor) + { + Set(key, entry, validFor); + return TaskCache.CompletedTask; + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj b/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj index aa20232fa8..b56169a56a 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj +++ b/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj @@ -17,7 +17,7 @@ - + diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs index 798679eebc..cdb6cb817f 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs @@ -25,7 +25,6 @@ namespace Microsoft.AspNetCore.ResponseCaching private readonly IResponseCachingPolicyProvider _policyProvider; private readonly IResponseCache _cache; private readonly IResponseCachingKeyProvider _keyProvider; - private readonly Func _onStartingCallback; public ResponseCachingMiddleware( RequestDelegate next, @@ -66,7 +65,6 @@ namespace Microsoft.AspNetCore.ResponseCaching _policyProvider = policyProvider; _cache = cache; _keyProvider = keyProvider; - _onStartingCallback = state => OnResponseStartingAsync((ResponseCachingContext)state); } public async Task Invoke(HttpContext httpContext) @@ -90,13 +88,10 @@ namespace Microsoft.AspNetCore.ResponseCaching try { - // Subscribe to OnStarting event - httpContext.Response.OnStarting(_onStartingCallback, context); - await _next(httpContext); // If there was no response body, check the response headers now. We can cache things like redirects. - await OnResponseStartingAsync(context); + await StartResponseAsync(context); // Finalize the cache entry await FinalizeCacheBodyAsync(context); @@ -219,10 +214,17 @@ namespace Microsoft.AspNetCore.ResponseCaching return false; } - internal async Task FinalizeCacheHeadersAsync(ResponseCachingContext context) + + /// + /// Finalize cache headers. + /// + /// + /// true if a vary by entry needs to be stored in the cache; otherwise false. + private bool OnFinalizeCacheHeaders(ResponseCachingContext context) { if (_policyProvider.IsResponseCacheable(context)) { + var storeVaryByEntry = false; context.ShouldCacheResponse = true; // Create the cache entry now @@ -262,7 +264,7 @@ namespace Microsoft.AspNetCore.ResponseCaching // Always overwrite the CachedVaryByRules to update the expiry information _logger.LogVaryByRulesUpdated(normalizedVaryHeaders, normalizedVaryQueryKeys); - await _cache.SetAsync(context.BaseKey, context.CachedVaryByRules, context.CachedResponseValidFor); + storeVaryByEntry = true; context.StorageVaryKey = _keyProvider.CreateStorageVaryByKey(context); } @@ -290,13 +292,31 @@ namespace Microsoft.AspNetCore.ResponseCaching context.CachedResponse.Headers[header.Key] = header.Value; } } + + return storeVaryByEntry; } - else + + context.ResponseCachingStream.DisableBuffering(); + return false; + } + + internal void FinalizeCacheHeaders(ResponseCachingContext context) + { + if (OnFinalizeCacheHeaders(context)) { - context.ResponseCachingStream.DisableBuffering(); + _cache.Set(context.BaseKey, context.CachedVaryByRules, context.CachedResponseValidFor); } } + internal Task FinalizeCacheHeadersAsync(ResponseCachingContext context) + { + if (OnFinalizeCacheHeaders(context)) + { + return _cache.SetAsync(context.BaseKey, context.CachedVaryByRules, context.CachedResponseValidFor); + } + return TaskCache.CompletedTask; + } + internal async Task FinalizeCacheBodyAsync(ResponseCachingContext context) { if (context.ShouldCacheResponse && context.ResponseCachingStream.BufferingEnabled) @@ -327,19 +347,38 @@ namespace Microsoft.AspNetCore.ResponseCaching } } - internal Task OnResponseStartingAsync(ResponseCachingContext context) + /// + /// Mark the response as started and set the response time if no reponse was started yet. + /// + /// + /// true if the response was not started before this call; otherwise false. + private bool OnStartResponse(ResponseCachingContext context) { if (!context.ResponseStarted) { context.ResponseStarted = true; context.ResponseTime = _options.SystemClock.UtcNow; + return true; + } + return false; + } + + internal void StartResponse(ResponseCachingContext context) + { + if (OnStartResponse(context)) + { + FinalizeCacheHeaders(context); + } + } + + internal Task StartResponseAsync(ResponseCachingContext context) + { + if (OnStartResponse(context)) + { return FinalizeCacheHeadersAsync(context); } - else - { - return TaskCache.CompletedTask; - } + return TaskCache.CompletedTask; } internal static void AddResponseCachingFeature(HttpContext context) @@ -355,7 +394,12 @@ namespace Microsoft.AspNetCore.ResponseCaching { // Shim response stream context.OriginalResponseStream = context.HttpContext.Response.Body; - context.ResponseCachingStream = new ResponseCachingStream(context.OriginalResponseStream, _options.MaximumBodySize, StreamUtilities.BodySegmentSize); + context.ResponseCachingStream = new ResponseCachingStream( + context.OriginalResponseStream, + _options.MaximumBodySize, + StreamUtilities.BodySegmentSize, + () => StartResponse(context), + () => StartResponseAsync(context)); context.HttpContext.Response.Body = context.ResponseCachingStream; // Shim IHttpSendFileFeature diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Streams/ResponseCachingStream.cs b/src/Microsoft.AspNetCore.ResponseCaching/Streams/ResponseCachingStream.cs index 6644063b88..aa5fc371eb 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Streams/ResponseCachingStream.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Streams/ResponseCachingStream.cs @@ -14,12 +14,16 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal private readonly long _maxBufferSize; private readonly int _segmentSize; private SegmentWriteStream _segmentWriteStream; + private Action _startResponseCallback; + private Func _startResponseCallbackAsync; - internal ResponseCachingStream(Stream innerStream, long maxBufferSize, int segmentSize) + internal ResponseCachingStream(Stream innerStream, long maxBufferSize, int segmentSize, Action startResponseCallback, Func startResponseCallbackAsync) { _innerStream = innerStream; _maxBufferSize = maxBufferSize; _segmentSize = segmentSize; + _startResponseCallback = startResponseCallback; + _startResponseCallbackAsync = startResponseCallbackAsync; _segmentWriteStream = new SegmentWriteStream(_segmentSize); } @@ -71,10 +75,32 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal } public override void Flush() - => _innerStream.Flush(); + { + try + { + _startResponseCallback(); + _innerStream.Flush(); + } + catch + { + DisableBuffering(); + throw; + } + } - public override Task FlushAsync(CancellationToken cancellationToken) - => _innerStream.FlushAsync(); + public override async Task FlushAsync(CancellationToken cancellationToken) + { + try + { + await _startResponseCallbackAsync(); + await _innerStream.FlushAsync(); + } + catch + { + DisableBuffering(); + throw; + } + } // Underlying stream is write-only, no need to override other read related methods public override int Read(byte[] buffer, int offset, int count) @@ -84,6 +110,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal { try { + _startResponseCallback(); _innerStream.Write(buffer, offset, count); } catch @@ -109,6 +136,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal { try { + await _startResponseCallbackAsync(); await _innerStream.WriteAsync(buffer, offset, count, cancellationToken); } catch diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs index 3cda27ef28..dfd9c12be9 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs @@ -359,7 +359,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } [Fact] - public async Task OnResponseStartingAsync_IfAllowResponseCaptureIsTrue_SetsResponseTime() + public async Task StartResponsegAsync_IfAllowResponseCaptureIsTrue_SetsResponseTime() { var clock = new TestClock { @@ -372,13 +372,13 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var context = TestUtils.CreateTestContext(); context.ResponseTime = null; - await middleware.OnResponseStartingAsync(context); + await middleware.StartResponseAsync(context); Assert.Equal(clock.UtcNow, context.ResponseTime); } [Fact] - public async Task OnResponseStartingAsync_IfAllowResponseCaptureIsTrue_SetsResponseTimeOnlyOnce() + public async Task StartResponseAsync_IfAllowResponseCaptureIsTrue_SetsResponseTimeOnlyOnce() { var clock = new TestClock { @@ -392,12 +392,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var initialTime = clock.UtcNow; context.ResponseTime = null; - await middleware.OnResponseStartingAsync(context); + await middleware.StartResponseAsync(context); Assert.Equal(initialTime, context.ResponseTime); clock.UtcNow += TimeSpan.FromSeconds(10); - await middleware.OnResponseStartingAsync(context); + await middleware.StartResponseAsync(context); Assert.NotEqual(clock.UtcNow, context.ResponseTime); Assert.Equal(initialTime, context.ResponseTime); } @@ -790,10 +790,9 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); var context = TestUtils.CreateTestContext(); - context.ShouldCacheResponse = false; middleware.ShimResponseStream(context); await context.HttpContext.Response.WriteAsync(new string('0', 10)); - + context.ShouldCacheResponse = false; await middleware.FinalizeCacheBodyAsync(context); diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs index e8347bf11a..25fc1360e8 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; @@ -233,11 +232,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfVaryHeader_Matches() { - var builders = TestUtils.CreateBuildersWithResponseCaching(requestDelegate: async (context) => - { - context.Response.Headers[HeaderNames.Vary] = HeaderNames.From; - await TestUtils.TestRequestDelegate(context); - }); + var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Response.Headers[HeaderNames.Vary] = HeaderNames.From); foreach (var builder in builders) { @@ -256,11 +251,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfVaryHeader_Mismatches() { - var builders = TestUtils.CreateBuildersWithResponseCaching(requestDelegate: async (context) => - { - context.Response.Headers[HeaderNames.Vary] = HeaderNames.From; - await TestUtils.TestRequestDelegate(context); - }); + var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Response.Headers[HeaderNames.Vary] = HeaderNames.From); foreach (var builder in builders) { @@ -280,11 +271,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfVaryQueryKeys_Matches() { - var builders = TestUtils.CreateBuildersWithResponseCaching(requestDelegate: async (context) => - { - context.Features.Get().VaryByQueryKeys = new[] { "query" }; - await TestUtils.TestRequestDelegate(context); - }); + var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Features.Get().VaryByQueryKeys = new[] { "query" }); foreach (var builder in builders) { @@ -302,11 +289,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfVaryQueryKeysExplicit_Matches_QueryKeyCaseInsensitive() { - var builders = TestUtils.CreateBuildersWithResponseCaching(requestDelegate: async (context) => - { - context.Features.Get().VaryByQueryKeys = new[] { "QueryA", "queryb" }; - await TestUtils.TestRequestDelegate(context); - }); + var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Features.Get().VaryByQueryKeys = new[] { "QueryA", "queryb" }); foreach (var builder in builders) { @@ -324,11 +307,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfVaryQueryKeyStar_Matches_QueryKeyCaseInsensitive() { - var builders = TestUtils.CreateBuildersWithResponseCaching(requestDelegate: async (context) => - { - context.Features.Get().VaryByQueryKeys = new[] { "*" }; - await TestUtils.TestRequestDelegate(context); - }); + var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Features.Get().VaryByQueryKeys = new[] { "*" }); foreach (var builder in builders) { @@ -346,11 +325,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfVaryQueryKeyExplicit_Matches_OrderInsensitive() { - var builders = TestUtils.CreateBuildersWithResponseCaching(requestDelegate: async (context) => - { - context.Features.Get().VaryByQueryKeys = new[] { "QueryB", "QueryA" }; - await TestUtils.TestRequestDelegate(context); - }); + var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Features.Get().VaryByQueryKeys = new[] { "QueryB", "QueryA" }); foreach (var builder in builders) { @@ -368,11 +343,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfVaryQueryKeyStar_Matches_OrderInsensitive() { - var builders = TestUtils.CreateBuildersWithResponseCaching(requestDelegate: async (context) => - { - context.Features.Get().VaryByQueryKeys = new[] { "*" }; - await TestUtils.TestRequestDelegate(context); - }); + var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Features.Get().VaryByQueryKeys = new[] { "*" }); foreach (var builder in builders) { @@ -390,11 +361,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfVaryQueryKey_Mismatches() { - var builders = TestUtils.CreateBuildersWithResponseCaching(requestDelegate: async (context) => - { - context.Features.Get().VaryByQueryKeys = new[] { "query" }; - await TestUtils.TestRequestDelegate(context); - }); + var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Features.Get().VaryByQueryKeys = new[] { "query" }); foreach (var builder in builders) { @@ -412,11 +379,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfVaryQueryKeyExplicit_Mismatch_QueryKeyCaseSensitive() { - var builders = TestUtils.CreateBuildersWithResponseCaching(requestDelegate: async (context) => - { - context.Features.Get().VaryByQueryKeys = new[] { "QueryA", "QueryB" }; - await TestUtils.TestRequestDelegate(context); - }); + var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Features.Get().VaryByQueryKeys = new[] { "QueryA", "QueryB" }); foreach (var builder in builders) { @@ -434,11 +397,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfVaryQueryKeyStar_Mismatch_QueryKeyValueCaseSensitive() { - var builders = TestUtils.CreateBuildersWithResponseCaching(requestDelegate: async (context) => - { - context.Features.Get().VaryByQueryKeys = new[] { "*" }; - await TestUtils.TestRequestDelegate(context); - }); + var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Features.Get().VaryByQueryKeys = new[] { "*" }); foreach (var builder in builders) { @@ -501,11 +460,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfSetCookie_IsSpecified() { - var builders = TestUtils.CreateBuildersWithResponseCaching(requestDelegate: async (context) => - { - var headers = context.Response.Headers[HeaderNames.SetCookie] = "cookieName=cookieValue"; - await TestUtils.TestRequestDelegate(context); - }); + var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Response.Headers[HeaderNames.SetCookie] = "cookieName=cookieValue"); foreach (var builder in builders) { @@ -557,11 +512,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests await next.Invoke(); }); }, - requestDelegate: async (context) => - { - await context.Features.Get().SendFileAsync("dummy", 0, 0, CancellationToken.None); - await TestUtils.TestRequestDelegate(context); - }); + contextAction: async context => await context.Features.Get().SendFileAsync("dummy", 0, 0, CancellationToken.None)); foreach (var builder in builders) { @@ -623,14 +574,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfInitialResponseContainsNoStore() { - var builders = TestUtils.CreateBuildersWithResponseCaching(requestDelegate: async (context) => - { - var headers = context.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() - { - NoStore = true - }; - await TestUtils.TestRequestDelegate(context); - }); + var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Response.Headers[HeaderNames.CacheControl] = CacheControlHeaderValue.NoStoreString); foreach (var builder in builders) { @@ -687,11 +631,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void Serves304_IfIfNoneMatch_Satisfied() { - var builders = TestUtils.CreateBuildersWithResponseCaching(requestDelegate: async (context) => - { - var headers = context.Response.GetTypedHeaders().ETag = new EntityTagHeaderValue("\"E1\""); - await TestUtils.TestRequestDelegate(context); - }); + var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Response.GetTypedHeaders().ETag = new EntityTagHeaderValue("\"E1\"")); foreach (var builder in builders) { @@ -711,11 +651,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfIfNoneMatch_NotSatisfied() { - var builders = TestUtils.CreateBuildersWithResponseCaching(requestDelegate: async (context) => - { - var headers = context.Response.GetTypedHeaders().ETag = new EntityTagHeaderValue("\"E1\""); - await TestUtils.TestRequestDelegate(context); - }); + var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Response.GetTypedHeaders().ETag = new EntityTagHeaderValue("\"E1\"")); foreach (var builder in builders) { @@ -797,11 +733,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_WithoutReplacingCachedVaryBy_OnCacheMiss() { - var builders = TestUtils.CreateBuildersWithResponseCaching(requestDelegate: async (context) => - { - context.Response.Headers[HeaderNames.Vary] = HeaderNames.From; - await TestUtils.TestRequestDelegate(context); - }); + var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Response.Headers[HeaderNames.Vary] = HeaderNames.From); foreach (var builder in builders) { @@ -823,11 +755,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesFreshContent_IfCachedVaryByUpdated_OnCacheMiss() { - var builders = TestUtils.CreateBuildersWithResponseCaching(requestDelegate: async (context) => - { - context.Response.Headers[HeaderNames.Vary] = context.Request.Headers[HeaderNames.Pragma]; - await TestUtils.TestRequestDelegate(context); - }); + var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Response.Headers[HeaderNames.Vary] = context.Request.Headers[HeaderNames.Pragma]); foreach (var builder in builders) { @@ -858,11 +786,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests [Fact] public async void ServesCachedContent_IfCachedVaryByNotUpdated_OnCacheMiss() { - var builders = TestUtils.CreateBuildersWithResponseCaching(requestDelegate: async (context) => - { - context.Response.Headers[HeaderNames.Vary] = context.Request.Headers[HeaderNames.Pragma]; - await TestUtils.TestRequestDelegate(context); - }); + var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Response.Headers[HeaderNames.Vary] = context.Request.Headers[HeaderNames.Pragma]); foreach (var builder in builders) { diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs index 88d1511375..fa18b927fc 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Net.Http; +using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; @@ -32,7 +33,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests StreamUtilities.BodySegmentSize = 10; } - internal static RequestDelegate TestRequestDelegate = async context => + private static bool TestRequestDelegate(HttpContext context, string guid) { var headers = context.Response.GetTypedHeaders(); @@ -42,7 +43,6 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests headers.Expires = DateTimeOffset.Now.AddSeconds(int.Parse(expires)); } - var uniqueId = Guid.NewGuid().ToString(); if (headers.CacheControl == null) { headers.CacheControl = new CacheControlHeaderValue @@ -57,13 +57,33 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests headers.CacheControl.MaxAge = string.IsNullOrEmpty(expires) ? TimeSpan.FromSeconds(10) : (TimeSpan?)null; } headers.Date = DateTimeOffset.UtcNow; - headers.Headers["X-Value"] = uniqueId; + headers.Headers["X-Value"] = guid; if (context.Request.Method != "HEAD") + { + return true; + } + return false; + } + + internal static async Task TestRequestDelegateWriteAsync(HttpContext context) + { + var uniqueId = Guid.NewGuid().ToString(); + if (TestRequestDelegate(context, uniqueId)) { await context.Response.WriteAsync(uniqueId); } - }; + } + + internal static Task TestRequestDelegateWrite(HttpContext context) + { + var uniqueId = Guid.NewGuid().ToString(); + if (TestRequestDelegate(context, uniqueId)) + { + context.Response.Write(uniqueId); + } + return TaskCache.CompletedTask; + } internal static IResponseCachingKeyProvider CreateTestKeyProvider() { @@ -78,37 +98,64 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests internal static IEnumerable CreateBuildersWithResponseCaching( Action configureDelegate = null, ResponseCachingOptions options = null, - RequestDelegate requestDelegate = null) + Action contextAction = null) + { + return CreateBuildersWithResponseCaching(configureDelegate, options, new RequestDelegate[] + { + context => + { + contextAction?.Invoke(context); + return TestRequestDelegateWrite(context); + }, + context => + { + contextAction?.Invoke(context); + return TestRequestDelegateWriteAsync(context); + }, + }); + } + + private static IEnumerable CreateBuildersWithResponseCaching( + Action configureDelegate = null, + ResponseCachingOptions options = null, + IEnumerable requestDelegates = null) { if (configureDelegate == null) { configureDelegate = app => { }; } - if (requestDelegate == null) + if (requestDelegates == null) { - requestDelegate = TestRequestDelegate; + requestDelegates = new RequestDelegate[] + { + TestRequestDelegateWriteAsync, + TestRequestDelegateWrite + }; } - // Test with in memory ResponseCache - yield return new WebHostBuilder() - .ConfigureServices(services => - { - services.AddResponseCaching(responseCachingOptions => + foreach (var requestDelegate in requestDelegates) + { + // Test with in memory ResponseCache + yield return new WebHostBuilder() + .ConfigureServices(services => { - if (options != null) + services.AddResponseCaching(responseCachingOptions => { - responseCachingOptions.MaximumBodySize = options.MaximumBodySize; - responseCachingOptions.UseCaseSensitivePaths = options.UseCaseSensitivePaths; - responseCachingOptions.SystemClock = options.SystemClock; - } + if (options != null) + { + responseCachingOptions.MaximumBodySize = options.MaximumBodySize; + responseCachingOptions.UseCaseSensitivePaths = options.UseCaseSensitivePaths; + responseCachingOptions.SystemClock = options.SystemClock; + } + }); + }) + .Configure(app => + { + configureDelegate(app); + app.UseResponseCaching(); + app.Run(requestDelegate); }); - }) - .Configure(app => - { - configureDelegate(app); - app.UseResponseCaching(); - app.Run(requestDelegate); - }); + } } internal static ResponseCachingMiddleware CreateTestMiddleware( @@ -181,6 +228,25 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests } } + internal static class HttpResponseWritingExtensions + { + internal static void Write(this HttpResponse response, string text) + { + if (response == null) + { + throw new ArgumentNullException(nameof(response)); + } + + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + + byte[] data = Encoding.UTF8.GetBytes(text); + response.Body.Write(data, 0, data.Length); + } + } + internal class LoggedMessage { internal static LoggedMessage RequestMethodNotCacheable => new LoggedMessage(1, LogLevel.Debug); @@ -289,23 +355,33 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests public int GetCount { get; private set; } public int SetCount { get; private set; } - public Task GetAsync(string key) + public IResponseCacheEntry Get(string key) { GetCount++; try { - return Task.FromResult(_storage[key]); + return _storage[key]; } catch { - return Task.FromResult(null); + return null; } } + public Task GetAsync(string key) + { + return Task.FromResult(Get(key)); + } + + public void Set(string key, IResponseCacheEntry entry, TimeSpan validFor) + { + SetCount++; + _storage[key] = entry; + } + public Task SetAsync(string key, IResponseCacheEntry entry, TimeSpan validFor) { - SetCount++; - _storage[key] = entry; + Set(key, entry, validFor); return TaskCache.CompletedTask; } } From 79f723c5e103d1aa56c1dd8dbd21d82108ecc958 Mon Sep 17 00:00:00 2001 From: Doug Bunting Date: Tue, 14 Feb 2017 13:23:01 -0800 Subject: [PATCH 079/188] Bump test projects up to .NET 4.5.2 - aspnet/Testing#248 - xUnit no longer supports .NET 4.5.1 - build tests for desktop .NET only on Windows --- .../Microsoft.AspNetCore.ResponseCaching.Tests.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj b/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj index 666a99690e..f0e5b4003a 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj @@ -3,7 +3,8 @@ - netcoreapp1.1;net451 + netcoreapp1.1;net452 + netcoreapp1.1 @@ -15,5 +16,4 @@ - From b2c30da48d615d6f6852bde970ebade3b128bf48 Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Tue, 14 Feb 2017 16:03:54 -0800 Subject: [PATCH 080/188] Downgrade to stable packages --- build/common.props | 3 +-- build/dependencies.props | 6 ++++++ 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 build/dependencies.props diff --git a/build/common.props b/build/common.props index 8c19f09a32..d3bc1fab1b 100644 --- a/build/common.props +++ b/build/common.props @@ -1,4 +1,5 @@ + @@ -8,8 +9,6 @@ $(MSBuildThisFileDirectory)Key.snk true true - 1.2.0-* - 1.6.2-* $(VersionSuffix)-$(BuildNumber) diff --git a/build/dependencies.props b/build/dependencies.props new file mode 100644 index 0000000000..e704edaec0 --- /dev/null +++ b/build/dependencies.props @@ -0,0 +1,6 @@ + + + 1.6.1 + 4.3.0 + + From 551fe5e6f1134ad10b851f442ff193c56f4af835 Mon Sep 17 00:00:00 2001 From: John Luo Date: Tue, 28 Feb 2017 14:42:00 -0800 Subject: [PATCH 081/188] React to HeaderUtilities renames --- .../ResponseCachingMiddleware.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs index cdb6cb817f..3184bcd01d 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs @@ -153,7 +153,7 @@ namespace Microsoft.AspNetCore.ResponseCaching // Note: int64 division truncates result and errors may be up to 1 second. This reduction in // accuracy of age calculation is considered appropriate since it is small compared to clock // skews and the "Age" header is an estimate of the real age of cached content. - response.Headers[HeaderNames.Age] = HeaderUtilities.FormatInt64(context.CachedEntryAge.Value.Ticks / TimeSpan.TicksPerSecond); + response.Headers[HeaderNames.Age] = HeaderUtilities.FormatNonNegativeInt64(context.CachedEntryAge.Value.Ticks / TimeSpan.TicksPerSecond); // Copy the cached response body var body = context.CachedResponse.Body; @@ -329,7 +329,7 @@ namespace Microsoft.AspNetCore.ResponseCaching // Add a content-length if required if (!response.ContentLength.HasValue && StringValues.IsNullOrEmpty(response.Headers[HeaderNames.TransferEncoding])) { - context.CachedResponse.Headers[HeaderNames.ContentLength] = HeaderUtilities.FormatInt64(bufferStream.Length); + context.CachedResponse.Headers[HeaderNames.ContentLength] = HeaderUtilities.FormatNonNegativeInt64(bufferStream.Length); } context.CachedResponse.Body = bufferStream; From f93e14efa0ccfc19e6aedfeb348275c84f4e0a69 Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Wed, 1 Mar 2017 18:14:13 -0800 Subject: [PATCH 082/188] Change korebuild branch and fix argument forwarding in bootstrapper --- build.ps1 | 16 ++++++++-------- build.sh | 22 +++++++++++----------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/build.ps1 b/build.ps1 index 0605b59c01..5bf0e2c113 100644 --- a/build.ps1 +++ b/build.ps1 @@ -1,6 +1,6 @@ $ErrorActionPreference = "Stop" -function DownloadWithRetry([string] $url, [string] $downloadLocation, [int] $retries) +function DownloadWithRetry([string] $url, [string] $downloadLocation, [int] $retries) { while($true) { @@ -19,7 +19,7 @@ function DownloadWithRetry([string] $url, [string] $downloadLocation, [int] $ret Start-Sleep -Seconds 10 } - else + else { $exception = $_.Exception throw $exception @@ -33,7 +33,7 @@ cd $PSScriptRoot $repoFolder = $PSScriptRoot $env:REPO_FOLDER = $repoFolder -$koreBuildZip="https://github.com/aspnet/KoreBuild/archive/feature/msbuild.zip" +$koreBuildZip="https://github.com/aspnet/KoreBuild/archive/dev.zip" if ($env:KOREBUILD_ZIP) { $koreBuildZip=$env:KOREBUILD_ZIP @@ -43,18 +43,18 @@ $buildFolder = ".build" $buildFile="$buildFolder\KoreBuild.ps1" if (!(Test-Path $buildFolder)) { - Write-Host "Downloading KoreBuild from $koreBuildZip" - + 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 @@ -64,4 +64,4 @@ if (!(Test-Path $buildFolder)) { } } -&"$buildFile" $args \ No newline at end of file +&"$buildFile" @args diff --git a/build.sh b/build.sh index 07997d6c83..b0bcadb579 100755 --- a/build.sh +++ b/build.sh @@ -2,7 +2,7 @@ repoFolder="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" cd $repoFolder -koreBuildZip="https://github.com/aspnet/KoreBuild/archive/feature/msbuild.zip" +koreBuildZip="https://github.com/aspnet/KoreBuild/archive/dev.zip" if [ ! -z $KOREBUILD_ZIP ]; then koreBuildZip=$KOREBUILD_ZIP fi @@ -12,12 +12,12 @@ buildFile="$buildFolder/KoreBuild.sh" if test ! -d $buildFolder; then echo "Downloading KoreBuild from $koreBuildZip" - - tempFolder="/tmp/KoreBuild-$(uuidgen)" + + 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 @@ -29,18 +29,18 @@ if test ! -d $buildFolder; then 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 + rm -rf $tempFolder fi fi -$buildFile -r $repoFolder "$@" \ No newline at end of file +$buildFile -r $repoFolder "$@" From 5515494067d900f1023818c8acf40c2dcd7b4760 Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Wed, 1 Mar 2017 18:25:45 -0800 Subject: [PATCH 083/188] Update AppVeyor and Travis settings --- .travis.yml | 2 +- appveyor.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index a0be886892..af659e9ae9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,4 +29,4 @@ branches: before_install: - if test "$TRAVIS_OS_NAME" == "osx"; then brew update; brew install openssl; ln -s /usr/local/opt/openssl/lib/libcrypto.1.0.0.dylib /usr/local/lib/; ln -s /usr/local/opt/openssl/lib/libssl.1.0.0.dylib /usr/local/lib/; fi script: - - ./build.sh --quiet verify + - ./build.sh diff --git a/appveyor.yml b/appveyor.yml index df67923781..3f828ce38e 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -7,7 +7,7 @@ branches: - dev - /^(.*\/)?ci-.*$/ build_script: - - build.cmd verify + - ps: .\build.ps1 clone_depth: 1 test: off deploy: off From 7ca0f84e0ab92839e7bc6bca07430e7df01d6725 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Thu, 9 Mar 2017 11:18:09 -0800 Subject: [PATCH 084/188] Update .travis.yml (#114) --- .travis.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index af659e9ae9..b8f60ce2e5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,8 +14,7 @@ env: global: - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true - DOTNET_CLI_TELEMETRY_OPTOUT: 1 -mono: - - 4.0.5 +mono: none os: - linux - osx From a48c0cacca5117b53e305d3ab79fc7e362630ed6 Mon Sep 17 00:00:00 2001 From: Ajay Bhargav Baaskaran Date: Tue, 14 Mar 2017 12:18:12 -0700 Subject: [PATCH 085/188] Using NullLogger types from Logging.Abstractions (#115) --- test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs index fa18b927fc..d5889b3554 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs @@ -15,6 +15,7 @@ using Microsoft.AspNetCore.ResponseCaching.Internal; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Internal; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.Options; From 5e066e2500859e2e9ed3849e3510e6d54e7f39d9 Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Tue, 14 Mar 2017 13:41:20 -0700 Subject: [PATCH 086/188] Update appveyor and travis settings --- .travis.yml | 1 - appveyor.yml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index b8f60ce2e5..e4c69a2a09 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,7 +18,6 @@ mono: none os: - linux - osx -osx_image: xcode7.3 branches: only: - master diff --git a/appveyor.yml b/appveyor.yml index 3f828ce38e..1041615c68 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -11,4 +11,4 @@ build_script: clone_depth: 1 test: off deploy: off -os: Visual Studio 2017 RC +os: Visual Studio 2017 From 6d59c8b589c47543ec45bf5a0a857a32a33e9637 Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Wed, 15 Mar 2017 16:50:26 -0700 Subject: [PATCH 087/188] Unify dependency versions to one file and remove workarounds --- build/dependencies.props | 5 ++++- .../ResponseCachingSample.csproj | 14 ++++++++------ samples/ResponseCachingSample/web.config | 9 --------- ....AspNetCore.ResponseCaching.Abstractions.csproj | 2 +- .../Microsoft.AspNetCore.ResponseCaching.csproj | 13 ++++++++----- ...crosoft.AspNetCore.ResponseCaching.Tests.csproj | 13 ++++++++----- 6 files changed, 29 insertions(+), 27 deletions(-) delete mode 100644 samples/ResponseCachingSample/web.config diff --git a/build/dependencies.props b/build/dependencies.props index e704edaec0..5a4c06ce33 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -1,6 +1,9 @@ - 1.6.1 + 1.2.0-* 4.3.0 + 1.6.1 + 15.0.0 + 2.2.0 diff --git a/samples/ResponseCachingSample/ResponseCachingSample.csproj b/samples/ResponseCachingSample/ResponseCachingSample.csproj index a86b0e11b3..2e700ad7b5 100644 --- a/samples/ResponseCachingSample/ResponseCachingSample.csproj +++ b/samples/ResponseCachingSample/ResponseCachingSample.csproj @@ -1,17 +1,19 @@ + + net451;netcoreapp1.1 - - win7-x64 - Exe - - - + + + + + + diff --git a/samples/ResponseCachingSample/web.config b/samples/ResponseCachingSample/web.config deleted file mode 100644 index f7ac679334..0000000000 --- a/samples/ResponseCachingSample/web.config +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj b/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj index 2cdfa4d58a..36a4ab76fe 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj +++ b/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj @@ -10,7 +10,7 @@ - + diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj b/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj index b56169a56a..95c7184935 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj +++ b/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj @@ -13,11 +13,14 @@ - - - - - + + + + + + + + diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj b/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj index f0e5b4003a..c423cbe639 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj @@ -9,11 +9,14 @@ - - - - - + + + + + + + + From a1a979d42eba027ee8d833368c4698ab1d644ac4 Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Tue, 21 Mar 2017 12:17:39 -0700 Subject: [PATCH 088/188] Update Travis to macOS Sierra [skip appveyor] --- .travis.yml | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index e4c69a2a09..2a46104677 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,15 +1,6 @@ language: csharp -sudo: required +sudo: false dist: trusty -addons: - apt: - packages: - - gettext - - libcurl4-openssl-dev - - libicu-dev - - libssl-dev - - libunwind8 - - zlib1g env: global: - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true @@ -18,6 +9,7 @@ mono: none os: - linux - osx +osx_image: xcode8.2 branches: only: - master From 25ec1f9bd7af5d4bd0b7c12defacd6e2656e470a Mon Sep 17 00:00:00 2001 From: Pranav K Date: Wed, 22 Mar 2017 18:23:00 -0700 Subject: [PATCH 089/188] Remove net451 as a cross-compile target --- .gitignore | 3 ++- samples/ResponseCachingSample/ResponseCachingSample.csproj | 2 +- .../Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj | 2 +- .../Microsoft.AspNetCore.ResponseCaching.csproj | 2 +- .../Microsoft.AspNetCore.ResponseCaching.Tests.csproj | 4 +++- 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index e71f35b3a6..563425dff6 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,5 @@ project.lock.json /.vs/ .build/ .testPublish/ -launchSettings.json \ No newline at end of file +launchSettings.json +global.json diff --git a/samples/ResponseCachingSample/ResponseCachingSample.csproj b/samples/ResponseCachingSample/ResponseCachingSample.csproj index 2e700ad7b5..a748acf16e 100644 --- a/samples/ResponseCachingSample/ResponseCachingSample.csproj +++ b/samples/ResponseCachingSample/ResponseCachingSample.csproj @@ -3,7 +3,7 @@ - net451;netcoreapp1.1 + net46;netcoreapp1.1 diff --git a/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj b/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj index 36a4ab76fe..3bc517f42c 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj +++ b/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj @@ -4,7 +4,7 @@ ASP.NET Core response caching middleware abstractions and feature interface definitions. - net451;netstandard1.3 + netstandard1.3 true aspnetcore;cache;caching diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj b/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj index 95c7184935..276b379bd3 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj +++ b/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj @@ -4,7 +4,7 @@ ASP.NET Core middleware for caching HTTP responses on the server. - net451;netstandard1.3 + netstandard1.3 $(NoWarn);CS1591 true true diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj b/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj index c423cbe639..1182564507 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj @@ -3,8 +3,10 @@ - netcoreapp1.1;net452 + netcoreapp1.1;net46 netcoreapp1.1 + true + true From 33a3be4a16e8ae4968a2c949f583c26e33c98977 Mon Sep 17 00:00:00 2001 From: Kiran Challa Date: Thu, 23 Mar 2017 16:08:11 -0700 Subject: [PATCH 090/188] Converted sample and test project to run on netcoreapp2.0 --- build/dependencies.props | 1 + samples/ResponseCachingSample/ResponseCachingSample.csproj | 2 +- .../Microsoft.AspNetCore.ResponseCaching.Tests.csproj | 4 ++-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index 5a4c06ce33..12a50aa67f 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,6 +3,7 @@ 1.2.0-* 4.3.0 1.6.1 + 2.0.0-* 15.0.0 2.2.0 diff --git a/samples/ResponseCachingSample/ResponseCachingSample.csproj b/samples/ResponseCachingSample/ResponseCachingSample.csproj index a748acf16e..1b3f3abc14 100644 --- a/samples/ResponseCachingSample/ResponseCachingSample.csproj +++ b/samples/ResponseCachingSample/ResponseCachingSample.csproj @@ -3,7 +3,7 @@ - net46;netcoreapp1.1 + net46;netcoreapp2.0 diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj b/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj index 1182564507..8480593651 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj @@ -3,8 +3,8 @@ - netcoreapp1.1;net46 - netcoreapp1.1 + netcoreapp2.0;net46 + netcoreapp2.0 true true From eb25f5d6ff2d4146be00614600073e3d039ce6ce Mon Sep 17 00:00:00 2001 From: Pranav K Date: Wed, 29 Mar 2017 11:30:36 -0700 Subject: [PATCH 091/188] Updating to 2.0.0 Internal.AspNetCore.Sdk --- build/common.props | 2 +- build/dependencies.props | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/build/common.props b/build/common.props index d3bc1fab1b..682f61089d 100644 --- a/build/common.props +++ b/build/common.props @@ -13,7 +13,7 @@ - + diff --git a/build/dependencies.props b/build/dependencies.props index 12a50aa67f..8c81df7f34 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -2,9 +2,10 @@ 1.2.0-* 4.3.0 + 2.0.0-* 1.6.1 2.0.0-* 15.0.0 2.2.0 - + \ No newline at end of file From 83dc7dd030b378a3ea39d6c70b88f2f5c3244ec8 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Mon, 3 Apr 2017 21:41:12 -0700 Subject: [PATCH 092/188] Updating versions to 2.0.0-preview1 --- build/dependencies.props | 2 +- version.props | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index 8c81df7f34..1973fc186f 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -1,6 +1,6 @@ - 1.2.0-* + 2.0.0-* 4.3.0 2.0.0-* 1.6.1 diff --git a/version.props b/version.props index 17fd5ac36d..c7150e64f4 100644 --- a/version.props +++ b/version.props @@ -1,7 +1,7 @@ - 1.2.0 + 2.0.0 preview1 From 04fe7fd31b7a51ce17cf92e72d0d64303f0ef299 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Tue, 25 Apr 2017 11:04:10 -0700 Subject: [PATCH 093/188] Use Bundled NETStandard.Library \ NETCoreApp versions instead of explicitly specifying one --- build/common.props | 2 +- build/dependencies.props | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/build/common.props b/build/common.props index 682f61089d..18f3550fd8 100644 --- a/build/common.props +++ b/build/common.props @@ -17,7 +17,7 @@ - + diff --git a/build/dependencies.props b/build/dependencies.props index 1973fc186f..01387147a4 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,9 +3,7 @@ 2.0.0-* 4.3.0 2.0.0-* - 1.6.1 - 2.0.0-* 15.0.0 2.2.0 - \ No newline at end of file + From 66b5458ce94e0483f5d49c7bdf84d909da5f8e10 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Tue, 25 Apr 2017 22:04:59 -0700 Subject: [PATCH 094/188] Branching for 2.0.0-preview1 --- NuGet.config | 2 +- build.ps1 | 2 +- build.sh | 2 +- build/dependencies.props | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/NuGet.config b/NuGet.config index 8e65695611..fa4304af9c 100644 --- a/NuGet.config +++ b/NuGet.config @@ -1,7 +1,7 @@ - + diff --git a/build.ps1 b/build.ps1 index 5bf0e2c113..225b1fe450 100644 --- a/build.ps1 +++ b/build.ps1 @@ -33,7 +33,7 @@ cd $PSScriptRoot $repoFolder = $PSScriptRoot $env:REPO_FOLDER = $repoFolder -$koreBuildZip="https://github.com/aspnet/KoreBuild/archive/dev.zip" +$koreBuildZip="https://github.com/aspnet/KoreBuild/archive/rel/2.0.0-preview1.zip" if ($env:KOREBUILD_ZIP) { $koreBuildZip=$env:KOREBUILD_ZIP diff --git a/build.sh b/build.sh index b0bcadb579..702b25c636 100755 --- a/build.sh +++ b/build.sh @@ -2,7 +2,7 @@ repoFolder="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" cd $repoFolder -koreBuildZip="https://github.com/aspnet/KoreBuild/archive/dev.zip" +koreBuildZip="https://github.com/aspnet/KoreBuild/archive/rel/2.0.0-preview1.zip" if [ ! -z $KOREBUILD_ZIP ]; then koreBuildZip=$KOREBUILD_ZIP fi diff --git a/build/dependencies.props b/build/dependencies.props index 01387147a4..c7741b724b 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -1,6 +1,6 @@ - 2.0.0-* + 2.0.0-preview1-* 4.3.0 2.0.0-* 15.0.0 From 3f9e0a09de142cb19396e419bec7c3428e380119 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Wed, 26 Apr 2017 07:13:31 -0700 Subject: [PATCH 095/188] Updating package version to preview2 --- version.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.props b/version.props index c7150e64f4..6af4f81de2 100644 --- a/version.props +++ b/version.props @@ -2,6 +2,6 @@ 2.0.0 - preview1 + preview2 From 0e83bce219f1476fd06e3d977ccd9954321bcde1 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Mon, 1 May 2017 12:40:05 -0700 Subject: [PATCH 096/188] Use the bundled NETStandard.Library package in netstandard targeting libraries --- build/dependencies.props | 1 + 1 file changed, 1 insertion(+) diff --git a/build/dependencies.props b/build/dependencies.props index c7741b724b..3db1eba0a2 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,6 +3,7 @@ 2.0.0-preview1-* 4.3.0 2.0.0-* + $(BundledNETStandardPackageVersion) 15.0.0 2.2.0 From 1be9dc10ab6e08f2c387e719322f6f8167ca9a55 Mon Sep 17 00:00:00 2001 From: John Luo Date: Fri, 5 May 2017 10:11:29 -0700 Subject: [PATCH 097/188] netcoreapp2.0 (#121) --- .gitignore | 1 + .../ResponseCachingSample/ResponseCachingSample.csproj | 2 +- ...rosoft.AspNetCore.ResponseCaching.Abstractions.csproj | 2 +- .../Microsoft.AspNetCore.ResponseCaching.csproj | 2 +- .../Streams/ResponseCachingStream.cs | 9 +-------- .../Streams/SegmentReadStream.cs | 8 -------- .../Streams/SegmentWriteStream.cs | 8 -------- .../Microsoft.AspNetCore.ResponseCaching.Tests.csproj | 3 +-- 8 files changed, 6 insertions(+), 29 deletions(-) diff --git a/.gitignore b/.gitignore index 563425dff6..23826aae91 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,7 @@ nuget.exe *.sln.ide project.lock.json /.vs/ +.vscode/ .build/ .testPublish/ launchSettings.json diff --git a/samples/ResponseCachingSample/ResponseCachingSample.csproj b/samples/ResponseCachingSample/ResponseCachingSample.csproj index 1b3f3abc14..c4f6bbf77e 100644 --- a/samples/ResponseCachingSample/ResponseCachingSample.csproj +++ b/samples/ResponseCachingSample/ResponseCachingSample.csproj @@ -3,7 +3,7 @@ - net46;netcoreapp2.0 + netcoreapp2.0 diff --git a/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj b/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj index 3bc517f42c..9cfefda6a4 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj +++ b/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj @@ -4,7 +4,7 @@ ASP.NET Core response caching middleware abstractions and feature interface definitions. - netstandard1.3 + netcoreapp2.0 true aspnetcore;cache;caching diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj b/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj index 276b379bd3..3b5cca5e23 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj +++ b/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj @@ -4,7 +4,7 @@ ASP.NET Core middleware for caching HTTP responses on the server. - netstandard1.3 + netcoreapp2.0 $(NoWarn);CS1591 true true diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Streams/ResponseCachingStream.cs b/src/Microsoft.AspNetCore.ResponseCaching/Streams/ResponseCachingStream.cs index aa5fc371eb..c9d476c97d 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Streams/ResponseCachingStream.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Streams/ResponseCachingStream.cs @@ -183,19 +183,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal } } -#if NETSTANDARD1_3 - public IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) -#else public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) -#endif { return StreamUtilities.ToIAsyncResult(WriteAsync(buffer, offset, count), callback, state); } -#if NETSTANDARD1_3 - public void EndWrite(IAsyncResult asyncResult) -#else + public override void EndWrite(IAsyncResult asyncResult) -#endif { if (asyncResult == null) { diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Streams/SegmentReadStream.cs b/src/Microsoft.AspNetCore.ResponseCaching/Streams/SegmentReadStream.cs index 51ff15ff41..83c60dd0c5 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Streams/SegmentReadStream.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Streams/SegmentReadStream.cs @@ -140,11 +140,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal return byteRead; } -#if NETSTANDARD1_3 - public IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) -#else public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) -#endif { var tcs = new TaskCompletionSource(state); @@ -176,11 +172,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal return tcs.Task; } -#if NETSTANDARD1_3 - public int EndRead(IAsyncResult asyncResult) -#else public override int EndRead(IAsyncResult asyncResult) -#endif { if (asyncResult == null) { diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Streams/SegmentWriteStream.cs b/src/Microsoft.AspNetCore.ResponseCaching/Streams/SegmentWriteStream.cs index 6fb93c5c7b..6efeaf589f 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Streams/SegmentWriteStream.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Streams/SegmentWriteStream.cs @@ -190,20 +190,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal _length++; } -#if NETSTANDARD1_3 - public IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) -#else public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) -#endif { return StreamUtilities.ToIAsyncResult(WriteAsync(buffer, offset, count), callback, state); } -#if NETSTANDARD1_3 - public void EndWrite(IAsyncResult asyncResult) -#else public override void EndWrite(IAsyncResult asyncResult) -#endif { if (asyncResult == null) { diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj b/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj index 8480593651..d9ea33474f 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj @@ -3,8 +3,7 @@ - netcoreapp2.0;net46 - netcoreapp2.0 + netcoreapp2.0 true true From 2b4cb21a039634ce56b0dab349aeab898affcf57 Mon Sep 17 00:00:00 2001 From: Pavel Krymets Date: Fri, 5 May 2017 10:31:08 -0700 Subject: [PATCH 098/188] Update InternalAspNetCoreSdkVersion --- build/dependencies.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/dependencies.props b/build/dependencies.props index 3db1eba0a2..f66ecf394c 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -2,7 +2,7 @@ 2.0.0-preview1-* 4.3.0 - 2.0.0-* + 2.1.0-* $(BundledNETStandardPackageVersion) 15.0.0 2.2.0 From 16984a6fe016c6a67197b67e6375c4c20c99bea0 Mon Sep 17 00:00:00 2001 From: Pavel Krymets Date: Wed, 10 May 2017 11:43:43 -0700 Subject: [PATCH 099/188] Remove unnecessary package references (#122) --- build/dependencies.props | 1 - 1 file changed, 1 deletion(-) diff --git a/build/dependencies.props b/build/dependencies.props index 09cfb1fe04..a894b64255 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -1,7 +1,6 @@ 2.0.0-* - 4.3.0 2.1.0-* $(BundledNETStandardPackageVersion) 15.0.0 From 72bb0750f41e89a854a46433b964df37d57c47c0 Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Fri, 12 May 2017 15:28:38 -0700 Subject: [PATCH 100/188] Upgrade test framework versions --- build/dependencies.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index a894b64255..2a28acf6a6 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,7 +3,7 @@ 2.0.0-* 2.1.0-* $(BundledNETStandardPackageVersion) - 15.0.0 - 2.2.0 + 15.3.0-* + 2.3.0-beta2-* From 83ddb276f62f8baa5db065d89b7ec4b08ac9f3c8 Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Mon, 15 May 2017 12:36:54 -0700 Subject: [PATCH 101/188] Remove workaround from test project --- .../Microsoft.AspNetCore.ResponseCaching.Tests.csproj | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj b/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj index d9ea33474f..c45a8baa78 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj @@ -4,8 +4,6 @@ netcoreapp2.0 - true - true From a05ad2217351088bf3f679ee8e0eb462eff183a8 Mon Sep 17 00:00:00 2001 From: John Luo Date: Mon, 22 May 2017 15:29:37 -0700 Subject: [PATCH 102/188] Target NETStandard2.0 --- build/common.props | 4 ++-- build/dependencies.props | 2 +- samples/ResponseCachingSample/ResponseCachingSample.csproj | 3 +++ .../Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj | 2 +- .../Microsoft.AspNetCore.ResponseCaching.csproj | 2 +- .../Microsoft.AspNetCore.ResponseCaching.Tests.csproj | 3 ++- 6 files changed, 10 insertions(+), 6 deletions(-) diff --git a/build/common.props b/build/common.props index 18f3550fd8..eb8fe230a6 100644 --- a/build/common.props +++ b/build/common.props @@ -16,8 +16,8 @@ - - + + diff --git a/build/dependencies.props b/build/dependencies.props index 2a28acf6a6..858f8e55f4 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -2,7 +2,7 @@ 2.0.0-* 2.1.0-* - $(BundledNETStandardPackageVersion) + 2.0.0-* 15.3.0-* 2.3.0-beta2-* diff --git a/samples/ResponseCachingSample/ResponseCachingSample.csproj b/samples/ResponseCachingSample/ResponseCachingSample.csproj index c4f6bbf77e..1661b293f4 100644 --- a/samples/ResponseCachingSample/ResponseCachingSample.csproj +++ b/samples/ResponseCachingSample/ResponseCachingSample.csproj @@ -16,4 +16,7 @@ + + + diff --git a/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj b/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj index 9cfefda6a4..4a8e021e0f 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj +++ b/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj @@ -4,7 +4,7 @@ ASP.NET Core response caching middleware abstractions and feature interface definitions. - netcoreapp2.0 + netstandard2.0 true aspnetcore;cache;caching diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj b/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj index 3b5cca5e23..f9bbe57da5 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj +++ b/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj @@ -4,7 +4,7 @@ ASP.NET Core middleware for caching HTTP responses on the server. - netcoreapp2.0 + netstandard2.0 $(NoWarn);CS1591 true true diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj b/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj index c45a8baa78..c18e392556 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj @@ -3,7 +3,8 @@ - netcoreapp2.0 + netcoreapp2.0;net461 + netcoreapp2.0 From 421ac4bc970b819be8e6f196070bd553d7ab51da Mon Sep 17 00:00:00 2001 From: Chris R Date: Fri, 19 May 2017 11:35:53 -0700 Subject: [PATCH 103/188] React to StringSegment changes --- .../ResponseCachingMiddleware.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs index 3184bcd01d..1e8890cfe3 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs @@ -435,7 +435,7 @@ namespace Microsoft.AspNetCore.ResponseCaching if (!StringValues.IsNullOrEmpty(ifNoneMatchHeader)) { - if (ifNoneMatchHeader.Count == 1 && string.Equals(ifNoneMatchHeader[0], EntityTagHeaderValue.Any.Tag, StringComparison.OrdinalIgnoreCase)) + if (ifNoneMatchHeader.Count == 1 && StringSegment.Equals(ifNoneMatchHeader[0], EntityTagHeaderValue.Any.Tag, StringComparison.OrdinalIgnoreCase)) { context.Logger.LogNotModifiedIfNoneMatchStar(); return true; @@ -444,7 +444,7 @@ namespace Microsoft.AspNetCore.ResponseCaching EntityTagHeaderValue eTag; IList ifNoneMatchEtags; if (!StringValues.IsNullOrEmpty(cachedResponseHeaders[HeaderNames.ETag]) - && EntityTagHeaderValue.TryParse(cachedResponseHeaders[HeaderNames.ETag], out eTag) + && EntityTagHeaderValue.TryParse(cachedResponseHeaders[HeaderNames.ETag].ToString(), out eTag) && EntityTagHeaderValue.TryParseList(ifNoneMatchHeader, out ifNoneMatchEtags)) { for (var i = 0; i < ifNoneMatchEtags.Count; i++) From 1bc5ade59fadd35d4bbeb576e6fff6f0a45ce9e8 Mon Sep 17 00:00:00 2001 From: Chris R Date: Tue, 23 May 2017 15:31:55 -0700 Subject: [PATCH 104/188] React to StringSegment changes --- .../Internal/ResponseCachingContext.cs | 4 ++-- .../Internal/ResponseCachingPolicyProvider.cs | 2 +- .../ResponseCachingMiddleware.cs | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingContext.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingContext.cs index f9f8e8657e..2d8c79b11b 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingContext.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingContext.cs @@ -65,7 +65,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal { _parsedResponseDate = true; DateTimeOffset date; - if (HeaderUtilities.TryParseDate(HttpContext.Response.Headers[HeaderNames.Date], out date)) + if (HeaderUtilities.TryParseDate(HttpContext.Response.Headers[HeaderNames.Date].ToString(), out date)) { _responseDate = date; } @@ -92,7 +92,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal { _parsedResponseExpires = true; DateTimeOffset expires; - if (HeaderUtilities.TryParseDate(HttpContext.Response.Headers[HeaderNames.Expires], out expires)) + if (HeaderUtilities.TryParseDate(HttpContext.Response.Headers[HeaderNames.Expires].ToString(), out expires)) { _responseExpires = expires; } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingPolicyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingPolicyProvider.cs index 2108ff3a2b..8ffc59612e 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingPolicyProvider.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingPolicyProvider.cs @@ -233,7 +233,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal { // Validate expiration DateTimeOffset expires; - if (HeaderUtilities.TryParseDate(context.CachedResponseHeaders[HeaderNames.Expires], out expires) && + if (HeaderUtilities.TryParseDate(context.CachedResponseHeaders[HeaderNames.Expires].ToString(), out expires) && context.ResponseTime.Value >= expires) { context.Logger.LogExpirationExpiresExceeded(context.ResponseTime.Value, expires); diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs index 1e8890cfe3..24fa3c1c40 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs @@ -464,14 +464,14 @@ namespace Microsoft.AspNetCore.ResponseCaching if (!StringValues.IsNullOrEmpty(ifModifiedSince)) { DateTimeOffset modified; - if (!HeaderUtilities.TryParseDate(cachedResponseHeaders[HeaderNames.LastModified], out modified) && - !HeaderUtilities.TryParseDate(cachedResponseHeaders[HeaderNames.Date], out modified)) + if (!HeaderUtilities.TryParseDate(cachedResponseHeaders[HeaderNames.LastModified].ToString(), out modified) && + !HeaderUtilities.TryParseDate(cachedResponseHeaders[HeaderNames.Date].ToString(), out modified)) { return false; } DateTimeOffset modifiedSince; - if (HeaderUtilities.TryParseDate(ifModifiedSince, out modifiedSince) && + if (HeaderUtilities.TryParseDate(ifModifiedSince.ToString(), out modifiedSince) && modified <= modifiedSince) { context.Logger.LogNotModifiedIfModifiedSinceSatisfied(modified, modifiedSince); From 6a17e4899c43f2bdfcbce344cf013cbe5028f667 Mon Sep 17 00:00:00 2001 From: Kiran Challa Date: Fri, 26 May 2017 12:44:02 -0700 Subject: [PATCH 105/188] Updated to use the latest shared runtime --- build/dependencies.props | 1 + 1 file changed, 1 insertion(+) diff --git a/build/dependencies.props b/build/dependencies.props index 858f8e55f4..4b6d757d0e 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,6 +3,7 @@ 2.0.0-* 2.1.0-* 2.0.0-* + 2.0.0-* 15.3.0-* 2.3.0-beta2-* From 7d378b1fe16e735ba7081a3d0a9b6923a437f1c5 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Wed, 31 May 2017 19:37:14 -0700 Subject: [PATCH 106/188] Branching for rel/2.0.0-preview2 --- NuGet.config | 7 ++++--- build/dependencies.props | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/NuGet.config b/NuGet.config index 8e65695611..c4bc056c4d 100644 --- a/NuGet.config +++ b/NuGet.config @@ -1,8 +1,9 @@ - + - + + - + \ No newline at end of file diff --git a/build/dependencies.props b/build/dependencies.props index 4b6d757d0e..f81fa63cb0 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -1,6 +1,6 @@ - 2.0.0-* + 2.0.0-preview2-* 2.1.0-* 2.0.0-* 2.0.0-* From d4b8464d0e956329ad7ff6b3d4324f08f6a05573 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Wed, 31 May 2017 19:53:31 -0700 Subject: [PATCH 107/188] Updating build scripts to point to 2.0.0-preview2 KoreBuild --- build.ps1 | 2 +- build.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.ps1 b/build.ps1 index 5bf0e2c113..3a2476b2b4 100644 --- a/build.ps1 +++ b/build.ps1 @@ -33,7 +33,7 @@ cd $PSScriptRoot $repoFolder = $PSScriptRoot $env:REPO_FOLDER = $repoFolder -$koreBuildZip="https://github.com/aspnet/KoreBuild/archive/dev.zip" +$koreBuildZip="https://github.com/aspnet/KoreBuild/archive/rel/2.0.0-preview2.zip" if ($env:KOREBUILD_ZIP) { $koreBuildZip=$env:KOREBUILD_ZIP diff --git a/build.sh b/build.sh index b0bcadb579..a40bdb87b1 100755 --- a/build.sh +++ b/build.sh @@ -2,7 +2,7 @@ repoFolder="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" cd $repoFolder -koreBuildZip="https://github.com/aspnet/KoreBuild/archive/dev.zip" +koreBuildZip="https://github.com/aspnet/KoreBuild/archive/rel/2.0.0-preview2.zip" if [ ! -z $KOREBUILD_ZIP ]; then koreBuildZip=$KOREBUILD_ZIP fi From f5227bd719929d1cd4366daf3e830e3c97ce3e00 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Thu, 1 Jun 2017 10:47:40 -0700 Subject: [PATCH 108/188] Updating versions to preview3 --- NuGet.config | 3 ++- version.props | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/NuGet.config b/NuGet.config index 8e65695611..4e8a1f6de1 100644 --- a/NuGet.config +++ b/NuGet.config @@ -1,6 +1,7 @@ - + + diff --git a/version.props b/version.props index 6af4f81de2..193a5999d8 100644 --- a/version.props +++ b/version.props @@ -2,6 +2,6 @@ 2.0.0 - preview2 + preview3 From 3909a5ce8874dd1bf0d251638c1da1d6e40b61c4 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Thu, 8 Jun 2017 08:52:32 -0700 Subject: [PATCH 109/188] Remove usage of TaskCache --- .../Internal/MemoryResponseCache.cs | 3 +-- .../Microsoft.AspNetCore.ResponseCaching.csproj | 1 - .../ResponseCachingMiddleware.cs | 5 ++--- .../Streams/SegmentWriteStream.cs | 3 +-- .../ResponseCachingMiddlewareTests.cs | 3 +-- .../TestUtils.cs | 9 ++++----- 6 files changed, 9 insertions(+), 15 deletions(-) diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs index 42c613bc2c..c96ba5e751 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs @@ -4,7 +4,6 @@ using System; using System.Threading.Tasks; using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Internal; namespace Microsoft.AspNetCore.ResponseCaching.Internal { @@ -86,7 +85,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal public Task SetAsync(string key, IResponseCacheEntry entry, TimeSpan validFor) { Set(key, entry, validFor); - return TaskCache.CompletedTask; + return Task.CompletedTask; } } } \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj b/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj index f9bbe57da5..fc70849ef1 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj +++ b/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj @@ -20,7 +20,6 @@ - diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs index 24fa3c1c40..1073181ac5 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs @@ -7,7 +7,6 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.ResponseCaching.Internal; -using Microsoft.Extensions.Internal; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; @@ -314,7 +313,7 @@ namespace Microsoft.AspNetCore.ResponseCaching { return _cache.SetAsync(context.BaseKey, context.CachedVaryByRules, context.CachedResponseValidFor); } - return TaskCache.CompletedTask; + return Task.CompletedTask; } internal async Task FinalizeCacheBodyAsync(ResponseCachingContext context) @@ -378,7 +377,7 @@ namespace Microsoft.AspNetCore.ResponseCaching { return FinalizeCacheHeadersAsync(context); } - return TaskCache.CompletedTask; + return Task.CompletedTask; } internal static void AddResponseCachingFeature(HttpContext context) diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Streams/SegmentWriteStream.cs b/src/Microsoft.AspNetCore.ResponseCaching/Streams/SegmentWriteStream.cs index 6efeaf589f..81df72a9d1 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Streams/SegmentWriteStream.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Streams/SegmentWriteStream.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.Internal; namespace Microsoft.AspNetCore.ResponseCaching.Internal { @@ -170,7 +169,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) { Write(buffer, offset, count); - return TaskCache.CompletedTask; + return Task.CompletedTask; } public override void WriteByte(byte value) diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs index dfd9c12be9..64aa0e3a1f 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs @@ -7,7 +7,6 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.ResponseCaching.Internal; -using Microsoft.Extensions.Internal; using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; @@ -857,7 +856,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var middleware = TestUtils.CreateTestMiddleware(next: httpContext => { responseCachingFeatureAdded = httpContext.Features.Get() != null; - return TaskCache.CompletedTask; + return Task.CompletedTask; }, policyProvider: new TestResponseCachingPolicyProvider { diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs index d5889b3554..7ccb4a5ae6 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs @@ -13,7 +13,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.ResponseCaching.Internal; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Internal; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Testing; @@ -83,7 +82,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { context.Response.Write(uniqueId); } - return TaskCache.CompletedTask; + return Task.CompletedTask; } internal static IResponseCachingKeyProvider CreateTestKeyProvider() @@ -169,7 +168,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { if (next == null) { - next = httpContext => TaskCache.CompletedTask; + next = httpContext => Task.CompletedTask; } if (cache == null) { @@ -294,7 +293,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests { public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellation) { - return TaskCache.CompletedTask; + return Task.CompletedTask; } } @@ -383,7 +382,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests public Task SetAsync(string key, IResponseCacheEntry entry, TimeSpan validFor) { Set(key, entry, validFor); - return TaskCache.CompletedTask; + return Task.CompletedTask; } } From 65cccd055048b96e5e663973010f6a44c24e84bd Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Mon, 26 Jun 2017 09:40:55 -0700 Subject: [PATCH 110/188] Adding libunwind8 to .travis.yml [skip appveyor] --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index 2a46104677..b10be14215 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,6 +10,10 @@ os: - linux - osx osx_image: xcode8.2 +addons: + apt: + packages: + - libunwind8 branches: only: - master From 2b9966dabcb63f87d8be0871bb82c61ad6b2e571 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Thu, 29 Jun 2017 08:49:29 -0700 Subject: [PATCH 111/188] Add NETStandardImplicitPackageVersion --- build/dependencies.props | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build/dependencies.props b/build/dependencies.props index 4b6d757d0e..88671e1345 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -1,7 +1,8 @@ - + 2.0.0-* 2.1.0-* + 2.0.0-* 2.0.0-* 2.0.0-* 15.3.0-* From d33791ea07db5d8fcde0599032f079a3f2ac5a7c Mon Sep 17 00:00:00 2001 From: Pranav K Date: Mon, 3 Jul 2017 14:07:40 -0700 Subject: [PATCH 112/188] Update LICENSE.txt text --- LICENSE.txt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/LICENSE.txt b/LICENSE.txt index 0bdc1962b6..7b2956ecee 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,10 +1,12 @@ -Copyright (c) .NET Foundation. All rights reserved. +Copyright (c) .NET Foundation and Contributors + +All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use -these files except in compliance with the License. You may obtain a copy of the +this file except in compliance with the License. You may obtain a copy of the License at -http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR From e4276480f4f9fd1123e6ce4488e3baca862dd979 Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Thu, 6 Jul 2017 10:38:59 -0700 Subject: [PATCH 113/188] React to aspnet/BuildTools#293 [ci skip] --- build/dependencies.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/dependencies.props b/build/dependencies.props index 88671e1345..2a9564357e 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -1,7 +1,7 @@  2.0.0-* - 2.1.0-* + 2.0.1-* 2.0.0-* 2.0.0-* 2.0.0-* From ae4154b0aafdef21d8471d450506efbcd411eb41 Mon Sep 17 00:00:00 2001 From: Ryan Brandenburg Date: Thu, 6 Jul 2017 12:24:38 -0700 Subject: [PATCH 114/188] Set "TreatWarningsAsErrors" before NuGet restore * Ensures our build stays clean of NuGet warnings --- build/common.props | 1 + 1 file changed, 1 insertion(+) diff --git a/build/common.props b/build/common.props index eb8fe230a6..63452ea4ca 100644 --- a/build/common.props +++ b/build/common.props @@ -10,6 +10,7 @@ true true $(VersionSuffix)-$(BuildNumber) + true From 8dae273b9ddd8d02472cb7048a3d5b13507b8d3d Mon Sep 17 00:00:00 2001 From: Pranav K Date: Thu, 6 Jul 2017 15:08:46 -0700 Subject: [PATCH 115/188] Update version suffix for 2.0.0 RTM release --- version.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.props b/version.props index 193a5999d8..eba6b16756 100644 --- a/version.props +++ b/version.props @@ -2,6 +2,6 @@ 2.0.0 - preview3 + rtm From cad0e9eff947dcc38ce18a71ca26ff5579fda099 Mon Sep 17 00:00:00 2001 From: Ryan Brandenburg Date: Thu, 6 Jul 2017 15:22:34 -0700 Subject: [PATCH 116/188] Remove NETSTandard.Library.NETFramework --- build/common.props | 4 ---- samples/ResponseCachingSample/ResponseCachingSample.csproj | 4 ---- 2 files changed, 8 deletions(-) diff --git a/build/common.props b/build/common.props index 63452ea4ca..fa3f456e8a 100644 --- a/build/common.props +++ b/build/common.props @@ -17,8 +17,4 @@ - - - - diff --git a/samples/ResponseCachingSample/ResponseCachingSample.csproj b/samples/ResponseCachingSample/ResponseCachingSample.csproj index 1661b293f4..010c9ca334 100644 --- a/samples/ResponseCachingSample/ResponseCachingSample.csproj +++ b/samples/ResponseCachingSample/ResponseCachingSample.csproj @@ -15,8 +15,4 @@ - - - - From 3ceed775ecec70830afaade24c86bdd8578553d1 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Mon, 10 Jul 2017 11:45:39 -0700 Subject: [PATCH 117/188] Branching for 2.0.0 rtm --- NuGet.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NuGet.config b/NuGet.config index 4e8a1f6de1..37f0d27ea0 100644 --- a/NuGet.config +++ b/NuGet.config @@ -2,7 +2,7 @@ - + From 46e0a26c5421d50c06aa0664eff244233b617c96 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Mon, 10 Jul 2017 11:58:00 -0700 Subject: [PATCH 118/188] Updating KoreBuild branch --- build.ps1 | 2 +- build.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/build.ps1 b/build.ps1 index 5bf0e2c113..1785334385 100644 --- a/build.ps1 +++ b/build.ps1 @@ -33,7 +33,7 @@ cd $PSScriptRoot $repoFolder = $PSScriptRoot $env:REPO_FOLDER = $repoFolder -$koreBuildZip="https://github.com/aspnet/KoreBuild/archive/dev.zip" +$koreBuildZip="https://github.com/aspnet/KoreBuild/archive/rel/2.0.0.zip" if ($env:KOREBUILD_ZIP) { $koreBuildZip=$env:KOREBUILD_ZIP diff --git a/build.sh b/build.sh index b0bcadb579..5e27ed8efb 100755 --- a/build.sh +++ b/build.sh @@ -2,7 +2,7 @@ repoFolder="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" cd $repoFolder -koreBuildZip="https://github.com/aspnet/KoreBuild/archive/dev.zip" +koreBuildZip="https://github.com/aspnet/KoreBuild/archive/rel/2.0.0.zip" if [ ! -z $KOREBUILD_ZIP ]; then koreBuildZip=$KOREBUILD_ZIP fi From f125329ed70a5aeaa28d5ecfaf433aed8e318a59 Mon Sep 17 00:00:00 2001 From: John Luo Date: Mon, 10 Jul 2017 03:18:17 -0700 Subject: [PATCH 119/188] Use private instance of MemoryCache and impose size limit --- .../Internal/CacheEntry/CacheEntryHelpers .cs | 88 +++++++++++++++++++ .../Internal/MemoryResponseCache.cs | 6 +- .../ResponseCachingMiddleware.cs | 19 ++++ .../ResponseCachingOptions.cs | 5 ++ .../ResponseCachingServicesExtensions.cs | 1 - .../ResponseCachingMiddlewareTests.cs | 33 +++++++ 6 files changed, 149 insertions(+), 3 deletions(-) create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CacheEntryHelpers .cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CacheEntryHelpers .cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CacheEntryHelpers .cs new file mode 100644 index 0000000000..f23286a77e --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CacheEntryHelpers .cs @@ -0,0 +1,88 @@ +// 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 static class CacheEntryHelpers + { + + internal static long EstimateCachedResponseSize(CachedResponse cachedResponse) + { + if (cachedResponse == null) + { + return 0L; + } + + checked + { + // StatusCode + long size = sizeof(int); + + // Headers + if (cachedResponse.Headers != null) + { + foreach (var item in cachedResponse.Headers) + { + size += item.Key.Length * sizeof(char) + EstimateStringValuesSize(item.Value); + } + } + + // Body + if (cachedResponse.Body != null) + { + size += cachedResponse.Body.Length; + } + + return size; + } + } + + internal static long EstimateCachedVaryByRulesySize(CachedVaryByRules cachedVaryByRules) + { + if (cachedVaryByRules == null) + { + return 0L; + } + + checked + { + var size = 0L; + + // VaryByKeyPrefix + if (!string.IsNullOrEmpty(cachedVaryByRules.VaryByKeyPrefix)) + { + size = cachedVaryByRules.VaryByKeyPrefix.Length * sizeof(char); + } + + // Headers + size += EstimateStringValuesSize(cachedVaryByRules.Headers); + + // QueryKeys + size += EstimateStringValuesSize(cachedVaryByRules.QueryKeys); + + return size; + } + } + + internal static long EstimateStringValuesSize(StringValues stringValues) + { + checked + { + var size = 0L; + + for (var i = 0; i < stringValues.Count; i++) + { + var stringValue = stringValues[i]; + if (!string.IsNullOrEmpty(stringValue)) + { + size += stringValues[i].Length * sizeof(char); + } + } + + return size; + } + } + } +} diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs index c96ba5e751..d69d48ebbd 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs @@ -67,7 +67,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal }, new MemoryCacheEntryOptions { - AbsoluteExpirationRelativeToNow = validFor + AbsoluteExpirationRelativeToNow = validFor, + Size = CacheEntryHelpers.EstimateCachedResponseSize(cachedResponse) }); } else @@ -77,7 +78,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal entry, new MemoryCacheEntryOptions { - AbsoluteExpirationRelativeToNow = validFor + AbsoluteExpirationRelativeToNow = validFor, + Size = CacheEntryHelpers.EstimateCachedVaryByRulesySize(entry as CachedVaryByRules) }); } } diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs index 1073181ac5..d2eee86ad7 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.ResponseCaching.Internal; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; @@ -26,6 +27,24 @@ namespace Microsoft.AspNetCore.ResponseCaching private readonly IResponseCachingKeyProvider _keyProvider; public ResponseCachingMiddleware( + RequestDelegate next, + IOptions options, + ILoggerFactory loggerFactory, + IResponseCachingPolicyProvider policyProvider, + IResponseCachingKeyProvider keyProvider) + : this( + next, + options, + loggerFactory, + policyProvider, + new MemoryResponseCache(new MemoryCache(new MemoryCacheOptions + { + SizeLimit = options.Value.SizeLimit + })), keyProvider) + { } + + // for testing + internal ResponseCachingMiddleware( RequestDelegate next, IOptions options, ILoggerFactory loggerFactory, diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingOptions.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingOptions.cs index 89f9d3285d..4fa75e2135 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingOptions.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingOptions.cs @@ -8,6 +8,11 @@ namespace Microsoft.AspNetCore.ResponseCaching { public class ResponseCachingOptions { + /// + /// The size limit for the response cache middleware in bytes. The default is set to 100 MB. + /// + public long SizeLimit { get; set; } = 100 * 1024 * 1024; + /// /// The largest cacheable size for the response body in bytes. The default is set to 64 MB. /// diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingServicesExtensions.cs b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingServicesExtensions.cs index 99187dfd74..ef6f815b5e 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingServicesExtensions.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingServicesExtensions.cs @@ -28,7 +28,6 @@ namespace Microsoft.Extensions.DependencyInjection services.AddMemoryCache(); services.TryAdd(ServiceDescriptor.Singleton()); services.TryAdd(ServiceDescriptor.Singleton()); - services.TryAdd(ServiceDescriptor.Singleton()); return services; } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs index 64aa0e3a1f..8f7076abe7 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs @@ -7,6 +7,7 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.ResponseCaching.Internal; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; @@ -823,6 +824,38 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests LoggedMessage.ResponseNotCached); } + [Fact] + public async Task FinalizeCacheBody_DoNotCache_IfSizeTooBig() + { + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware( + testSink: sink, + keyProvider: new TestResponseCachingKeyProvider("BaseKey"), + cache: new MemoryResponseCache(new MemoryCache(new MemoryCacheOptions + { + SizeLimit = 100 + }))); + var context = TestUtils.CreateTestContext(); + + context.ShouldCacheResponse = true; + middleware.ShimResponseStream(context); + + await context.HttpContext.Response.WriteAsync(new string('0', 101)); + + context.CachedResponse = new CachedResponse() { Headers = new HeaderDictionary() }; + context.CachedResponseValidFor = TimeSpan.FromSeconds(10); + + await middleware.FinalizeCacheBodyAsync(context); + + // The response cached message will be logged but the adding of the entry will no-op + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.ResponseCached); + + // The entry cannot be retrieved + Assert.False(await middleware.TryServeFromCacheAsync(context)); + } + [Fact] public void AddResponseCachingFeature_SecondInvocation_Throws() { From 071c127ddf4738289d0b7ba8e40bce9be668c349 Mon Sep 17 00:00:00 2001 From: Ryan Brandenburg Date: Fri, 7 Jul 2017 14:57:39 -0700 Subject: [PATCH 120/188] Skip first time experience on Appveyor --- appveyor.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 1041615c68..31efd8196f 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,4 +1,4 @@ -init: +init: - git config --global core.autocrlf true branches: only: @@ -9,6 +9,10 @@ branches: build_script: - ps: .\build.ps1 clone_depth: 1 +environment: + global: + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + DOTNET_CLI_TELEMETRY_OPTOUT: 1 test: off deploy: off os: Visual Studio 2017 From 288383c4454e5debf23fcf9174d92087822d78eb Mon Sep 17 00:00:00 2001 From: Ryan Brandenburg Date: Fri, 21 Jul 2017 13:02:17 -0700 Subject: [PATCH 121/188] 2.0.0-rtm to 2.1.0-preview1 --- version.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.props b/version.props index eba6b16756..1ea46af42a 100644 --- a/version.props +++ b/version.props @@ -1,7 +1,7 @@ - 2.0.0 - rtm + 2.1.0 + preview1 From 39d3036e5b9cd0325249edbd76fcd44b155d82cf Mon Sep 17 00:00:00 2001 From: Ryan Brandenburg Date: Mon, 24 Jul 2017 17:58:24 -0700 Subject: [PATCH 122/188] Set AspNetCoreVersion --- build/dependencies.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index 2a9564357e..e4e708d655 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -1,6 +1,6 @@ - + - 2.0.0-* + 2.1.0-* 2.0.1-* 2.0.0-* 2.0.0-* From 4cccecff751f0ae3495d47c24f06831a567ae40f Mon Sep 17 00:00:00 2001 From: Pranav K Date: Tue, 25 Jul 2017 15:14:37 -0700 Subject: [PATCH 123/188] Updating to InternalAspNetCoreSdkVersion 2.1.1-* --- build/dependencies.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/dependencies.props b/build/dependencies.props index e4e708d655..7a9758238c 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -1,7 +1,7 @@ 2.1.0-* - 2.0.1-* + 2.1.1-* 2.0.0-* 2.0.0-* 2.0.0-* From b1194ea59385eafb8645376975b3660f7bec771e Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Tue, 25 Jul 2017 16:34:23 -0700 Subject: [PATCH 124/188] Update bootstrappers to use the compiled version of KoreBuild [ci skip] --- .gitattributes | 1 + .gitignore | 1 + build.cmd | 2 +- build.ps1 | 218 +++++++++++++++++++++++++--------- build.sh | 224 +++++++++++++++++++++++++++++------ build/common.props | 2 +- version.props => version.xml | 3 +- 7 files changed, 357 insertions(+), 94 deletions(-) rename version.props => version.xml (55%) diff --git a/.gitattributes b/.gitattributes index bdaa5ba982..97b827b758 100644 --- a/.gitattributes +++ b/.gitattributes @@ -48,3 +48,4 @@ *.fsproj text=auto *.dbproj text=auto *.sln text=auto eol=crlf +*.sh eol=lf diff --git a/.gitignore b/.gitignore index 23826aae91..1996ae5484 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,4 @@ project.lock.json .testPublish/ launchSettings.json global.json +korebuild-lock.txt diff --git a/build.cmd b/build.cmd index 7d4894cb4a..b6c8d24864 100644 --- a/build.cmd +++ b/build.cmd @@ -1,2 +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 +PowerShell -NoProfile -NoLogo -ExecutionPolicy unrestricted -Command "[System.Threading.Thread]::CurrentThread.CurrentCulture = ''; [System.Threading.Thread]::CurrentThread.CurrentUICulture = '';& '%~dp0build.ps1' %*; exit $LASTEXITCODE" diff --git a/build.ps1 b/build.ps1 index 5bf0e2c113..d5eb4d5cf2 100644 --- a/build.ps1 +++ b/build.ps1 @@ -1,67 +1,177 @@ -$ErrorActionPreference = "Stop" +#!/usr/bin/env powershell +#requires -version 4 -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 +<# +.SYNOPSIS +Build this repository +.DESCRIPTION +Downloads korebuild if required. Then builds the repository. + +.PARAMETER Path +The folder to build. Defaults to the folder containing this script. + +.PARAMETER Channel +The channel of KoreBuild to download. Overrides the value from the config file. + +.PARAMETER DotNetHome +The directory where .NET Core tools will be stored. + +.PARAMETER ToolsSource +The base url where build tools can be downloaded. Overrides the value from the config file. + +.PARAMETER Update +Updates KoreBuild to the latest version even if a lock file is present. + +.PARAMETER ConfigFile +The path to the configuration file that stores values. Defaults to version.xml. + +.PARAMETER MSBuildArgs +Arguments to be passed to MSBuild + +.NOTES +This function will create a file $PSScriptRoot/korebuild-lock.txt. This lock file can be committed to source, but does not have to be. +When the lockfile is not present, KoreBuild will create one using latest available version from $Channel. + +The $ConfigFile is expected to be an XML file. It is optional, and the configuration values in it are optional as well. + +.EXAMPLE +Example config file: +```xml + + + + dev + https://aspnetcore.blob.core.windows.net/buildtools + + +``` +#> +[CmdletBinding(PositionalBinding = $false)] +param( + [string]$Path = $PSScriptRoot, + [Alias('c')] + [string]$Channel, + [Alias('d')] + [string]$DotNetHome, + [Alias('s')] + [string]$ToolsSource, + [Alias('u')] + [switch]$Update, + [string]$ConfigFile = (Join-Path $PSScriptRoot 'version.xml'), + [Parameter(ValueFromRemainingArguments = $true)] + [string[]]$MSBuildArgs +) + +Set-StrictMode -Version 2 +$ErrorActionPreference = 'Stop' + +# +# Functions +# + +function Get-KoreBuild { + + $lockFile = Join-Path $Path 'korebuild-lock.txt' + + if (!(Test-Path $lockFile) -or $Update) { + Get-RemoteFile "$ToolsSource/korebuild/channels/$Channel/latest.txt" $lockFile + } + + $version = Get-Content $lockFile | Where-Object { $_ -like 'version:*' } | Select-Object -first 1 + if (!$version) { + Write-Error "Failed to parse version from $lockFile. Expected a line that begins with 'version:'" + } + $version = $version.TrimStart('version:').Trim() + $korebuildPath = Join-Paths $DotNetHome ('buildtools', 'korebuild', $version) + + if (!(Test-Path $korebuildPath)) { + Write-Host -ForegroundColor Magenta "Downloading KoreBuild $version" + New-Item -ItemType Directory -Path $korebuildPath | Out-Null + $remotePath = "$ToolsSource/korebuild/artifacts/$version/korebuild.$version.zip" + + try { + $tmpfile = Join-Path ([IO.Path]::GetTempPath()) "KoreBuild-$([guid]::NewGuid()).zip" + Get-RemoteFile $remotePath $tmpfile + if (Get-Command -Name 'Expand-Archive' -ErrorAction Ignore) { + # Use built-in commands where possible as they are cross-plat compatible + Expand-Archive -Path $tmpfile -DestinationPath $korebuildPath } - else - { - $exception = $_.Exception - throw $exception + else { + # Fallback to old approach for old installations of PowerShell + Add-Type -AssemblyName System.IO.Compression.FileSystem + [System.IO.Compression.ZipFile]::ExtractToDirectory($tmpfile, $korebuildPath) } } + catch { + Remove-Item -Recurse -Force $korebuildPath -ErrorAction Ignore + throw + } + finally { + Remove-Item $tmpfile -ErrorAction Ignore + } } + + return $korebuildPath } -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 +function Join-Paths([string]$path, [string[]]$childPaths) { + $childPaths | ForEach-Object { $path = Join-Path $path $_ } + return $path } -$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 +function Get-RemoteFile([string]$RemotePath, [string]$LocalPath) { + if ($RemotePath -notlike 'http*') { + Copy-Item $RemotePath $LocalPath + return } + + $retries = 10 + while ($retries -gt 0) { + $retries -= 1 + try { + Invoke-WebRequest -UseBasicParsing -Uri $RemotePath -OutFile $LocalPath + return + } + catch { + Write-Verbose "Request failed. $retries retries remaining" + } + } + + Write-Error "Download failed: '$RemotePath'." } -&"$buildFile" @args +# +# Main +# + +# Load configuration or set defaults + +if (Test-Path $ConfigFile) { + [xml] $config = Get-Content $ConfigFile + if (!($Channel)) { [string] $Channel = Select-Xml -Xml $config -XPath '/Project/PropertyGroup/KoreBuildChannel' } + if (!($ToolsSource)) { [string] $ToolsSource = Select-Xml -Xml $config -XPath '/Project/PropertyGroup/KoreBuildToolsSource' } +} + +if (!$DotNetHome) { + $DotNetHome = if ($env:DOTNET_HOME) { $env:DOTNET_HOME } ` + elseif ($env:USERPROFILE) { Join-Path $env:USERPROFILE '.dotnet'} ` + elseif ($env:HOME) {Join-Path $env:HOME '.dotnet'}` + else { Join-Path $PSScriptRoot '.dotnet'} +} + +if (!$Channel) { $Channel = 'dev' } +if (!$ToolsSource) { $ToolsSource = 'https://aspnetcore.blob.core.windows.net/buildtools' } + +# Execute + +$korebuildPath = Get-KoreBuild +Import-Module -Force -Scope Local (Join-Path $korebuildPath 'KoreBuild.psd1') + +try { + Install-Tools $ToolsSource $DotNetHome + Invoke-RepositoryBuild $Path @MSBuildArgs +} +finally { + Remove-Module 'KoreBuild' -ErrorAction Ignore +} diff --git a/build.sh b/build.sh index b0bcadb579..ab590e62f1 100755 --- a/build.sh +++ b/build.sh @@ -1,46 +1,196 @@ #!/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 +set -euo pipefail -buildFolder=".build" -buildFile="$buildFolder/KoreBuild.sh" +# +# variables +# -if test ! -d $buildFolder; then - echo "Downloading KoreBuild from $koreBuildZip" +RESET="\033[0m" +RED="\033[0;31m" +MAGENTA="\033[0;95m" +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +[ -z "${DOTNET_HOME:-}"] && DOTNET_HOME="$HOME/.dotnet" +config_file="$DIR/version.xml" +verbose=false +update=false +repo_path="$DIR" +channel='' +tools_source='' - tempFolder="/tmp/KoreBuild-$(uuidgen)" - mkdir $tempFolder +# +# Functions +# +__usage() { + echo "Usage: $(basename ${BASH_SOURCE[0]}) [options] [[--] ...]" + echo "" + echo "Arguments:" + echo " ... Arguments passed to MSBuild. Variable number of arguments allowed." + echo "" + echo "Options:" + echo " --verbose Show verbose output." + echo " -c|--channel The channel of KoreBuild to download. Overrides the value from the config file.." + echo " --config-file TThe path to the configuration file that stores values. Defaults to version.xml." + echo " -d|--dotnet-home The directory where .NET Core tools will be stored. Defaults to '\$DOTNET_HOME' or '\$HOME/.dotnet." + echo " --path The directory to build. Defaults to the directory containing the script." + echo " -s|--tools-source The base url where build tools can be downloaded. Overrides the value from the config file." + echo " -u|--update Update to the latest KoreBuild even if the lock file is present." + echo "" + echo "Description:" + echo " This function will create a file \$DIR/korebuild-lock.txt. This lock file can be committed to source, but does not have to be." + echo " When the lockfile is not present, KoreBuild will create one using latest available version from \$channel." - 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 + if [[ "${1:-}" != '--no-exit' ]]; then + exit 2 fi +} + +get_korebuild() { + local lock_file="$repo_path/korebuild-lock.txt" + if [ ! -f $lock_file ] || [ "$update" = true ]; then + __get_remote_file "$tools_source/korebuild/channels/$channel/latest.txt" $lock_file + fi + local version="$(grep 'version:*' -m 1 $lock_file)" + if [[ "$version" == '' ]]; then + __error "Failed to parse version from $lock_file. Expected a line that begins with 'version:'" + return 1 + fi + version="$(echo ${version#version:} | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" + local korebuild_path="$DOTNET_HOME/buildtools/korebuild/$version" + + { + if [ ! -d "$korebuild_path" ]; then + mkdir -p "$korebuild_path" + local remote_path="$tools_source/korebuild/artifacts/$version/korebuild.$version.zip" + tmpfile="$(mktemp)" + echo -e "${MAGENTA}Downloading KoreBuild ${version}${RESET}" + if __get_remote_file $remote_path $tmpfile; then + unzip -q -d "$korebuild_path" $tmpfile + fi + rm $tmpfile || true + fi + + source "$korebuild_path/KoreBuild.sh" + } || { + if [ -d "$korebuild_path" ]; then + echo "Cleaning up after failed installation" + rm -rf "$korebuild_path" || true + fi + return 1 + } +} + +__error() { + echo -e "${RED}$@${RESET}" 1>&2 +} + +__machine_has() { + hash "$1" > /dev/null 2>&1 + return $? +} + +__get_remote_file() { + local remote_path=$1 + local local_path=$2 + + if [[ "$remote_path" != 'http'* ]]; then + cp $remote_path $local_path + return 0 + fi + + failed=false + if __machine_has wget; then + wget --tries 10 --quiet -O $local_path $remote_path || failed=true + fi + + if [ "$failed" = true ] && __machine_has curl; then + failed=false + curl --retry 10 -sSL -f --create-dirs -o $local_path $remote_path || failed=true + fi + + if [ "$failed" = true ]; then + __error "Download failed: $remote_path" 1>&2 + return 1 + fi +} + +__read_dom () { local IFS=\> ; read -d \< ENTITY CONTENT ;} + +# +# main +# + +while [[ $# > 0 ]]; do + case $1 in + -\?|-h|--help) + __usage --no-exit + exit 0 + ;; + -c|--channel|-Channel) + shift + channel=${1:-} + [ -z "$channel" ] && __usage + ;; + --config-file|-ConfigFile) + shift + config_file="${1:-}" + [ -z "$config_file" ] && __usage + ;; + -d|--dotnet-home|-DotNetHome) + shift + DOTNET_HOME=${1:-} + [ -z "$DOTNET_HOME" ] && __usage + ;; + --path|-Path) + shift + repo_path="${1:-}" + [ -z "$repo_path" ] && __usage + ;; + -s|--tools-source|-ToolsSource) + shift + tools_source="${1:-}" + [ -z "$tools_source" ] && __usage + ;; + -u|--update|-Update) + update=true + ;; + --verbose|-Verbose) + verbose=true + ;; + --) + shift + break + ;; + *) + break + ;; + esac + shift +done + +if ! __machine_has unzip; then + __error 'Missing required command: unzip' + exit 1 fi -$buildFile -r $repoFolder "$@" +if ! __machine_has curl && ! __machine_has wget; then + __error 'Missing required command. Either wget or curl is required.' + exit 1 +fi + +if [ -f $config_file ]; then + comment=false + while __read_dom; do + if [ "$comment" = true ]; then [[ $CONTENT == *'-->'* ]] && comment=false ; continue; fi + if [[ $ENTITY == '!--'* ]]; then comment=true; continue; fi + if [ -z "$channel" ] && [[ $ENTITY == "KoreBuildChannel" ]]; then channel=$CONTENT; fi + if [ -z "$tools_source" ] && [[ $ENTITY == "KoreBuildToolsSource" ]]; then tools_source=$CONTENT; fi + done < $config_file +fi + +[ -z "$channel" ] && channel='dev' +[ -z "$tools_source" ] && tools_source='https://aspnetcore.blob.core.windows.net/buildtools' + +get_korebuild +install_tools "$tools_source" "$DOTNET_HOME" +invoke_repository_build "$repo_path" $@ diff --git a/build/common.props b/build/common.props index fa3f456e8a..f929177635 100644 --- a/build/common.props +++ b/build/common.props @@ -1,6 +1,6 @@ - + Microsoft ASP.NET Core diff --git a/version.props b/version.xml similarity index 55% rename from version.props rename to version.xml index 1ea46af42a..3c05022b7d 100644 --- a/version.props +++ b/version.xml @@ -1,6 +1,7 @@ - + + dev 2.1.0 preview1 From 73c136de249414174e977b535ad28694b80dbd96 Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Wed, 26 Jul 2017 10:28:51 -0700 Subject: [PATCH 125/188] Fix syntax warning when running build.sh on older versions of bash [ci skip] --- build.sh | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/build.sh b/build.sh index ab590e62f1..5568c6182a 100755 --- a/build.sh +++ b/build.sh @@ -10,7 +10,7 @@ RESET="\033[0m" RED="\033[0;31m" MAGENTA="\033[0;95m" DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -[ -z "${DOTNET_HOME:-}"] && DOTNET_HOME="$HOME/.dotnet" +[ -z "${DOTNET_HOME:-}" ] && DOTNET_HOME="$HOME/.dotnet" config_file="$DIR/version.xml" verbose=false update=false @@ -22,7 +22,7 @@ tools_source='' # Functions # __usage() { - echo "Usage: $(basename ${BASH_SOURCE[0]}) [options] [[--] ...]" + echo "Usage: $(basename "${BASH_SOURCE[0]}") [options] [[--] ...]" echo "" echo "Arguments:" echo " ... Arguments passed to MSBuild. Variable number of arguments allowed." @@ -46,16 +46,17 @@ __usage() { } get_korebuild() { + local version local lock_file="$repo_path/korebuild-lock.txt" - if [ ! -f $lock_file ] || [ "$update" = true ]; then - __get_remote_file "$tools_source/korebuild/channels/$channel/latest.txt" $lock_file + if [ ! -f "$lock_file" ] || [ "$update" = true ]; then + __get_remote_file "$tools_source/korebuild/channels/$channel/latest.txt" "$lock_file" fi - local version="$(grep 'version:*' -m 1 $lock_file)" + version="$(grep 'version:*' -m 1 "$lock_file")" if [[ "$version" == '' ]]; then __error "Failed to parse version from $lock_file. Expected a line that begins with 'version:'" return 1 fi - version="$(echo ${version#version:} | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" + version="$(echo "${version#version:}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" local korebuild_path="$DOTNET_HOME/buildtools/korebuild/$version" { @@ -64,10 +65,10 @@ get_korebuild() { local remote_path="$tools_source/korebuild/artifacts/$version/korebuild.$version.zip" tmpfile="$(mktemp)" echo -e "${MAGENTA}Downloading KoreBuild ${version}${RESET}" - if __get_remote_file $remote_path $tmpfile; then - unzip -q -d "$korebuild_path" $tmpfile + if __get_remote_file "$remote_path" "$tmpfile"; then + unzip -q -d "$korebuild_path" "$tmpfile" fi - rm $tmpfile || true + rm "$tmpfile" || true fi source "$korebuild_path/KoreBuild.sh" @@ -81,7 +82,7 @@ get_korebuild() { } __error() { - echo -e "${RED}$@${RESET}" 1>&2 + echo -e "${RED}$*${RESET}" 1>&2 } __machine_has() { @@ -94,18 +95,18 @@ __get_remote_file() { local local_path=$2 if [[ "$remote_path" != 'http'* ]]; then - cp $remote_path $local_path + cp "$remote_path" "$local_path" return 0 fi failed=false if __machine_has wget; then - wget --tries 10 --quiet -O $local_path $remote_path || failed=true + wget --tries 10 --quiet -O "$local_path" "$remote_path" || failed=true fi if [ "$failed" = true ] && __machine_has curl; then failed=false - curl --retry 10 -sSL -f --create-dirs -o $local_path $remote_path || failed=true + curl --retry 10 -sSL -f --create-dirs -o "$local_path" "$remote_path" || failed=true fi if [ "$failed" = true ]; then @@ -114,13 +115,13 @@ __get_remote_file() { fi } -__read_dom () { local IFS=\> ; read -d \< ENTITY CONTENT ;} +__read_dom () { local IFS=\> ; read -r -d \< ENTITY CONTENT ;} # # main # -while [[ $# > 0 ]]; do +while [[ $# -gt 0 ]]; do case $1 in -\?|-h|--help) __usage --no-exit @@ -128,7 +129,7 @@ while [[ $# > 0 ]]; do ;; -c|--channel|-Channel) shift - channel=${1:-} + channel="${1:-}" [ -z "$channel" ] && __usage ;; --config-file|-ConfigFile) @@ -138,7 +139,7 @@ while [[ $# > 0 ]]; do ;; -d|--dotnet-home|-DotNetHome) shift - DOTNET_HOME=${1:-} + DOTNET_HOME="${1:-}" [ -z "$DOTNET_HOME" ] && __usage ;; --path|-Path) @@ -178,14 +179,14 @@ if ! __machine_has curl && ! __machine_has wget; then exit 1 fi -if [ -f $config_file ]; then +if [ -f "$config_file" ]; then comment=false while __read_dom; do if [ "$comment" = true ]; then [[ $CONTENT == *'-->'* ]] && comment=false ; continue; fi if [[ $ENTITY == '!--'* ]]; then comment=true; continue; fi if [ -z "$channel" ] && [[ $ENTITY == "KoreBuildChannel" ]]; then channel=$CONTENT; fi if [ -z "$tools_source" ] && [[ $ENTITY == "KoreBuildToolsSource" ]]; then tools_source=$CONTENT; fi - done < $config_file + done < "$config_file" fi [ -z "$channel" ] && channel='dev' @@ -193,4 +194,4 @@ fi get_korebuild install_tools "$tools_source" "$DOTNET_HOME" -invoke_repository_build "$repo_path" $@ +invoke_repository_build "$repo_path" "$@" From a94324b5fbd996587dc3a543e7b42cf94f5ce2ff Mon Sep 17 00:00:00 2001 From: John Luo Date: Wed, 2 Aug 2017 12:44:47 -0700 Subject: [PATCH 126/188] Update __get_remote_file logic --- build.sh | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/build.sh b/build.sh index 5568c6182a..8eace4c20d 100755 --- a/build.sh +++ b/build.sh @@ -99,17 +99,16 @@ __get_remote_file() { return 0 fi - failed=false + local succeeded=false if __machine_has wget; then - wget --tries 10 --quiet -O "$local_path" "$remote_path" || failed=true + wget --tries 10 --quiet -O "$local_path" "$remote_path" && succeeded=true fi - if [ "$failed" = true ] && __machine_has curl; then - failed=false - curl --retry 10 -sSL -f --create-dirs -o "$local_path" "$remote_path" || failed=true + if [ "$succeeded" = false ] && __machine_has curl; then + curl --retry 10 -sSL -f --create-dirs -o "$local_path" "$remote_path" && succeeded=true fi - if [ "$failed" = true ]; then + if [ "$succeeded" = false ]; then __error "Download failed: $remote_path" 1>&2 return 1 fi From 5533976a53e654138e876ccd4d832ba40d803bd7 Mon Sep 17 00:00:00 2001 From: John Luo Date: Wed, 2 Aug 2017 14:33:37 -0700 Subject: [PATCH 127/188] Ensure fallback to curl after failed wget --- build.sh | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/build.sh b/build.sh index 8eace4c20d..11cdbe5504 100755 --- a/build.sh +++ b/build.sh @@ -99,16 +99,19 @@ __get_remote_file() { return 0 fi - local succeeded=false + local failed=false if __machine_has wget; then - wget --tries 10 --quiet -O "$local_path" "$remote_path" && succeeded=true + wget --tries 10 --quiet -O "$local_path" "$remote_path" || failed=true + else + failed=true fi - if [ "$succeeded" = false ] && __machine_has curl; then - curl --retry 10 -sSL -f --create-dirs -o "$local_path" "$remote_path" && succeeded=true + if [ "$failed" = true ] && __machine_has curl; then + failed=false + curl --retry 10 -sSL -f --create-dirs -o "$local_path" "$remote_path" || failed=true fi - if [ "$succeeded" = false ]; then + if [ "$failed" = true ]; then __error "Download failed: $remote_path" 1>&2 return 1 fi From 853e62216e2f276d811f877f079c55eaeec8c453 Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Tue, 22 Aug 2017 17:44:08 -0700 Subject: [PATCH 128/188] Upgrade to xunit 2.3.0-beta4 Includes a few changes as required by new analyzers added in the upgrade --- build/dependencies.props | 4 ++-- .../ResponseCachingMiddlewareTests.cs | 4 ++-- .../SegmentWriteStreamTests.cs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index 7a9758238c..7dde6b27b6 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -5,7 +5,7 @@ 2.0.0-* 2.0.0-* 2.0.0-* - 15.3.0-* - 2.3.0-beta2-* + 15.3.0 + 2.3.0-beta4-build3742 diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs index 8f7076abe7..831e9ea67e 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs @@ -200,7 +200,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests // Verify modifications in the past succeeds context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10)); Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context)); - Assert.Equal(1, sink.Writes.Count); + Assert.Single(sink.Writes); // Verify modifications at present succeeds context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow); @@ -232,7 +232,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10)); Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context)); - Assert.Equal(1, sink.Writes.Count); + Assert.Single(sink.Writes); // Verify modifications at present context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/SegmentWriteStreamTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/SegmentWriteStreamTests.cs index 203b685b8d..6043128e7b 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/SegmentWriteStreamTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/SegmentWriteStreamTests.cs @@ -40,7 +40,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests var stream = new SegmentWriteStream(1); Assert.True(stream.CanWrite); - Assert.Equal(0, stream.GetSegments().Count); + Assert.Empty(stream.GetSegments()); Assert.False(stream.CanWrite); } From 7e22244eedb939466e2b16cb3d986408d66b05e0 Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Tue, 29 Aug 2017 12:13:48 -0700 Subject: [PATCH 129/188] Use Directory.Build.props/targets --- appveyor.yml => .appveyor.yml | 0 build/common.props => Directory.Build.props | 12 +++------ Directory.Build.targets | 2 ++ ResponseCaching.sln | 26 ++++++++++++++----- .../ResponseCachingSample.csproj | 4 +-- src/Directory.Build.props | 7 +++++ ...etCore.ResponseCaching.Abstractions.csproj | 2 -- ...icrosoft.AspNetCore.ResponseCaching.csproj | 2 -- test/Directory.Build.props | 7 +++++ ...ft.AspNetCore.ResponseCaching.Tests.csproj | 2 -- 10 files changed, 41 insertions(+), 23 deletions(-) rename appveyor.yml => .appveyor.yml (100%) rename build/common.props => Directory.Build.props (60%) create mode 100644 Directory.Build.targets create mode 100644 src/Directory.Build.props create mode 100644 test/Directory.Build.props diff --git a/appveyor.yml b/.appveyor.yml similarity index 100% rename from appveyor.yml rename to .appveyor.yml diff --git a/build/common.props b/Directory.Build.props similarity index 60% rename from build/common.props rename to Directory.Build.props index f929177635..4b572668e3 100644 --- a/build/common.props +++ b/Directory.Build.props @@ -1,20 +1,16 @@ - - - + + + Microsoft ASP.NET Core https://github.com/aspnet/ResponseCaching git - $(MSBuildThisFileDirectory)Key.snk + $(MSBuildThisFileDirectory)build\Key.snk true true $(VersionSuffix)-$(BuildNumber) true - - - - diff --git a/Directory.Build.targets b/Directory.Build.targets new file mode 100644 index 0000000000..f75adf7e4d --- /dev/null +++ b/Directory.Build.targets @@ -0,0 +1,2 @@ + + diff --git a/ResponseCaching.sln b/ResponseCaching.sln index 23eb129413..7792c35d85 100644 --- a/ResponseCaching.sln +++ b/ResponseCaching.sln @@ -1,21 +1,32 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26127.0 +VisualStudioVersion = 15.0.26730.10 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{367AABAF-E03C-4491-A9A7-BDDE8903D1B4}" + ProjectSection(SolutionItems) = preProject + src\Directory.Build.props = src\Directory.Build.props + EndProjectSection 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}" + ProjectSection(SolutionItems) = preProject + test\Directory.Build.props = test\Directory.Build.props + EndProjectSection EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ResponseCachingSample", "samples\ResponseCachingSample\ResponseCachingSample.csproj", "{1139BDEE-FA15-474D-8855-0AB91F23CF26}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ResponseCachingSample", "samples\ResponseCachingSample\ResponseCachingSample.csproj", "{1139BDEE-FA15-474D-8855-0AB91F23CF26}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.ResponseCaching.Tests", "test\Microsoft.AspNetCore.ResponseCaching.Tests\Microsoft.AspNetCore.ResponseCaching.Tests.csproj", "{151B2027-3936-44B9-A4A0-E1E5902125AB}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.ResponseCaching.Tests", "test\Microsoft.AspNetCore.ResponseCaching.Tests\Microsoft.AspNetCore.ResponseCaching.Tests.csproj", "{151B2027-3936-44B9-A4A0-E1E5902125AB}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.ResponseCaching", "src\Microsoft.AspNetCore.ResponseCaching\Microsoft.AspNetCore.ResponseCaching.csproj", "{D1031270-DBD3-4F02-A3DC-3E7DADE8EBE6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.ResponseCaching", "src\Microsoft.AspNetCore.ResponseCaching\Microsoft.AspNetCore.ResponseCaching.csproj", "{D1031270-DBD3-4F02-A3DC-3E7DADE8EBE6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.ResponseCaching.Abstractions", "src\Microsoft.AspNetCore.ResponseCaching.Abstractions\Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj", "{2D1022E8-CBB6-478D-A420-CB888D0EF7B7}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.ResponseCaching.Abstractions", "src\Microsoft.AspNetCore.ResponseCaching.Abstractions\Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj", "{2D1022E8-CBB6-478D-A420-CB888D0EF7B7}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B984DDCF-0D61-44C4-9D30-2BC59EE6BD29}" + ProjectSection(SolutionItems) = preProject + Directory.Build.props = Directory.Build.props + Directory.Build.targets = Directory.Build.targets + EndProjectSection EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -49,4 +60,7 @@ Global {D1031270-DBD3-4F02-A3DC-3E7DADE8EBE6} = {367AABAF-E03C-4491-A9A7-BDDE8903D1B4} {2D1022E8-CBB6-478D-A420-CB888D0EF7B7} = {367AABAF-E03C-4491-A9A7-BDDE8903D1B4} EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {6F6B4994-06D7-4D35-B0F7-F60913AA8402} + EndGlobalSection EndGlobal diff --git a/samples/ResponseCachingSample/ResponseCachingSample.csproj b/samples/ResponseCachingSample/ResponseCachingSample.csproj index 010c9ca334..05d34a7065 100644 --- a/samples/ResponseCachingSample/ResponseCachingSample.csproj +++ b/samples/ResponseCachingSample/ResponseCachingSample.csproj @@ -1,6 +1,4 @@ - - - + netcoreapp2.0 diff --git a/src/Directory.Build.props b/src/Directory.Build.props new file mode 100644 index 0000000000..d704a37df9 --- /dev/null +++ b/src/Directory.Build.props @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj b/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj index 4a8e021e0f..fe0cd9d0e9 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj +++ b/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj @@ -1,7 +1,5 @@  - - ASP.NET Core response caching middleware abstractions and feature interface definitions. netstandard2.0 diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj b/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj index fc70849ef1..1f83ba27d6 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj +++ b/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj @@ -1,7 +1,5 @@  - - ASP.NET Core middleware for caching HTTP responses on the server. netstandard2.0 diff --git a/test/Directory.Build.props b/test/Directory.Build.props new file mode 100644 index 0000000000..d704a37df9 --- /dev/null +++ b/test/Directory.Build.props @@ -0,0 +1,7 @@ + + + + + + + diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj b/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj index c18e392556..0955b74daf 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj @@ -1,7 +1,5 @@  - - netcoreapp2.0;net461 netcoreapp2.0 From e87f13d267a2972701cce0f49bba5f181bf3cfb8 Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Tue, 29 Aug 2017 12:14:28 -0700 Subject: [PATCH 130/188] Use PackageLineup to manage PackageReference versions --- Directory.Build.props | 1 - Directory.Build.targets | 14 +++++++++++++- NuGet.config | 1 - build/dependencies.props | 11 ----------- build/repo.props | 6 ++++++ .../ResponseCachingSample.csproj | 6 +++--- src/Directory.Build.props | 2 +- ....AspNetCore.ResponseCaching.Abstractions.csproj | 2 +- .../Microsoft.AspNetCore.ResponseCaching.csproj | 8 ++++---- test/Directory.Build.props | 2 +- ...crosoft.AspNetCore.ResponseCaching.Tests.csproj | 10 +++++----- 11 files changed, 34 insertions(+), 29 deletions(-) delete mode 100644 build/dependencies.props create mode 100644 build/repo.props diff --git a/Directory.Build.props b/Directory.Build.props index 4b572668e3..232d943af6 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,5 +1,4 @@  - diff --git a/Directory.Build.targets b/Directory.Build.targets index f75adf7e4d..bc118fd907 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -1,2 +1,14 @@ - + + + + <_BootstrapperFile Condition=" $([MSBuild]::IsOSUnixLike()) ">build.sh + <_BootstrapperFile Condition="! $([MSBuild]::IsOSUnixLike()) ">build.cmd + <_BootstrapperError> + Package references have not been pinned. Run './$(_BootstrapperFile) /t:Pin'. + Also, you can run './$(_BootstrapperFile) /t:Restore' which will pin *and* restore packages. '$(_BootstrapperFile)' can be found in '$(MSBuildThisFileDirectory)'. + + + + + diff --git a/NuGet.config b/NuGet.config index 4e8a1f6de1..20060c934e 100644 --- a/NuGet.config +++ b/NuGet.config @@ -3,7 +3,6 @@ - diff --git a/build/dependencies.props b/build/dependencies.props deleted file mode 100644 index 7dde6b27b6..0000000000 --- a/build/dependencies.props +++ /dev/null @@ -1,11 +0,0 @@ - - - 2.1.0-* - 2.1.1-* - 2.0.0-* - 2.0.0-* - 2.0.0-* - 15.3.0 - 2.3.0-beta4-build3742 - - diff --git a/build/repo.props b/build/repo.props new file mode 100644 index 0000000000..13fe1c296a --- /dev/null +++ b/build/repo.props @@ -0,0 +1,6 @@ + + + + + + diff --git a/samples/ResponseCachingSample/ResponseCachingSample.csproj b/samples/ResponseCachingSample/ResponseCachingSample.csproj index 05d34a7065..b276533658 100644 --- a/samples/ResponseCachingSample/ResponseCachingSample.csproj +++ b/samples/ResponseCachingSample/ResponseCachingSample.csproj @@ -9,8 +9,8 @@ - - - + + + diff --git a/src/Directory.Build.props b/src/Directory.Build.props index d704a37df9..9d9a3de33a 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,6 +2,6 @@ - + diff --git a/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj b/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj index fe0cd9d0e9..171e634aaf 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj +++ b/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj b/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj index 1f83ba27d6..9f50870312 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj +++ b/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj @@ -14,10 +14,10 @@ - - - - + + + + diff --git a/test/Directory.Build.props b/test/Directory.Build.props index d704a37df9..9d9a3de33a 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -2,6 +2,6 @@ - + diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj b/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj index 0955b74daf..29d5cfe758 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj @@ -10,11 +10,11 @@ - - - - - + + + + + From 7131735652d421366d2d8a75f4eabc3f317dd661 Mon Sep 17 00:00:00 2001 From: Justin Kotalik Date: Thu, 21 Sep 2017 17:57:52 -0700 Subject: [PATCH 131/188] Increase Minimum Version of Visual Studio to 15.3.0 --- ResponseCaching.sln | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ResponseCaching.sln b/ResponseCaching.sln index 7792c35d85..69a5397248 100644 --- a/ResponseCaching.sln +++ b/ResponseCaching.sln @@ -1,7 +1,7 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.26730.10 -MinimumVisualStudioVersion = 10.0.40219.1 +MinimumVisualStudioVersion = 15.0.26730.03 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{367AABAF-E03C-4491-A9A7-BDDE8903D1B4}" ProjectSection(SolutionItems) = preProject src\Directory.Build.props = src\Directory.Build.props From dfb7c280a6848fe351ddfd79f1c8942cb9a8a766 Mon Sep 17 00:00:00 2001 From: Ryan Brandenburg Date: Wed, 20 Sep 2017 13:23:07 -0700 Subject: [PATCH 132/188] Update bootstrappers --- .appveyor.yml | 4 +- build.cmd | 2 +- build.sh | 197 +------------------------------------- run.cmd | 2 + build.ps1 => run.ps1 | 56 +++++++---- run.sh | 223 +++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 266 insertions(+), 218 deletions(-) create mode 100644 run.cmd rename build.ps1 => run.ps1 (73%) create mode 100755 run.sh diff --git a/.appveyor.yml b/.appveyor.yml index 31efd8196f..46038786c9 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -1,4 +1,4 @@ -init: +init: - git config --global core.autocrlf true branches: only: @@ -7,7 +7,7 @@ branches: - dev - /^(.*\/)?ci-.*$/ build_script: - - ps: .\build.ps1 + - ps: .\run.ps1 default-build clone_depth: 1 environment: global: diff --git a/build.cmd b/build.cmd index b6c8d24864..c0050bda12 100644 --- a/build.cmd +++ b/build.cmd @@ -1,2 +1,2 @@ @ECHO OFF -PowerShell -NoProfile -NoLogo -ExecutionPolicy unrestricted -Command "[System.Threading.Thread]::CurrentThread.CurrentCulture = ''; [System.Threading.Thread]::CurrentThread.CurrentUICulture = '';& '%~dp0build.ps1' %*; exit $LASTEXITCODE" +PowerShell -NoProfile -NoLogo -ExecutionPolicy unrestricted -Command "[System.Threading.Thread]::CurrentThread.CurrentCulture = ''; [System.Threading.Thread]::CurrentThread.CurrentUICulture = '';& '%~dp0run.ps1' default-build %*; exit $LASTEXITCODE" diff --git a/build.sh b/build.sh index 11cdbe5504..98a4b22765 100755 --- a/build.sh +++ b/build.sh @@ -1,199 +1,8 @@ #!/usr/bin/env bash set -euo pipefail - -# -# variables -# - -RESET="\033[0m" -RED="\033[0;31m" -MAGENTA="\033[0;95m" DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -[ -z "${DOTNET_HOME:-}" ] && DOTNET_HOME="$HOME/.dotnet" -config_file="$DIR/version.xml" -verbose=false -update=false -repo_path="$DIR" -channel='' -tools_source='' -# -# Functions -# -__usage() { - echo "Usage: $(basename "${BASH_SOURCE[0]}") [options] [[--] ...]" - echo "" - echo "Arguments:" - echo " ... Arguments passed to MSBuild. Variable number of arguments allowed." - echo "" - echo "Options:" - echo " --verbose Show verbose output." - echo " -c|--channel The channel of KoreBuild to download. Overrides the value from the config file.." - echo " --config-file TThe path to the configuration file that stores values. Defaults to version.xml." - echo " -d|--dotnet-home The directory where .NET Core tools will be stored. Defaults to '\$DOTNET_HOME' or '\$HOME/.dotnet." - echo " --path The directory to build. Defaults to the directory containing the script." - echo " -s|--tools-source The base url where build tools can be downloaded. Overrides the value from the config file." - echo " -u|--update Update to the latest KoreBuild even if the lock file is present." - echo "" - echo "Description:" - echo " This function will create a file \$DIR/korebuild-lock.txt. This lock file can be committed to source, but does not have to be." - echo " When the lockfile is not present, KoreBuild will create one using latest available version from \$channel." - - if [[ "${1:-}" != '--no-exit' ]]; then - exit 2 - fi -} - -get_korebuild() { - local version - local lock_file="$repo_path/korebuild-lock.txt" - if [ ! -f "$lock_file" ] || [ "$update" = true ]; then - __get_remote_file "$tools_source/korebuild/channels/$channel/latest.txt" "$lock_file" - fi - version="$(grep 'version:*' -m 1 "$lock_file")" - if [[ "$version" == '' ]]; then - __error "Failed to parse version from $lock_file. Expected a line that begins with 'version:'" - return 1 - fi - version="$(echo "${version#version:}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" - local korebuild_path="$DOTNET_HOME/buildtools/korebuild/$version" - - { - if [ ! -d "$korebuild_path" ]; then - mkdir -p "$korebuild_path" - local remote_path="$tools_source/korebuild/artifacts/$version/korebuild.$version.zip" - tmpfile="$(mktemp)" - echo -e "${MAGENTA}Downloading KoreBuild ${version}${RESET}" - if __get_remote_file "$remote_path" "$tmpfile"; then - unzip -q -d "$korebuild_path" "$tmpfile" - fi - rm "$tmpfile" || true - fi - - source "$korebuild_path/KoreBuild.sh" - } || { - if [ -d "$korebuild_path" ]; then - echo "Cleaning up after failed installation" - rm -rf "$korebuild_path" || true - fi - return 1 - } -} - -__error() { - echo -e "${RED}$*${RESET}" 1>&2 -} - -__machine_has() { - hash "$1" > /dev/null 2>&1 - return $? -} - -__get_remote_file() { - local remote_path=$1 - local local_path=$2 - - if [[ "$remote_path" != 'http'* ]]; then - cp "$remote_path" "$local_path" - return 0 - fi - - local failed=false - if __machine_has wget; then - wget --tries 10 --quiet -O "$local_path" "$remote_path" || failed=true - else - failed=true - fi - - if [ "$failed" = true ] && __machine_has curl; then - failed=false - curl --retry 10 -sSL -f --create-dirs -o "$local_path" "$remote_path" || failed=true - fi - - if [ "$failed" = true ]; then - __error "Download failed: $remote_path" 1>&2 - return 1 - fi -} - -__read_dom () { local IFS=\> ; read -r -d \< ENTITY CONTENT ;} - -# -# main -# - -while [[ $# -gt 0 ]]; do - case $1 in - -\?|-h|--help) - __usage --no-exit - exit 0 - ;; - -c|--channel|-Channel) - shift - channel="${1:-}" - [ -z "$channel" ] && __usage - ;; - --config-file|-ConfigFile) - shift - config_file="${1:-}" - [ -z "$config_file" ] && __usage - ;; - -d|--dotnet-home|-DotNetHome) - shift - DOTNET_HOME="${1:-}" - [ -z "$DOTNET_HOME" ] && __usage - ;; - --path|-Path) - shift - repo_path="${1:-}" - [ -z "$repo_path" ] && __usage - ;; - -s|--tools-source|-ToolsSource) - shift - tools_source="${1:-}" - [ -z "$tools_source" ] && __usage - ;; - -u|--update|-Update) - update=true - ;; - --verbose|-Verbose) - verbose=true - ;; - --) - shift - break - ;; - *) - break - ;; - esac - shift -done - -if ! __machine_has unzip; then - __error 'Missing required command: unzip' - exit 1 -fi - -if ! __machine_has curl && ! __machine_has wget; then - __error 'Missing required command. Either wget or curl is required.' - exit 1 -fi - -if [ -f "$config_file" ]; then - comment=false - while __read_dom; do - if [ "$comment" = true ]; then [[ $CONTENT == *'-->'* ]] && comment=false ; continue; fi - if [[ $ENTITY == '!--'* ]]; then comment=true; continue; fi - if [ -z "$channel" ] && [[ $ENTITY == "KoreBuildChannel" ]]; then channel=$CONTENT; fi - if [ -z "$tools_source" ] && [[ $ENTITY == "KoreBuildToolsSource" ]]; then tools_source=$CONTENT; fi - done < "$config_file" -fi - -[ -z "$channel" ] && channel='dev' -[ -z "$tools_source" ] && tools_source='https://aspnetcore.blob.core.windows.net/buildtools' - -get_korebuild -install_tools "$tools_source" "$DOTNET_HOME" -invoke_repository_build "$repo_path" "$@" +# Call "sync" between "chmod" and execution to prevent "text file busy" error in Docker (aufs) +chmod +x "$DIR/run.sh"; sync +"$DIR/run.sh" default-build "$@" diff --git a/run.cmd b/run.cmd new file mode 100644 index 0000000000..d52d5c7e68 --- /dev/null +++ b/run.cmd @@ -0,0 +1,2 @@ +@ECHO OFF +PowerShell -NoProfile -NoLogo -ExecutionPolicy unrestricted -Command "[System.Threading.Thread]::CurrentThread.CurrentCulture = ''; [System.Threading.Thread]::CurrentThread.CurrentUICulture = '';& '%~dp0run.ps1' %*; exit $LASTEXITCODE" diff --git a/build.ps1 b/run.ps1 similarity index 73% rename from build.ps1 rename to run.ps1 index d5eb4d5cf2..49c2899856 100644 --- a/build.ps1 +++ b/run.ps1 @@ -3,10 +3,13 @@ <# .SYNOPSIS -Build this repository +Executes KoreBuild commands. .DESCRIPTION -Downloads korebuild if required. Then builds the repository. +Downloads korebuild if required. Then executes the KoreBuild command. To see available commands, execute with `-Command help`. + +.PARAMETER Command +The KoreBuild command to run. .PARAMETER Path The folder to build. Defaults to the folder containing this script. @@ -24,31 +27,32 @@ The base url where build tools can be downloaded. Overrides the value from the c Updates KoreBuild to the latest version even if a lock file is present. .PARAMETER ConfigFile -The path to the configuration file that stores values. Defaults to version.xml. +The path to the configuration file that stores values. Defaults to korebuild.json. -.PARAMETER MSBuildArgs -Arguments to be passed to MSBuild +.PARAMETER Arguments +Arguments to be passed to the command .NOTES This function will create a file $PSScriptRoot/korebuild-lock.txt. This lock file can be committed to source, but does not have to be. When the lockfile is not present, KoreBuild will create one using latest available version from $Channel. -The $ConfigFile is expected to be an XML file. It is optional, and the configuration values in it are optional as well. +The $ConfigFile is expected to be an JSON file. It is optional, and the configuration values in it are optional as well. Any options set +in the file are overridden by command line parameters. .EXAMPLE Example config file: -```xml - - - - dev - https://aspnetcore.blob.core.windows.net/buildtools - - +```json +{ + "$schema": "https://raw.githubusercontent.com/aspnet/BuildTools/dev/tools/korebuild.schema.json", + "channel": "dev", + "toolsSource": "https://aspnetcore.blob.core.windows.net/buildtools" +} ``` #> [CmdletBinding(PositionalBinding = $false)] param( + [Parameter(Mandatory=$true, Position = 0)] + [string]$Command, [string]$Path = $PSScriptRoot, [Alias('c')] [string]$Channel, @@ -58,9 +62,9 @@ param( [string]$ToolsSource, [Alias('u')] [switch]$Update, - [string]$ConfigFile = (Join-Path $PSScriptRoot 'version.xml'), + [string]$ConfigFile, [Parameter(ValueFromRemainingArguments = $true)] - [string[]]$MSBuildArgs + [string[]]$Arguments ) Set-StrictMode -Version 2 @@ -147,10 +151,20 @@ function Get-RemoteFile([string]$RemotePath, [string]$LocalPath) { # Load configuration or set defaults +$Path = Resolve-Path $Path +if (!$ConfigFile) { $ConfigFile = Join-Path $Path 'korebuild.json' } + if (Test-Path $ConfigFile) { - [xml] $config = Get-Content $ConfigFile - if (!($Channel)) { [string] $Channel = Select-Xml -Xml $config -XPath '/Project/PropertyGroup/KoreBuildChannel' } - if (!($ToolsSource)) { [string] $ToolsSource = Select-Xml -Xml $config -XPath '/Project/PropertyGroup/KoreBuildToolsSource' } + try { + $config = Get-Content -Raw -Encoding UTF8 -Path $ConfigFile | ConvertFrom-Json + if ($config) { + if (!($Channel) -and (Get-Member -Name 'channel' -InputObject $config)) { [string] $Channel = $config.channel } + if (!($ToolsSource) -and (Get-Member -Name 'toolsSource' -InputObject $config)) { [string] $ToolsSource = $config.toolsSource} + } + } catch { + Write-Warning "$ConfigFile could not be read. Its settings will be ignored." + Write-Warning $Error[0] + } } if (!$DotNetHome) { @@ -169,8 +183,8 @@ $korebuildPath = Get-KoreBuild Import-Module -Force -Scope Local (Join-Path $korebuildPath 'KoreBuild.psd1') try { - Install-Tools $ToolsSource $DotNetHome - Invoke-RepositoryBuild $Path @MSBuildArgs + Set-KoreBuildSettings -ToolsSource $ToolsSource -DotNetHome $DotNetHome -RepoPath $Path -ConfigFile $ConfigFile + Invoke-KoreBuildCommand $Command @Arguments } finally { Remove-Module 'KoreBuild' -ErrorAction Ignore diff --git a/run.sh b/run.sh new file mode 100755 index 0000000000..c278423acc --- /dev/null +++ b/run.sh @@ -0,0 +1,223 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# +# variables +# + +RESET="\033[0m" +RED="\033[0;31m" +YELLOW="\033[0;33m" +MAGENTA="\033[0;95m" +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +[ -z "${DOTNET_HOME:-}" ] && DOTNET_HOME="$HOME/.dotnet" +verbose=false +update=false +repo_path="$DIR" +channel='' +tools_source='' + +# +# Functions +# +__usage() { + echo "Usage: $(basename "${BASH_SOURCE[0]}") command [options] [[--] ...]" + echo "" + echo "Arguments:" + echo " command The command to be run." + echo " ... Arguments passed to the command. Variable number of arguments allowed." + echo "" + echo "Options:" + echo " --verbose Show verbose output." + echo " -c|--channel The channel of KoreBuild to download. Overrides the value from the config file.." + echo " --config-file The path to the configuration file that stores values. Defaults to korebuild.json." + echo " -d|--dotnet-home The directory where .NET Core tools will be stored. Defaults to '\$DOTNET_HOME' or '\$HOME/.dotnet." + echo " --path The directory to build. Defaults to the directory containing the script." + echo " -s|--tools-source|-ToolsSource The base url where build tools can be downloaded. Overrides the value from the config file." + echo " -u|--update Update to the latest KoreBuild even if the lock file is present." + echo "" + echo "Description:" + echo " This function will create a file \$DIR/korebuild-lock.txt. This lock file can be committed to source, but does not have to be." + echo " When the lockfile is not present, KoreBuild will create one using latest available version from \$channel." + + if [[ "${1:-}" != '--no-exit' ]]; then + exit 2 + fi +} + +get_korebuild() { + local version + local lock_file="$repo_path/korebuild-lock.txt" + if [ ! -f "$lock_file" ] || [ "$update" = true ]; then + __get_remote_file "$tools_source/korebuild/channels/$channel/latest.txt" "$lock_file" + fi + version="$(grep 'version:*' -m 1 "$lock_file")" + if [[ "$version" == '' ]]; then + __error "Failed to parse version from $lock_file. Expected a line that begins with 'version:'" + return 1 + fi + version="$(echo "${version#version:}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" + local korebuild_path="$DOTNET_HOME/buildtools/korebuild/$version" + + { + if [ ! -d "$korebuild_path" ]; then + mkdir -p "$korebuild_path" + local remote_path="$tools_source/korebuild/artifacts/$version/korebuild.$version.zip" + tmpfile="$(mktemp)" + echo -e "${MAGENTA}Downloading KoreBuild ${version}${RESET}" + if __get_remote_file "$remote_path" "$tmpfile"; then + unzip -q -d "$korebuild_path" "$tmpfile" + fi + rm "$tmpfile" || true + fi + + source "$korebuild_path/KoreBuild.sh" + } || { + if [ -d "$korebuild_path" ]; then + echo "Cleaning up after failed installation" + rm -rf "$korebuild_path" || true + fi + return 1 + } +} + +__error() { + echo -e "${RED}error: $*${RESET}" 1>&2 +} + +__warn() { + echo -e "${YELLOW}warning: $*${RESET}" +} + +__machine_has() { + hash "$1" > /dev/null 2>&1 + return $? +} + +__get_remote_file() { + local remote_path=$1 + local local_path=$2 + + if [[ "$remote_path" != 'http'* ]]; then + cp "$remote_path" "$local_path" + return 0 + fi + + local failed=false + if __machine_has wget; then + wget --tries 10 --quiet -O "$local_path" "$remote_path" || failed=true + else + failed=true + fi + + if [ "$failed" = true ] && __machine_has curl; then + failed=false + curl --retry 10 -sSL -f --create-dirs -o "$local_path" "$remote_path" || failed=true + fi + + if [ "$failed" = true ]; then + __error "Download failed: $remote_path" 1>&2 + return 1 + fi +} + +# +# main +# + +command="${1:-}" +shift + +while [[ $# -gt 0 ]]; do + case $1 in + -\?|-h|--help) + __usage --no-exit + exit 0 + ;; + -c|--channel|-Channel) + shift + channel="${1:-}" + [ -z "$channel" ] && __usage + ;; + --config-file|-ConfigFile) + shift + config_file="${1:-}" + [ -z "$config_file" ] && __usage + if [ ! -f "$config_file" ]; then + __error "Invalid value for --config-file. $config_file does not exist." + exit 1 + fi + ;; + -d|--dotnet-home|-DotNetHome) + shift + DOTNET_HOME="${1:-}" + [ -z "$DOTNET_HOME" ] && __usage + ;; + --path|-Path) + shift + repo_path="${1:-}" + [ -z "$repo_path" ] && __usage + ;; + -s|--tools-source|-ToolsSource) + shift + tools_source="${1:-}" + [ -z "$tools_source" ] && __usage + ;; + -u|--update|-Update) + update=true + ;; + --verbose|-Verbose) + verbose=true + ;; + --) + shift + break + ;; + *) + break + ;; + esac + shift +done + +if ! __machine_has unzip; then + __error 'Missing required command: unzip' + exit 1 +fi + +if ! __machine_has curl && ! __machine_has wget; then + __error 'Missing required command. Either wget or curl is required.' + exit 1 +fi + +[ -z "${config_file:-}" ] && config_file="$repo_path/korebuild.json" +if [ -f "$config_file" ]; then + if __machine_has jq ; then + if jq '.' "$config_file" >/dev/null ; then + config_channel="$(jq -r 'select(.channel!=null) | .channel' "$config_file")" + config_tools_source="$(jq -r 'select(.toolsSource!=null) | .toolsSource' "$config_file")" + else + __warn "$config_file is invalid JSON. Its settings will be ignored." + fi + elif __machine_has python ; then + if python -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'))" >/dev/null ; then + config_channel="$(python -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'));print(obj['channel'] if 'channel' in obj else '')")" + config_tools_source="$(python -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'));print(obj['toolsSource'] if 'toolsSource' in obj else '')")" + else + __warn "$config_file is invalid JSON. Its settings will be ignored." + fi + else + __warn 'Missing required command: jq or pyton. Could not parse the JSON file. Its settings will be ignored.' + fi + + [ ! -z "${config_channel:-}" ] && channel="$config_channel" + [ ! -z "${config_tools_source:-}" ] && tools_source="$config_tools_source" +fi + +[ -z "$channel" ] && channel='dev' +[ -z "$tools_source" ] && tools_source='https://aspnetcore.blob.core.windows.net/buildtools' + +get_korebuild +set_korebuildsettings "$tools_source" "$DOTNET_HOME" "$repo_path" "$config_file" +invoke_korebuild_command "$command" "$@" From 85bd16418984058a4292f8071118d2794b8d8e07 Mon Sep 17 00:00:00 2001 From: Ryan Brandenburg Date: Mon, 16 Oct 2017 12:52:21 -0700 Subject: [PATCH 133/188] Add RepositoryRoot --- Directory.Build.props | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 232d943af6..e87d90070f 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,10 +1,11 @@ - + Microsoft ASP.NET Core https://github.com/aspnet/ResponseCaching git + $(MSBuildThisFileDirectory) $(MSBuildThisFileDirectory)build\Key.snk true true From f616f3e891405d89ad21f4ff53848ca289784fb6 Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Wed, 1 Nov 2017 16:27:57 -0700 Subject: [PATCH 134/188] Pin tool and package versions to make builds more repeatable Part of aspnet/Universe#575 --- .gitignore | 1 - Directory.Build.props | 6 ++--- Directory.Build.targets | 17 ++++---------- NuGet.config | 1 + build/dependencies.props | 22 +++++++++++++++++++ build/repo.props | 9 ++++---- korebuild-lock.txt | 2 ++ korebuild.json | 4 ++++ .../ResponseCachingSample.csproj | 6 ++--- src/Directory.Build.props | 2 +- ...etCore.ResponseCaching.Abstractions.csproj | 2 +- ...icrosoft.AspNetCore.ResponseCaching.csproj | 8 +++---- test/Directory.Build.props | 2 +- ...ft.AspNetCore.ResponseCaching.Tests.csproj | 10 ++++----- version.props | 10 +++++++++ version.xml | 8 ------- 16 files changed, 66 insertions(+), 44 deletions(-) create mode 100644 build/dependencies.props create mode 100644 korebuild-lock.txt create mode 100644 korebuild.json create mode 100644 version.props delete mode 100644 version.xml diff --git a/.gitignore b/.gitignore index 1996ae5484..23826aae91 100644 --- a/.gitignore +++ b/.gitignore @@ -31,4 +31,3 @@ project.lock.json .testPublish/ launchSettings.json global.json -korebuild-lock.txt diff --git a/Directory.Build.props b/Directory.Build.props index e87d90070f..c2ce65916b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,5 +1,6 @@ - - + + + Microsoft ASP.NET Core @@ -9,7 +10,6 @@ $(MSBuildThisFileDirectory)build\Key.snk true true - $(VersionSuffix)-$(BuildNumber) true diff --git a/Directory.Build.targets b/Directory.Build.targets index bc118fd907..e83ff95e39 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -1,14 +1,5 @@ - - - - <_BootstrapperFile Condition=" $([MSBuild]::IsOSUnixLike()) ">build.sh - <_BootstrapperFile Condition="! $([MSBuild]::IsOSUnixLike()) ">build.cmd - <_BootstrapperError> - Package references have not been pinned. Run './$(_BootstrapperFile) /t:Pin'. - Also, you can run './$(_BootstrapperFile) /t:Restore' which will pin *and* restore packages. '$(_BootstrapperFile)' can be found in '$(MSBuildThisFileDirectory)'. - - - - - + + + $(MicrosoftNETCoreApp20PackageVersion) + diff --git a/NuGet.config b/NuGet.config index 20060c934e..4e8a1f6de1 100644 --- a/NuGet.config +++ b/NuGet.config @@ -3,6 +3,7 @@ + diff --git a/build/dependencies.props b/build/dependencies.props new file mode 100644 index 0000000000..13dc9fa8ef --- /dev/null +++ b/build/dependencies.props @@ -0,0 +1,22 @@ + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + + + 2.1.0-preview1-15550 + 2.1.0-preview1-27498 + 2.1.0-preview1-27498 + 2.1.0-preview1-27498 + 2.1.0-preview1-27498 + 2.1.0-preview1-27498 + 2.1.0-preview1-27498 + 2.1.0-preview1-27498 + 2.1.0-preview1-27498 + 2.1.0-preview1-27498 + 2.0.0 + 15.3.0 + 2.3.0 + 2.3.0 + + + diff --git a/build/repo.props b/build/repo.props index 13fe1c296a..b55e651b87 100644 --- a/build/repo.props +++ b/build/repo.props @@ -1,6 +1,7 @@  - - - - + + + Internal.AspNetCore.Universe.Lineup + https://dotnet.myget.org/F/aspnetcore-ci-dev/api/v3/index.json + diff --git a/korebuild-lock.txt b/korebuild-lock.txt new file mode 100644 index 0000000000..36d8056037 --- /dev/null +++ b/korebuild-lock.txt @@ -0,0 +1,2 @@ +version:2.1.0-preview1-15550 +commithash:0dd080d0d87b4d1966ec0af9961dc8bacc04f84f diff --git a/korebuild.json b/korebuild.json new file mode 100644 index 0000000000..bd5d51a51b --- /dev/null +++ b/korebuild.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://raw.githubusercontent.com/aspnet/BuildTools/dev/tools/korebuild.schema.json", + "channel": "dev" +} diff --git a/samples/ResponseCachingSample/ResponseCachingSample.csproj b/samples/ResponseCachingSample/ResponseCachingSample.csproj index b276533658..3fb1e2a958 100644 --- a/samples/ResponseCachingSample/ResponseCachingSample.csproj +++ b/samples/ResponseCachingSample/ResponseCachingSample.csproj @@ -9,8 +9,8 @@ - - - + + + diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 9d9a3de33a..1e0980f663 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -2,6 +2,6 @@ - + diff --git a/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj b/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj index 171e634aaf..28f2df9c86 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj +++ b/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj @@ -8,7 +8,7 @@ - + diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj b/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj index 9f50870312..c2547522ff 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj +++ b/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj @@ -14,10 +14,10 @@ - - - - + + + + diff --git a/test/Directory.Build.props b/test/Directory.Build.props index 9d9a3de33a..1e0980f663 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -2,6 +2,6 @@ - + diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj b/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj index 29d5cfe758..9ffdf248a2 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj @@ -10,11 +10,11 @@ - - - - - + + + + + diff --git a/version.props b/version.props new file mode 100644 index 0000000000..5c4a7c32d1 --- /dev/null +++ b/version.props @@ -0,0 +1,10 @@ + + + 2.1.0 + preview1 + $(VersionPrefix) + $(VersionPrefix)-$(VersionSuffix)-final + t000 + $(VersionSuffix)-$(BuildNumber) + + diff --git a/version.xml b/version.xml deleted file mode 100644 index 3c05022b7d..0000000000 --- a/version.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - dev - 2.1.0 - preview1 - - From 511b9ab323f56b100dec5cddf3d718cebe5ff1fe Mon Sep 17 00:00:00 2001 From: Pranav K Date: Wed, 15 Nov 2017 14:09:58 -0800 Subject: [PATCH 135/188] Update samples and tests to target netcoreapp2.1 --- Directory.Build.props | 4 ++++ korebuild-lock.txt | 4 ++-- samples/ResponseCachingSample/ResponseCachingSample.csproj | 2 +- test/Directory.Build.props | 7 +++++++ .../Microsoft.AspNetCore.ResponseCaching.Tests.csproj | 3 +-- 5 files changed, 15 insertions(+), 5 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index c2ce65916b..39f0831964 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,4 +1,8 @@  + + diff --git a/korebuild-lock.txt b/korebuild-lock.txt index 36d8056037..95f4613014 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.1.0-preview1-15550 -commithash:0dd080d0d87b4d1966ec0af9961dc8bacc04f84f +version:2.1.0-preview1-15567 +commithash:903e3104807b1bb8cddd28bdef205b1e2dc021d1 diff --git a/samples/ResponseCachingSample/ResponseCachingSample.csproj b/samples/ResponseCachingSample/ResponseCachingSample.csproj index 3fb1e2a958..3739141e06 100644 --- a/samples/ResponseCachingSample/ResponseCachingSample.csproj +++ b/samples/ResponseCachingSample/ResponseCachingSample.csproj @@ -1,7 +1,7 @@  - netcoreapp2.0 + netcoreapp2.1 diff --git a/test/Directory.Build.props b/test/Directory.Build.props index 1e0980f663..270e1fa209 100644 --- a/test/Directory.Build.props +++ b/test/Directory.Build.props @@ -1,6 +1,13 @@  + + netcoreapp2.1 + $(DeveloperBuildTestTfms) + netcoreapp2.1;netcoreapp2.0 + $(StandardTestTfms);net461 + + diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj b/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj index 9ffdf248a2..79223f0c73 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj @@ -1,8 +1,7 @@  - netcoreapp2.0;net461 - netcoreapp2.0 + $(StandardTestTfms) From 9cea093888d4df531f498385193383e770e51b56 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Fri, 17 Nov 2017 13:00:26 -0800 Subject: [PATCH 136/188] Use MicrosoftNETCoreApp21PackageVersion to determine the runtime framework in netcoreapp2.1 --- Directory.Build.targets | 1 + build/dependencies.props | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/Directory.Build.targets b/Directory.Build.targets index e83ff95e39..894b1d0cf8 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -1,5 +1,6 @@  $(MicrosoftNETCoreApp20PackageVersion) + $(MicrosoftNETCoreApp21PackageVersion) diff --git a/build/dependencies.props b/build/dependencies.props index 13dc9fa8ef..55a4d08b0b 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -1,4 +1,4 @@ - + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) @@ -14,6 +14,7 @@ 2.1.0-preview1-27498 2.1.0-preview1-27498 2.0.0 + 2.1.0-preview1-25907-02 15.3.0 2.3.0 2.3.0 From 19e943e6b9023408d98428e9b0dccca752e9f0a9 Mon Sep 17 00:00:00 2001 From: John Luo Date: Mon, 6 Nov 2017 14:12:28 -0800 Subject: [PATCH 137/188] Update base key - Add Scheme, Host, Port and PathBase --- .../Internal/ResponseCachingKeyProvider.cs | 15 +++++++++++---- .../Internal/StringBuilderExtensions.cs | 10 ++++++---- .../ResponseCachingKeyProviderTests.cs | 8 ++++---- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingKeyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingKeyProvider.cs index 4cb9fcf604..fe010f7cc4 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingKeyProvider.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingKeyProvider.cs @@ -39,7 +39,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal return new string[] { CreateStorageVaryByKey(context) }; } - // GET/PATH + // GETSCHEMEHOST:PORT/PATHBASE/PATH public string CreateBaseKey(ResponseCachingContext context) { if (context == null) @@ -54,15 +54,22 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal { builder .AppendUpperInvariant(request.Method) - .Append(KeyDelimiter); + .Append(KeyDelimiter) + .AppendUpperInvariant(request.Scheme) + .Append(KeyDelimiter) + .AppendUpperInvariant(request.Host.Value); if (_options.UseCaseSensitivePaths) { - builder.Append(request.Path.Value); + builder + .Append(request.PathBase.Value) + .Append(request.Path.Value); } else { - builder.AppendUpperInvariant(request.Path.Value); + builder + .AppendUpperInvariant(request.PathBase.Value) + .AppendUpperInvariant(request.Path.Value); } return builder.ToString(); diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/StringBuilderExtensions.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/StringBuilderExtensions.cs index fc718ffeb0..d533fbf870 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/StringBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/StringBuilderExtensions.cs @@ -1,7 +1,6 @@ // 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.Text; namespace Microsoft.AspNetCore.ResponseCaching.Internal @@ -10,10 +9,13 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal { internal static StringBuilder AppendUpperInvariant(this StringBuilder builder, string value) { - builder.EnsureCapacity(builder.Length + value.Length); - for (var i = 0; i < value.Length; i++) + if (string.IsNullOrEmpty(value)) { - builder.Append(char.ToUpperInvariant(value[i])); + builder.EnsureCapacity(builder.Length + value.Length); + for (var i = 0; i < value.Length; i++) + { + builder.Append(char.ToUpperInvariant(value[i])); + } } return builder; diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingKeyProviderTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingKeyProviderTests.cs index dbe9996e56..1a46e23862 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingKeyProviderTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingKeyProviderTests.cs @@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests private static readonly char KeyDelimiter = '\x1e'; [Fact] - public void ResponseCachingKeyProvider_CreateStorageBaseKey_IncludesOnlyNormalizedMethodAndPath() + public void ResponseCachingKeyProvider_CreateStorageBaseKey_IncludesOnlyNormalizedMethodSchemeHostPortAndPath() { var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); var context = TestUtils.CreateTestContext(); @@ -24,7 +24,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.HttpContext.Request.PathBase = "/pathBase"; context.HttpContext.Request.QueryString = new QueryString("?query.Key=a&query.Value=b"); - Assert.Equal($"HEAD{KeyDelimiter}/PATH/SUBPATH", cacheKeyProvider.CreateBaseKey(context)); + Assert.Equal($"HEAD{KeyDelimiter}HTTPS{KeyDelimiter}EXAMPLE.COM:80/PATHBASE/PATH/SUBPATH", cacheKeyProvider.CreateBaseKey(context)); } [Fact] @@ -38,7 +38,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.HttpContext.Request.Method = HttpMethods.Get; context.HttpContext.Request.Path = "/Path"; - Assert.Equal($"{HttpMethods.Get}{KeyDelimiter}/PATH", cacheKeyProvider.CreateBaseKey(context)); + Assert.Equal($"{HttpMethods.Get}{KeyDelimiter}{KeyDelimiter}/PATH", cacheKeyProvider.CreateBaseKey(context)); } [Fact] @@ -52,7 +52,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests context.HttpContext.Request.Method = HttpMethods.Get; context.HttpContext.Request.Path = "/Path"; - Assert.Equal($"{HttpMethods.Get}{KeyDelimiter}/Path", cacheKeyProvider.CreateBaseKey(context)); + Assert.Equal($"{HttpMethods.Get}{KeyDelimiter}{KeyDelimiter}/Path", cacheKeyProvider.CreateBaseKey(context)); } [Fact] From a24ae8bd162ca12ee5582eee1d3f13ad3cb7d49d Mon Sep 17 00:00:00 2001 From: John Luo Date: Mon, 20 Nov 2017 17:19:50 -0800 Subject: [PATCH 138/188] Typo while correcting for feedback --- .../Internal/StringBuilderExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/StringBuilderExtensions.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/StringBuilderExtensions.cs index d533fbf870..98cfa7e172 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/StringBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/StringBuilderExtensions.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal { internal static StringBuilder AppendUpperInvariant(this StringBuilder builder, string value) { - if (string.IsNullOrEmpty(value)) + if (!string.IsNullOrEmpty(value)) { builder.EnsureCapacity(builder.Length + value.Length); for (var i = 0; i < value.Length; i++) From 445de58e89af5259c353cd7b78bc944d4774d70b Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Mon, 20 Nov 2017 12:18:31 -0800 Subject: [PATCH 139/188] Use MSBuild to set NuGet feeds instead of NuGet.config --- Directory.Build.props | 1 + NuGet.config | 4 +--- build/sources.props | 16 ++++++++++++++++ 3 files changed, 18 insertions(+), 3 deletions(-) create mode 100644 build/sources.props diff --git a/Directory.Build.props b/Directory.Build.props index 39f0831964..10c6cc3933 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -5,6 +5,7 @@ + Microsoft ASP.NET Core diff --git a/NuGet.config b/NuGet.config index 4e8a1f6de1..e32bddfd51 100644 --- a/NuGet.config +++ b/NuGet.config @@ -2,8 +2,6 @@ - - - + diff --git a/build/sources.props b/build/sources.props new file mode 100644 index 0000000000..c03f3ddb60 --- /dev/null +++ b/build/sources.props @@ -0,0 +1,16 @@ + + + + + $(DotNetRestoreSources) + + $(RestoreSources); + https://dotnet.myget.org/F/aspnetcore-ci-dev/api/v3/index.json; + https://dotnet.myget.org/F/aspnetcore-tools/api/v3/index.json; + + + $(RestoreSources); + https://api.nuget.org/v3/index.json; + + + From dc0639d9883afa1e1971309e93892e9a5942a036 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Tue, 21 Nov 2017 15:48:45 -0800 Subject: [PATCH 140/188] Replace aspnetcore-ci-dev feed with aspnetcore-dev --- build/dependencies.props | 22 +++++++++++----------- build/repo.props | 2 +- build/sources.props | 2 +- korebuild-lock.txt | 4 ++-- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index 55a4d08b0b..5117705186 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -1,18 +1,18 @@ - + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 2.1.0-preview1-15550 - 2.1.0-preview1-27498 - 2.1.0-preview1-27498 - 2.1.0-preview1-27498 - 2.1.0-preview1-27498 - 2.1.0-preview1-27498 - 2.1.0-preview1-27498 - 2.1.0-preview1-27498 - 2.1.0-preview1-27498 - 2.1.0-preview1-27498 + 2.1.0-preview1-15576 + 2.1.0-preview1-27644 + 2.1.0-preview1-27644 + 2.1.0-preview1-27644 + 2.1.0-preview1-27644 + 2.1.0-preview1-27644 + 2.1.0-preview1-27644 + 2.1.0-preview1-27644 + 2.1.0-preview1-27644 + 2.1.0-preview1-27644 2.0.0 2.1.0-preview1-25907-02 15.3.0 diff --git a/build/repo.props b/build/repo.props index b55e651b87..07c5f08325 100644 --- a/build/repo.props +++ b/build/repo.props @@ -2,6 +2,6 @@ Internal.AspNetCore.Universe.Lineup - https://dotnet.myget.org/F/aspnetcore-ci-dev/api/v3/index.json + https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json diff --git a/build/sources.props b/build/sources.props index c03f3ddb60..9feff29d09 100644 --- a/build/sources.props +++ b/build/sources.props @@ -5,7 +5,7 @@ $(DotNetRestoreSources) $(RestoreSources); - https://dotnet.myget.org/F/aspnetcore-ci-dev/api/v3/index.json; + https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json; https://dotnet.myget.org/F/aspnetcore-tools/api/v3/index.json; diff --git a/korebuild-lock.txt b/korebuild-lock.txt index 95f4613014..1a99066b7c 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.1.0-preview1-15567 -commithash:903e3104807b1bb8cddd28bdef205b1e2dc021d1 +version:2.1.0-preview1-15576 +commithash:2f3856d2ba4f659fcb9253215b83946a06794a27 From 1219e97e27b923e0cd3fb01fe004c541dbd6749b Mon Sep 17 00:00:00 2001 From: John Luo Date: Tue, 28 Nov 2017 15:42:52 -0800 Subject: [PATCH 141/188] Avoid LOH Limit is 85000 bytes not 85*1024 bytes --- .../Streams/StreamUtilities.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Streams/StreamUtilities.cs b/src/Microsoft.AspNetCore.ResponseCaching/Streams/StreamUtilities.cs index 4ce5a4ebe0..d128a9f8f2 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Streams/StreamUtilities.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Streams/StreamUtilities.cs @@ -10,10 +10,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal internal static class StreamUtilities { /// - /// The segment size for buffering the response body in bytes. The default is set to 84 KB. + /// The segment size for buffering the response body in bytes. The default is set to 80 KB (81920 Bytes) to avoid allocations on the LOH. /// // Internal for testing - internal static int BodySegmentSize { get; set; } = 84 * 1024; + internal static int BodySegmentSize { get; set; } = 81920; internal static IAsyncResult ToIAsyncResult(Task task, AsyncCallback callback, object state) { From 4ca0c362a5363236a85f2991a62474cb73aaa170 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Wed, 29 Nov 2017 14:09:29 -0800 Subject: [PATCH 142/188] Specify runtime versions to install --- build/repo.props | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/build/repo.props b/build/repo.props index 07c5f08325..78b0ce5879 100644 --- a/build/repo.props +++ b/build/repo.props @@ -1,7 +1,14 @@  + + Internal.AspNetCore.Universe.Lineup https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json + + + + + From 52e8ffefb9b22c7c591aec15845baf07ed99356c Mon Sep 17 00:00:00 2001 From: Ryan Brandenburg Date: Fri, 1 Dec 2017 10:26:42 -0800 Subject: [PATCH 143/188] Update bootstrappers --- run.ps1 | 17 +++++++++++------ run.sh | 30 +++++++++++++++++++----------- 2 files changed, 30 insertions(+), 17 deletions(-) diff --git a/run.ps1 b/run.ps1 index 49c2899856..27dcf848f8 100644 --- a/run.ps1 +++ b/run.ps1 @@ -29,6 +29,9 @@ Updates KoreBuild to the latest version even if a lock file is present. .PARAMETER ConfigFile The path to the configuration file that stores values. Defaults to korebuild.json. +.PARAMETER ToolsSourceSuffix +The Suffix to append to the end of the ToolsSource. Useful for query strings in blob stores. + .PARAMETER Arguments Arguments to be passed to the command @@ -51,7 +54,7 @@ Example config file: #> [CmdletBinding(PositionalBinding = $false)] param( - [Parameter(Mandatory=$true, Position = 0)] + [Parameter(Mandatory = $true, Position = 0)] [string]$Command, [string]$Path = $PSScriptRoot, [Alias('c')] @@ -63,6 +66,7 @@ param( [Alias('u')] [switch]$Update, [string]$ConfigFile, + [string]$ToolsSourceSuffix, [Parameter(ValueFromRemainingArguments = $true)] [string[]]$Arguments ) @@ -79,7 +83,7 @@ function Get-KoreBuild { $lockFile = Join-Path $Path 'korebuild-lock.txt' if (!(Test-Path $lockFile) -or $Update) { - Get-RemoteFile "$ToolsSource/korebuild/channels/$Channel/latest.txt" $lockFile + Get-RemoteFile "$ToolsSource/korebuild/channels/$Channel/latest.txt" $lockFile $ToolsSourceSuffix } $version = Get-Content $lockFile | Where-Object { $_ -like 'version:*' } | Select-Object -first 1 @@ -96,7 +100,7 @@ function Get-KoreBuild { try { $tmpfile = Join-Path ([IO.Path]::GetTempPath()) "KoreBuild-$([guid]::NewGuid()).zip" - Get-RemoteFile $remotePath $tmpfile + Get-RemoteFile $remotePath $tmpfile $ToolsSourceSuffix if (Get-Command -Name 'Expand-Archive' -ErrorAction Ignore) { # Use built-in commands where possible as they are cross-plat compatible Expand-Archive -Path $tmpfile -DestinationPath $korebuildPath @@ -124,7 +128,7 @@ function Join-Paths([string]$path, [string[]]$childPaths) { return $path } -function Get-RemoteFile([string]$RemotePath, [string]$LocalPath) { +function Get-RemoteFile([string]$RemotePath, [string]$LocalPath, [string]$RemoteSuffix) { if ($RemotePath -notlike 'http*') { Copy-Item $RemotePath $LocalPath return @@ -134,7 +138,7 @@ function Get-RemoteFile([string]$RemotePath, [string]$LocalPath) { while ($retries -gt 0) { $retries -= 1 try { - Invoke-WebRequest -UseBasicParsing -Uri $RemotePath -OutFile $LocalPath + Invoke-WebRequest -UseBasicParsing -Uri $($RemotePath + $RemoteSuffix) -OutFile $LocalPath return } catch { @@ -161,7 +165,8 @@ if (Test-Path $ConfigFile) { if (!($Channel) -and (Get-Member -Name 'channel' -InputObject $config)) { [string] $Channel = $config.channel } if (!($ToolsSource) -and (Get-Member -Name 'toolsSource' -InputObject $config)) { [string] $ToolsSource = $config.toolsSource} } - } catch { + } + catch { Write-Warning "$ConfigFile could not be read. Its settings will be ignored." Write-Warning $Error[0] } diff --git a/run.sh b/run.sh index c278423acc..834961fc3a 100755 --- a/run.sh +++ b/run.sh @@ -17,6 +17,7 @@ update=false repo_path="$DIR" channel='' tools_source='' +tools_source_suffix='' # # Functions @@ -29,13 +30,14 @@ __usage() { echo " ... Arguments passed to the command. Variable number of arguments allowed." echo "" echo "Options:" - echo " --verbose Show verbose output." - echo " -c|--channel The channel of KoreBuild to download. Overrides the value from the config file.." - echo " --config-file The path to the configuration file that stores values. Defaults to korebuild.json." - echo " -d|--dotnet-home The directory where .NET Core tools will be stored. Defaults to '\$DOTNET_HOME' or '\$HOME/.dotnet." - echo " --path The directory to build. Defaults to the directory containing the script." - echo " -s|--tools-source|-ToolsSource The base url where build tools can be downloaded. Overrides the value from the config file." - echo " -u|--update Update to the latest KoreBuild even if the lock file is present." + echo " --verbose Show verbose output." + echo " -c|--channel The channel of KoreBuild to download. Overrides the value from the config file.." + echo " --config-file The path to the configuration file that stores values. Defaults to korebuild.json." + echo " -d|--dotnet-home The directory where .NET Core tools will be stored. Defaults to '\$DOTNET_HOME' or '\$HOME/.dotnet." + echo " --path The directory to build. Defaults to the directory containing the script." + echo " -s|--tools-source|-ToolsSource The base url where build tools can be downloaded. Overrides the value from the config file." + echo " --tools-source-suffix|-ToolsSourceSuffix The suffix to append to tools-source. Useful for query strings." + echo " -u|--update Update to the latest KoreBuild even if the lock file is present." echo "" echo "Description:" echo " This function will create a file \$DIR/korebuild-lock.txt. This lock file can be committed to source, but does not have to be." @@ -50,7 +52,7 @@ get_korebuild() { local version local lock_file="$repo_path/korebuild-lock.txt" if [ ! -f "$lock_file" ] || [ "$update" = true ]; then - __get_remote_file "$tools_source/korebuild/channels/$channel/latest.txt" "$lock_file" + __get_remote_file "$tools_source/korebuild/channels/$channel/latest.txt" "$lock_file" "$tools_source_suffix" fi version="$(grep 'version:*' -m 1 "$lock_file")" if [[ "$version" == '' ]]; then @@ -66,7 +68,7 @@ get_korebuild() { local remote_path="$tools_source/korebuild/artifacts/$version/korebuild.$version.zip" tmpfile="$(mktemp)" echo -e "${MAGENTA}Downloading KoreBuild ${version}${RESET}" - if __get_remote_file "$remote_path" "$tmpfile"; then + if __get_remote_file "$remote_path" "$tmpfile" "$tools_source_suffix"; then unzip -q -d "$korebuild_path" "$tmpfile" fi rm "$tmpfile" || true @@ -98,6 +100,7 @@ __machine_has() { __get_remote_file() { local remote_path=$1 local local_path=$2 + local remote_path_suffix=$3 if [[ "$remote_path" != 'http'* ]]; then cp "$remote_path" "$local_path" @@ -106,14 +109,14 @@ __get_remote_file() { local failed=false if __machine_has wget; then - wget --tries 10 --quiet -O "$local_path" "$remote_path" || failed=true + wget --tries 10 --quiet -O "$local_path" "${remote_path}${remote_path_suffix}" || failed=true else failed=true fi if [ "$failed" = true ] && __machine_has curl; then failed=false - curl --retry 10 -sSL -f --create-dirs -o "$local_path" "$remote_path" || failed=true + curl --retry 10 -sSL -f --create-dirs -o "$local_path" "${remote_path}${remote_path_suffix}" || failed=true fi if [ "$failed" = true ]; then @@ -164,6 +167,11 @@ while [[ $# -gt 0 ]]; do tools_source="${1:-}" [ -z "$tools_source" ] && __usage ;; + --tools-source-suffix|-ToolsSourceSuffix) + shift + tools_source_suffix="${1:-}" + [ -z "$tools_source_suffix" ] && __usage + ;; -u|--update|-Update) update=true ;; From 620d7ecd9acbadbafd9d9b204a84a5d7abd674b1 Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Sun, 10 Dec 2017 13:40:28 -0800 Subject: [PATCH 144/188] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 26 +++++++++++++------------- korebuild-lock.txt | 4 ++-- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index 5117705186..fef318564e 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,21 +3,21 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 2.1.0-preview1-15576 - 2.1.0-preview1-27644 - 2.1.0-preview1-27644 - 2.1.0-preview1-27644 - 2.1.0-preview1-27644 - 2.1.0-preview1-27644 - 2.1.0-preview1-27644 - 2.1.0-preview1-27644 - 2.1.0-preview1-27644 - 2.1.0-preview1-27644 + 2.1.0-preview1-15618 + 2.1.0-preview1-27773 + 2.1.0-preview1-27773 + 2.1.0-preview1-27773 + 2.1.0-preview1-27773 + 2.1.0-preview1-27773 + 2.1.0-preview1-27773 + 2.1.0-preview1-27773 + 2.1.0-preview1-27773 + 2.1.0-preview1-27773 2.0.0 - 2.1.0-preview1-25907-02 + 2.1.0-preview1-25915-01 15.3.0 - 2.3.0 - 2.3.0 + 2.3.1 + 2.3.1 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index 1a99066b7c..e7cce93009 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.1.0-preview1-15576 -commithash:2f3856d2ba4f659fcb9253215b83946a06794a27 +version:2.1.0-preview1-15618 +commithash:00ce1383114015fe89b221146036e59e6bc11219 From 7aba56291eca609aa8fa39bb076feebda6033b25 Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Wed, 13 Dec 2017 21:35:47 +0000 Subject: [PATCH 145/188] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 22 +++++++++++----------- korebuild-lock.txt | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index fef318564e..bdb53d8cc3 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,18 +3,18 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 2.1.0-preview1-15618 - 2.1.0-preview1-27773 - 2.1.0-preview1-27773 - 2.1.0-preview1-27773 - 2.1.0-preview1-27773 - 2.1.0-preview1-27773 - 2.1.0-preview1-27773 - 2.1.0-preview1-27773 - 2.1.0-preview1-27773 - 2.1.0-preview1-27773 + 2.1.0-preview1-15626 + 2.1.0-preview1-27807 + 2.1.0-preview1-27807 + 2.1.0-preview1-27807 + 2.1.0-preview1-27807 + 2.1.0-preview1-27807 + 2.1.0-preview1-27807 + 2.1.0-preview1-27807 + 2.1.0-preview1-27807 + 2.1.0-preview1-27807 2.0.0 - 2.1.0-preview1-25915-01 + 2.1.0-preview1-26008-01 15.3.0 2.3.1 2.3.1 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index e7cce93009..8d52a6128c 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.1.0-preview1-15618 -commithash:00ce1383114015fe89b221146036e59e6bc11219 +version:2.1.0-preview1-15626 +commithash:fd6410e9c90c428bc01238372303ad09cb9ec889 From 8d91714ed6564628c08f71bda3d11bac32bfb539 Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Mon, 18 Dec 2017 17:49:06 -0800 Subject: [PATCH 146/188] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index bdb53d8cc3..e4e0bb2a21 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -4,17 +4,17 @@ 2.1.0-preview1-15626 - 2.1.0-preview1-27807 - 2.1.0-preview1-27807 - 2.1.0-preview1-27807 - 2.1.0-preview1-27807 - 2.1.0-preview1-27807 - 2.1.0-preview1-27807 - 2.1.0-preview1-27807 - 2.1.0-preview1-27807 - 2.1.0-preview1-27807 + 2.1.0-preview1-27849 + 2.1.0-preview1-27849 + 2.1.0-preview1-27849 + 2.1.0-preview1-27849 + 2.1.0-preview1-27849 + 2.1.0-preview1-27849 + 2.1.0-preview1-27849 + 2.1.0-preview1-27849 + 2.1.0-preview1-27849 2.0.0 - 2.1.0-preview1-26008-01 + 2.1.0-preview1-26016-05 15.3.0 2.3.1 2.3.1 From 99fff3c7e84a57b60d18f10dccb1e414e7ab37fb Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Sun, 31 Dec 2017 21:52:05 +0000 Subject: [PATCH 147/188] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 20 ++++++++++---------- korebuild-lock.txt | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index e4e0bb2a21..7b89dbd2eb 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,16 +3,16 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 2.1.0-preview1-15626 - 2.1.0-preview1-27849 - 2.1.0-preview1-27849 - 2.1.0-preview1-27849 - 2.1.0-preview1-27849 - 2.1.0-preview1-27849 - 2.1.0-preview1-27849 - 2.1.0-preview1-27849 - 2.1.0-preview1-27849 - 2.1.0-preview1-27849 + 2.1.0-preview1-15651 + 2.1.0-preview1-27942 + 2.1.0-preview1-27942 + 2.1.0-preview1-27942 + 2.1.0-preview1-27942 + 2.1.0-preview1-27942 + 2.1.0-preview1-27942 + 2.1.0-preview1-27942 + 2.1.0-preview1-27942 + 2.1.0-preview1-27942 2.0.0 2.1.0-preview1-26016-05 15.3.0 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index 8d52a6128c..7c2e97aa79 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.1.0-preview1-15626 -commithash:fd6410e9c90c428bc01238372303ad09cb9ec889 +version:2.1.0-preview1-15651 +commithash:ebf2365121c2c6a6a0fbfa9b0f37bb5effc89323 From 80575be9c82b0bbf740789985d28f820c330f6ab Mon Sep 17 00:00:00 2001 From: Eilon Lipton Date: Tue, 2 Jan 2018 14:10:58 -0800 Subject: [PATCH 148/188] Create ISSUE_TEMPLATE.md --- .github/ISSUE_TEMPLATE.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE.md diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000000..101a084f0a --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,3 @@ +THIS ISSUE TRACKER IS CLOSED - please log new issues here: https://github.com/aspnet/Home/issues + +For information about this change, see https://github.com/aspnet/Announcements/issues/283 From ba63da88fdeb19c18ff9605dd5965529c08d364d Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Thu, 4 Jan 2018 01:58:30 +0000 Subject: [PATCH 149/188] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index 7b89dbd2eb..96e316d582 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -4,15 +4,15 @@ 2.1.0-preview1-15651 - 2.1.0-preview1-27942 - 2.1.0-preview1-27942 - 2.1.0-preview1-27942 - 2.1.0-preview1-27942 - 2.1.0-preview1-27942 - 2.1.0-preview1-27942 - 2.1.0-preview1-27942 - 2.1.0-preview1-27942 - 2.1.0-preview1-27942 + 2.1.0-preview1-27965 + 2.1.0-preview1-27965 + 2.1.0-preview1-27965 + 2.1.0-preview1-27965 + 2.1.0-preview1-27965 + 2.1.0-preview1-27965 + 2.1.0-preview1-27965 + 2.1.0-preview1-27965 + 2.1.0-preview1-27965 2.0.0 2.1.0-preview1-26016-05 15.3.0 From 5b239d44c0d749600a60ccd2951e0a67edd11a10 Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Sat, 6 Jan 2018 15:30:41 -0800 Subject: [PATCH 150/188] Update dependencies.props [auto-updated: dependencies] --- korebuild-lock.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/korebuild-lock.txt b/korebuild-lock.txt index 7c2e97aa79..2146d006d7 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.1.0-preview1-15651 -commithash:ebf2365121c2c6a6a0fbfa9b0f37bb5effc89323 +version:2.1.0-preview1-15661 +commithash:c9349d4c8a495d3085d9b879214d80f2f45e2193 From f9ce4f22f164477e8958d1c44459dae2777f43fc Mon Sep 17 00:00:00 2001 From: Pranav K Date: Tue, 23 Jan 2018 15:32:35 -0800 Subject: [PATCH 151/188] Branching for 2.1.0-preview1 --- build/dependencies.props | 22 +++++++++++----------- build/repo.props | 4 ++-- build/sources.props | 4 ++-- korebuild-lock.txt | 4 ++-- 4 files changed, 17 insertions(+), 17 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index 96e316d582..69e6cdf1ce 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,18 +3,18 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 2.1.0-preview1-15651 - 2.1.0-preview1-27965 - 2.1.0-preview1-27965 - 2.1.0-preview1-27965 - 2.1.0-preview1-27965 - 2.1.0-preview1-27965 - 2.1.0-preview1-27965 - 2.1.0-preview1-27965 - 2.1.0-preview1-27965 - 2.1.0-preview1-27965 + 2.1.0-preview1-15679 + 2.1.0-preview1-28153 + 2.1.0-preview1-28153 + 2.1.0-preview1-28153 + 2.1.0-preview1-28153 + 2.1.0-preview1-28153 + 2.1.0-preview1-28153 + 2.1.0-preview1-28153 + 2.1.0-preview1-28153 + 2.1.0-preview1-28153 2.0.0 - 2.1.0-preview1-26016-05 + 2.1.0-preview1-26115-03 15.3.0 2.3.1 2.3.1 diff --git a/build/repo.props b/build/repo.props index 78b0ce5879..d94ff7d00d 100644 --- a/build/repo.props +++ b/build/repo.props @@ -1,10 +1,10 @@ - + Internal.AspNetCore.Universe.Lineup - https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json + https://dotnet.myget.org/F/aspnetcore-release/api/v3/index.json diff --git a/build/sources.props b/build/sources.props index 9feff29d09..5d66393335 100644 --- a/build/sources.props +++ b/build/sources.props @@ -1,11 +1,11 @@ - + $(DotNetRestoreSources) $(RestoreSources); - https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json; + https://dotnet.myget.org/F/aspnetcore-release/api/v3/index.json; https://dotnet.myget.org/F/aspnetcore-tools/api/v3/index.json; diff --git a/korebuild-lock.txt b/korebuild-lock.txt index 2146d006d7..a474bc0e35 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.1.0-preview1-15661 -commithash:c9349d4c8a495d3085d9b879214d80f2f45e2193 +version:2.1.0-preview1-15679 +commithash:5347461137cb45a77ddcc0b55b2478092de43338 From fe0696c9baa02261839c77f49acb3cf0ed6813e9 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Wed, 24 Jan 2018 15:00:29 -0800 Subject: [PATCH 152/188] Updating version to preview2 --- version.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.props b/version.props index 5c4a7c32d1..370d5ababd 100644 --- a/version.props +++ b/version.props @@ -1,7 +1,7 @@ 2.1.0 - preview1 + preview2 $(VersionPrefix) $(VersionPrefix)-$(VersionSuffix)-final t000 From c749fd0a8066a8bb2c5401323592f6076af248ec Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Wed, 31 Jan 2018 15:01:13 -0800 Subject: [PATCH 153/188] Update dependencies.props to 2.1.0-preview-28193, build tools to 2.1.0-preview1-1010 [ci skip] Scripted changes: - updated travis and appveyor.yml files to only build dev, ci, and release branches - updated dependencies.props - updated korebuild-lock.txt - updated korebuild.json to release/2.1 channel --- .appveyor.yml | 15 +++++++-------- .travis.yml | 23 ++++++++++++----------- build/dependencies.props | 22 +++++++++++----------- korebuild-lock.txt | 4 ++-- korebuild.json | 4 ++-- 5 files changed, 34 insertions(+), 34 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 46038786c9..4eea96ab69 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -1,18 +1,17 @@ init: - - git config --global core.autocrlf true +- git config --global core.autocrlf true branches: only: - - master - - release - - dev - - /^(.*\/)?ci-.*$/ + - dev + - /^release\/.*$/ + - /^(.*\/)?ci-.*$/ build_script: - - ps: .\run.ps1 default-build +- ps: .\run.ps1 default-build clone_depth: 1 environment: global: DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true DOTNET_CLI_TELEMETRY_OPTOUT: 1 -test: off -deploy: off +test: 'off' +deploy: 'off' os: Visual Studio 2017 diff --git a/.travis.yml b/.travis.yml index b10be14215..64bdbb4441 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,24 +3,25 @@ sudo: false dist: trusty env: global: - - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true - - DOTNET_CLI_TELEMETRY_OPTOUT: 1 + - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + - DOTNET_CLI_TELEMETRY_OPTOUT: 1 mono: none os: - - linux - - osx +- linux +- osx osx_image: xcode8.2 addons: apt: packages: - - libunwind8 + - libunwind8 branches: only: - - master - - release - - dev - - /^(.*\/)?ci-.*$/ + - dev + - /^release\/.*$/ + - /^(.*\/)?ci-.*$/ before_install: - - if test "$TRAVIS_OS_NAME" == "osx"; then brew update; brew install openssl; ln -s /usr/local/opt/openssl/lib/libcrypto.1.0.0.dylib /usr/local/lib/; ln -s /usr/local/opt/openssl/lib/libssl.1.0.0.dylib /usr/local/lib/; fi +- if test "$TRAVIS_OS_NAME" == "osx"; then brew update; brew install openssl; ln -s + /usr/local/opt/openssl/lib/libcrypto.1.0.0.dylib /usr/local/lib/; ln -s /usr/local/opt/openssl/lib/libssl.1.0.0.dylib + /usr/local/lib/; fi script: - - ./build.sh +- ./build.sh diff --git a/build/dependencies.props b/build/dependencies.props index 69e6cdf1ce..fbd7c75ee6 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,18 +3,18 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 2.1.0-preview1-15679 - 2.1.0-preview1-28153 - 2.1.0-preview1-28153 - 2.1.0-preview1-28153 - 2.1.0-preview1-28153 - 2.1.0-preview1-28153 - 2.1.0-preview1-28153 - 2.1.0-preview1-28153 - 2.1.0-preview1-28153 - 2.1.0-preview1-28153 + 2.1.0-preview1-1010 + 2.1.0-preview1-28193 + 2.1.0-preview1-28193 + 2.1.0-preview1-28193 + 2.1.0-preview1-28193 + 2.1.0-preview1-28193 + 2.1.0-preview1-28193 + 2.1.0-preview1-28193 + 2.1.0-preview1-28193 + 2.1.0-preview1-28193 2.0.0 - 2.1.0-preview1-26115-03 + 2.1.0-preview1-26122-01 15.3.0 2.3.1 2.3.1 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index a474bc0e35..851bfbf203 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.1.0-preview1-15679 -commithash:5347461137cb45a77ddcc0b55b2478092de43338 +version:2.1.0-preview1-1010 +commithash:75ca924dfbd673c38841025b04c4dcd93b84f56d diff --git a/korebuild.json b/korebuild.json index bd5d51a51b..678d8bb948 100644 --- a/korebuild.json +++ b/korebuild.json @@ -1,4 +1,4 @@ { - "$schema": "https://raw.githubusercontent.com/aspnet/BuildTools/dev/tools/korebuild.schema.json", - "channel": "dev" + "$schema": "https://raw.githubusercontent.com/aspnet/BuildTools/release/2.1/tools/korebuild.schema.json", + "channel": "release/2.1" } From 6cdbdd883dd95ebff71d2171e4142e8968aff474 Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Thu, 1 Feb 2018 04:23:13 +0000 Subject: [PATCH 154/188] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 22 +++++++++++----------- korebuild-lock.txt | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index 96e316d582..6e3f6677a2 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,18 +3,18 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 2.1.0-preview1-15651 - 2.1.0-preview1-27965 - 2.1.0-preview1-27965 - 2.1.0-preview1-27965 - 2.1.0-preview1-27965 - 2.1.0-preview1-27965 - 2.1.0-preview1-27965 - 2.1.0-preview1-27965 - 2.1.0-preview1-27965 - 2.1.0-preview1-27965 + 2.1.0-preview2-15692 + 2.1.0-preview2-28215 + 2.1.0-preview2-28215 + 2.1.0-preview2-28215 + 2.1.0-preview2-28215 + 2.1.0-preview2-28215 + 2.1.0-preview2-28215 + 2.1.0-preview2-28215 + 2.1.0-preview2-28215 + 2.1.0-preview2-28215 2.0.0 - 2.1.0-preview1-26016-05 + 2.1.0-preview2-26130-04 15.3.0 2.3.1 2.3.1 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index 2146d006d7..232cb858c2 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.1.0-preview1-15661 -commithash:c9349d4c8a495d3085d9b879214d80f2f45e2193 +version:2.1.0-preview2-15692 +commithash:5d9f445ce3f8492451a6f461df7e739bbed6a7f8 From ae790ce91d20866f1bad58489b0c906ea4f64bef Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Sat, 3 Feb 2018 03:01:19 +0000 Subject: [PATCH 155/188] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 20 ++++++++++---------- korebuild-lock.txt | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index 6e3f6677a2..11bb2108de 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,16 +3,16 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 2.1.0-preview2-15692 - 2.1.0-preview2-28215 - 2.1.0-preview2-28215 - 2.1.0-preview2-28215 - 2.1.0-preview2-28215 - 2.1.0-preview2-28215 - 2.1.0-preview2-28215 - 2.1.0-preview2-28215 - 2.1.0-preview2-28215 - 2.1.0-preview2-28215 + 2.1.0-preview2-15694 + 2.1.0-preview2-30020 + 2.1.0-preview2-30020 + 2.1.0-preview2-30020 + 2.1.0-preview2-30020 + 2.1.0-preview2-30020 + 2.1.0-preview2-30020 + 2.1.0-preview2-30020 + 2.1.0-preview2-30020 + 2.1.0-preview2-30020 2.0.0 2.1.0-preview2-26130-04 15.3.0 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index 232cb858c2..6f294ef0e6 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.1.0-preview2-15692 -commithash:5d9f445ce3f8492451a6f461df7e739bbed6a7f8 +version:2.1.0-preview2-15694 +commithash:f61af02b48e89592c9aadb7ebaebe84228666c3b From aa5dcfe8b1b068f954bfcb78ba767f1973fa5a75 Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Fri, 9 Feb 2018 11:58:29 -0800 Subject: [PATCH 156/188] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 20 ++++++++++---------- korebuild-lock.txt | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index 11bb2108de..6234b558d6 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,16 +3,16 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 2.1.0-preview2-15694 - 2.1.0-preview2-30020 - 2.1.0-preview2-30020 - 2.1.0-preview2-30020 - 2.1.0-preview2-30020 - 2.1.0-preview2-30020 - 2.1.0-preview2-30020 - 2.1.0-preview2-30020 - 2.1.0-preview2-30020 - 2.1.0-preview2-30020 + 2.1.0-preview2-15698 + 2.1.0-preview2-30066 + 2.1.0-preview2-30066 + 2.1.0-preview2-30066 + 2.1.0-preview2-30066 + 2.1.0-preview2-30066 + 2.1.0-preview2-30066 + 2.1.0-preview2-30066 + 2.1.0-preview2-30066 + 2.1.0-preview2-30066 2.0.0 2.1.0-preview2-26130-04 15.3.0 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index 6f294ef0e6..3e2b56b91b 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.1.0-preview2-15694 -commithash:f61af02b48e89592c9aadb7ebaebe84228666c3b +version:2.1.0-preview2-15698 +commithash:7216e5068cb1957e09d45fcbe58a744dd5c2de73 From 606577778a161a27afae28521a5224279607795b Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Sun, 11 Feb 2018 12:39:15 -0800 Subject: [PATCH 157/188] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index 6234b558d6..ac248f81b3 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -4,20 +4,20 @@ 2.1.0-preview2-15698 - 2.1.0-preview2-30066 - 2.1.0-preview2-30066 - 2.1.0-preview2-30066 - 2.1.0-preview2-30066 - 2.1.0-preview2-30066 - 2.1.0-preview2-30066 - 2.1.0-preview2-30066 - 2.1.0-preview2-30066 - 2.1.0-preview2-30066 + 2.1.0-preview2-30077 + 2.1.0-preview2-30077 + 2.1.0-preview2-30077 + 2.1.0-preview2-30077 + 2.1.0-preview2-30077 + 2.1.0-preview2-30077 + 2.1.0-preview2-30077 + 2.1.0-preview2-30077 + 2.1.0-preview2-30077 2.0.0 2.1.0-preview2-26130-04 15.3.0 2.3.1 - 2.3.1 + 2.4.0-beta.1.build3945 From aa078d33b1e1b44c1923060a6316877098c78b2f Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Sun, 18 Feb 2018 12:30:17 -0800 Subject: [PATCH 158/188] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 20 ++++++++++---------- korebuild-lock.txt | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index ac248f81b3..191aaaf611 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,16 +3,16 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 2.1.0-preview2-15698 - 2.1.0-preview2-30077 - 2.1.0-preview2-30077 - 2.1.0-preview2-30077 - 2.1.0-preview2-30077 - 2.1.0-preview2-30077 - 2.1.0-preview2-30077 - 2.1.0-preview2-30077 - 2.1.0-preview2-30077 - 2.1.0-preview2-30077 + 2.1.0-preview2-15707 + 2.1.0-preview2-30131 + 2.1.0-preview2-30131 + 2.1.0-preview2-30131 + 2.1.0-preview2-30131 + 2.1.0-preview2-30131 + 2.1.0-preview2-30131 + 2.1.0-preview2-30131 + 2.1.0-preview2-30131 + 2.1.0-preview2-30131 2.0.0 2.1.0-preview2-26130-04 15.3.0 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index 3e2b56b91b..89d0ad3d15 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.1.0-preview2-15698 -commithash:7216e5068cb1957e09d45fcbe58a744dd5c2de73 +version:2.1.0-preview2-15707 +commithash:e74e53f129ab34332947fea7ac7b7591b027cb22 From 3e0e1ed24b5c8a5000a09f8652746959e8034a96 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Wed, 21 Feb 2018 18:27:11 -0800 Subject: [PATCH 159/188] Use FeatureBranchVersionSuffix when generating VersionSuffix --- version.props | 1 + 1 file changed, 1 insertion(+) diff --git a/version.props b/version.props index 370d5ababd..65c8a07e37 100644 --- a/version.props +++ b/version.props @@ -5,6 +5,7 @@ $(VersionPrefix) $(VersionPrefix)-$(VersionSuffix)-final t000 + $(VersionSuffix)-$([System.Text.RegularExpressions.Regex]::Replace('$(FeatureBranchVersionSuffix)', '[^\w-]', '-')) $(VersionSuffix)-$(BuildNumber) From c29531f2eb2f087608244021512dd310ebbb3d5b Mon Sep 17 00:00:00 2001 From: John Luo Date: Wed, 21 Feb 2018 15:03:55 -0800 Subject: [PATCH 160/188] Delimit key values for query strings --- .../Internal/ResponseCachingKeyProvider.cs | 10 ++++++++++ .../ResponseCachingKeyProviderTests.cs | 19 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingKeyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingKeyProvider.cs index fe010f7cc4..5524d8f687 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingKeyProvider.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingKeyProvider.cs @@ -15,6 +15,8 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal { // Use the record separator for delimiting components of the cache key to avoid possible collisions private static readonly char KeyDelimiter = '\x1e'; + // Use the unit separator for delimiting subcomponents of the cache key to avoid possible collisions + private static readonly char KeySubDelimiter = '\x1f'; private readonly ObjectPool _builderPool; private readonly ResponseCachingOptions _options; @@ -147,6 +149,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal for (var i = 0; i < query.Value.Count; i++) { + if (i > 0) + { + builder.Append(KeySubDelimiter); + } builder.Append(query.Value[i]); } } @@ -163,6 +169,10 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal for (var j = 0; j < queryKeyValues.Count; j++) { + if (j > 0) + { + builder.Append(KeySubDelimiter); + } builder.Append(queryKeyValues[j]); } } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingKeyProviderTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingKeyProviderTests.cs index 1a46e23862..1c3cd0e485 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingKeyProviderTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingKeyProviderTests.cs @@ -11,6 +11,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests public class ResponseCachingKeyProviderTests { private static readonly char KeyDelimiter = '\x1e'; + private static readonly char KeySubDelimiter = '\x1f'; [Fact] public void ResponseCachingKeyProvider_CreateStorageBaseKey_IncludesOnlyNormalizedMethodSchemeHostPortAndPath() @@ -143,6 +144,24 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests cacheKeyProvider.CreateStorageVaryByKey(context)); } + [Fact] + public void ResponseCachingKeyProvider_CreateStorageVaryKey_QueryKeysValuesNotConsolidated() + { + var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); + var context = TestUtils.CreateTestContext(); + context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueA&QueryA=ValueB"); + context.CachedVaryByRules = new CachedVaryByRules() + { + VaryByKeyPrefix = FastGuid.NewGuid().IdString, + QueryKeys = new string[] { "*" } + }; + + // To support case insensitivity, all query keys are converted to upper case. + // Explicit query keys uses the casing specified in the setting. + Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}QUERYA=ValueA{KeySubDelimiter}ValueB", + cacheKeyProvider.CreateStorageVaryByKey(context)); + } + [Fact] public void ResponseCachingKeyProvider_CreateStorageVaryKey_IncludesListedHeadersAndQueryKeys() { From 1437f00e848ec94e4cbfc99928b3bf14d579cede Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Mon, 26 Feb 2018 11:14:16 -0800 Subject: [PATCH 161/188] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 22 +++++++++++----------- korebuild-lock.txt | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index 191aaaf611..3cb1b746eb 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,19 +3,19 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 2.1.0-preview2-15707 - 2.1.0-preview2-30131 - 2.1.0-preview2-30131 - 2.1.0-preview2-30131 - 2.1.0-preview2-30131 - 2.1.0-preview2-30131 - 2.1.0-preview2-30131 - 2.1.0-preview2-30131 - 2.1.0-preview2-30131 - 2.1.0-preview2-30131 + 2.1.0-preview2-15721 + 2.1.0-preview2-30187 + 2.1.0-preview2-30187 + 2.1.0-preview2-30187 + 2.1.0-preview2-30187 + 2.1.0-preview2-30187 + 2.1.0-preview2-30187 + 2.1.0-preview2-30187 + 2.1.0-preview2-30187 + 2.1.0-preview2-30187 2.0.0 2.1.0-preview2-26130-04 - 15.3.0 + 15.6.0 2.3.1 2.4.0-beta.1.build3945 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index 89d0ad3d15..e6c7fddffa 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.1.0-preview2-15707 -commithash:e74e53f129ab34332947fea7ac7b7591b027cb22 +version:2.1.0-preview2-15721 +commithash:f9bb4be59e39938ec59a6975257e26099b0d03c1 From d9778252d0d499b458206dfcea09683b0c71b66f Mon Sep 17 00:00:00 2001 From: John Luo Date: Fri, 23 Feb 2018 15:09:50 -0800 Subject: [PATCH 162/188] Sort header and query values --- .../Internal/ResponseCachingKeyProvider.cs | 49 +++++++++++++++---- .../ResponseCachingKeyProviderTests.cs | 34 +++++++++++++ 2 files changed, 73 insertions(+), 10 deletions(-) diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingKeyProvider.cs b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingKeyProvider.cs index 5524d8f687..d69d9008eb 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingKeyProvider.cs +++ b/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingKeyProvider.cs @@ -82,7 +82,7 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal } } - // BaseKeyHHeaderName=HeaderValueQQueryName=QueryValue + // BaseKeyHHeaderName=HeaderValueQQueryName=QueryValue1QueryValue2 public string CreateStorageVaryByKey(ResponseCachingContext context) { if (context == null) @@ -124,9 +124,12 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal .Append(header) .Append("="); - for (var j = 0; j < headerValues.Count; j++) + var headerValuesArray = headerValues.ToArray(); + Array.Sort(headerValuesArray, StringComparer.Ordinal); + + for (var j = 0; j < headerValuesArray.Length; j++) { - builder.Append(headerValues[j]); + builder.Append(headerValuesArray[j]); } } } @@ -141,19 +144,27 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal if (varyByRules.QueryKeys.Count == 1 && string.Equals(varyByRules.QueryKeys[0], "*", StringComparison.Ordinal)) { // Vary by all available query keys - foreach (var query in context.HttpContext.Request.Query.OrderBy(q => q.Key, StringComparer.OrdinalIgnoreCase)) + var queryArray = context.HttpContext.Request.Query.ToArray(); + // Query keys are aggregated case-insensitively whereas the query values are compared ordinally. + Array.Sort(queryArray, QueryKeyComparer.OrdinalIgnoreCase); + + for (var i = 0; i < queryArray.Length; i++) { builder.Append(KeyDelimiter) - .AppendUpperInvariant(query.Key) + .AppendUpperInvariant(queryArray[i].Key) .Append("="); - for (var i = 0; i < query.Value.Count; i++) + var queryValueArray = queryArray[i].Value.ToArray(); + Array.Sort(queryValueArray, StringComparer.Ordinal); + + for (var j = 0; j < queryValueArray.Length; j++) { - if (i > 0) + if (j > 0) { builder.Append(KeySubDelimiter); } - builder.Append(query.Value[i]); + + builder.Append(queryValueArray[j]); } } } @@ -167,13 +178,17 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal .Append(queryKey) .Append("="); - for (var j = 0; j < queryKeyValues.Count; j++) + var queryValueArray = queryKeyValues.ToArray(); + Array.Sort(queryValueArray, StringComparer.Ordinal); + + for (var j = 0; j < queryValueArray.Length; j++) { if (j > 0) { builder.Append(KeySubDelimiter); } - builder.Append(queryKeyValues[j]); + + builder.Append(queryValueArray[j]); } } } @@ -186,5 +201,19 @@ namespace Microsoft.AspNetCore.ResponseCaching.Internal _builderPool.Return(builder); } } + + private class QueryKeyComparer : IComparer> + { + private StringComparer _stringComparer; + + public static QueryKeyComparer OrdinalIgnoreCase { get; } = new QueryKeyComparer(StringComparer.OrdinalIgnoreCase); + + public QueryKeyComparer(StringComparer stringComparer) + { + _stringComparer = stringComparer; + } + + public int Compare(KeyValuePair x, KeyValuePair y) => _stringComparer.Compare(x.Key, y.Key); + } } } diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingKeyProviderTests.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingKeyProviderTests.cs index 1c3cd0e485..36bd3da0c8 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingKeyProviderTests.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingKeyProviderTests.cs @@ -94,6 +94,22 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests cacheKeyProvider.CreateStorageVaryByKey(context)); } + [Fact] + public void ResponseCachingKeyProvider_CreateStorageVaryKey_HeaderValuesAreSorted() + { + var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); + var context = TestUtils.CreateTestContext(); + context.HttpContext.Request.Headers["HeaderA"] = "ValueB"; + context.HttpContext.Request.Headers.Append("HeaderA", "ValueA"); + context.CachedVaryByRules = new CachedVaryByRules() + { + Headers = new string[] { "HeaderA", "HeaderC" } + }; + + Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueAValueB{KeyDelimiter}HeaderC=", + cacheKeyProvider.CreateStorageVaryByKey(context)); + } + [Fact] public void ResponseCachingKeyProvider_CreateStorageVaryKey_IncludesListedQueryKeysOnly() { @@ -162,6 +178,24 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests cacheKeyProvider.CreateStorageVaryByKey(context)); } + [Fact] + public void ResponseCachingKeyProvider_CreateStorageVaryKey_QueryKeysValuesAreSorted() + { + var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); + var context = TestUtils.CreateTestContext(); + context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueB&QueryA=ValueA"); + context.CachedVaryByRules = new CachedVaryByRules() + { + VaryByKeyPrefix = FastGuid.NewGuid().IdString, + QueryKeys = new string[] { "*" } + }; + + // To support case insensitivity, all query keys are converted to upper case. + // Explicit query keys uses the casing specified in the setting. + Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}QUERYA=ValueA{KeySubDelimiter}ValueB", + cacheKeyProvider.CreateStorageVaryByKey(context)); + } + [Fact] public void ResponseCachingKeyProvider_CreateStorageVaryKey_IncludesListedHeadersAndQueryKeys() { From 799e80677123994bd41c8509cd1f2acf2ab7ed53 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Tue, 6 Mar 2018 10:05:43 -0800 Subject: [PATCH 163/188] Use dotnet-core feed in repos --- build/sources.props | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/build/sources.props b/build/sources.props index 9feff29d09..9215df9751 100644 --- a/build/sources.props +++ b/build/sources.props @@ -1,10 +1,11 @@ - + $(DotNetRestoreSources) $(RestoreSources); + https://dotnet.myget.org/F/dotnet-core/api/v3/index.json; https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json; https://dotnet.myget.org/F/aspnetcore-tools/api/v3/index.json; From df7d3a8f47a924ca3fce92ba6a51df5f2e1b9d7c Mon Sep 17 00:00:00 2001 From: Pranav K Date: Tue, 6 Mar 2018 10:05:43 -0800 Subject: [PATCH 164/188] Prepend FeatureBranchVersionPrefix if FeatureBranchVersionSuffix is specified --- version.props | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/version.props b/version.props index 65c8a07e37..a11ea1ed52 100644 --- a/version.props +++ b/version.props @@ -5,7 +5,8 @@ $(VersionPrefix) $(VersionPrefix)-$(VersionSuffix)-final t000 - $(VersionSuffix)-$([System.Text.RegularExpressions.Regex]::Replace('$(FeatureBranchVersionSuffix)', '[^\w-]', '-')) + a- + $(FeatureBranchVersionPrefix)$(VersionSuffix)-$([System.Text.RegularExpressions.Regex]::Replace('$(FeatureBranchVersionSuffix)', '[^\w-]', '-')) $(VersionSuffix)-$(BuildNumber) From 673e7f550068537193a94f6ce323f9fbe9bf6e71 Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Thu, 8 Mar 2018 13:12:52 -0800 Subject: [PATCH 165/188] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 22 +++++++++++----------- korebuild-lock.txt | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index 3cb1b746eb..03386ea686 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,18 +3,18 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 2.1.0-preview2-15721 - 2.1.0-preview2-30187 - 2.1.0-preview2-30187 - 2.1.0-preview2-30187 - 2.1.0-preview2-30187 - 2.1.0-preview2-30187 - 2.1.0-preview2-30187 - 2.1.0-preview2-30187 - 2.1.0-preview2-30187 - 2.1.0-preview2-30187 + 2.1.0-preview2-15728 + 2.1.0-preview2-30272 + 2.1.0-preview2-30272 + 2.1.0-preview2-30272 + 2.1.0-preview2-30272 + 2.1.0-preview2-30272 + 2.1.0-preview2-30272 + 2.1.0-preview2-30272 + 2.1.0-preview2-30272 + 2.1.0-preview2-30272 2.0.0 - 2.1.0-preview2-26130-04 + 2.1.0-preview2-26225-03 15.6.0 2.3.1 2.4.0-beta.1.build3945 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index e6c7fddffa..138d848db1 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.1.0-preview2-15721 -commithash:f9bb4be59e39938ec59a6975257e26099b0d03c1 +version:2.1.0-preview2-15728 +commithash:393377068ddcf51dfee0536536d455f57a828b06 From cb157320caddf9df3b6da7176b91e1343c8171a4 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Fri, 16 Mar 2018 11:16:38 -0700 Subject: [PATCH 166/188] Branching for 2.1.0-preview2 --- build/dependencies.props | 22 +++++++++++----------- build/repo.props | 4 ++-- build/sources.props | 2 +- korebuild-lock.txt | 4 ++-- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index 03386ea686..723b440e38 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,18 +3,18 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 2.1.0-preview2-15728 - 2.1.0-preview2-30272 - 2.1.0-preview2-30272 - 2.1.0-preview2-30272 - 2.1.0-preview2-30272 - 2.1.0-preview2-30272 - 2.1.0-preview2-30272 - 2.1.0-preview2-30272 - 2.1.0-preview2-30272 - 2.1.0-preview2-30272 + 2.1.0-preview2-15742 + 2.1.0-preview2-30355 + 2.1.0-preview2-30355 + 2.1.0-preview2-30355 + 2.1.0-preview2-30355 + 2.1.0-preview2-30355 + 2.1.0-preview2-30355 + 2.1.0-preview2-30355 + 2.1.0-preview2-30355 + 2.1.0-preview2-30355 2.0.0 - 2.1.0-preview2-26225-03 + 2.1.0-preview2-26314-02 15.6.0 2.3.1 2.4.0-beta.1.build3945 diff --git a/build/repo.props b/build/repo.props index 78b0ce5879..d94ff7d00d 100644 --- a/build/repo.props +++ b/build/repo.props @@ -1,10 +1,10 @@ - + Internal.AspNetCore.Universe.Lineup - https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json + https://dotnet.myget.org/F/aspnetcore-release/api/v3/index.json diff --git a/build/sources.props b/build/sources.props index 9215df9751..36045f12b5 100644 --- a/build/sources.props +++ b/build/sources.props @@ -6,7 +6,7 @@ $(RestoreSources); https://dotnet.myget.org/F/dotnet-core/api/v3/index.json; - https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json; + https://dotnet.myget.org/F/aspnetcore-release/api/v3/index.json; https://dotnet.myget.org/F/aspnetcore-tools/api/v3/index.json; diff --git a/korebuild-lock.txt b/korebuild-lock.txt index 138d848db1..e40ef6651b 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.1.0-preview2-15728 -commithash:393377068ddcf51dfee0536536d455f57a828b06 +version:2.1.0-preview2-15742 +commithash:21fbb0f2c3fe4a9216e2d59632b98cfd7d685962 From 15c599c5653746a2e5a9d0c85fcb7a6d68c942f9 Mon Sep 17 00:00:00 2001 From: Pranav K Date: Fri, 16 Mar 2018 11:27:58 -0700 Subject: [PATCH 167/188] Update version prefix to preview3 --- version.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.props b/version.props index a11ea1ed52..24f2b00a0a 100644 --- a/version.props +++ b/version.props @@ -1,7 +1,7 @@ 2.1.0 - preview2 + preview3 $(VersionPrefix) $(VersionPrefix)-$(VersionSuffix)-final t000 From 7e28614391c759ea7164718d5ee79074b7e31cee Mon Sep 17 00:00:00 2001 From: Pranav K Date: Fri, 16 Mar 2018 12:33:18 -0700 Subject: [PATCH 168/188] Update KoreBuild channel --- korebuild.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/korebuild.json b/korebuild.json index bd5d51a51b..678d8bb948 100644 --- a/korebuild.json +++ b/korebuild.json @@ -1,4 +1,4 @@ { - "$schema": "https://raw.githubusercontent.com/aspnet/BuildTools/dev/tools/korebuild.schema.json", - "channel": "dev" + "$schema": "https://raw.githubusercontent.com/aspnet/BuildTools/release/2.1/tools/korebuild.schema.json", + "channel": "release/2.1" } From 07db6cc038be62a0dd604b9e530a7159e76aab52 Mon Sep 17 00:00:00 2001 From: Ryan Brandenburg Date: Wed, 14 Mar 2018 15:35:09 -0700 Subject: [PATCH 169/188] Set 2.0 baselines --- build/dependencies.props | 2 +- korebuild-lock.txt | 4 +- .../baseline.netcore.json | 34 +++ .../baseline.netcore.json | 252 ++++++++++++++++++ 4 files changed, 289 insertions(+), 3 deletions(-) create mode 100644 src/Microsoft.AspNetCore.ResponseCaching.Abstractions/baseline.netcore.json create mode 100644 src/Microsoft.AspNetCore.ResponseCaching/baseline.netcore.json diff --git a/build/dependencies.props b/build/dependencies.props index 723b440e38..2f5cd67d20 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,7 +3,7 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 2.1.0-preview2-15742 + 2.1.0-preview2-15744 2.1.0-preview2-30355 2.1.0-preview2-30355 2.1.0-preview2-30355 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index e40ef6651b..f531e7b0f7 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.1.0-preview2-15742 -commithash:21fbb0f2c3fe4a9216e2d59632b98cfd7d685962 +version:2.1.0-preview2-15744 +commithash:9e15cb6062ab5b9790d3fa699e018543a6950713 diff --git a/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/baseline.netcore.json b/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/baseline.netcore.json new file mode 100644 index 0000000000..2dc8b812b7 --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/baseline.netcore.json @@ -0,0 +1,34 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.ResponseCaching.Abstractions, Version=2.0.2.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.AspNetCore.ResponseCaching.IResponseCachingFeature", + "Visibility": "Public", + "Kind": "Interface", + "Abstract": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_VaryByQueryKeys", + "Parameters": [], + "ReturnType": "System.String[]", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_VaryByQueryKeys", + "Parameters": [ + { + "Name": "value", + "Type": "System.String[]" + } + ], + "ReturnType": "System.Void", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.ResponseCaching/baseline.netcore.json b/src/Microsoft.AspNetCore.ResponseCaching/baseline.netcore.json new file mode 100644 index 0000000000..050ac753d0 --- /dev/null +++ b/src/Microsoft.AspNetCore.ResponseCaching/baseline.netcore.json @@ -0,0 +1,252 @@ +{ + "AssemblyIdentity": "Microsoft.AspNetCore.ResponseCaching, Version=2.0.2.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "Types": [ + { + "Name": "Microsoft.Extensions.DependencyInjection.ResponseCachingServicesExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "AddResponseCaching", + "Parameters": [ + { + "Name": "services", + "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection" + } + ], + "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "AddResponseCaching", + "Parameters": [ + { + "Name": "services", + "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection" + }, + { + "Name": "configureOptions", + "Type": "System.Action" + } + ], + "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.Builder.ResponseCachingExtensions", + "Visibility": "Public", + "Kind": "Class", + "Abstract": true, + "Static": true, + "Sealed": true, + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "UseResponseCaching", + "Parameters": [ + { + "Name": "app", + "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" + } + ], + "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", + "Static": true, + "Extension": true, + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.ResponseCaching.ResponseCachingFeature", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [ + "Microsoft.AspNetCore.ResponseCaching.IResponseCachingFeature" + ], + "Members": [ + { + "Kind": "Method", + "Name": "get_VaryByQueryKeys", + "Parameters": [], + "ReturnType": "System.String[]", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.ResponseCaching.IResponseCachingFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_VaryByQueryKeys", + "Parameters": [ + { + "Name": "value", + "Type": "System.String[]" + } + ], + "ReturnType": "System.Void", + "Sealed": true, + "Virtual": true, + "ImplementedInterface": "Microsoft.AspNetCore.ResponseCaching.IResponseCachingFeature", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.ResponseCaching.ResponseCachingMiddleware", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "Invoke", + "Parameters": [ + { + "Name": "httpContext", + "Type": "Microsoft.AspNetCore.Http.HttpContext" + } + ], + "ReturnType": "System.Threading.Tasks.Task", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [ + { + "Name": "next", + "Type": "Microsoft.AspNetCore.Http.RequestDelegate" + }, + { + "Name": "options", + "Type": "Microsoft.Extensions.Options.IOptions" + }, + { + "Name": "loggerFactory", + "Type": "Microsoft.Extensions.Logging.ILoggerFactory" + }, + { + "Name": "policyProvider", + "Type": "Microsoft.AspNetCore.ResponseCaching.Internal.IResponseCachingPolicyProvider" + }, + { + "Name": "keyProvider", + "Type": "Microsoft.AspNetCore.ResponseCaching.Internal.IResponseCachingKeyProvider" + } + ], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + }, + { + "Name": "Microsoft.AspNetCore.ResponseCaching.ResponseCachingOptions", + "Visibility": "Public", + "Kind": "Class", + "ImplementedInterfaces": [], + "Members": [ + { + "Kind": "Method", + "Name": "get_SizeLimit", + "Parameters": [], + "ReturnType": "System.Int64", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_SizeLimit", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int64" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_MaximumBodySize", + "Parameters": [], + "ReturnType": "System.Int64", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_MaximumBodySize", + "Parameters": [ + { + "Name": "value", + "Type": "System.Int64" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "get_UseCaseSensitivePaths", + "Parameters": [], + "ReturnType": "System.Boolean", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Method", + "Name": "set_UseCaseSensitivePaths", + "Parameters": [ + { + "Name": "value", + "Type": "System.Boolean" + } + ], + "ReturnType": "System.Void", + "Visibility": "Public", + "GenericParameter": [] + }, + { + "Kind": "Constructor", + "Name": ".ctor", + "Parameters": [], + "Visibility": "Public", + "GenericParameter": [] + } + ], + "GenericParameters": [] + } + ] +} \ No newline at end of file From ef6967e4399f321a52f29cf7dc607f3b1fd6e523 Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Sun, 25 Mar 2018 15:53:00 -0700 Subject: [PATCH 170/188] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 24 ++++++++++++------------ korebuild-lock.txt | 4 ++-- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index 03386ea686..13a734f2bf 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,19 +3,19 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 2.1.0-preview2-15728 - 2.1.0-preview2-30272 - 2.1.0-preview2-30272 - 2.1.0-preview2-30272 - 2.1.0-preview2-30272 - 2.1.0-preview2-30272 - 2.1.0-preview2-30272 - 2.1.0-preview2-30272 - 2.1.0-preview2-30272 - 2.1.0-preview2-30272 + 2.1.0-preview3-17001 + 2.1.0-preview3-32037 + 2.1.0-preview3-32037 + 2.1.0-preview3-32037 + 2.1.0-preview3-32037 + 2.1.0-preview3-32037 + 2.1.0-preview3-32037 + 2.1.0-preview3-32037 + 2.1.0-preview3-32037 + 2.1.0-preview3-32037 2.0.0 - 2.1.0-preview2-26225-03 - 15.6.0 + 2.1.0-preview2-26314-02 + 15.6.1 2.3.1 2.4.0-beta.1.build3945 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index 138d848db1..3a326c7d58 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.1.0-preview2-15728 -commithash:393377068ddcf51dfee0536536d455f57a828b06 +version:2.1.0-preview3-17001 +commithash:dda68c56abf0d3b911fe6a2315872c446b314585 From 7865e2d4bdbf4a7fed690e2e4c8ba8597380d743 Mon Sep 17 00:00:00 2001 From: "Nate McMaster (automated)" Date: Wed, 28 Mar 2018 11:01:04 -0700 Subject: [PATCH 171/188] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 24 ++++++++++++------------ korebuild-lock.txt | 4 ++-- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index 2f5cd67d20..02d2292098 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,19 +3,19 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 2.1.0-preview2-15744 - 2.1.0-preview2-30355 - 2.1.0-preview2-30355 - 2.1.0-preview2-30355 - 2.1.0-preview2-30355 - 2.1.0-preview2-30355 - 2.1.0-preview2-30355 - 2.1.0-preview2-30355 - 2.1.0-preview2-30355 - 2.1.0-preview2-30355 + 2.1.0-preview2-15749 + 2.1.0-preview2-30478 + 2.1.0-preview2-30478 + 2.1.0-preview2-30478 + 2.1.0-preview2-30478 + 2.1.0-preview2-30478 + 2.1.0-preview2-30478 + 2.1.0-preview2-30478 + 2.1.0-preview2-30478 + 2.1.0-preview2-30478 2.0.0 - 2.1.0-preview2-26314-02 - 15.6.0 + 2.1.0-preview2-26326-03 + 15.6.1 2.3.1 2.4.0-beta.1.build3945 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index f531e7b0f7..b8e036fe2c 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.1.0-preview2-15744 -commithash:9e15cb6062ab5b9790d3fa699e018543a6950713 +version:2.1.0-preview2-15749 +commithash:5544c9ab20fa5e24b9e155d8958a3c3b6f5f9df9 From 04ef4c071d18874484f8fd4656406d6598df9111 Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Tue, 3 Apr 2018 22:40:21 +0000 Subject: [PATCH 172/188] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 22 +++++++++++----------- korebuild-lock.txt | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index 13a734f2bf..af49dfb32e 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,18 +3,18 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 2.1.0-preview3-17001 - 2.1.0-preview3-32037 - 2.1.0-preview3-32037 - 2.1.0-preview3-32037 - 2.1.0-preview3-32037 - 2.1.0-preview3-32037 - 2.1.0-preview3-32037 - 2.1.0-preview3-32037 - 2.1.0-preview3-32037 - 2.1.0-preview3-32037 + 2.1.0-preview3-17002 + 2.1.0-preview3-32110 + 2.1.0-preview3-32110 + 2.1.0-preview3-32110 + 2.1.0-preview3-32110 + 2.1.0-preview3-32110 + 2.1.0-preview3-32110 + 2.1.0-preview3-32110 + 2.1.0-preview3-32110 + 2.1.0-preview3-32110 2.0.0 - 2.1.0-preview2-26314-02 + 2.1.0-preview3-26331-01 15.6.1 2.3.1 2.4.0-beta.1.build3945 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index 3a326c7d58..b3af0b8bce 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.1.0-preview3-17001 -commithash:dda68c56abf0d3b911fe6a2315872c446b314585 +version:2.1.0-preview3-17002 +commithash:b8e4e6ab104adc94c0719bb74229870e9b584a7f From 0efe83b4f24ac0efaa8a0452b4ab72eeb8e01f44 Mon Sep 17 00:00:00 2001 From: John Luo Date: Thu, 12 Apr 2018 15:56:09 -0700 Subject: [PATCH 173/188] Update usage of TestSink --- .../TestUtils.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs b/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs index 7ccb4a5ae6..09f21a8878 100644 --- a/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs +++ b/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Net.Http; using System.Text; using System.Threading; @@ -212,13 +213,15 @@ namespace Microsoft.AspNetCore.ResponseCaching.Tests }; } - internal static void AssertLoggedMessages(List messages, params LoggedMessage[] expectedMessages) + internal static void AssertLoggedMessages(IEnumerable messages, params LoggedMessage[] expectedMessages) { - Assert.Equal(messages.Count, expectedMessages.Length); - for (var i = 0; i < messages.Count; i++) + var messageList = messages.ToList(); + Assert.Equal(messageList.Count, expectedMessages.Length); + + for (var i = 0; i < messageList.Count; i++) { - Assert.Equal(expectedMessages[i].EventId, messages[i].EventId); - Assert.Equal(expectedMessages[i].LogLevel, messages[i].LogLevel); + Assert.Equal(expectedMessages[i].EventId, messageList[i].EventId); + Assert.Equal(expectedMessages[i].LogLevel, messageList[i].LogLevel); } } From 3d0c2af20f6d4026fdfdb590a5512776f8f5b4fb Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Sun, 15 Apr 2018 14:24:10 -0700 Subject: [PATCH 174/188] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 22 +++++++++++----------- korebuild-lock.txt | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index af49dfb32e..2d26451b57 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,18 +3,18 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 2.1.0-preview3-17002 - 2.1.0-preview3-32110 - 2.1.0-preview3-32110 - 2.1.0-preview3-32110 - 2.1.0-preview3-32110 - 2.1.0-preview3-32110 - 2.1.0-preview3-32110 - 2.1.0-preview3-32110 - 2.1.0-preview3-32110 - 2.1.0-preview3-32110 + 2.1.0-preview3-17018 + 2.1.0-preview3-32233 + 2.1.0-preview3-32233 + 2.1.0-preview3-32233 + 2.1.0-preview3-32233 + 2.1.0-preview3-32233 + 2.1.0-preview3-32233 + 2.1.0-preview3-32233 + 2.1.0-preview3-32233 + 2.1.0-preview3-32233 2.0.0 - 2.1.0-preview3-26331-01 + 2.1.0-preview3-26413-05 15.6.1 2.3.1 2.4.0-beta.1.build3945 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index b3af0b8bce..b419d767b9 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.1.0-preview3-17002 -commithash:b8e4e6ab104adc94c0719bb74229870e9b584a7f +version:2.1.0-preview3-17018 +commithash:af264ca131f212b5ba8aafbc5110fc0fc510a2be From 532af71577408c42b197b3f294c447470406ba4b Mon Sep 17 00:00:00 2001 From: Ryan Brandenburg Date: Mon, 16 Apr 2018 17:01:33 -0700 Subject: [PATCH 175/188] Branching for 2.1.0-rc1 --- build/repo.props | 3 ++- korebuild.json | 4 ++-- version.props | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/build/repo.props b/build/repo.props index 78b0ce5879..dab1601c88 100644 --- a/build/repo.props +++ b/build/repo.props @@ -1,9 +1,10 @@ - + Internal.AspNetCore.Universe.Lineup + 2.1.0-rc1-* https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json diff --git a/korebuild.json b/korebuild.json index bd5d51a51b..678d8bb948 100644 --- a/korebuild.json +++ b/korebuild.json @@ -1,4 +1,4 @@ { - "$schema": "https://raw.githubusercontent.com/aspnet/BuildTools/dev/tools/korebuild.schema.json", - "channel": "dev" + "$schema": "https://raw.githubusercontent.com/aspnet/BuildTools/release/2.1/tools/korebuild.schema.json", + "channel": "release/2.1" } diff --git a/version.props b/version.props index 24f2b00a0a..e27532787e 100644 --- a/version.props +++ b/version.props @@ -1,7 +1,7 @@ 2.1.0 - preview3 + rc1 $(VersionPrefix) $(VersionPrefix)-$(VersionSuffix)-final t000 From e619bb7187133b7a9fc7c1fe83f5f0a0c2de3935 Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Thu, 19 Apr 2018 16:44:04 -0700 Subject: [PATCH 176/188] Set NETStandardImplicitPackageVersion via dependencies.props --- Directory.Build.targets | 1 + build/dependencies.props | 1 + 2 files changed, 2 insertions(+) diff --git a/Directory.Build.targets b/Directory.Build.targets index 894b1d0cf8..53b3f6e1da 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -2,5 +2,6 @@ $(MicrosoftNETCoreApp20PackageVersion) $(MicrosoftNETCoreApp21PackageVersion) + $(NETStandardLibrary20PackageVersion) diff --git a/build/dependencies.props b/build/dependencies.props index 2d26451b57..d79af74365 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -16,6 +16,7 @@ 2.0.0 2.1.0-preview3-26413-05 15.6.1 + 2.0.1 2.3.1 2.4.0-beta.1.build3945 From 1103b2e3ed12dc7b6fe2d15fd383a9b6b8998d9e Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Thu, 19 Apr 2018 22:34:15 -0700 Subject: [PATCH 177/188] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 22 +++++++++++----------- korebuild-lock.txt | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index d79af74365..0adf387fc8 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,18 +3,18 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 2.1.0-preview3-17018 - 2.1.0-preview3-32233 - 2.1.0-preview3-32233 - 2.1.0-preview3-32233 - 2.1.0-preview3-32233 - 2.1.0-preview3-32233 - 2.1.0-preview3-32233 - 2.1.0-preview3-32233 - 2.1.0-preview3-32233 - 2.1.0-preview3-32233 + 2.1.0-rc1-15774 + 2.1.0-rc1-30613 + 2.1.0-rc1-30613 + 2.1.0-rc1-30613 + 2.1.0-rc1-30613 + 2.1.0-rc1-30613 + 2.1.0-rc1-30613 + 2.1.0-rc1-30613 + 2.1.0-rc1-30613 + 2.1.0-rc1-30613 2.0.0 - 2.1.0-preview3-26413-05 + 2.1.0-rc1-26419-02 15.6.1 2.0.1 2.3.1 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index b419d767b9..9d4ef8c888 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.1.0-preview3-17018 -commithash:af264ca131f212b5ba8aafbc5110fc0fc510a2be +version:2.1.0-rc1-15774 +commithash:ed5ca9de3c652347dbb0158a9a65eff3471d2114 From bd1a0c676851dc14f962bbe58961664f6b94d04d Mon Sep 17 00:00:00 2001 From: "Nate McMaster (automated)" Date: Mon, 30 Apr 2018 14:51:44 -0700 Subject: [PATCH 178/188] Bump version to 2.1.0-rtm --- version.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.props b/version.props index e27532787e..b9552451d8 100644 --- a/version.props +++ b/version.props @@ -1,7 +1,7 @@ 2.1.0 - rc1 + rtm $(VersionPrefix) $(VersionPrefix)-$(VersionSuffix)-final t000 From 4cbf2f4849f81390863e80e03c7286f107246d76 Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Fri, 4 May 2018 07:48:42 -0700 Subject: [PATCH 179/188] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 24 ++++++++++++------------ korebuild-lock.txt | 4 ++-- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index 0adf387fc8..dc7c3bee8c 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,20 +3,20 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 2.1.0-rc1-15774 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 - 2.1.0-rc1-30613 + 2.1.0-rtm-15783 + 2.1.0-rtm-30721 + 2.1.0-rtm-30721 + 2.1.0-rtm-30721 + 2.1.0-rtm-30721 + 2.1.0-rtm-30721 + 2.1.0-rtm-30721 + 2.1.0-rtm-30721 + 2.1.0-rtm-30721 + 2.1.0-rtm-30721 2.0.0 - 2.1.0-rc1-26419-02 + 2.1.0-rtm-26502-02 15.6.1 - 2.0.1 + 2.0.3 2.3.1 2.4.0-beta.1.build3945 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index 9d4ef8c888..3673744db9 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.1.0-rc1-15774 -commithash:ed5ca9de3c652347dbb0158a9a65eff3471d2114 +version:2.1.0-rtm-15783 +commithash:5fc2b2f607f542a2ffde11c19825e786fc1a3774 From ca86f693f010cab258f1fea84752adf2c0fed000 Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Tue, 29 May 2018 09:50:57 -0700 Subject: [PATCH 180/188] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 22 +++++++++++----------- korebuild-lock.txt | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index dc7c3bee8c..3e81ae4e40 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,18 +3,18 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 2.1.0-rtm-15783 - 2.1.0-rtm-30721 - 2.1.0-rtm-30721 - 2.1.0-rtm-30721 - 2.1.0-rtm-30721 - 2.1.0-rtm-30721 - 2.1.0-rtm-30721 - 2.1.0-rtm-30721 - 2.1.0-rtm-30721 - 2.1.0-rtm-30721 + 2.1.1-rtm-15790 + 2.1.0 + 2.1.0 + 2.1.0 + 2.1.0 + 2.1.0 + 2.1.0 + 2.1.0 + 2.1.0 + 2.1.0 2.0.0 - 2.1.0-rtm-26502-02 + 2.1.0 15.6.1 2.0.3 2.3.1 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index 3673744db9..cd5b409a1e 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.1.0-rtm-15783 -commithash:5fc2b2f607f542a2ffde11c19825e786fc1a3774 +version:2.1.1-rtm-15790 +commithash:274c65868e735f29f4078c1884c61c4371ee1fc0 From 4b707e2fe801048946b5fdf5bc63638b891fc556 Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Tue, 5 Jun 2018 09:11:42 -0700 Subject: [PATCH 181/188] Bumping version from 2.1.0 to 2.1.1 --- version.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.props b/version.props index b9552451d8..669c874829 100644 --- a/version.props +++ b/version.props @@ -1,6 +1,6 @@ - + - 2.1.0 + 2.1.1 rtm $(VersionPrefix) $(VersionPrefix)-$(VersionSuffix)-final From 9e7c650c8a22158fc1099fa1f028bca93166fc8a Mon Sep 17 00:00:00 2001 From: "ASP.NET CI" Date: Tue, 12 Jun 2018 19:32:21 +0000 Subject: [PATCH 182/188] Update dependencies.props [auto-updated: dependencies] --- build/dependencies.props | 22 +++++++++++----------- korebuild-lock.txt | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index 3e81ae4e40..ab5df1ecdf 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,18 +3,18 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 2.1.1-rtm-15790 - 2.1.0 - 2.1.0 - 2.1.0 - 2.1.0 - 2.1.0 - 2.1.0 - 2.1.0 - 2.1.0 - 2.1.0 + 2.1.1-rtm-15793 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.1 2.0.0 - 2.1.0 + 2.1.1 15.6.1 2.0.3 2.3.1 diff --git a/korebuild-lock.txt b/korebuild-lock.txt index cd5b409a1e..bc84e0cd53 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.1.1-rtm-15790 -commithash:274c65868e735f29f4078c1884c61c4371ee1fc0 +version:2.1.1-rtm-15793 +commithash:988313f4b064d6c69fc6f7b845b6384a6af3447a From 2328792540a3832dac8790dd21496d9ddc237065 Mon Sep 17 00:00:00 2001 From: Ryan Brandenburg Date: Thu, 14 Jun 2018 10:29:49 -0700 Subject: [PATCH 183/188] Set 2.1 baselines --- .../baseline.netcore.json | 2 +- src/Microsoft.AspNetCore.ResponseCaching/baseline.netcore.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/baseline.netcore.json b/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/baseline.netcore.json index 2dc8b812b7..f8993e5232 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/baseline.netcore.json +++ b/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/baseline.netcore.json @@ -1,5 +1,5 @@ { - "AssemblyIdentity": "Microsoft.AspNetCore.ResponseCaching.Abstractions, Version=2.0.2.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "AssemblyIdentity": "Microsoft.AspNetCore.ResponseCaching.Abstractions, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", "Types": [ { "Name": "Microsoft.AspNetCore.ResponseCaching.IResponseCachingFeature", diff --git a/src/Microsoft.AspNetCore.ResponseCaching/baseline.netcore.json b/src/Microsoft.AspNetCore.ResponseCaching/baseline.netcore.json index 050ac753d0..9bec30264e 100644 --- a/src/Microsoft.AspNetCore.ResponseCaching/baseline.netcore.json +++ b/src/Microsoft.AspNetCore.ResponseCaching/baseline.netcore.json @@ -1,5 +1,5 @@ { - "AssemblyIdentity": "Microsoft.AspNetCore.ResponseCaching, Version=2.0.2.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", + "AssemblyIdentity": "Microsoft.AspNetCore.ResponseCaching, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", "Types": [ { "Name": "Microsoft.Extensions.DependencyInjection.ResponseCachingServicesExtensions", From 88796f41f879e201504d1d21fb2a6b45032d5e40 Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Wed, 27 Jun 2018 13:39:50 -0700 Subject: [PATCH 184/188] Bumping version from 2.1.1 to 2.1.2 --- version.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.props b/version.props index 669c874829..478dfd16ed 100644 --- a/version.props +++ b/version.props @@ -1,6 +1,6 @@  - 2.1.1 + 2.1.2 rtm $(VersionPrefix) $(VersionPrefix)-$(VersionSuffix)-final From 856dce53b3db865ff68dd05e37b8f943c3cddfcc Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Wed, 11 Jul 2018 15:06:41 -0700 Subject: [PATCH 185/188] Reverting version from 2.1.2 back to 2.1.1 As a result of changing the way we apply servicing updates to aspnet core, this repo did not need the version bump because there are no planned product changes in this repo. --- version.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.props b/version.props index 478dfd16ed..669c874829 100644 --- a/version.props +++ b/version.props @@ -1,6 +1,6 @@  - 2.1.2 + 2.1.1 rtm $(VersionPrefix) $(VersionPrefix)-$(VersionSuffix)-final From 25d648d4fbac5f055385af16762af2eed7b16d43 Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Wed, 11 Jul 2018 18:49:40 -0700 Subject: [PATCH 186/188] Updating dependencies to 2.1.2 and adding a section for pinned variable versions --- build/dependencies.props | 15 +++++++++++---- korebuild-lock.txt | 4 ++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index ab5df1ecdf..7529a89aa3 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -2,23 +2,30 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - - 2.1.1-rtm-15793 + + + + 2.1.3-rtm-15802 2.1.1 2.1.1 2.1.1 - 2.1.1 + 2.1.2 2.1.1 2.1.1 2.1.1 2.1.1 2.1.1 2.0.0 - 2.1.1 + 2.1.2 15.6.1 2.0.3 2.3.1 2.4.0-beta.1.build3945 + + + + + diff --git a/korebuild-lock.txt b/korebuild-lock.txt index bc84e0cd53..251c227c83 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.1.1-rtm-15793 -commithash:988313f4b064d6c69fc6f7b845b6384a6af3447a +version:2.1.3-rtm-15802 +commithash:a7c08b45b440a7d2058a0aa1eaa3eb6ba811976a From 777b2fbf7e768221c528243773037a50423b7b34 Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Thu, 12 Jul 2018 11:57:52 -0700 Subject: [PATCH 187/188] Pin version variables to the ASP.NET Core 2.1.2 baseline This reverts our previous policy of cascading versions on all servicing updates. This moves variables into the 'pinned' section, and points them to the latest stable release (versions that were used at the time of the 2.1.2 release). --- build/dependencies.props | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/build/dependencies.props b/build/dependencies.props index 7529a89aa3..6735646e70 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -4,17 +4,8 @@ - + 2.1.3-rtm-15802 - 2.1.1 - 2.1.1 - 2.1.1 - 2.1.2 - 2.1.1 - 2.1.1 - 2.1.1 - 2.1.1 - 2.1.1 2.0.0 2.1.2 15.6.1 @@ -27,5 +18,15 @@ - - + + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.2 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.1 + 2.1.1 + + \ No newline at end of file From ce2f7908e4ce4819e78cc9b5bd5aa2be69412ff0 Mon Sep 17 00:00:00 2001 From: Ryan Brandenburg Date: Wed, 21 Nov 2018 15:06:37 -0800 Subject: [PATCH 188/188] Reorganize source code in preparation to move into aspnet/AspNetCore Prior to reorganization, this source code was found in https://github.com/aspnet/ResponseCaching/tree/777b2fbf7e768221c528243773037a50423b7b34 --- .appveyor.yml | 17 -- .gitattributes | 51 ---- .github/ISSUE_TEMPLATE.md | 3 - .travis.yml | 27 -- CONTRIBUTING.md | 4 - LICENSE.txt | 14 -- NuGet.config | 7 - build.cmd | 2 - build.sh | 8 - korebuild-lock.txt | 2 - korebuild.json | 4 - run.cmd | 2 - run.ps1 | 196 --------------- run.sh | 231 ------------------ .gitignore => src/ResponseCaching/.gitignore | 0 .../ResponseCaching/Directory.Build.props | 0 .../ResponseCaching/Directory.Build.targets | 0 .../ResponseCaching/NuGetPackageVerifier.json | 0 README.md => src/ResponseCaching/README.md | 0 .../ResponseCaching/ResponseCaching.sln | 0 {build => src/ResponseCaching/build}/Key.snk | Bin .../ResponseCaching/build}/dependencies.props | 0 .../ResponseCaching/build}/repo.props | 0 .../ResponseCaching/build}/sources.props | 0 .../samples}/ResponseCachingSample/README.md | 0 .../ResponseCachingSample.csproj | 0 .../samples}/ResponseCachingSample/Startup.cs | 0 .../src}/Directory.Build.props | 0 .../IResponseCachingFeature.cs | 0 ...etCore.ResponseCaching.Abstractions.csproj | 0 .../baseline.netcore.json | 0 .../Internal/CacheEntry/CacheEntryHelpers .cs | 0 .../Internal/CacheEntry/CachedResponse.cs | 0 .../Internal/CacheEntry/CachedVaryByRules.cs | 0 .../Internal/FastGuid.cs | 0 .../Internal/ISystemClock.cs | 0 .../Internal/Interfaces/IResponseCache.cs | 0 .../Interfaces/IResponseCacheEntry.cs | 0 .../Interfaces/IResponseCachingKeyProvider.cs | 0 .../IResponseCachingPolicyProvider.cs | 0 .../Internal/LoggerExtensions.cs | 0 .../Internal/MemoryCachedResponse.cs | 0 .../Internal/MemoryResponseCache.cs | 0 .../Internal/ResponseCachingContext.cs | 0 .../Internal/ResponseCachingKeyProvider.cs | 0 .../Internal/ResponseCachingPolicyProvider.cs | 0 .../Internal/SendFileFeatureWrapper.cs | 0 .../Internal/StringBuilderExtensions.cs | 0 .../Internal/SystemClock.cs | 0 ...icrosoft.AspNetCore.ResponseCaching.csproj | 0 .../Properties/AssemblyInfo.cs | 0 .../ResponseCachingExtensions.cs | 0 .../ResponseCachingFeature.cs | 0 .../ResponseCachingMiddleware.cs | 0 .../ResponseCachingOptions.cs | 0 .../ResponseCachingServicesExtensions.cs | 0 .../Streams/ResponseCachingStream.cs | 0 .../Streams/SegmentReadStream.cs | 0 .../Streams/SegmentWriteStream.cs | 0 .../Streams/StreamUtilities.cs | 0 .../baseline.netcore.json | 0 .../test}/Directory.Build.props | 0 ...ft.AspNetCore.ResponseCaching.Tests.csproj | 0 .../ResponseCachingFeatureTests.cs | 0 .../ResponseCachingKeyProviderTests.cs | 0 .../ResponseCachingMiddlewareTests.cs | 0 .../ResponseCachingPolicyProviderTests.cs | 0 .../ResponseCachingTests.cs | 0 .../SegmentReadStreamTests.cs | 0 .../SegmentWriteStreamTests.cs | 0 .../TestUtils.cs | 0 .../ResponseCaching/version.props | 0 72 files changed, 568 deletions(-) delete mode 100644 .appveyor.yml delete mode 100644 .gitattributes delete mode 100644 .github/ISSUE_TEMPLATE.md delete mode 100644 .travis.yml delete mode 100644 CONTRIBUTING.md delete mode 100644 LICENSE.txt delete mode 100644 NuGet.config delete mode 100644 build.cmd delete mode 100755 build.sh delete mode 100644 korebuild-lock.txt delete mode 100644 korebuild.json delete mode 100644 run.cmd delete mode 100644 run.ps1 delete mode 100755 run.sh rename .gitignore => src/ResponseCaching/.gitignore (100%) rename Directory.Build.props => src/ResponseCaching/Directory.Build.props (100%) rename Directory.Build.targets => src/ResponseCaching/Directory.Build.targets (100%) rename NuGetPackageVerifier.json => src/ResponseCaching/NuGetPackageVerifier.json (100%) rename README.md => src/ResponseCaching/README.md (100%) rename ResponseCaching.sln => src/ResponseCaching/ResponseCaching.sln (100%) rename {build => src/ResponseCaching/build}/Key.snk (100%) rename {build => src/ResponseCaching/build}/dependencies.props (100%) rename {build => src/ResponseCaching/build}/repo.props (100%) rename {build => src/ResponseCaching/build}/sources.props (100%) rename {samples => src/ResponseCaching/samples}/ResponseCachingSample/README.md (100%) rename {samples => src/ResponseCaching/samples}/ResponseCachingSample/ResponseCachingSample.csproj (100%) rename {samples => src/ResponseCaching/samples}/ResponseCachingSample/Startup.cs (100%) rename src/{ => ResponseCaching/src}/Directory.Build.props (100%) rename src/{ => ResponseCaching/src}/Microsoft.AspNetCore.ResponseCaching.Abstractions/IResponseCachingFeature.cs (100%) rename src/{ => ResponseCaching/src}/Microsoft.AspNetCore.ResponseCaching.Abstractions/Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj (100%) rename src/{ => ResponseCaching/src}/Microsoft.AspNetCore.ResponseCaching.Abstractions/baseline.netcore.json (100%) rename src/{ => ResponseCaching/src}/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CacheEntryHelpers .cs (100%) rename src/{ => ResponseCaching/src}/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CachedResponse.cs (100%) rename src/{ => ResponseCaching/src}/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CachedVaryByRules.cs (100%) rename src/{ => ResponseCaching/src}/Microsoft.AspNetCore.ResponseCaching/Internal/FastGuid.cs (100%) rename src/{ => ResponseCaching/src}/Microsoft.AspNetCore.ResponseCaching/Internal/ISystemClock.cs (100%) rename src/{ => ResponseCaching/src}/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCache.cs (100%) rename src/{ => ResponseCaching/src}/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCacheEntry.cs (100%) rename src/{ => ResponseCaching/src}/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCachingKeyProvider.cs (100%) rename src/{ => ResponseCaching/src}/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCachingPolicyProvider.cs (100%) rename src/{ => ResponseCaching/src}/Microsoft.AspNetCore.ResponseCaching/Internal/LoggerExtensions.cs (100%) rename src/{ => ResponseCaching/src}/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryCachedResponse.cs (100%) rename src/{ => ResponseCaching/src}/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs (100%) rename src/{ => ResponseCaching/src}/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingContext.cs (100%) rename src/{ => ResponseCaching/src}/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingKeyProvider.cs (100%) rename src/{ => ResponseCaching/src}/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingPolicyProvider.cs (100%) rename src/{ => ResponseCaching/src}/Microsoft.AspNetCore.ResponseCaching/Internal/SendFileFeatureWrapper.cs (100%) rename src/{ => ResponseCaching/src}/Microsoft.AspNetCore.ResponseCaching/Internal/StringBuilderExtensions.cs (100%) rename src/{ => ResponseCaching/src}/Microsoft.AspNetCore.ResponseCaching/Internal/SystemClock.cs (100%) rename src/{ => ResponseCaching/src}/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj (100%) rename src/{ => ResponseCaching/src}/Microsoft.AspNetCore.ResponseCaching/Properties/AssemblyInfo.cs (100%) rename src/{ => ResponseCaching/src}/Microsoft.AspNetCore.ResponseCaching/ResponseCachingExtensions.cs (100%) rename src/{ => ResponseCaching/src}/Microsoft.AspNetCore.ResponseCaching/ResponseCachingFeature.cs (100%) rename src/{ => ResponseCaching/src}/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs (100%) rename src/{ => ResponseCaching/src}/Microsoft.AspNetCore.ResponseCaching/ResponseCachingOptions.cs (100%) rename src/{ => ResponseCaching/src}/Microsoft.AspNetCore.ResponseCaching/ResponseCachingServicesExtensions.cs (100%) rename src/{ => ResponseCaching/src}/Microsoft.AspNetCore.ResponseCaching/Streams/ResponseCachingStream.cs (100%) rename src/{ => ResponseCaching/src}/Microsoft.AspNetCore.ResponseCaching/Streams/SegmentReadStream.cs (100%) rename src/{ => ResponseCaching/src}/Microsoft.AspNetCore.ResponseCaching/Streams/SegmentWriteStream.cs (100%) rename src/{ => ResponseCaching/src}/Microsoft.AspNetCore.ResponseCaching/Streams/StreamUtilities.cs (100%) rename src/{ => ResponseCaching/src}/Microsoft.AspNetCore.ResponseCaching/baseline.netcore.json (100%) rename {test => src/ResponseCaching/test}/Directory.Build.props (100%) rename {test => src/ResponseCaching/test}/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj (100%) rename {test => src/ResponseCaching/test}/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingFeatureTests.cs (100%) rename {test => src/ResponseCaching/test}/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingKeyProviderTests.cs (100%) rename {test => src/ResponseCaching/test}/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs (100%) rename {test => src/ResponseCaching/test}/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingPolicyProviderTests.cs (100%) rename {test => src/ResponseCaching/test}/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs (100%) rename {test => src/ResponseCaching/test}/Microsoft.AspNetCore.ResponseCaching.Tests/SegmentReadStreamTests.cs (100%) rename {test => src/ResponseCaching/test}/Microsoft.AspNetCore.ResponseCaching.Tests/SegmentWriteStreamTests.cs (100%) rename {test => src/ResponseCaching/test}/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs (100%) rename version.props => src/ResponseCaching/version.props (100%) diff --git a/.appveyor.yml b/.appveyor.yml deleted file mode 100644 index 4eea96ab69..0000000000 --- a/.appveyor.yml +++ /dev/null @@ -1,17 +0,0 @@ -init: -- git config --global core.autocrlf true -branches: - only: - - dev - - /^release\/.*$/ - - /^(.*\/)?ci-.*$/ -build_script: -- ps: .\run.ps1 default-build -clone_depth: 1 -environment: - global: - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true - DOTNET_CLI_TELEMETRY_OPTOUT: 1 -test: 'off' -deploy: 'off' -os: Visual Studio 2017 diff --git a/.gitattributes b/.gitattributes deleted file mode 100644 index 97b827b758..0000000000 --- a/.gitattributes +++ /dev/null @@ -1,51 +0,0 @@ -*.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 diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 101a084f0a..0000000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,3 +0,0 @@ -THIS ISSUE TRACKER IS CLOSED - please log new issues here: https://github.com/aspnet/Home/issues - -For information about this change, see https://github.com/aspnet/Announcements/issues/283 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 64bdbb4441..0000000000 --- a/.travis.yml +++ /dev/null @@ -1,27 +0,0 @@ -language: csharp -sudo: false -dist: trusty -env: - global: - - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true - - DOTNET_CLI_TELEMETRY_OPTOUT: 1 -mono: none -os: -- linux -- osx -osx_image: xcode8.2 -addons: - apt: - packages: - - libunwind8 -branches: - only: - - dev - - /^release\/.*$/ - - /^(.*\/)?ci-.*$/ -before_install: -- if test "$TRAVIS_OS_NAME" == "osx"; then brew update; brew install openssl; ln -s - /usr/local/opt/openssl/lib/libcrypto.1.0.0.dylib /usr/local/lib/; ln -s /usr/local/opt/openssl/lib/libssl.1.0.0.dylib - /usr/local/lib/; fi -script: -- ./build.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 64ff041d5c..0000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,4 +0,0 @@ -Contributing -====== - -Information on contributing to this repo is in the [Contributing Guide](https://github.com/aspnet/Home/blob/dev/CONTRIBUTING.md) in the Home repo. diff --git a/LICENSE.txt b/LICENSE.txt deleted file mode 100644 index 7b2956ecee..0000000000 --- a/LICENSE.txt +++ /dev/null @@ -1,14 +0,0 @@ -Copyright (c) .NET Foundation and Contributors - -All rights reserved. - -Licensed under the Apache License, Version 2.0 (the "License"); you may not use -this file except in compliance with the License. You may obtain a copy of the -License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software distributed -under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR -CONDITIONS OF ANY KIND, either express or implied. See the License for the -specific language governing permissions and limitations under the License. diff --git a/NuGet.config b/NuGet.config deleted file mode 100644 index e32bddfd51..0000000000 --- a/NuGet.config +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/build.cmd b/build.cmd deleted file mode 100644 index c0050bda12..0000000000 --- a/build.cmd +++ /dev/null @@ -1,2 +0,0 @@ -@ECHO OFF -PowerShell -NoProfile -NoLogo -ExecutionPolicy unrestricted -Command "[System.Threading.Thread]::CurrentThread.CurrentCulture = ''; [System.Threading.Thread]::CurrentThread.CurrentUICulture = '';& '%~dp0run.ps1' default-build %*; exit $LASTEXITCODE" diff --git a/build.sh b/build.sh deleted file mode 100755 index 98a4b22765..0000000000 --- a/build.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" - -# Call "sync" between "chmod" and execution to prevent "text file busy" error in Docker (aufs) -chmod +x "$DIR/run.sh"; sync -"$DIR/run.sh" default-build "$@" diff --git a/korebuild-lock.txt b/korebuild-lock.txt deleted file mode 100644 index 251c227c83..0000000000 --- a/korebuild-lock.txt +++ /dev/null @@ -1,2 +0,0 @@ -version:2.1.3-rtm-15802 -commithash:a7c08b45b440a7d2058a0aa1eaa3eb6ba811976a diff --git a/korebuild.json b/korebuild.json deleted file mode 100644 index 678d8bb948..0000000000 --- a/korebuild.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "$schema": "https://raw.githubusercontent.com/aspnet/BuildTools/release/2.1/tools/korebuild.schema.json", - "channel": "release/2.1" -} diff --git a/run.cmd b/run.cmd deleted file mode 100644 index d52d5c7e68..0000000000 --- a/run.cmd +++ /dev/null @@ -1,2 +0,0 @@ -@ECHO OFF -PowerShell -NoProfile -NoLogo -ExecutionPolicy unrestricted -Command "[System.Threading.Thread]::CurrentThread.CurrentCulture = ''; [System.Threading.Thread]::CurrentThread.CurrentUICulture = '';& '%~dp0run.ps1' %*; exit $LASTEXITCODE" diff --git a/run.ps1 b/run.ps1 deleted file mode 100644 index 27dcf848f8..0000000000 --- a/run.ps1 +++ /dev/null @@ -1,196 +0,0 @@ -#!/usr/bin/env powershell -#requires -version 4 - -<# -.SYNOPSIS -Executes KoreBuild commands. - -.DESCRIPTION -Downloads korebuild if required. Then executes the KoreBuild command. To see available commands, execute with `-Command help`. - -.PARAMETER Command -The KoreBuild command to run. - -.PARAMETER Path -The folder to build. Defaults to the folder containing this script. - -.PARAMETER Channel -The channel of KoreBuild to download. Overrides the value from the config file. - -.PARAMETER DotNetHome -The directory where .NET Core tools will be stored. - -.PARAMETER ToolsSource -The base url where build tools can be downloaded. Overrides the value from the config file. - -.PARAMETER Update -Updates KoreBuild to the latest version even if a lock file is present. - -.PARAMETER ConfigFile -The path to the configuration file that stores values. Defaults to korebuild.json. - -.PARAMETER ToolsSourceSuffix -The Suffix to append to the end of the ToolsSource. Useful for query strings in blob stores. - -.PARAMETER Arguments -Arguments to be passed to the command - -.NOTES -This function will create a file $PSScriptRoot/korebuild-lock.txt. This lock file can be committed to source, but does not have to be. -When the lockfile is not present, KoreBuild will create one using latest available version from $Channel. - -The $ConfigFile is expected to be an JSON file. It is optional, and the configuration values in it are optional as well. Any options set -in the file are overridden by command line parameters. - -.EXAMPLE -Example config file: -```json -{ - "$schema": "https://raw.githubusercontent.com/aspnet/BuildTools/dev/tools/korebuild.schema.json", - "channel": "dev", - "toolsSource": "https://aspnetcore.blob.core.windows.net/buildtools" -} -``` -#> -[CmdletBinding(PositionalBinding = $false)] -param( - [Parameter(Mandatory = $true, Position = 0)] - [string]$Command, - [string]$Path = $PSScriptRoot, - [Alias('c')] - [string]$Channel, - [Alias('d')] - [string]$DotNetHome, - [Alias('s')] - [string]$ToolsSource, - [Alias('u')] - [switch]$Update, - [string]$ConfigFile, - [string]$ToolsSourceSuffix, - [Parameter(ValueFromRemainingArguments = $true)] - [string[]]$Arguments -) - -Set-StrictMode -Version 2 -$ErrorActionPreference = 'Stop' - -# -# Functions -# - -function Get-KoreBuild { - - $lockFile = Join-Path $Path 'korebuild-lock.txt' - - if (!(Test-Path $lockFile) -or $Update) { - Get-RemoteFile "$ToolsSource/korebuild/channels/$Channel/latest.txt" $lockFile $ToolsSourceSuffix - } - - $version = Get-Content $lockFile | Where-Object { $_ -like 'version:*' } | Select-Object -first 1 - if (!$version) { - Write-Error "Failed to parse version from $lockFile. Expected a line that begins with 'version:'" - } - $version = $version.TrimStart('version:').Trim() - $korebuildPath = Join-Paths $DotNetHome ('buildtools', 'korebuild', $version) - - if (!(Test-Path $korebuildPath)) { - Write-Host -ForegroundColor Magenta "Downloading KoreBuild $version" - New-Item -ItemType Directory -Path $korebuildPath | Out-Null - $remotePath = "$ToolsSource/korebuild/artifacts/$version/korebuild.$version.zip" - - try { - $tmpfile = Join-Path ([IO.Path]::GetTempPath()) "KoreBuild-$([guid]::NewGuid()).zip" - Get-RemoteFile $remotePath $tmpfile $ToolsSourceSuffix - if (Get-Command -Name 'Expand-Archive' -ErrorAction Ignore) { - # Use built-in commands where possible as they are cross-plat compatible - Expand-Archive -Path $tmpfile -DestinationPath $korebuildPath - } - else { - # Fallback to old approach for old installations of PowerShell - Add-Type -AssemblyName System.IO.Compression.FileSystem - [System.IO.Compression.ZipFile]::ExtractToDirectory($tmpfile, $korebuildPath) - } - } - catch { - Remove-Item -Recurse -Force $korebuildPath -ErrorAction Ignore - throw - } - finally { - Remove-Item $tmpfile -ErrorAction Ignore - } - } - - return $korebuildPath -} - -function Join-Paths([string]$path, [string[]]$childPaths) { - $childPaths | ForEach-Object { $path = Join-Path $path $_ } - return $path -} - -function Get-RemoteFile([string]$RemotePath, [string]$LocalPath, [string]$RemoteSuffix) { - if ($RemotePath -notlike 'http*') { - Copy-Item $RemotePath $LocalPath - return - } - - $retries = 10 - while ($retries -gt 0) { - $retries -= 1 - try { - Invoke-WebRequest -UseBasicParsing -Uri $($RemotePath + $RemoteSuffix) -OutFile $LocalPath - return - } - catch { - Write-Verbose "Request failed. $retries retries remaining" - } - } - - Write-Error "Download failed: '$RemotePath'." -} - -# -# Main -# - -# Load configuration or set defaults - -$Path = Resolve-Path $Path -if (!$ConfigFile) { $ConfigFile = Join-Path $Path 'korebuild.json' } - -if (Test-Path $ConfigFile) { - try { - $config = Get-Content -Raw -Encoding UTF8 -Path $ConfigFile | ConvertFrom-Json - if ($config) { - if (!($Channel) -and (Get-Member -Name 'channel' -InputObject $config)) { [string] $Channel = $config.channel } - if (!($ToolsSource) -and (Get-Member -Name 'toolsSource' -InputObject $config)) { [string] $ToolsSource = $config.toolsSource} - } - } - catch { - Write-Warning "$ConfigFile could not be read. Its settings will be ignored." - Write-Warning $Error[0] - } -} - -if (!$DotNetHome) { - $DotNetHome = if ($env:DOTNET_HOME) { $env:DOTNET_HOME } ` - elseif ($env:USERPROFILE) { Join-Path $env:USERPROFILE '.dotnet'} ` - elseif ($env:HOME) {Join-Path $env:HOME '.dotnet'}` - else { Join-Path $PSScriptRoot '.dotnet'} -} - -if (!$Channel) { $Channel = 'dev' } -if (!$ToolsSource) { $ToolsSource = 'https://aspnetcore.blob.core.windows.net/buildtools' } - -# Execute - -$korebuildPath = Get-KoreBuild -Import-Module -Force -Scope Local (Join-Path $korebuildPath 'KoreBuild.psd1') - -try { - Set-KoreBuildSettings -ToolsSource $ToolsSource -DotNetHome $DotNetHome -RepoPath $Path -ConfigFile $ConfigFile - Invoke-KoreBuildCommand $Command @Arguments -} -finally { - Remove-Module 'KoreBuild' -ErrorAction Ignore -} diff --git a/run.sh b/run.sh deleted file mode 100755 index 834961fc3a..0000000000 --- a/run.sh +++ /dev/null @@ -1,231 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -# -# variables -# - -RESET="\033[0m" -RED="\033[0;31m" -YELLOW="\033[0;33m" -MAGENTA="\033[0;95m" -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" -[ -z "${DOTNET_HOME:-}" ] && DOTNET_HOME="$HOME/.dotnet" -verbose=false -update=false -repo_path="$DIR" -channel='' -tools_source='' -tools_source_suffix='' - -# -# Functions -# -__usage() { - echo "Usage: $(basename "${BASH_SOURCE[0]}") command [options] [[--] ...]" - echo "" - echo "Arguments:" - echo " command The command to be run." - echo " ... Arguments passed to the command. Variable number of arguments allowed." - echo "" - echo "Options:" - echo " --verbose Show verbose output." - echo " -c|--channel The channel of KoreBuild to download. Overrides the value from the config file.." - echo " --config-file The path to the configuration file that stores values. Defaults to korebuild.json." - echo " -d|--dotnet-home The directory where .NET Core tools will be stored. Defaults to '\$DOTNET_HOME' or '\$HOME/.dotnet." - echo " --path The directory to build. Defaults to the directory containing the script." - echo " -s|--tools-source|-ToolsSource The base url where build tools can be downloaded. Overrides the value from the config file." - echo " --tools-source-suffix|-ToolsSourceSuffix The suffix to append to tools-source. Useful for query strings." - echo " -u|--update Update to the latest KoreBuild even if the lock file is present." - echo "" - echo "Description:" - echo " This function will create a file \$DIR/korebuild-lock.txt. This lock file can be committed to source, but does not have to be." - echo " When the lockfile is not present, KoreBuild will create one using latest available version from \$channel." - - if [[ "${1:-}" != '--no-exit' ]]; then - exit 2 - fi -} - -get_korebuild() { - local version - local lock_file="$repo_path/korebuild-lock.txt" - if [ ! -f "$lock_file" ] || [ "$update" = true ]; then - __get_remote_file "$tools_source/korebuild/channels/$channel/latest.txt" "$lock_file" "$tools_source_suffix" - fi - version="$(grep 'version:*' -m 1 "$lock_file")" - if [[ "$version" == '' ]]; then - __error "Failed to parse version from $lock_file. Expected a line that begins with 'version:'" - return 1 - fi - version="$(echo "${version#version:}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" - local korebuild_path="$DOTNET_HOME/buildtools/korebuild/$version" - - { - if [ ! -d "$korebuild_path" ]; then - mkdir -p "$korebuild_path" - local remote_path="$tools_source/korebuild/artifacts/$version/korebuild.$version.zip" - tmpfile="$(mktemp)" - echo -e "${MAGENTA}Downloading KoreBuild ${version}${RESET}" - if __get_remote_file "$remote_path" "$tmpfile" "$tools_source_suffix"; then - unzip -q -d "$korebuild_path" "$tmpfile" - fi - rm "$tmpfile" || true - fi - - source "$korebuild_path/KoreBuild.sh" - } || { - if [ -d "$korebuild_path" ]; then - echo "Cleaning up after failed installation" - rm -rf "$korebuild_path" || true - fi - return 1 - } -} - -__error() { - echo -e "${RED}error: $*${RESET}" 1>&2 -} - -__warn() { - echo -e "${YELLOW}warning: $*${RESET}" -} - -__machine_has() { - hash "$1" > /dev/null 2>&1 - return $? -} - -__get_remote_file() { - local remote_path=$1 - local local_path=$2 - local remote_path_suffix=$3 - - if [[ "$remote_path" != 'http'* ]]; then - cp "$remote_path" "$local_path" - return 0 - fi - - local failed=false - if __machine_has wget; then - wget --tries 10 --quiet -O "$local_path" "${remote_path}${remote_path_suffix}" || failed=true - else - failed=true - fi - - if [ "$failed" = true ] && __machine_has curl; then - failed=false - curl --retry 10 -sSL -f --create-dirs -o "$local_path" "${remote_path}${remote_path_suffix}" || failed=true - fi - - if [ "$failed" = true ]; then - __error "Download failed: $remote_path" 1>&2 - return 1 - fi -} - -# -# main -# - -command="${1:-}" -shift - -while [[ $# -gt 0 ]]; do - case $1 in - -\?|-h|--help) - __usage --no-exit - exit 0 - ;; - -c|--channel|-Channel) - shift - channel="${1:-}" - [ -z "$channel" ] && __usage - ;; - --config-file|-ConfigFile) - shift - config_file="${1:-}" - [ -z "$config_file" ] && __usage - if [ ! -f "$config_file" ]; then - __error "Invalid value for --config-file. $config_file does not exist." - exit 1 - fi - ;; - -d|--dotnet-home|-DotNetHome) - shift - DOTNET_HOME="${1:-}" - [ -z "$DOTNET_HOME" ] && __usage - ;; - --path|-Path) - shift - repo_path="${1:-}" - [ -z "$repo_path" ] && __usage - ;; - -s|--tools-source|-ToolsSource) - shift - tools_source="${1:-}" - [ -z "$tools_source" ] && __usage - ;; - --tools-source-suffix|-ToolsSourceSuffix) - shift - tools_source_suffix="${1:-}" - [ -z "$tools_source_suffix" ] && __usage - ;; - -u|--update|-Update) - update=true - ;; - --verbose|-Verbose) - verbose=true - ;; - --) - shift - break - ;; - *) - break - ;; - esac - shift -done - -if ! __machine_has unzip; then - __error 'Missing required command: unzip' - exit 1 -fi - -if ! __machine_has curl && ! __machine_has wget; then - __error 'Missing required command. Either wget or curl is required.' - exit 1 -fi - -[ -z "${config_file:-}" ] && config_file="$repo_path/korebuild.json" -if [ -f "$config_file" ]; then - if __machine_has jq ; then - if jq '.' "$config_file" >/dev/null ; then - config_channel="$(jq -r 'select(.channel!=null) | .channel' "$config_file")" - config_tools_source="$(jq -r 'select(.toolsSource!=null) | .toolsSource' "$config_file")" - else - __warn "$config_file is invalid JSON. Its settings will be ignored." - fi - elif __machine_has python ; then - if python -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'))" >/dev/null ; then - config_channel="$(python -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'));print(obj['channel'] if 'channel' in obj else '')")" - config_tools_source="$(python -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'));print(obj['toolsSource'] if 'toolsSource' in obj else '')")" - else - __warn "$config_file is invalid JSON. Its settings will be ignored." - fi - else - __warn 'Missing required command: jq or pyton. Could not parse the JSON file. Its settings will be ignored.' - fi - - [ ! -z "${config_channel:-}" ] && channel="$config_channel" - [ ! -z "${config_tools_source:-}" ] && tools_source="$config_tools_source" -fi - -[ -z "$channel" ] && channel='dev' -[ -z "$tools_source" ] && tools_source='https://aspnetcore.blob.core.windows.net/buildtools' - -get_korebuild -set_korebuildsettings "$tools_source" "$DOTNET_HOME" "$repo_path" "$config_file" -invoke_korebuild_command "$command" "$@" diff --git a/.gitignore b/src/ResponseCaching/.gitignore similarity index 100% rename from .gitignore rename to src/ResponseCaching/.gitignore diff --git a/Directory.Build.props b/src/ResponseCaching/Directory.Build.props similarity index 100% rename from Directory.Build.props rename to src/ResponseCaching/Directory.Build.props diff --git a/Directory.Build.targets b/src/ResponseCaching/Directory.Build.targets similarity index 100% rename from Directory.Build.targets rename to src/ResponseCaching/Directory.Build.targets diff --git a/NuGetPackageVerifier.json b/src/ResponseCaching/NuGetPackageVerifier.json similarity index 100% rename from NuGetPackageVerifier.json rename to src/ResponseCaching/NuGetPackageVerifier.json diff --git a/README.md b/src/ResponseCaching/README.md similarity index 100% rename from README.md rename to src/ResponseCaching/README.md diff --git a/ResponseCaching.sln b/src/ResponseCaching/ResponseCaching.sln similarity index 100% rename from ResponseCaching.sln rename to src/ResponseCaching/ResponseCaching.sln diff --git a/build/Key.snk b/src/ResponseCaching/build/Key.snk similarity index 100% rename from build/Key.snk rename to src/ResponseCaching/build/Key.snk diff --git a/build/dependencies.props b/src/ResponseCaching/build/dependencies.props similarity index 100% rename from build/dependencies.props rename to src/ResponseCaching/build/dependencies.props diff --git a/build/repo.props b/src/ResponseCaching/build/repo.props similarity index 100% rename from build/repo.props rename to src/ResponseCaching/build/repo.props diff --git a/build/sources.props b/src/ResponseCaching/build/sources.props similarity index 100% rename from build/sources.props rename to src/ResponseCaching/build/sources.props diff --git a/samples/ResponseCachingSample/README.md b/src/ResponseCaching/samples/ResponseCachingSample/README.md similarity index 100% rename from samples/ResponseCachingSample/README.md rename to src/ResponseCaching/samples/ResponseCachingSample/README.md diff --git a/samples/ResponseCachingSample/ResponseCachingSample.csproj b/src/ResponseCaching/samples/ResponseCachingSample/ResponseCachingSample.csproj similarity index 100% rename from samples/ResponseCachingSample/ResponseCachingSample.csproj rename to src/ResponseCaching/samples/ResponseCachingSample/ResponseCachingSample.csproj diff --git a/samples/ResponseCachingSample/Startup.cs b/src/ResponseCaching/samples/ResponseCachingSample/Startup.cs similarity index 100% rename from samples/ResponseCachingSample/Startup.cs rename to src/ResponseCaching/samples/ResponseCachingSample/Startup.cs diff --git a/src/Directory.Build.props b/src/ResponseCaching/src/Directory.Build.props similarity index 100% rename from src/Directory.Build.props rename to src/ResponseCaching/src/Directory.Build.props diff --git a/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/IResponseCachingFeature.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/IResponseCachingFeature.cs similarity index 100% rename from src/Microsoft.AspNetCore.ResponseCaching.Abstractions/IResponseCachingFeature.cs rename to src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/IResponseCachingFeature.cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj similarity index 100% rename from src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj rename to src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj diff --git a/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/baseline.netcore.json b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/baseline.netcore.json similarity index 100% rename from src/Microsoft.AspNetCore.ResponseCaching.Abstractions/baseline.netcore.json rename to src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching.Abstractions/baseline.netcore.json diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CacheEntryHelpers .cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CacheEntryHelpers .cs similarity index 100% rename from src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CacheEntryHelpers .cs rename to src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CacheEntryHelpers .cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CachedResponse.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CachedResponse.cs similarity index 100% rename from src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CachedResponse.cs rename to src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CachedResponse.cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CachedVaryByRules.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CachedVaryByRules.cs similarity index 100% rename from src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CachedVaryByRules.cs rename to src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CachedVaryByRules.cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/FastGuid.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/FastGuid.cs similarity index 100% rename from src/Microsoft.AspNetCore.ResponseCaching/Internal/FastGuid.cs rename to src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/FastGuid.cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ISystemClock.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/ISystemClock.cs similarity index 100% rename from src/Microsoft.AspNetCore.ResponseCaching/Internal/ISystemClock.cs rename to src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/ISystemClock.cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCache.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCache.cs similarity index 100% rename from src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCache.cs rename to src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCache.cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCacheEntry.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCacheEntry.cs similarity index 100% rename from src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCacheEntry.cs rename to src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCacheEntry.cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCachingKeyProvider.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCachingKeyProvider.cs similarity index 100% rename from src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCachingKeyProvider.cs rename to src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCachingKeyProvider.cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCachingPolicyProvider.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCachingPolicyProvider.cs similarity index 100% rename from src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCachingPolicyProvider.cs rename to src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCachingPolicyProvider.cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/LoggerExtensions.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/LoggerExtensions.cs similarity index 100% rename from src/Microsoft.AspNetCore.ResponseCaching/Internal/LoggerExtensions.cs rename to src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/LoggerExtensions.cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryCachedResponse.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryCachedResponse.cs similarity index 100% rename from src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryCachedResponse.cs rename to src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryCachedResponse.cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs similarity index 100% rename from src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs rename to src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingContext.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingContext.cs similarity index 100% rename from src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingContext.cs rename to src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingContext.cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingKeyProvider.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingKeyProvider.cs similarity index 100% rename from src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingKeyProvider.cs rename to src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingKeyProvider.cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingPolicyProvider.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingPolicyProvider.cs similarity index 100% rename from src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingPolicyProvider.cs rename to src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingPolicyProvider.cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/SendFileFeatureWrapper.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/SendFileFeatureWrapper.cs similarity index 100% rename from src/Microsoft.AspNetCore.ResponseCaching/Internal/SendFileFeatureWrapper.cs rename to src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/SendFileFeatureWrapper.cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/StringBuilderExtensions.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/StringBuilderExtensions.cs similarity index 100% rename from src/Microsoft.AspNetCore.ResponseCaching/Internal/StringBuilderExtensions.cs rename to src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/StringBuilderExtensions.cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Internal/SystemClock.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/SystemClock.cs similarity index 100% rename from src/Microsoft.AspNetCore.ResponseCaching/Internal/SystemClock.cs rename to src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Internal/SystemClock.cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj similarity index 100% rename from src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj rename to src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Properties/AssemblyInfo.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Properties/AssemblyInfo.cs similarity index 100% rename from src/Microsoft.AspNetCore.ResponseCaching/Properties/AssemblyInfo.cs rename to src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Properties/AssemblyInfo.cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingExtensions.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingExtensions.cs similarity index 100% rename from src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingExtensions.cs rename to src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingExtensions.cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingFeature.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingFeature.cs similarity index 100% rename from src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingFeature.cs rename to src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingFeature.cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs similarity index 100% rename from src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs rename to src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingMiddleware.cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingOptions.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingOptions.cs similarity index 100% rename from src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingOptions.cs rename to src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingOptions.cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingServicesExtensions.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingServicesExtensions.cs similarity index 100% rename from src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingServicesExtensions.cs rename to src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingServicesExtensions.cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Streams/ResponseCachingStream.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Streams/ResponseCachingStream.cs similarity index 100% rename from src/Microsoft.AspNetCore.ResponseCaching/Streams/ResponseCachingStream.cs rename to src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Streams/ResponseCachingStream.cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Streams/SegmentReadStream.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Streams/SegmentReadStream.cs similarity index 100% rename from src/Microsoft.AspNetCore.ResponseCaching/Streams/SegmentReadStream.cs rename to src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Streams/SegmentReadStream.cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Streams/SegmentWriteStream.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Streams/SegmentWriteStream.cs similarity index 100% rename from src/Microsoft.AspNetCore.ResponseCaching/Streams/SegmentWriteStream.cs rename to src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Streams/SegmentWriteStream.cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/Streams/StreamUtilities.cs b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Streams/StreamUtilities.cs similarity index 100% rename from src/Microsoft.AspNetCore.ResponseCaching/Streams/StreamUtilities.cs rename to src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/Streams/StreamUtilities.cs diff --git a/src/Microsoft.AspNetCore.ResponseCaching/baseline.netcore.json b/src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/baseline.netcore.json similarity index 100% rename from src/Microsoft.AspNetCore.ResponseCaching/baseline.netcore.json rename to src/ResponseCaching/src/Microsoft.AspNetCore.ResponseCaching/baseline.netcore.json diff --git a/test/Directory.Build.props b/src/ResponseCaching/test/Directory.Build.props similarity index 100% rename from test/Directory.Build.props rename to src/ResponseCaching/test/Directory.Build.props diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj b/src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj similarity index 100% rename from test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj rename to src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingFeatureTests.cs b/src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingFeatureTests.cs similarity index 100% rename from test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingFeatureTests.cs rename to src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingFeatureTests.cs diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingKeyProviderTests.cs b/src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingKeyProviderTests.cs similarity index 100% rename from test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingKeyProviderTests.cs rename to src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingKeyProviderTests.cs diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs b/src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs similarity index 100% rename from test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs rename to src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingMiddlewareTests.cs diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingPolicyProviderTests.cs b/src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingPolicyProviderTests.cs similarity index 100% rename from test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingPolicyProviderTests.cs rename to src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingPolicyProviderTests.cs diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs b/src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs similarity index 100% rename from test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs rename to src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingTests.cs diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/SegmentReadStreamTests.cs b/src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/SegmentReadStreamTests.cs similarity index 100% rename from test/Microsoft.AspNetCore.ResponseCaching.Tests/SegmentReadStreamTests.cs rename to src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/SegmentReadStreamTests.cs diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/SegmentWriteStreamTests.cs b/src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/SegmentWriteStreamTests.cs similarity index 100% rename from test/Microsoft.AspNetCore.ResponseCaching.Tests/SegmentWriteStreamTests.cs rename to src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/SegmentWriteStreamTests.cs diff --git a/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs b/src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs similarity index 100% rename from test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs rename to src/ResponseCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs diff --git a/version.props b/src/ResponseCaching/version.props similarity index 100% rename from version.props rename to src/ResponseCaching/version.props