Merge remote-tracking branch 'ResponseCaching/rybrande/release21ToSrc' into rybrande/Mondo2.1
This commit is contained in:
commit
c8e271e8ac
|
|
@ -0,0 +1,33 @@
|
||||||
|
[Oo]bj/
|
||||||
|
[Bb]in/
|
||||||
|
TestResults/
|
||||||
|
.nuget/
|
||||||
|
_ReSharper.*/
|
||||||
|
packages/
|
||||||
|
artifacts/
|
||||||
|
PublishProfiles/
|
||||||
|
*.user
|
||||||
|
*.suo
|
||||||
|
*.cache
|
||||||
|
*.docstates
|
||||||
|
_ReSharper.*
|
||||||
|
nuget.exe
|
||||||
|
*net45.csproj
|
||||||
|
*net451.csproj
|
||||||
|
*k10.csproj
|
||||||
|
*.psess
|
||||||
|
*.vsp
|
||||||
|
*.pidb
|
||||||
|
*.userprefs
|
||||||
|
*DS_Store
|
||||||
|
*.ncrunchsolution
|
||||||
|
*.*sdf
|
||||||
|
*.ipch
|
||||||
|
*.sln.ide
|
||||||
|
project.lock.json
|
||||||
|
/.vs/
|
||||||
|
.vscode/
|
||||||
|
.build/
|
||||||
|
.testPublish/
|
||||||
|
launchSettings.json
|
||||||
|
global.json
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
<Project>
|
||||||
|
<Import
|
||||||
|
Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), AspNetCoreSettings.props))\AspNetCoreSettings.props"
|
||||||
|
Condition=" '$(CI)' != 'true' AND '$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory), AspNetCoreSettings.props))' != '' " />
|
||||||
|
|
||||||
|
<Import Project="version.props" />
|
||||||
|
<Import Project="build\dependencies.props" />
|
||||||
|
<Import Project="build\sources.props" />
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<Product>Microsoft ASP.NET Core</Product>
|
||||||
|
<RepositoryUrl>https://github.com/aspnet/ResponseCaching</RepositoryUrl>
|
||||||
|
<RepositoryType>git</RepositoryType>
|
||||||
|
<RepositoryRoot>$(MSBuildThisFileDirectory)</RepositoryRoot>
|
||||||
|
<AssemblyOriginatorKeyFile>$(MSBuildThisFileDirectory)build\Key.snk</AssemblyOriginatorKeyFile>
|
||||||
|
<SignAssembly>true</SignAssembly>
|
||||||
|
<PublicSign Condition="'$(OS)' != 'Windows_NT'">true</PublicSign>
|
||||||
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<RuntimeFrameworkVersion Condition=" '$(TargetFramework)' == 'netcoreapp2.0' ">$(MicrosoftNETCoreApp20PackageVersion)</RuntimeFrameworkVersion>
|
||||||
|
<RuntimeFrameworkVersion Condition=" '$(TargetFramework)' == 'netcoreapp2.1' ">$(MicrosoftNETCoreApp21PackageVersion)</RuntimeFrameworkVersion>
|
||||||
|
<NETStandardImplicitPackageVersion Condition=" '$(TargetFramework)' == 'netstandard2.0' ">$(NETStandardLibrary20PackageVersion)</NETStandardImplicitPackageVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"Default": {
|
||||||
|
"rules": [
|
||||||
|
"DefaultCompositeRule"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
ASP.NET Core Response Caching
|
||||||
|
========
|
||||||
|
AppVeyor: [](https://ci.appveyor.com/project/aspnetci/ResponseCaching/branch/dev)
|
||||||
|
|
||||||
|
Travis: [](https://travis-ci.org/aspnet/ResponseCaching)
|
||||||
|
|
||||||
|
This repo hosts the ASP.NET Core middleware for response caching.
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
|
# Visual Studio 15
|
||||||
|
VisualStudioVersion = 15.0.26730.10
|
||||||
|
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
|
||||||
|
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("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ResponseCachingSample", "samples\ResponseCachingSample\ResponseCachingSample.csproj", "{1139BDEE-FA15-474D-8855-0AB91F23CF26}"
|
||||||
|
EndProject
|
||||||
|
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("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.ResponseCaching", "src\Microsoft.AspNetCore.ResponseCaching\Microsoft.AspNetCore.ResponseCaching.csproj", "{D1031270-DBD3-4F02-A3DC-3E7DADE8EBE6}"
|
||||||
|
EndProject
|
||||||
|
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
|
||||||
|
Debug|Any CPU = Debug|Any CPU
|
||||||
|
Release|Any CPU = Release|Any CPU
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
|
{1139BDEE-FA15-474D-8855-0AB91F23CF26}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{1139BDEE-FA15-474D-8855-0AB91F23CF26}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{1139BDEE-FA15-474D-8855-0AB91F23CF26}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{1139BDEE-FA15-474D-8855-0AB91F23CF26}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{151B2027-3936-44B9-A4A0-E1E5902125AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{151B2027-3936-44B9-A4A0-E1E5902125AB}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{151B2027-3936-44B9-A4A0-E1E5902125AB}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{151B2027-3936-44B9-A4A0-E1E5902125AB}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{D1031270-DBD3-4F02-A3DC-3E7DADE8EBE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{D1031270-DBD3-4F02-A3DC-3E7DADE8EBE6}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{D1031270-DBD3-4F02-A3DC-3E7DADE8EBE6}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{D1031270-DBD3-4F02-A3DC-3E7DADE8EBE6}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{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
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(NestedProjects) = preSolution
|
||||||
|
{1139BDEE-FA15-474D-8855-0AB91F23CF26} = {C51DF5BD-B53D-4795-BC01-A9AB066BF286}
|
||||||
|
{151B2027-3936-44B9-A4A0-E1E5902125AB} = {89A50974-E9D4-4F87-ACF2-6A6005E64931}
|
||||||
|
{D1031270-DBD3-4F02-A3DC-3E7DADE8EBE6} = {367AABAF-E03C-4491-A9A7-BDDE8903D1B4}
|
||||||
|
{2D1022E8-CBB6-478D-A420-CB888D0EF7B7} = {367AABAF-E03C-4491-A9A7-BDDE8903D1B4}
|
||||||
|
EndGlobalSection
|
||||||
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
|
SolutionGuid = {6F6B4994-06D7-4D35-B0F7-F60913AA8402}
|
||||||
|
EndGlobalSection
|
||||||
|
EndGlobal
|
||||||
Binary file not shown.
|
|
@ -0,0 +1,32 @@
|
||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<!-- These package versions may be overridden or updated by automation. -->
|
||||||
|
<PropertyGroup Label="Package Versions: Auto">
|
||||||
|
<InternalAspNetCoreSdkPackageVersion>2.1.3-rtm-15802</InternalAspNetCoreSdkPackageVersion>
|
||||||
|
<MicrosoftNETCoreApp20PackageVersion>2.0.0</MicrosoftNETCoreApp20PackageVersion>
|
||||||
|
<MicrosoftNETCoreApp21PackageVersion>2.1.2</MicrosoftNETCoreApp21PackageVersion>
|
||||||
|
<MicrosoftNETTestSdkPackageVersion>15.6.1</MicrosoftNETTestSdkPackageVersion>
|
||||||
|
<NETStandardLibrary20PackageVersion>2.0.3</NETStandardLibrary20PackageVersion>
|
||||||
|
<XunitPackageVersion>2.3.1</XunitPackageVersion>
|
||||||
|
<XunitRunnerVisualStudioPackageVersion>2.4.0-beta.1.build3945</XunitRunnerVisualStudioPackageVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<!-- This may import a generated file which may override the variables above. -->
|
||||||
|
<Import Project="$(DotNetPackageVersionPropsPath)" Condition=" '$(DotNetPackageVersionPropsPath)' != '' " />
|
||||||
|
|
||||||
|
<!-- These are package versions that should not be overridden or updated by automation. -->
|
||||||
|
<PropertyGroup Label="Package Versions: Pinned">
|
||||||
|
<MicrosoftAspNetCoreHttpExtensionsPackageVersion>2.1.1</MicrosoftAspNetCoreHttpExtensionsPackageVersion>
|
||||||
|
<MicrosoftAspNetCoreHttpPackageVersion>2.1.1</MicrosoftAspNetCoreHttpPackageVersion>
|
||||||
|
<MicrosoftAspNetCoreServerIISIntegrationPackageVersion>2.1.1</MicrosoftAspNetCoreServerIISIntegrationPackageVersion>
|
||||||
|
<MicrosoftAspNetCoreServerKestrelPackageVersion>2.1.2</MicrosoftAspNetCoreServerKestrelPackageVersion>
|
||||||
|
<MicrosoftAspNetCoreTestHostPackageVersion>2.1.1</MicrosoftAspNetCoreTestHostPackageVersion>
|
||||||
|
<MicrosoftExtensionsCachingMemoryPackageVersion>2.1.1</MicrosoftExtensionsCachingMemoryPackageVersion>
|
||||||
|
<MicrosoftExtensionsLoggingAbstractionsPackageVersion>2.1.1</MicrosoftExtensionsLoggingAbstractionsPackageVersion>
|
||||||
|
<MicrosoftExtensionsLoggingTestingPackageVersion>2.1.1</MicrosoftExtensionsLoggingTestingPackageVersion>
|
||||||
|
<MicrosoftExtensionsPrimitivesPackageVersion>2.1.1</MicrosoftExtensionsPrimitivesPackageVersion>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
<Project>
|
||||||
|
<Import Project="dependencies.props" />
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<!-- These properties are use by the automation that updates dependencies.props -->
|
||||||
|
<LineupPackageId>Internal.AspNetCore.Universe.Lineup</LineupPackageId>
|
||||||
|
<LineupPackageVersion>2.1.0-rc1-*</LineupPackageVersion>
|
||||||
|
<LineupPackageRestoreSource>https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json</LineupPackageRestoreSource>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<DotNetCoreRuntime Include="$(MicrosoftNETCoreApp20PackageVersion)" />
|
||||||
|
<DotNetCoreRuntime Include="$(MicrosoftNETCoreApp21PackageVersion)" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
<Project>
|
||||||
|
<Import Project="$(DotNetRestoreSourcePropsPath)" Condition="'$(DotNetRestoreSourcePropsPath)' != ''"/>
|
||||||
|
|
||||||
|
<PropertyGroup Label="RestoreSources">
|
||||||
|
<RestoreSources>$(DotNetRestoreSources)</RestoreSources>
|
||||||
|
<RestoreSources Condition="'$(DotNetBuildOffline)' != 'true' AND '$(AspNetUniverseBuildOffline)' != 'true' ">
|
||||||
|
$(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;
|
||||||
|
</RestoreSources>
|
||||||
|
<RestoreSources Condition="'$(DotNetBuildOffline)' != 'true'">
|
||||||
|
$(RestoreSources);
|
||||||
|
https://api.nuget.org/v3/index.json;
|
||||||
|
</RestoreSources>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>netcoreapp2.1</TargetFramework>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.ResponseCaching\Microsoft.AspNetCore.ResponseCaching.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Server.IISIntegration" Version="$(MicrosoftAspNetCoreServerIISIntegrationPackageVersion)" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Server.Kestrel" Version="$(MicrosoftAspNetCoreServerKestrelPackageVersion)" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="$(MicrosoftExtensionsCachingMemoryPackageVersion)" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Net.Http.Headers;
|
||||||
|
|
||||||
|
namespace ResponseCachingSample
|
||||||
|
{
|
||||||
|
public class Startup
|
||||||
|
{
|
||||||
|
public void ConfigureServices(IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddResponseCaching();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Configure(IApplicationBuilder app)
|
||||||
|
{
|
||||||
|
app.UseResponseCaching();
|
||||||
|
app.Run(async (context) =>
|
||||||
|
{
|
||||||
|
context.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue()
|
||||||
|
{
|
||||||
|
Public = true,
|
||||||
|
MaxAge = TimeSpan.FromSeconds(10)
|
||||||
|
};
|
||||||
|
context.Response.Headers[HeaderNames.Vary] = new string[] { "Accept-Encoding" };
|
||||||
|
|
||||||
|
await context.Response.WriteAsync("Hello World! " + DateTime.UtcNow);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Main(string[] args)
|
||||||
|
{
|
||||||
|
var host = new WebHostBuilder()
|
||||||
|
.UseKestrel()
|
||||||
|
.UseContentRoot(Directory.GetCurrentDirectory())
|
||||||
|
.UseIISIntegration()
|
||||||
|
.UseStartup<Startup>()
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
host.Run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<Project>
|
||||||
|
<Import Project="..\Directory.Build.props" />
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Internal.AspNetCore.Sdk" PrivateAssets="All" Version="$(InternalAspNetCoreSdkPackageVersion)" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A feature for configuring additional response cache options on the HTTP response.
|
||||||
|
/// </summary>
|
||||||
|
public interface IResponseCachingFeature
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the query keys used by the response cache middleware for calculating secondary vary keys.
|
||||||
|
/// </summary>
|
||||||
|
string[] VaryByQueryKeys { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<Description>ASP.NET Core response caching middleware abstractions and feature interface definitions.</Description>
|
||||||
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
<PackageTags>aspnetcore;cache;caching</PackageTags>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Primitives" Version="$(MicrosoftExtensionsPrimitivesPackageVersion)" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
{
|
||||||
|
"AssemblyIdentity": "Microsoft.AspNetCore.ResponseCaching.Abstractions, Version=2.1.1.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": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +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 System;
|
||||||
|
using System.IO;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.ResponseCaching.Internal
|
||||||
|
{
|
||||||
|
public class CachedResponse : IResponseCacheEntry
|
||||||
|
{
|
||||||
|
public DateTimeOffset Created { get; set; }
|
||||||
|
|
||||||
|
public int StatusCode { get; set; }
|
||||||
|
|
||||||
|
public IHeaderDictionary Headers { get; set; }
|
||||||
|
|
||||||
|
public Stream Body { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +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.Internal
|
||||||
|
{
|
||||||
|
public class CachedVaryByRules : IResponseCacheEntry
|
||||||
|
{
|
||||||
|
public string VaryByKeyPrefix { get; set; }
|
||||||
|
|
||||||
|
public StringValues Headers { get; set; }
|
||||||
|
|
||||||
|
public StringValues QueryKeys { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,79 @@
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Abstracts the system clock to facilitate testing.
|
||||||
|
/// </summary>
|
||||||
|
internal interface ISystemClock
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the current system time in UTC.
|
||||||
|
/// </summary>
|
||||||
|
DateTimeOffset UtcNow { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.ResponseCaching.Internal
|
||||||
|
{
|
||||||
|
public interface IResponseCache
|
||||||
|
{
|
||||||
|
IResponseCacheEntry Get(string key);
|
||||||
|
Task<IResponseCacheEntry> GetAsync(string key);
|
||||||
|
|
||||||
|
void Set(string key, IResponseCacheEntry entry, TimeSpan validFor);
|
||||||
|
Task SetAsync(string key, IResponseCacheEntry entry, TimeSpan validFor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +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.
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.ResponseCaching.Internal
|
||||||
|
{
|
||||||
|
public interface IResponseCacheEntry
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.Internal
|
||||||
|
{
|
||||||
|
public interface IResponseCachingKeyProvider
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Create a base key for a response cache entry.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The <see cref="ResponseCachingContext"/>.</param>
|
||||||
|
/// <returns>The created base key.</returns>
|
||||||
|
string CreateBaseKey(ResponseCachingContext context);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create a vary key for storing cached responses.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The <see cref="ResponseCachingContext"/>.</param>
|
||||||
|
/// <returns>The created vary key.</returns>
|
||||||
|
string CreateStorageVaryByKey(ResponseCachingContext context);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Create one or more vary keys for looking up cached responses.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The <see cref="ResponseCachingContext"/>.</param>
|
||||||
|
/// <returns>An ordered <see cref="IEnumerable{T}"/> containing the vary keys to try when looking up items.</returns>
|
||||||
|
IEnumerable<string> CreateLookupVaryByKeys(ResponseCachingContext context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
// 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
|
||||||
|
{
|
||||||
|
public interface IResponseCachingPolicyProvider
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Determine whether the response caching logic should be attempted for the incoming HTTP request.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The <see cref="ResponseCachingContext"/>.</param>
|
||||||
|
/// <returns><c>true</c> if response caching logic should be attempted; otherwise <c>false</c>.</returns>
|
||||||
|
bool AttemptResponseCaching(ResponseCachingContext context);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determine whether a cache lookup is allowed for the incoming HTTP request.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The <see cref="ResponseCachingContext"/>.</param>
|
||||||
|
/// <returns><c>true</c> if cache lookup for this request is allowed; otherwise <c>false</c>.</returns>
|
||||||
|
bool AllowCacheLookup(ResponseCachingContext context);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determine whether storage of the response is allowed for the incoming HTTP request.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The <see cref="ResponseCachingContext"/>.</param>
|
||||||
|
/// <returns><c>true</c> if storage of the response for this request is allowed; otherwise <c>false</c>.</returns>
|
||||||
|
bool AllowCacheStorage(ResponseCachingContext context);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determine whether the response received by the middleware can be cached for future requests.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The <see cref="ResponseCachingContext"/>.</param>
|
||||||
|
/// <returns><c>true</c> if the response is cacheable; otherwise <c>false</c>.</returns>
|
||||||
|
bool IsResponseCacheable(ResponseCachingContext context);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determine whether the response retrieved from the response cache is fresh and can be served.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context">The <see cref="ResponseCachingContext"/>.</param>
|
||||||
|
/// <returns><c>true</c> if the cached entry is fresh; otherwise <c>false</c>.</returns>
|
||||||
|
bool IsCachedEntryFresh(ResponseCachingContext context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,310 @@
|
||||||
|
// 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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Defines *all* the logger messages produced by response caching
|
||||||
|
/// </summary>
|
||||||
|
internal static class LoggerExtensions
|
||||||
|
{
|
||||||
|
private static Action<ILogger, string, Exception> _logRequestMethodNotCacheable;
|
||||||
|
private static Action<ILogger, Exception> _logRequestWithAuthorizationNotCacheable;
|
||||||
|
private static Action<ILogger, Exception> _logRequestWithNoCacheNotCacheable;
|
||||||
|
private static Action<ILogger, Exception> _logRequestWithPragmaNoCacheNotCacheable;
|
||||||
|
private static Action<ILogger, TimeSpan, Exception> _logExpirationMinFreshAdded;
|
||||||
|
private static Action<ILogger, TimeSpan, TimeSpan, Exception> _logExpirationSharedMaxAgeExceeded;
|
||||||
|
private static Action<ILogger, TimeSpan, TimeSpan, Exception> _logExpirationMustRevalidate;
|
||||||
|
private static Action<ILogger, TimeSpan, TimeSpan, TimeSpan, Exception> _logExpirationMaxStaleSatisfied;
|
||||||
|
private static Action<ILogger, TimeSpan, TimeSpan, Exception> _logExpirationMaxAgeExceeded;
|
||||||
|
private static Action<ILogger, DateTimeOffset, DateTimeOffset, Exception> _logExpirationExpiresExceeded;
|
||||||
|
private static Action<ILogger, Exception> _logResponseWithoutPublicNotCacheable;
|
||||||
|
private static Action<ILogger, Exception> _logResponseWithNoStoreNotCacheable;
|
||||||
|
private static Action<ILogger, Exception> _logResponseWithNoCacheNotCacheable;
|
||||||
|
private static Action<ILogger, Exception> _logResponseWithSetCookieNotCacheable;
|
||||||
|
private static Action<ILogger, Exception> _logResponseWithVaryStarNotCacheable;
|
||||||
|
private static Action<ILogger, Exception> _logResponseWithPrivateNotCacheable;
|
||||||
|
private static Action<ILogger, int, Exception> _logResponseWithUnsuccessfulStatusCodeNotCacheable;
|
||||||
|
private static Action<ILogger, Exception> _logNotModifiedIfNoneMatchStar;
|
||||||
|
private static Action<ILogger, EntityTagHeaderValue, Exception> _logNotModifiedIfNoneMatchMatched;
|
||||||
|
private static Action<ILogger, DateTimeOffset, DateTimeOffset, Exception> _logNotModifiedIfModifiedSinceSatisfied;
|
||||||
|
private static Action<ILogger, Exception> _logNotModifiedServed;
|
||||||
|
private static Action<ILogger, Exception> _logCachedResponseServed;
|
||||||
|
private static Action<ILogger, Exception> _logGatewayTimeoutServed;
|
||||||
|
private static Action<ILogger, Exception> _logNoResponseServed;
|
||||||
|
private static Action<ILogger, string, string, Exception> _logVaryByRulesUpdated;
|
||||||
|
private static Action<ILogger, Exception> _logResponseCached;
|
||||||
|
private static Action<ILogger, Exception> _logResponseNotCached;
|
||||||
|
private static Action<ILogger, Exception> _logResponseContentLengthMismatchNotCached;
|
||||||
|
private static Action<ILogger, TimeSpan, TimeSpan, Exception> _logExpirationInfiniteMaxStaleSatisfied;
|
||||||
|
|
||||||
|
static LoggerExtensions()
|
||||||
|
{
|
||||||
|
_logRequestMethodNotCacheable = LoggerMessage.Define<string>(
|
||||||
|
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<TimeSpan>(
|
||||||
|
logLevel: LogLevel.Debug,
|
||||||
|
eventId: 5,
|
||||||
|
formatString: "Adding a minimum freshness requirement of {Duration} specified by the 'min-fresh' cache directive.");
|
||||||
|
_logExpirationSharedMaxAgeExceeded = LoggerMessage.Define<TimeSpan, TimeSpan>(
|
||||||
|
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<TimeSpan, TimeSpan>(
|
||||||
|
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' or 'proxy-revalidate' cache directive is specified.");
|
||||||
|
_logExpirationMaxStaleSatisfied = LoggerMessage.Define<TimeSpan, TimeSpan, TimeSpan>(
|
||||||
|
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<TimeSpan, TimeSpan>(
|
||||||
|
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<DateTimeOffset, DateTimeOffset>(
|
||||||
|
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<int>(
|
||||||
|
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<EntityTagHeaderValue>(
|
||||||
|
logLevel: LogLevel.Debug,
|
||||||
|
eventId: 19,
|
||||||
|
formatString: $"The ETag {{ETag}} in the '{HeaderNames.IfNoneMatch}' header matched the ETag of a cached entry.");
|
||||||
|
_logNotModifiedIfModifiedSinceSatisfied = LoggerMessage.Define<DateTimeOffset, DateTimeOffset>(
|
||||||
|
logLevel: LogLevel.Debug,
|
||||||
|
eventId: 20,
|
||||||
|
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,
|
||||||
|
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<string, string>(
|
||||||
|
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.");
|
||||||
|
_logExpirationInfiniteMaxStaleSatisfied = LoggerMessage.Define<TimeSpan, TimeSpan>(
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
_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 LogNotModifiedIfModifiedSinceSatisfied(this ILogger logger, DateTimeOffset lastModified, DateTimeOffset ifModifiedSince)
|
||||||
|
{
|
||||||
|
_logNotModifiedIfModifiedSinceSatisfied(logger, lastModified, ifModifiedSince, 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void LogExpirationInfiniteMaxStaleSatisfied(this ILogger logger, TimeSpan age, TimeSpan maxAge)
|
||||||
|
{
|
||||||
|
_logExpirationInfiniteMaxStaleSatisfied(logger, age, maxAge, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<byte[]> BodySegments { get; set; }
|
||||||
|
|
||||||
|
public long BodyLength { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
// 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.Memory;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.ResponseCaching.Internal
|
||||||
|
{
|
||||||
|
public class MemoryResponseCache : IResponseCache
|
||||||
|
{
|
||||||
|
private readonly IMemoryCache _cache;
|
||||||
|
|
||||||
|
public MemoryResponseCache(IMemoryCache cache)
|
||||||
|
{
|
||||||
|
if (cache == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(cache));
|
||||||
|
}
|
||||||
|
|
||||||
|
_cache = cache;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IResponseCacheEntry Get(string key)
|
||||||
|
{
|
||||||
|
var entry = _cache.Get(key);
|
||||||
|
|
||||||
|
var memoryCachedResponse = entry as MemoryCachedResponse;
|
||||||
|
if (memoryCachedResponse != null)
|
||||||
|
{
|
||||||
|
return new CachedResponse
|
||||||
|
{
|
||||||
|
Created = memoryCachedResponse.Created,
|
||||||
|
StatusCode = memoryCachedResponse.StatusCode,
|
||||||
|
Headers = memoryCachedResponse.Headers,
|
||||||
|
Body = new SegmentReadStream(memoryCachedResponse.BodySegments, memoryCachedResponse.BodyLength)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return entry as IResponseCacheEntry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<IResponseCacheEntry> 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);
|
||||||
|
cachedResponse.Body.CopyTo(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,
|
||||||
|
Size = CacheEntryHelpers.EstimateCachedResponseSize(cachedResponse)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_cache.Set(
|
||||||
|
key,
|
||||||
|
entry,
|
||||||
|
new MemoryCacheEntryOptions
|
||||||
|
{
|
||||||
|
AbsoluteExpirationRelativeToNow = validFor,
|
||||||
|
Size = CacheEntryHelpers.EstimateCachedVaryByRulesySize(entry as CachedVaryByRules)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task SetAsync(string key, IResponseCacheEntry entry, TimeSpan validFor)
|
||||||
|
{
|
||||||
|
Set(key, entry, validFor);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,134 @@
|
||||||
|
// 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;
|
||||||
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Net.Http.Headers;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.ResponseCaching.Internal
|
||||||
|
{
|
||||||
|
public class ResponseCachingContext
|
||||||
|
{
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
HttpContext = httpContext;
|
||||||
|
Logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
public HttpContext HttpContext { get; }
|
||||||
|
|
||||||
|
public DateTimeOffset? ResponseTime { get; internal set; }
|
||||||
|
|
||||||
|
public TimeSpan? CachedEntryAge { get; internal set; }
|
||||||
|
|
||||||
|
public CachedVaryByRules CachedVaryByRules { get; internal set; }
|
||||||
|
|
||||||
|
internal ILogger Logger { get; }
|
||||||
|
|
||||||
|
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; }
|
||||||
|
|
||||||
|
internal ResponseCachingStream ResponseCachingStream { get; set; }
|
||||||
|
|
||||||
|
internal IHttpSendFileFeature OriginalSendFileFeature { get; set; }
|
||||||
|
|
||||||
|
internal IHeaderDictionary CachedResponseHeaders { get; set; }
|
||||||
|
|
||||||
|
internal DateTimeOffset? ResponseDate
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (!_parsedResponseDate)
|
||||||
|
{
|
||||||
|
_parsedResponseDate = true;
|
||||||
|
DateTimeOffset date;
|
||||||
|
if (HeaderUtilities.TryParseDate(HttpContext.Response.Headers[HeaderNames.Date].ToString(), out date))
|
||||||
|
{
|
||||||
|
_responseDate = date;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_responseDate = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
DateTimeOffset expires;
|
||||||
|
if (HeaderUtilities.TryParseDate(HttpContext.Response.Headers[HeaderNames.Expires].ToString(), out expires))
|
||||||
|
{
|
||||||
|
_responseExpires = expires;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_responseExpires = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _responseExpires;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal TimeSpan? ResponseSharedMaxAge
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (!_parsedResponseSharedMaxAge)
|
||||||
|
{
|
||||||
|
_parsedResponseSharedMaxAge = true;
|
||||||
|
HeaderUtilities.TryParseSeconds(HttpContext.Response.Headers[HeaderNames.CacheControl], CacheControlHeaderValue.SharedMaxAgeString, out _responseSharedMaxAge);
|
||||||
|
}
|
||||||
|
return _responseSharedMaxAge;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal TimeSpan? ResponseMaxAge
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (!_parsedResponseMaxAge)
|
||||||
|
{
|
||||||
|
_parsedResponseMaxAge = true;
|
||||||
|
HeaderUtilities.TryParseSeconds(HttpContext.Response.Headers[HeaderNames.CacheControl], CacheControlHeaderValue.MaxAgeString, out _responseMaxAge);
|
||||||
|
}
|
||||||
|
return _responseMaxAge;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,219 @@
|
||||||
|
// 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.Extensions.ObjectPool;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.ResponseCaching.Internal
|
||||||
|
{
|
||||||
|
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';
|
||||||
|
// Use the unit separator for delimiting subcomponents of the cache key to avoid possible collisions
|
||||||
|
private static readonly char KeySubDelimiter = '\x1f';
|
||||||
|
|
||||||
|
private readonly ObjectPool<StringBuilder> _builderPool;
|
||||||
|
private readonly ResponseCachingOptions _options;
|
||||||
|
|
||||||
|
public ResponseCachingKeyProvider(ObjectPoolProvider poolProvider, IOptions<ResponseCachingOptions> options)
|
||||||
|
{
|
||||||
|
if (poolProvider == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(poolProvider));
|
||||||
|
}
|
||||||
|
if (options == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(options));
|
||||||
|
}
|
||||||
|
|
||||||
|
_builderPool = poolProvider.CreateStringBuilderPool();
|
||||||
|
_options = options.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<string> CreateLookupVaryByKeys(ResponseCachingContext context)
|
||||||
|
{
|
||||||
|
return new string[] { CreateStorageVaryByKey(context) };
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET<delimiter>SCHEME<delimiter>HOST:PORT/PATHBASE/PATH
|
||||||
|
public string CreateBaseKey(ResponseCachingContext context)
|
||||||
|
{
|
||||||
|
if (context == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
var request = context.HttpContext.Request;
|
||||||
|
var builder = _builderPool.Get();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
builder
|
||||||
|
.AppendUpperInvariant(request.Method)
|
||||||
|
.Append(KeyDelimiter)
|
||||||
|
.AppendUpperInvariant(request.Scheme)
|
||||||
|
.Append(KeyDelimiter)
|
||||||
|
.AppendUpperInvariant(request.Host.Value);
|
||||||
|
|
||||||
|
if (_options.UseCaseSensitivePaths)
|
||||||
|
{
|
||||||
|
builder
|
||||||
|
.Append(request.PathBase.Value)
|
||||||
|
.Append(request.Path.Value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
builder
|
||||||
|
.AppendUpperInvariant(request.PathBase.Value)
|
||||||
|
.AppendUpperInvariant(request.Path.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_builderPool.Return(builder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BaseKey<delimiter>H<delimiter>HeaderName=HeaderValue<delimiter>Q<delimiter>QueryName=QueryValue1<subdelimiter>QueryValue2
|
||||||
|
public string CreateStorageVaryByKey(ResponseCachingContext context)
|
||||||
|
{
|
||||||
|
if (context == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
var varyByRules = context.CachedVaryByRules;
|
||||||
|
if (varyByRules == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"{nameof(CachedVaryByRules)} must not be null on the {nameof(ResponseCachingContext)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((StringValues.IsNullOrEmpty(varyByRules.Headers) && StringValues.IsNullOrEmpty(varyByRules.QueryKeys)))
|
||||||
|
{
|
||||||
|
return varyByRules.VaryByKeyPrefix;
|
||||||
|
}
|
||||||
|
|
||||||
|
var request = context.HttpContext.Request;
|
||||||
|
var builder = _builderPool.Get();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Prepend with the Guid of the CachedVaryByRules
|
||||||
|
builder.Append(varyByRules.VaryByKeyPrefix);
|
||||||
|
|
||||||
|
// Vary by headers
|
||||||
|
if (varyByRules?.Headers.Count > 0)
|
||||||
|
{
|
||||||
|
// Append a group separator for the header segment of the cache key
|
||||||
|
builder.Append(KeyDelimiter)
|
||||||
|
.Append('H');
|
||||||
|
|
||||||
|
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("=");
|
||||||
|
|
||||||
|
var headerValuesArray = headerValues.ToArray();
|
||||||
|
Array.Sort(headerValuesArray, StringComparer.Ordinal);
|
||||||
|
|
||||||
|
for (var j = 0; j < headerValuesArray.Length; j++)
|
||||||
|
{
|
||||||
|
builder.Append(headerValuesArray[j]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vary by query keys
|
||||||
|
if (varyByRules?.QueryKeys.Count > 0)
|
||||||
|
{
|
||||||
|
// Append a group separator for the query key segment of the cache key
|
||||||
|
builder.Append(KeyDelimiter)
|
||||||
|
.Append('Q');
|
||||||
|
|
||||||
|
if (varyByRules.QueryKeys.Count == 1 && string.Equals(varyByRules.QueryKeys[0], "*", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
// Vary by all available query keys
|
||||||
|
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(queryArray[i].Key)
|
||||||
|
.Append("=");
|
||||||
|
|
||||||
|
var queryValueArray = queryArray[i].Value.ToArray();
|
||||||
|
Array.Sort(queryValueArray, StringComparer.Ordinal);
|
||||||
|
|
||||||
|
for (var j = 0; j < queryValueArray.Length; j++)
|
||||||
|
{
|
||||||
|
if (j > 0)
|
||||||
|
{
|
||||||
|
builder.Append(KeySubDelimiter);
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Append(queryValueArray[j]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
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("=");
|
||||||
|
|
||||||
|
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(queryValueArray[j]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_builderPool.Return(builder);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class QueryKeyComparer : IComparer<KeyValuePair<string, StringValues>>
|
||||||
|
{
|
||||||
|
private StringComparer _stringComparer;
|
||||||
|
|
||||||
|
public static QueryKeyComparer OrdinalIgnoreCase { get; } = new QueryKeyComparer(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
public QueryKeyComparer(StringComparer stringComparer)
|
||||||
|
{
|
||||||
|
_stringComparer = stringComparer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int Compare(KeyValuePair<string, StringValues> x, KeyValuePair<string, StringValues> y) => _stringComparer.Compare(x.Key, y.Key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,248 @@
|
||||||
|
// 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.Primitives;
|
||||||
|
using Microsoft.Net.Http.Headers;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.ResponseCaching.Internal
|
||||||
|
{
|
||||||
|
public class ResponseCachingPolicyProvider : IResponseCachingPolicyProvider
|
||||||
|
{
|
||||||
|
public virtual bool AttemptResponseCaching(ResponseCachingContext context)
|
||||||
|
{
|
||||||
|
var request = context.HttpContext.Request;
|
||||||
|
|
||||||
|
// Verify the method
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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]))
|
||||||
|
{
|
||||||
|
if (HeaderUtilities.ContainsCacheDirective(request.Headers[HeaderNames.CacheControl], CacheControlHeaderValue.NoCacheString))
|
||||||
|
{
|
||||||
|
context.Logger.LogRequestWithNoCacheNotCacheable();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Support for legacy HTTP 1.0 cache directive
|
||||||
|
var pragmaHeaderValues = request.Headers[HeaderNames.Pragma];
|
||||||
|
if (HeaderUtilities.ContainsCacheDirective(request.Headers[HeaderNames.Pragma], CacheControlHeaderValue.NoCacheString))
|
||||||
|
{
|
||||||
|
context.Logger.LogRequestWithPragmaNoCacheNotCacheable();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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];
|
||||||
|
|
||||||
|
// Only cache pages explicitly marked with public
|
||||||
|
if (!HeaderUtilities.ContainsCacheDirective(responseCacheControlHeader, CacheControlHeaderValue.PublicString))
|
||||||
|
{
|
||||||
|
context.Logger.LogResponseWithoutPublicNotCacheable();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check response no-store
|
||||||
|
if (HeaderUtilities.ContainsCacheDirective(responseCacheControlHeader, CacheControlHeaderValue.NoStoreString))
|
||||||
|
{
|
||||||
|
context.Logger.LogResponseWithNoStoreNotCacheable();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check no-cache
|
||||||
|
if (HeaderUtilities.ContainsCacheDirective(responseCacheControlHeader, CacheControlHeaderValue.NoCacheString))
|
||||||
|
{
|
||||||
|
context.Logger.LogResponseWithNoCacheNotCacheable();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var response = context.HttpContext.Response;
|
||||||
|
|
||||||
|
// Do not cache responses with Set-Cookie headers
|
||||||
|
if (!StringValues.IsNullOrEmpty(response.Headers[HeaderNames.SetCookie]))
|
||||||
|
{
|
||||||
|
context.Logger.LogResponseWithSetCookieNotCacheable();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not cache responses varying by *
|
||||||
|
var varyHeader = response.Headers[HeaderNames.Vary];
|
||||||
|
if (varyHeader.Count == 1 && string.Equals(varyHeader, "*", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
context.Logger.LogResponseWithVaryStarNotCacheable();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check private
|
||||||
|
if (HeaderUtilities.ContainsCacheDirective(responseCacheControlHeader, CacheControlHeaderValue.PrivateString))
|
||||||
|
{
|
||||||
|
context.Logger.LogResponseWithPrivateNotCacheable();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check response code
|
||||||
|
if (response.StatusCode != StatusCodes.Status200OK)
|
||||||
|
{
|
||||||
|
context.Logger.LogResponseWithUnsuccessfulStatusCodeNotCacheable(response.StatusCode);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check response freshness
|
||||||
|
if (!context.ResponseDate.HasValue)
|
||||||
|
{
|
||||||
|
if (!context.ResponseSharedMaxAge.HasValue &&
|
||||||
|
!context.ResponseMaxAge.HasValue &&
|
||||||
|
context.ResponseTime.Value >= context.ResponseExpires)
|
||||||
|
{
|
||||||
|
context.Logger.LogExpirationExpiresExceeded(context.ResponseTime.Value, context.ResponseExpires.Value);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var age = context.ResponseTime.Value - context.ResponseDate.Value;
|
||||||
|
|
||||||
|
// Validate shared max age
|
||||||
|
if (age >= context.ResponseSharedMaxAge)
|
||||||
|
{
|
||||||
|
context.Logger.LogExpirationSharedMaxAgeExceeded(age, context.ResponseSharedMaxAge.Value);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else if (!context.ResponseSharedMaxAge.HasValue)
|
||||||
|
{
|
||||||
|
// Validate max age
|
||||||
|
if (age >= context.ResponseMaxAge)
|
||||||
|
{
|
||||||
|
context.Logger.LogExpirationMaxAgeExceeded(age, context.ResponseMaxAge.Value);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else if (!context.ResponseMaxAge.HasValue)
|
||||||
|
{
|
||||||
|
// Validate expiration
|
||||||
|
if (context.ResponseTime.Value >= context.ResponseExpires)
|
||||||
|
{
|
||||||
|
context.Logger.LogExpirationExpiresExceeded(context.ResponseTime.Value, context.ResponseExpires.Value);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public virtual bool IsCachedEntryFresh(ResponseCachingContext context)
|
||||||
|
{
|
||||||
|
var age = context.CachedEntryAge.Value;
|
||||||
|
var cachedCacheControlHeaders = context.CachedResponseHeaders[HeaderNames.CacheControl];
|
||||||
|
var requestCacheControlHeaders = context.HttpContext.Request.Headers[HeaderNames.CacheControl];
|
||||||
|
|
||||||
|
// Add min-fresh requirements
|
||||||
|
TimeSpan? minFresh;
|
||||||
|
if (HeaderUtilities.TryParseSeconds(requestCacheControlHeaders, CacheControlHeaderValue.MinFreshString, out minFresh))
|
||||||
|
{
|
||||||
|
age += minFresh.Value;
|
||||||
|
context.Logger.LogExpirationMinFreshAdded(minFresh.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate shared max age, this overrides any max age settings for shared caches
|
||||||
|
TimeSpan? cachedSharedMaxAge;
|
||||||
|
HeaderUtilities.TryParseSeconds(cachedCacheControlHeaders, CacheControlHeaderValue.SharedMaxAgeString, out cachedSharedMaxAge);
|
||||||
|
|
||||||
|
if (age >= cachedSharedMaxAge)
|
||||||
|
{
|
||||||
|
// shared max age implies must revalidate
|
||||||
|
context.Logger.LogExpirationSharedMaxAgeExceeded(age, cachedSharedMaxAge.Value);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else if (!cachedSharedMaxAge.HasValue)
|
||||||
|
{
|
||||||
|
TimeSpan? requestMaxAge;
|
||||||
|
HeaderUtilities.TryParseSeconds(requestCacheControlHeaders, CacheControlHeaderValue.MaxAgeString, out requestMaxAge);
|
||||||
|
|
||||||
|
TimeSpan? cachedMaxAge;
|
||||||
|
HeaderUtilities.TryParseSeconds(cachedCacheControlHeaders, CacheControlHeaderValue.MaxAgeString, out cachedMaxAge);
|
||||||
|
|
||||||
|
var lowestMaxAge = cachedMaxAge < requestMaxAge ? cachedMaxAge : requestMaxAge ?? cachedMaxAge;
|
||||||
|
// Validate max age
|
||||||
|
if (age >= lowestMaxAge)
|
||||||
|
{
|
||||||
|
// 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 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);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.Logger.LogExpirationMaxAgeExceeded(age, lowestMaxAge.Value);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else if (!cachedMaxAge.HasValue && !requestMaxAge.HasValue)
|
||||||
|
{
|
||||||
|
// Validate expiration
|
||||||
|
DateTimeOffset expires;
|
||||||
|
if (HeaderUtilities.TryParseDate(context.CachedResponseHeaders[HeaderNames.Expires].ToString(), out expires) &&
|
||||||
|
context.ResponseTime.Value >= expires)
|
||||||
|
{
|
||||||
|
context.Logger.LogExpirationExpiresExceeded(context.ResponseTime.Value, expires);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 ResponseCachingStream _responseCachingStream;
|
||||||
|
|
||||||
|
public SendFileFeatureWrapper(IHttpSendFileFeature originalSendFileFeature, ResponseCachingStream responseCachingStream)
|
||||||
|
{
|
||||||
|
_originalSendFileFeature = originalSendFileFeature;
|
||||||
|
_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)
|
||||||
|
{
|
||||||
|
_responseCachingStream.DisableBuffering();
|
||||||
|
return _originalSendFileFeature.SendFileAsync(path, offset, length, cancellation);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.Text;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.ResponseCaching.Internal
|
||||||
|
{
|
||||||
|
internal static class StringBuilderExtensions
|
||||||
|
{
|
||||||
|
internal static StringBuilder AppendUpperInvariant(this StringBuilder builder, string value)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrEmpty(value))
|
||||||
|
{
|
||||||
|
builder.EnsureCapacity(builder.Length + value.Length);
|
||||||
|
for (var i = 0; i < value.Length; i++)
|
||||||
|
{
|
||||||
|
builder.Append(char.ToUpperInvariant(value[i]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Provides access to the normal system clock.
|
||||||
|
/// </summary>
|
||||||
|
internal class SystemClock : ISystemClock
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Retrieves the current system time in UTC.
|
||||||
|
/// </summary>
|
||||||
|
public DateTimeOffset UtcNow
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return DateTimeOffset.UtcNow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<Description>ASP.NET Core middleware for caching HTTP responses on the server.</Description>
|
||||||
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
|
<NoWarn>$(NoWarn);CS1591</NoWarn>
|
||||||
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
|
<PackageTags>aspnetcore;cache;caching</PackageTags>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\Microsoft.AspNetCore.ResponseCaching.Abstractions\Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Http.Extensions" Version="$(MicrosoftAspNetCoreHttpExtensionsPackageVersion)" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.Http" Version="$(MicrosoftAspNetCoreHttpPackageVersion)" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="$(MicrosoftExtensionsCachingMemoryPackageVersion)" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="$(MicrosoftExtensionsLoggingAbstractionsPackageVersion)" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +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.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.ResponseCaching.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||||
|
|
@ -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<ResponseCachingMiddleware>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
// 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 class ResponseCachingFeature : IResponseCachingFeature
|
||||||
|
{
|
||||||
|
private string[] _varyByQueryKeys;
|
||||||
|
|
||||||
|
public string[] VaryByQueryKeys
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _varyByQueryKeys;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value?.Length > 1)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < value.Length; 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,528 @@
|
||||||
|
// 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.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;
|
||||||
|
using Microsoft.Net.Http.Headers;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.ResponseCaching
|
||||||
|
{
|
||||||
|
public class ResponseCachingMiddleware
|
||||||
|
{
|
||||||
|
private static readonly TimeSpan DefaultExpirationTimeSpan = TimeSpan.FromSeconds(10);
|
||||||
|
|
||||||
|
private readonly RequestDelegate _next;
|
||||||
|
private readonly ResponseCachingOptions _options;
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
private readonly IResponseCachingPolicyProvider _policyProvider;
|
||||||
|
private readonly IResponseCache _cache;
|
||||||
|
private readonly IResponseCachingKeyProvider _keyProvider;
|
||||||
|
|
||||||
|
public ResponseCachingMiddleware(
|
||||||
|
RequestDelegate next,
|
||||||
|
IOptions<ResponseCachingOptions> 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<ResponseCachingOptions> options,
|
||||||
|
ILoggerFactory loggerFactory,
|
||||||
|
IResponseCachingPolicyProvider policyProvider,
|
||||||
|
IResponseCache cache,
|
||||||
|
IResponseCachingKeyProvider keyProvider)
|
||||||
|
{
|
||||||
|
if (next == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(next));
|
||||||
|
}
|
||||||
|
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 (cache == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(cache));
|
||||||
|
}
|
||||||
|
if (keyProvider == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(keyProvider));
|
||||||
|
}
|
||||||
|
|
||||||
|
_next = next;
|
||||||
|
_options = options.Value;
|
||||||
|
_logger = loggerFactory.CreateLogger<ResponseCachingMiddleware>();
|
||||||
|
_policyProvider = policyProvider;
|
||||||
|
_cache = cache;
|
||||||
|
_keyProvider = keyProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task Invoke(HttpContext httpContext)
|
||||||
|
{
|
||||||
|
var context = new ResponseCachingContext(httpContext, _logger);
|
||||||
|
|
||||||
|
// Should we attempt any caching logic?
|
||||||
|
if (_policyProvider.AttemptResponseCaching(context))
|
||||||
|
{
|
||||||
|
// Can this request be served from cache?
|
||||||
|
if (_policyProvider.AllowCacheLookup(context) && await TryServeFromCacheAsync(context))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should we store the response to this request?
|
||||||
|
if (_policyProvider.AllowCacheStorage(context))
|
||||||
|
{
|
||||||
|
// Hook up to listen to the response stream
|
||||||
|
ShimResponseStream(context);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _next(httpContext);
|
||||||
|
|
||||||
|
// If there was no response body, check the response headers now. We can cache things like redirects.
|
||||||
|
await StartResponseAsync(context);
|
||||||
|
|
||||||
|
// Finalize the cache entry
|
||||||
|
await FinalizeCacheBodyAsync(context);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
UnshimResponseStream(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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<bool> TryServeCachedResponseAsync(ResponseCachingContext context, IResponseCacheEntry cacheEntry)
|
||||||
|
{
|
||||||
|
var cachedResponse = cacheEntry as CachedResponse;
|
||||||
|
if (cachedResponse == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.CachedResponse = cachedResponse;
|
||||||
|
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;
|
||||||
|
|
||||||
|
if (_policyProvider.IsCachedEntryFresh(context))
|
||||||
|
{
|
||||||
|
// Check conditional request rules
|
||||||
|
if (ContentIsNotModified(context))
|
||||||
|
{
|
||||||
|
_logger.LogNotModifiedServed();
|
||||||
|
context.HttpContext.Response.StatusCode = StatusCodes.Status304NotModified;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
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[header.Key] = header.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.FormatNonNegativeInt64(context.CachedEntryAge.Value.Ticks / TimeSpan.TicksPerSecond);
|
||||||
|
|
||||||
|
// Copy the cached response body
|
||||||
|
var body = context.CachedResponse.Body;
|
||||||
|
if (body.Length > 0)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await body.CopyToAsync(response.Body, StreamUtilities.BodySegmentSize, context.HttpContext.RequestAborted);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
context.HttpContext.Abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_logger.LogCachedResponseServed();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal async Task<bool> TryServeFromCacheAsync(ResponseCachingContext context)
|
||||||
|
{
|
||||||
|
context.BaseKey = _keyProvider.CreateBaseKey(context);
|
||||||
|
var cacheEntry = await _cache.GetAsync(context.BaseKey);
|
||||||
|
|
||||||
|
var cachedVaryByRules = cacheEntry as CachedVaryByRules;
|
||||||
|
if (cachedVaryByRules != null)
|
||||||
|
{
|
||||||
|
// Request contains vary rules, recompute key(s) and try again
|
||||||
|
context.CachedVaryByRules = cachedVaryByRules;
|
||||||
|
|
||||||
|
foreach (var varyKey in _keyProvider.CreateLookupVaryByKeys(context))
|
||||||
|
{
|
||||||
|
if (await TryServeCachedResponseAsync(context, await _cache.GetAsync(varyKey)))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (await TryServeCachedResponseAsync(context, cacheEntry))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (HeaderUtilities.ContainsCacheDirective(context.HttpContext.Request.Headers[HeaderNames.CacheControl], CacheControlHeaderValue.OnlyIfCachedString))
|
||||||
|
{
|
||||||
|
_logger.LogGatewayTimeoutServed();
|
||||||
|
context.HttpContext.Response.StatusCode = StatusCodes.Status504GatewayTimeout;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogNoResponseServed();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finalize cache headers.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context"></param>
|
||||||
|
/// <returns><c>true</c> if a vary by entry needs to be stored in the cache; otherwise <c>false</c>.</returns>
|
||||||
|
private bool OnFinalizeCacheHeaders(ResponseCachingContext context)
|
||||||
|
{
|
||||||
|
if (_policyProvider.IsResponseCacheable(context))
|
||||||
|
{
|
||||||
|
var storeVaryByEntry = false;
|
||||||
|
context.ShouldCacheResponse = true;
|
||||||
|
|
||||||
|
// Create the cache entry now
|
||||||
|
var response = context.HttpContext.Response;
|
||||||
|
var varyHeaders = new StringValues(response.Headers.GetCommaSeparatedValues(HeaderNames.Vary));
|
||||||
|
var varyQueryKeys = new StringValues(context.HttpContext.Features.Get<IResponseCachingFeature>()?.VaryByQueryKeys);
|
||||||
|
context.CachedResponseValidFor = context.ResponseSharedMaxAge ??
|
||||||
|
context.ResponseMaxAge ??
|
||||||
|
(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))
|
||||||
|
{
|
||||||
|
// Normalize order and casing of vary by rules
|
||||||
|
var normalizedVaryHeaders = GetOrderCasingNormalizedStringValues(varyHeaders);
|
||||||
|
var normalizedVaryQueryKeys = GetOrderCasingNormalizedStringValues(varyQueryKeys);
|
||||||
|
|
||||||
|
// Update vary rules if they are different
|
||||||
|
if (context.CachedVaryByRules == null ||
|
||||||
|
!StringValues.Equals(context.CachedVaryByRules.QueryKeys, normalizedVaryQueryKeys) ||
|
||||||
|
!StringValues.Equals(context.CachedVaryByRules.Headers, normalizedVaryHeaders))
|
||||||
|
{
|
||||||
|
context.CachedVaryByRules = new CachedVaryByRules
|
||||||
|
{
|
||||||
|
VaryByKeyPrefix = FastGuid.NewGuid().IdString,
|
||||||
|
Headers = normalizedVaryHeaders,
|
||||||
|
QueryKeys = normalizedVaryQueryKeys
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always overwrite the CachedVaryByRules to update the expiry information
|
||||||
|
_logger.LogVaryByRulesUpdated(normalizedVaryHeaders, normalizedVaryQueryKeys);
|
||||||
|
storeVaryByEntry = true;
|
||||||
|
|
||||||
|
context.StorageVaryKey = _keyProvider.CreateStorageVaryByKey(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure date header is set
|
||||||
|
if (!context.ResponseDate.HasValue)
|
||||||
|
{
|
||||||
|
context.ResponseDate = context.ResponseTime.Value;
|
||||||
|
// Setting the date on the raw response headers.
|
||||||
|
context.HttpContext.Response.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(context.ResponseDate.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the response on the state
|
||||||
|
context.CachedResponse = new CachedResponse
|
||||||
|
{
|
||||||
|
Created = context.ResponseDate.Value,
|
||||||
|
StatusCode = context.HttpContext.Response.StatusCode,
|
||||||
|
Headers = new HeaderDictionary()
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var header in context.HttpContext.Response.Headers)
|
||||||
|
{
|
||||||
|
if (!string.Equals(header.Key, HeaderNames.Age, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
context.CachedResponse.Headers[header.Key] = header.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return storeVaryByEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.ResponseCachingStream.DisableBuffering();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void FinalizeCacheHeaders(ResponseCachingContext context)
|
||||||
|
{
|
||||||
|
if (OnFinalizeCacheHeaders(context))
|
||||||
|
{
|
||||||
|
_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 Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal async Task FinalizeCacheBodyAsync(ResponseCachingContext context)
|
||||||
|
{
|
||||||
|
if (context.ShouldCacheResponse && context.ResponseCachingStream.BufferingEnabled)
|
||||||
|
{
|
||||||
|
var contentLength = context.HttpContext.Response.ContentLength;
|
||||||
|
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.FormatNonNegativeInt64(bufferStream.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.CachedResponse.Body = bufferStream;
|
||||||
|
_logger.LogResponseCached();
|
||||||
|
await _cache.SetAsync(context.StorageVaryKey ?? context.BaseKey, context.CachedResponse, context.CachedResponseValidFor);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogResponseContentLengthMismatchNotCached();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.LogResponseNotCached();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Mark the response as started and set the response time if no reponse was started yet.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="context"></param>
|
||||||
|
/// <returns><c>true</c> if the response was not started before this call; otherwise <c>false</c>.</returns>
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void AddResponseCachingFeature(HttpContext context)
|
||||||
|
{
|
||||||
|
if (context.Features.Get<IResponseCachingFeature>() != 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<IResponseCachingFeature>(new ResponseCachingFeature());
|
||||||
|
}
|
||||||
|
|
||||||
|
internal void ShimResponseStream(ResponseCachingContext context)
|
||||||
|
{
|
||||||
|
// Shim response stream
|
||||||
|
context.OriginalResponseStream = context.HttpContext.Response.Body;
|
||||||
|
context.ResponseCachingStream = new ResponseCachingStream(
|
||||||
|
context.OriginalResponseStream,
|
||||||
|
_options.MaximumBodySize,
|
||||||
|
StreamUtilities.BodySegmentSize,
|
||||||
|
() => StartResponse(context),
|
||||||
|
() => StartResponseAsync(context));
|
||||||
|
context.HttpContext.Response.Body = context.ResponseCachingStream;
|
||||||
|
|
||||||
|
// Shim IHttpSendFileFeature
|
||||||
|
context.OriginalSendFileFeature = context.HttpContext.Features.Get<IHttpSendFileFeature>();
|
||||||
|
if (context.OriginalSendFileFeature != null)
|
||||||
|
{
|
||||||
|
context.HttpContext.Features.Set<IHttpSendFileFeature>(new SendFileFeatureWrapper(context.OriginalSendFileFeature, context.ResponseCachingStream));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add IResponseCachingFeature
|
||||||
|
AddResponseCachingFeature(context.HttpContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void RemoveResponseCachingFeature(HttpContext context) =>
|
||||||
|
context.Features.Set<IResponseCachingFeature>(null);
|
||||||
|
|
||||||
|
internal static void UnshimResponseStream(ResponseCachingContext context)
|
||||||
|
{
|
||||||
|
// Unshim response stream
|
||||||
|
context.HttpContext.Response.Body = context.OriginalResponseStream;
|
||||||
|
|
||||||
|
// Unshim IHttpSendFileFeature
|
||||||
|
context.HttpContext.Features.Set(context.OriginalSendFileFeature);
|
||||||
|
|
||||||
|
// Remove IResponseCachingFeature
|
||||||
|
RemoveResponseCachingFeature(context.HttpContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool ContentIsNotModified(ResponseCachingContext context)
|
||||||
|
{
|
||||||
|
var cachedResponseHeaders = context.CachedResponseHeaders;
|
||||||
|
var ifNoneMatchHeader = context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch];
|
||||||
|
|
||||||
|
if (!StringValues.IsNullOrEmpty(ifNoneMatchHeader))
|
||||||
|
{
|
||||||
|
if (ifNoneMatchHeader.Count == 1 && StringSegment.Equals(ifNoneMatchHeader[0], EntityTagHeaderValue.Any.Tag, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
context.Logger.LogNotModifiedIfNoneMatchStar();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
EntityTagHeaderValue eTag;
|
||||||
|
IList<EntityTagHeaderValue> ifNoneMatchEtags;
|
||||||
|
if (!StringValues.IsNullOrEmpty(cachedResponseHeaders[HeaderNames.ETag])
|
||||||
|
&& EntityTagHeaderValue.TryParse(cachedResponseHeaders[HeaderNames.ETag].ToString(), out eTag)
|
||||||
|
&& EntityTagHeaderValue.TryParseList(ifNoneMatchHeader, out ifNoneMatchEtags))
|
||||||
|
{
|
||||||
|
for (var i = 0; i < ifNoneMatchEtags.Count; i++)
|
||||||
|
{
|
||||||
|
var requestETag = ifNoneMatchEtags[i];
|
||||||
|
if (eTag.Compare(requestETag, useStrongComparison: false))
|
||||||
|
{
|
||||||
|
context.Logger.LogNotModifiedIfNoneMatchMatched(requestETag);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var ifModifiedSince = context.HttpContext.Request.Headers[HeaderNames.IfModifiedSince];
|
||||||
|
if (!StringValues.IsNullOrEmpty(ifModifiedSince))
|
||||||
|
{
|
||||||
|
DateTimeOffset 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.ToString(), out modifiedSince) &&
|
||||||
|
modified <= modifiedSince)
|
||||||
|
{
|
||||||
|
context.Logger.LogNotModifiedIfModifiedSinceSatisfied(modified, modifiedSince);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize order and casing
|
||||||
|
internal static StringValues GetOrderCasingNormalizedStringValues(StringValues stringValues)
|
||||||
|
{
|
||||||
|
if (stringValues.Count == 1)
|
||||||
|
{
|
||||||
|
return new StringValues(stringValues.ToString().ToUpperInvariant());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var originalArray = stringValues.ToArray();
|
||||||
|
var newArray = new string[originalArray.Length];
|
||||||
|
|
||||||
|
for (var 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
// 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.ResponseCaching
|
||||||
|
{
|
||||||
|
public class ResponseCachingOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The size limit for the response cache middleware in bytes. The default is set to 100 MB.
|
||||||
|
/// </summary>
|
||||||
|
public long SizeLimit { get; set; } = 100 * 1024 * 1024;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The largest cacheable size for the response body in bytes. The default is set to 64 MB.
|
||||||
|
/// </summary>
|
||||||
|
public long MaximumBodySize { get; set; } = 64 * 1024 * 1024;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// <c>true</c> if request paths are case-sensitive; otherwise <c>false</c>. The default is to treat paths as case-insensitive.
|
||||||
|
/// </summary>
|
||||||
|
public bool UseCaseSensitivePaths { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// For testing purposes only.
|
||||||
|
/// </summary>
|
||||||
|
[EditorBrowsable(EditorBrowsableState.Never)]
|
||||||
|
internal ISystemClock SystemClock { get; set; } = new SystemClock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
// 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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Extension methods for the ResponseCaching middleware.
|
||||||
|
/// </summary>
|
||||||
|
public static class ResponseCachingServicesExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Add response caching services.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="services">The <see cref="IServiceCollection"/> for adding services.</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static IServiceCollection AddResponseCaching(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
if (services == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(services));
|
||||||
|
}
|
||||||
|
|
||||||
|
services.AddMemoryCache();
|
||||||
|
services.TryAdd(ServiceDescriptor.Singleton<IResponseCachingPolicyProvider, ResponseCachingPolicyProvider>());
|
||||||
|
services.TryAdd(ServiceDescriptor.Singleton<IResponseCachingKeyProvider, ResponseCachingKeyProvider>());
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Add response caching services and configure the related options.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="services">The <see cref="IServiceCollection"/> for adding services.</param>
|
||||||
|
/// <param name="configureOptions">A delegate to configure the <see cref="ResponseCachingOptions"/>.</param>
|
||||||
|
/// <returns></returns>
|
||||||
|
public static IServiceCollection AddResponseCaching(this IServiceCollection services, Action<ResponseCachingOptions> configureOptions)
|
||||||
|
{
|
||||||
|
if (services == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(services));
|
||||||
|
}
|
||||||
|
if (configureOptions == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(configureOptions));
|
||||||
|
}
|
||||||
|
|
||||||
|
services.Configure(configureOptions);
|
||||||
|
services.AddResponseCaching();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,200 @@
|
||||||
|
// 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 ResponseCachingStream : Stream
|
||||||
|
{
|
||||||
|
private readonly Stream _innerStream;
|
||||||
|
private readonly long _maxBufferSize;
|
||||||
|
private readonly int _segmentSize;
|
||||||
|
private SegmentWriteStream _segmentWriteStream;
|
||||||
|
private Action _startResponseCallback;
|
||||||
|
private Func<Task> _startResponseCallbackAsync;
|
||||||
|
|
||||||
|
internal ResponseCachingStream(Stream innerStream, long maxBufferSize, int segmentSize, Action startResponseCallback, Func<Task> startResponseCallbackAsync)
|
||||||
|
{
|
||||||
|
_innerStream = innerStream;
|
||||||
|
_maxBufferSize = maxBufferSize;
|
||||||
|
_segmentSize = segmentSize;
|
||||||
|
_startResponseCallback = startResponseCallback;
|
||||||
|
_startResponseCallbackAsync = startResponseCallbackAsync;
|
||||||
|
_segmentWriteStream = new SegmentWriteStream(_segmentSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal bool BufferingEnabled { get; private 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
|
||||||
|
{
|
||||||
|
DisableBuffering();
|
||||||
|
_innerStream.Position = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
_segmentWriteStream.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()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_startResponseCallback();
|
||||||
|
_innerStream.Flush();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
DisableBuffering();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
=> _innerStream.Read(buffer, offset, count);
|
||||||
|
|
||||||
|
public override void Write(byte[] buffer, int offset, int count)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_startResponseCallback();
|
||||||
|
_innerStream.Write(buffer, offset, count);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
DisableBuffering();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (BufferingEnabled)
|
||||||
|
{
|
||||||
|
if (_segmentWriteStream.Length + count > _maxBufferSize)
|
||||||
|
{
|
||||||
|
DisableBuffering();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_segmentWriteStream.Write(buffer, offset, count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _startResponseCallbackAsync();
|
||||||
|
await _innerStream.WriteAsync(buffer, offset, count, cancellationToken);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
DisableBuffering();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (BufferingEnabled)
|
||||||
|
{
|
||||||
|
if (_segmentWriteStream.Length + count > _maxBufferSize)
|
||||||
|
{
|
||||||
|
DisableBuffering();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await _segmentWriteStream.WriteAsync(buffer, offset, count, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void WriteByte(byte value)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_innerStream.WriteByte(value);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
DisableBuffering();
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (BufferingEnabled)
|
||||||
|
{
|
||||||
|
if (_segmentWriteStream.Length + 1 > _maxBufferSize)
|
||||||
|
{
|
||||||
|
DisableBuffering();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_segmentWriteStream.WriteByte(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
|
||||||
|
{
|
||||||
|
return StreamUtilities.ToIAsyncResult(WriteAsync(buffer, offset, count), callback, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void EndWrite(IAsyncResult asyncResult)
|
||||||
|
{
|
||||||
|
if (asyncResult == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(asyncResult));
|
||||||
|
}
|
||||||
|
((Task)asyncResult).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,230 @@
|
||||||
|
// 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<byte[]> _segments;
|
||||||
|
private readonly long _length;
|
||||||
|
private int _segmentIndex;
|
||||||
|
private int _segmentOffset;
|
||||||
|
private long _position;
|
||||||
|
|
||||||
|
internal SegmentReadStream(List<byte[]> 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<int> 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
|
||||||
|
{
|
||||||
|
var tcs = new TaskCompletionSource<int>(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;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override int EndRead(IAsyncResult asyncResult)
|
||||||
|
{
|
||||||
|
if (asyncResult == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(asyncResult));
|
||||||
|
}
|
||||||
|
return ((Task<int>)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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.ResponseCaching.Internal
|
||||||
|
{
|
||||||
|
internal class SegmentWriteStream : Stream
|
||||||
|
{
|
||||||
|
private readonly List<byte[]> _segments = new List<byte[]>();
|
||||||
|
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<byte[]> 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 Task.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++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
|
||||||
|
{
|
||||||
|
return StreamUtilities.ToIAsyncResult(WriteAsync(buffer, offset, count), callback, state);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void EndWrite(IAsyncResult asyncResult)
|
||||||
|
{
|
||||||
|
if (asyncResult == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(asyncResult));
|
||||||
|
}
|
||||||
|
((Task)asyncResult).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 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.
|
||||||
|
/// </summary>
|
||||||
|
// Internal for testing
|
||||||
|
internal static int BodySegmentSize { get; set; } = 81920;
|
||||||
|
|
||||||
|
internal static IAsyncResult ToIAsyncResult(Task task, AsyncCallback callback, object state)
|
||||||
|
{
|
||||||
|
var tcs = new TaskCompletionSource<int>(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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,252 @@
|
||||||
|
{
|
||||||
|
"AssemblyIdentity": "Microsoft.AspNetCore.ResponseCaching, Version=2.1.1.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<Microsoft.AspNetCore.ResponseCaching.ResponseCachingOptions>"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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<Microsoft.AspNetCore.ResponseCaching.ResponseCachingOptions>"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
<Project>
|
||||||
|
<Import Project="..\Directory.Build.props" />
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<DeveloperBuildTestTfms>netcoreapp2.1</DeveloperBuildTestTfms>
|
||||||
|
<StandardTestTfms>$(DeveloperBuildTestTfms)</StandardTestTfms>
|
||||||
|
<StandardTestTfms Condition=" '$(DeveloperBuild)' != 'true' ">netcoreapp2.1;netcoreapp2.0</StandardTestTfms>
|
||||||
|
<StandardTestTfms Condition=" '$(DeveloperBuild)' != 'true' AND '$(OS)' == 'Windows_NT' ">$(StandardTestTfms);net461</StandardTestTfms>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Internal.AspNetCore.Sdk" PrivateAssets="All" Version="$(InternalAspNetCoreSdkPackageVersion)" />
|
||||||
|
</ItemGroup>
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.ResponseCaching\Microsoft.AspNetCore.ResponseCaching.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="$(MicrosoftAspNetCoreTestHostPackageVersion)" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Logging.Testing" Version="$(MicrosoftExtensionsLoggingTestingPackageVersion)" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNETTestSdkPackageVersion)" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="$(XunitRunnerVisualStudioPackageVersion)" />
|
||||||
|
<PackageReference Include="xunit" Version="$(XunitPackageVersion)" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -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<string[]> ValidNullOrEmptyVaryRules
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return new TheoryData<string[]>
|
||||||
|
{
|
||||||
|
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<string[]> InvalidVaryRules
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return new TheoryData<string[]>
|
||||||
|
{
|
||||||
|
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<ArgumentException>(() => new ResponseCachingFeature().VaryByQueryKeys = value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,218 @@
|
||||||
|
// 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 ResponseCachingKeyProviderTests
|
||||||
|
{
|
||||||
|
private static readonly char KeyDelimiter = '\x1e';
|
||||||
|
private static readonly char KeySubDelimiter = '\x1f';
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResponseCachingKeyProvider_CreateStorageBaseKey_IncludesOnlyNormalizedMethodSchemeHostPortAndPath()
|
||||||
|
{
|
||||||
|
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}HTTPS{KeyDelimiter}EXAMPLE.COM:80/PATHBASE/PATH/SUBPATH", cacheKeyProvider.CreateBaseKey(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResponseCachingKeyProvider_CreateStorageBaseKey_CaseInsensitivePath_NormalizesPath()
|
||||||
|
{
|
||||||
|
var cacheKeyProvider = TestUtils.CreateTestKeyProvider(new ResponseCachingOptions()
|
||||||
|
{
|
||||||
|
UseCaseSensitivePaths = false
|
||||||
|
});
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
context.HttpContext.Request.Method = HttpMethods.Get;
|
||||||
|
context.HttpContext.Request.Path = "/Path";
|
||||||
|
|
||||||
|
Assert.Equal($"{HttpMethods.Get}{KeyDelimiter}{KeyDelimiter}/PATH", cacheKeyProvider.CreateBaseKey(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResponseCachingKeyProvider_CreateStorageBaseKey_CaseSensitivePath_PreservesPathCase()
|
||||||
|
{
|
||||||
|
var cacheKeyProvider = TestUtils.CreateTestKeyProvider(new ResponseCachingOptions()
|
||||||
|
{
|
||||||
|
UseCaseSensitivePaths = true
|
||||||
|
});
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
context.HttpContext.Request.Method = HttpMethods.Get;
|
||||||
|
context.HttpContext.Request.Path = "/Path";
|
||||||
|
|
||||||
|
Assert.Equal($"{HttpMethods.Get}{KeyDelimiter}{KeyDelimiter}/Path", cacheKeyProvider.CreateBaseKey(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResponseCachingKeyProvider_CreateStorageVaryByKey_Throws_IfVaryByRulesIsNull()
|
||||||
|
{
|
||||||
|
var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
|
||||||
|
Assert.Throws<InvalidOperationException>(() => cacheKeyProvider.CreateStorageVaryByKey(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResponseCachingKeyProvider_CreateStorageVaryKey_ReturnsCachedVaryByGuid_IfVaryByRulesIsEmpty()
|
||||||
|
{
|
||||||
|
var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
context.CachedVaryByRules = new CachedVaryByRules()
|
||||||
|
{
|
||||||
|
VaryByKeyPrefix = FastGuid.NewGuid().IdString
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}", cacheKeyProvider.CreateStorageVaryByKey(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResponseCachingKeyProvider_CreateStorageVaryKey_IncludesListedHeadersOnly()
|
||||||
|
{
|
||||||
|
var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
context.HttpContext.Request.Headers["HeaderA"] = "ValueA";
|
||||||
|
context.HttpContext.Request.Headers["HeaderB"] = "ValueB";
|
||||||
|
context.CachedVaryByRules = new CachedVaryByRules()
|
||||||
|
{
|
||||||
|
Headers = new string[] { "HeaderA", "HeaderC" }
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=",
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueA&QueryB=ValueB");
|
||||||
|
context.CachedVaryByRules = new CachedVaryByRules()
|
||||||
|
{
|
||||||
|
VaryByKeyPrefix = FastGuid.NewGuid().IdString,
|
||||||
|
QueryKeys = new string[] { "QueryA", "QueryC" }
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}QueryA=ValueA{KeyDelimiter}QueryC=",
|
||||||
|
cacheKeyProvider.CreateStorageVaryByKey(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResponseCachingKeyProvider_CreateStorageVaryKey_IncludesQueryKeys_QueryKeyCaseInsensitive_UseQueryKeyCasing()
|
||||||
|
{
|
||||||
|
var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
context.HttpContext.Request.QueryString = new QueryString("?queryA=ValueA&queryB=ValueB");
|
||||||
|
context.CachedVaryByRules = new CachedVaryByRules()
|
||||||
|
{
|
||||||
|
VaryByKeyPrefix = FastGuid.NewGuid().IdString,
|
||||||
|
QueryKeys = new string[] { "QueryA", "QueryC" }
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}QueryA=ValueA{KeyDelimiter}QueryC=",
|
||||||
|
cacheKeyProvider.CreateStorageVaryByKey(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ResponseCachingKeyProvider_CreateStorageVaryKey_IncludesAllQueryKeysGivenAsterisk()
|
||||||
|
{
|
||||||
|
var cacheKeyProvider = TestUtils.CreateTestKeyProvider();
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueA&QueryB=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{KeyDelimiter}QUERYB=ValueB",
|
||||||
|
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_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()
|
||||||
|
{
|
||||||
|
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("?QueryA=ValueA&QueryB=ValueB");
|
||||||
|
context.CachedVaryByRules = new CachedVaryByRules()
|
||||||
|
{
|
||||||
|
VaryByKeyPrefix = FastGuid.NewGuid().IdString,
|
||||||
|
Headers = new string[] { "HeaderA", "HeaderC" },
|
||||||
|
QueryKeys = new string[] { "QueryA", "QueryC" }
|
||||||
|
};
|
||||||
|
|
||||||
|
Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC={KeyDelimiter}Q{KeyDelimiter}QueryA=ValueA{KeyDelimiter}QueryC=",
|
||||||
|
cacheKeyProvider.CreateStorageVaryByKey(context));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,940 @@
|
||||||
|
// 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.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;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
|
{
|
||||||
|
public class ResponseCachingMiddlewareTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task TryServeFromCacheAsync_OnlyIfCached_Serves504()
|
||||||
|
{
|
||||||
|
var cache = new TestResponseCache();
|
||||||
|
var sink = new TestSink();
|
||||||
|
var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider());
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
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);
|
||||||
|
TestUtils.AssertLoggedMessages(
|
||||||
|
sink.Writes,
|
||||||
|
LoggedMessage.GatewayTimeoutServed);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task TryServeFromCacheAsync_CachedResponseNotFound_Fails()
|
||||||
|
{
|
||||||
|
var cache = new TestResponseCache();
|
||||||
|
var sink = new TestSink();
|
||||||
|
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, cache.GetCount);
|
||||||
|
TestUtils.AssertLoggedMessages(
|
||||||
|
sink.Writes,
|
||||||
|
LoggedMessage.NoResponseServed);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task TryServeFromCacheAsync_CachedResponseFound_Succeeds()
|
||||||
|
{
|
||||||
|
var cache = new TestResponseCache();
|
||||||
|
var sink = new TestSink();
|
||||||
|
var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider("BaseKey"));
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
|
||||||
|
await cache.SetAsync(
|
||||||
|
"BaseKey",
|
||||||
|
new CachedResponse()
|
||||||
|
{
|
||||||
|
Headers = new HeaderDictionary(),
|
||||||
|
Body = new SegmentReadStream(new List<byte[]>(0), 0)
|
||||||
|
},
|
||||||
|
TimeSpan.Zero);
|
||||||
|
|
||||||
|
Assert.True(await middleware.TryServeFromCacheAsync(context));
|
||||||
|
Assert.Equal(1, cache.GetCount);
|
||||||
|
TestUtils.AssertLoggedMessages(
|
||||||
|
sink.Writes,
|
||||||
|
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<byte[]>(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()
|
||||||
|
{
|
||||||
|
var cache = new TestResponseCache();
|
||||||
|
var sink = new TestSink();
|
||||||
|
var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider("BaseKey", "VaryKey"));
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
|
||||||
|
await cache.SetAsync(
|
||||||
|
"BaseKey",
|
||||||
|
new CachedVaryByRules(),
|
||||||
|
TimeSpan.Zero);
|
||||||
|
|
||||||
|
Assert.False(await middleware.TryServeFromCacheAsync(context));
|
||||||
|
Assert.Equal(2, cache.GetCount);
|
||||||
|
TestUtils.AssertLoggedMessages(
|
||||||
|
sink.Writes,
|
||||||
|
LoggedMessage.NoResponseServed);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task TryServeFromCacheAsync_VaryByRuleFound_CachedResponseFound_Succeeds()
|
||||||
|
{
|
||||||
|
var cache = new TestResponseCache();
|
||||||
|
var sink = new TestSink();
|
||||||
|
var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider("BaseKey", new[] { "VaryKey", "VaryKey2" }));
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
|
||||||
|
await cache.SetAsync(
|
||||||
|
"BaseKey",
|
||||||
|
new CachedVaryByRules(),
|
||||||
|
TimeSpan.Zero);
|
||||||
|
await cache.SetAsync(
|
||||||
|
"BaseKeyVaryKey2",
|
||||||
|
new CachedResponse()
|
||||||
|
{
|
||||||
|
Headers = new HeaderDictionary(),
|
||||||
|
Body = new SegmentReadStream(new List<byte[]>(0), 0)
|
||||||
|
},
|
||||||
|
TimeSpan.Zero);
|
||||||
|
|
||||||
|
Assert.True(await middleware.TryServeFromCacheAsync(context));
|
||||||
|
Assert.Equal(3, cache.GetCount);
|
||||||
|
TestUtils.AssertLoggedMessages(
|
||||||
|
sink.Writes,
|
||||||
|
LoggedMessage.CachedResponseServed);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task TryServeFromCacheAsync_CachedResponseFound_Serves304IfPossible()
|
||||||
|
{
|
||||||
|
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.Request.Headers[HeaderNames.IfNoneMatch] = "*";
|
||||||
|
|
||||||
|
await cache.SetAsync(
|
||||||
|
"BaseKey",
|
||||||
|
new CachedResponse()
|
||||||
|
{
|
||||||
|
Body = new SegmentReadStream(new List<byte[]>(0), 0)
|
||||||
|
},
|
||||||
|
TimeSpan.Zero);
|
||||||
|
|
||||||
|
Assert.True(await middleware.TryServeFromCacheAsync(context));
|
||||||
|
Assert.Equal(1, cache.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 HeaderDictionary();
|
||||||
|
|
||||||
|
Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context));
|
||||||
|
Assert.Empty(sink.Writes);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
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.IfModifiedSince] = HeaderUtilities.FormatDate(utcNow);
|
||||||
|
|
||||||
|
// Verify modifications in the past succeeds
|
||||||
|
context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10));
|
||||||
|
Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context));
|
||||||
|
Assert.Single(sink.Writes);
|
||||||
|
|
||||||
|
// Verify modifications at present succeeds
|
||||||
|
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[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10));
|
||||||
|
Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context));
|
||||||
|
|
||||||
|
// Verify logging
|
||||||
|
TestUtils.AssertLoggedMessages(
|
||||||
|
sink.Writes,
|
||||||
|
LoggedMessage.NotModifiedIfModifiedSinceSatisfied,
|
||||||
|
LoggedMessage.NotModifiedIfModifiedSinceSatisfied);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
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.IfModifiedSince] = HeaderUtilities.FormatDate(utcNow);
|
||||||
|
|
||||||
|
// Verify modifications in the past succeeds
|
||||||
|
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.Single(sink.Writes);
|
||||||
|
|
||||||
|
// Verify modifications at present
|
||||||
|
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[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
|
||||||
|
TestUtils.AssertLoggedMessages(
|
||||||
|
sink.Writes,
|
||||||
|
LoggedMessage.NotModifiedIfModifiedSinceSatisfied,
|
||||||
|
LoggedMessage.NotModifiedIfModifiedSinceSatisfied);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
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 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();
|
||||||
|
Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context));
|
||||||
|
TestUtils.AssertLoggedMessages(
|
||||||
|
sink.Writes,
|
||||||
|
LoggedMessage.NotModifiedIfNoneMatchStar);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
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 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\"";
|
||||||
|
Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context));
|
||||||
|
Assert.Empty(sink.Writes);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ContentIsNotModified_IfNoneMatch_AnyWithoutETagInResponse_False()
|
||||||
|
{
|
||||||
|
var sink = new TestSink();
|
||||||
|
var context = TestUtils.CreateTestContext(sink);
|
||||||
|
context.CachedResponseHeaders = new HeaderDictionary();
|
||||||
|
context.HttpContext.Request.Headers[HeaderNames.IfNoneMatch] = "\"E1\"";
|
||||||
|
|
||||||
|
Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context));
|
||||||
|
Assert.Empty(sink.Writes);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TheoryData<EntityTagHeaderValue, EntityTagHeaderValue> EquivalentWeakETags
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return new TheoryData<EntityTagHeaderValue, EntityTagHeaderValue>
|
||||||
|
{
|
||||||
|
{ 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 ContentIsNotModified_IfNoneMatch_ExplicitWithMatch_True(EntityTagHeaderValue responseETag, EntityTagHeaderValue requestETag)
|
||||||
|
{
|
||||||
|
var sink = new TestSink();
|
||||||
|
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));
|
||||||
|
TestUtils.AssertLoggedMessages(
|
||||||
|
sink.Writes,
|
||||||
|
LoggedMessage.NotModifiedIfNoneMatchMatched);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ContentIsNotModified_IfNoneMatch_ExplicitWithoutMatch_False()
|
||||||
|
{
|
||||||
|
var sink = new TestSink();
|
||||||
|
var context = TestUtils.CreateTestContext(sink);
|
||||||
|
context.CachedResponseHeaders = new HeaderDictionary();
|
||||||
|
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 StartResponsegAsync_IfAllowResponseCaptureIsTrue_SetsResponseTime()
|
||||||
|
{
|
||||||
|
var clock = new TestClock
|
||||||
|
{
|
||||||
|
UtcNow = DateTimeOffset.UtcNow
|
||||||
|
};
|
||||||
|
var middleware = TestUtils.CreateTestMiddleware(options: new ResponseCachingOptions
|
||||||
|
{
|
||||||
|
SystemClock = clock
|
||||||
|
});
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
context.ResponseTime = null;
|
||||||
|
|
||||||
|
await middleware.StartResponseAsync(context);
|
||||||
|
|
||||||
|
Assert.Equal(clock.UtcNow, context.ResponseTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task StartResponseAsync_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.StartResponseAsync(context);
|
||||||
|
Assert.Equal(initialTime, context.ResponseTime);
|
||||||
|
|
||||||
|
clock.UtcNow += TimeSpan.FromSeconds(10);
|
||||||
|
|
||||||
|
await middleware.StartResponseAsync(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
|
||||||
|
}.ToString();
|
||||||
|
|
||||||
|
Assert.False(context.ShouldCacheResponse);
|
||||||
|
|
||||||
|
await middleware.FinalizeCacheHeadersAsync(context);
|
||||||
|
|
||||||
|
Assert.True(context.ShouldCacheResponse);
|
||||||
|
Assert.Empty(sink.Writes);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
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);
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
|
||||||
|
await middleware.FinalizeCacheHeadersAsync(context);
|
||||||
|
|
||||||
|
Assert.Equal(TimeSpan.FromSeconds(10), context.CachedResponseValidFor);
|
||||||
|
Assert.Empty(sink.Writes);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task FinalizeCacheHeadersAsync_ResponseValidity_UseExpiryIfAvailable()
|
||||||
|
{
|
||||||
|
var clock = new TestClock
|
||||||
|
{
|
||||||
|
UtcNow = DateTimeOffset.MinValue
|
||||||
|
};
|
||||||
|
var sink = new TestSink();
|
||||||
|
var middleware = TestUtils.CreateTestMiddleware(testSink: sink, options: new ResponseCachingOptions
|
||||||
|
{
|
||||||
|
SystemClock = clock
|
||||||
|
});
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
|
||||||
|
context.ResponseTime = clock.UtcNow;
|
||||||
|
context.HttpContext.Response.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(clock.UtcNow + TimeSpan.FromSeconds(11));
|
||||||
|
|
||||||
|
await middleware.FinalizeCacheHeadersAsync(context);
|
||||||
|
|
||||||
|
Assert.Equal(TimeSpan.FromSeconds(11), context.CachedResponseValidFor);
|
||||||
|
Assert.Empty(sink.Writes);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task FinalizeCacheHeadersAsync_ResponseValidity_UseMaxAgeIfAvailable()
|
||||||
|
{
|
||||||
|
var clock = new TestClock
|
||||||
|
{
|
||||||
|
UtcNow = DateTimeOffset.UtcNow
|
||||||
|
};
|
||||||
|
var sink = new TestSink();
|
||||||
|
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(clock.UtcNow + TimeSpan.FromSeconds(11));
|
||||||
|
|
||||||
|
await middleware.FinalizeCacheHeadersAsync(context);
|
||||||
|
|
||||||
|
Assert.Equal(TimeSpan.FromSeconds(12), context.CachedResponseValidFor);
|
||||||
|
Assert.Empty(sink.Writes);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task FinalizeCacheHeadersAsync_ResponseValidity_UseSharedMaxAgeIfAvailable()
|
||||||
|
{
|
||||||
|
var clock = new TestClock
|
||||||
|
{
|
||||||
|
UtcNow = DateTimeOffset.UtcNow
|
||||||
|
};
|
||||||
|
var sink = new TestSink();
|
||||||
|
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(clock.UtcNow + TimeSpan.FromSeconds(11));
|
||||||
|
|
||||||
|
await middleware.FinalizeCacheHeadersAsync(context);
|
||||||
|
|
||||||
|
Assert.Equal(TimeSpan.FromSeconds(13), context.CachedResponseValidFor);
|
||||||
|
Assert.Empty(sink.Writes);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task FinalizeCacheHeadersAsync_UpdateCachedVaryByRules_IfNotEquivalentToPrevious()
|
||||||
|
{
|
||||||
|
var cache = new TestResponseCache();
|
||||||
|
var sink = new TestSink();
|
||||||
|
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<IResponseCachingFeature>(new ResponseCachingFeature()
|
||||||
|
{
|
||||||
|
VaryByQueryKeys = new StringValues(new[] { "queryB", "QUERYA" })
|
||||||
|
});
|
||||||
|
var cachedVaryByRules = new CachedVaryByRules()
|
||||||
|
{
|
||||||
|
Headers = new StringValues(new[] { "HeaderA", "HeaderB" }),
|
||||||
|
QueryKeys = new StringValues(new[] { "QueryA", "QueryB" })
|
||||||
|
};
|
||||||
|
context.CachedVaryByRules = cachedVaryByRules;
|
||||||
|
|
||||||
|
await middleware.FinalizeCacheHeadersAsync(context);
|
||||||
|
|
||||||
|
Assert.Equal(1, cache.SetCount);
|
||||||
|
Assert.NotSame(cachedVaryByRules, context.CachedVaryByRules);
|
||||||
|
TestUtils.AssertLoggedMessages(
|
||||||
|
sink.Writes,
|
||||||
|
LoggedMessage.VaryByRulesUpdated);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task FinalizeCacheHeadersAsync_UpdateCachedVaryByRules_IfEquivalentToPrevious()
|
||||||
|
{
|
||||||
|
var cache = new TestResponseCache();
|
||||||
|
var sink = new TestSink();
|
||||||
|
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<IResponseCachingFeature>(new ResponseCachingFeature()
|
||||||
|
{
|
||||||
|
VaryByQueryKeys = new StringValues(new[] { "queryB", "QUERYA" })
|
||||||
|
});
|
||||||
|
var cachedVaryByRules = new CachedVaryByRules()
|
||||||
|
{
|
||||||
|
VaryByKeyPrefix = FastGuid.NewGuid().IdString,
|
||||||
|
Headers = new StringValues(new[] { "HEADERA", "HEADERB" }),
|
||||||
|
QueryKeys = new StringValues(new[] { "QUERYA", "QUERYB" })
|
||||||
|
};
|
||||||
|
context.CachedVaryByRules = cachedVaryByRules;
|
||||||
|
|
||||||
|
await middleware.FinalizeCacheHeadersAsync(context);
|
||||||
|
|
||||||
|
// An update to the cache is always made but the entry should be the same
|
||||||
|
Assert.Equal(1, cache.SetCount);
|
||||||
|
Assert.Same(cachedVaryByRules, context.CachedVaryByRules);
|
||||||
|
TestUtils.AssertLoggedMessages(
|
||||||
|
sink.Writes,
|
||||||
|
LoggedMessage.VaryByRulesUpdated);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TheoryData<StringValues> NullOrEmptyVaryRules
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return new TheoryData<StringValues>
|
||||||
|
{
|
||||||
|
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 FinalizeCacheHeadersAsync_UpdateCachedVaryByRules_NullOrEmptyRules(StringValues vary)
|
||||||
|
{
|
||||||
|
var cache = new TestResponseCache();
|
||||||
|
var sink = new TestSink();
|
||||||
|
var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache);
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
|
||||||
|
context.HttpContext.Response.Headers[HeaderNames.Vary] = vary;
|
||||||
|
context.HttpContext.Features.Set<IResponseCachingFeature>(new ResponseCachingFeature()
|
||||||
|
{
|
||||||
|
VaryByQueryKeys = vary
|
||||||
|
});
|
||||||
|
|
||||||
|
await middleware.FinalizeCacheHeadersAsync(context);
|
||||||
|
|
||||||
|
// Vary rules should not be updated
|
||||||
|
Assert.Equal(0, cache.SetCount);
|
||||||
|
Assert.Empty(sink.Writes);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task FinalizeCacheHeadersAsync_AddsDate_IfNoneSpecified()
|
||||||
|
{
|
||||||
|
var clock = new TestClock
|
||||||
|
{
|
||||||
|
UtcNow = DateTimeOffset.UtcNow
|
||||||
|
};
|
||||||
|
var sink = new TestSink();
|
||||||
|
var middleware = TestUtils.CreateTestMiddleware(testSink: sink, options: new ResponseCachingOptions
|
||||||
|
{
|
||||||
|
SystemClock = clock
|
||||||
|
});
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
|
||||||
|
Assert.True(StringValues.IsNullOrEmpty(context.HttpContext.Response.Headers[HeaderNames.Date]));
|
||||||
|
|
||||||
|
await middleware.FinalizeCacheHeadersAsync(context);
|
||||||
|
|
||||||
|
Assert.Equal(HeaderUtilities.FormatDate(clock.UtcNow), context.HttpContext.Response.Headers[HeaderNames.Date]);
|
||||||
|
Assert.Empty(sink.Writes);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
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);
|
||||||
|
|
||||||
|
Assert.Equal(HeaderUtilities.FormatDate(utcNow), context.HttpContext.Response.Headers[HeaderNames.Date]);
|
||||||
|
|
||||||
|
await middleware.FinalizeCacheHeadersAsync(context);
|
||||||
|
|
||||||
|
Assert.Equal(HeaderUtilities.FormatDate(utcNow), context.HttpContext.Response.Headers[HeaderNames.Date]);
|
||||||
|
Assert.Empty(sink.Writes);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task FinalizeCacheHeadersAsync_StoresCachedResponse_InState()
|
||||||
|
{
|
||||||
|
var sink = new TestSink();
|
||||||
|
var middleware = TestUtils.CreateTestMiddleware(testSink: sink);
|
||||||
|
var context = TestUtils.CreateTestContext();
|
||||||
|
|
||||||
|
Assert.Null(context.CachedResponse);
|
||||||
|
|
||||||
|
await middleware.FinalizeCacheHeadersAsync(context);
|
||||||
|
|
||||||
|
Assert.NotNull(context.CachedResponse);
|
||||||
|
Assert.Empty(sink.Writes);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
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.FinalizeCacheHeadersAsync(context);
|
||||||
|
|
||||||
|
Assert.Equal(new StringValues(new[] { "HEADERA", "HEADERB" }), context.CachedVaryByRules.Headers);
|
||||||
|
TestUtils.AssertLoggedMessages(
|
||||||
|
sink.Writes,
|
||||||
|
LoggedMessage.VaryByRulesUpdated);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task FinalizeCacheBody_Cache_IfContentLengthMatches()
|
||||||
|
{
|
||||||
|
var cache = new TestResponseCache();
|
||||||
|
var sink = new TestSink();
|
||||||
|
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.CachedResponse = new CachedResponse();
|
||||||
|
context.BaseKey = "BaseKey";
|
||||||
|
context.CachedResponseValidFor = TimeSpan.FromSeconds(10);
|
||||||
|
|
||||||
|
await middleware.FinalizeCacheBodyAsync(context);
|
||||||
|
|
||||||
|
Assert.Equal(1, cache.SetCount);
|
||||||
|
TestUtils.AssertLoggedMessages(
|
||||||
|
sink.Writes,
|
||||||
|
LoggedMessage.ResponseCached);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task FinalizeCacheBody_DoNotCache_IfContentLengthMismatches()
|
||||||
|
{
|
||||||
|
var cache = new TestResponseCache();
|
||||||
|
var sink = new TestSink();
|
||||||
|
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.CachedResponse = new CachedResponse();
|
||||||
|
context.BaseKey = "BaseKey";
|
||||||
|
context.CachedResponseValidFor = TimeSpan.FromSeconds(10);
|
||||||
|
|
||||||
|
await middleware.FinalizeCacheBodyAsync(context);
|
||||||
|
|
||||||
|
Assert.Equal(0, cache.SetCount);
|
||||||
|
TestUtils.AssertLoggedMessages(
|
||||||
|
sink.Writes,
|
||||||
|
LoggedMessage.ResponseContentLengthMismatchNotCached);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task FinalizeCacheBody_Cache_IfContentLengthAbsent()
|
||||||
|
{
|
||||||
|
var cache = new TestResponseCache();
|
||||||
|
var sink = new TestSink();
|
||||||
|
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.CachedResponse = new CachedResponse()
|
||||||
|
{
|
||||||
|
Headers = new HeaderDictionary()
|
||||||
|
};
|
||||||
|
context.BaseKey = "BaseKey";
|
||||||
|
context.CachedResponseValidFor = TimeSpan.FromSeconds(10);
|
||||||
|
|
||||||
|
await middleware.FinalizeCacheBodyAsync(context);
|
||||||
|
|
||||||
|
Assert.Equal(1, cache.SetCount);
|
||||||
|
TestUtils.AssertLoggedMessages(
|
||||||
|
sink.Writes,
|
||||||
|
LoggedMessage.ResponseCached);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task FinalizeCacheBody_DoNotCache_IfShouldCacheResponseFalse()
|
||||||
|
{
|
||||||
|
var cache = new TestResponseCache();
|
||||||
|
var sink = new TestSink();
|
||||||
|
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 = false;
|
||||||
|
|
||||||
|
await middleware.FinalizeCacheBodyAsync(context);
|
||||||
|
|
||||||
|
Assert.Equal(0, cache.SetCount);
|
||||||
|
TestUtils.AssertLoggedMessages(
|
||||||
|
sink.Writes,
|
||||||
|
LoggedMessage.ResponseNotCached);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task FinalizeCacheBody_DoNotCache_IfBufferingDisabled()
|
||||||
|
{
|
||||||
|
var cache = new TestResponseCache();
|
||||||
|
var sink = new TestSink();
|
||||||
|
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.ResponseCachingStream.DisableBuffering();
|
||||||
|
|
||||||
|
await middleware.FinalizeCacheBodyAsync(context);
|
||||||
|
|
||||||
|
Assert.Equal(0, cache.SetCount);
|
||||||
|
TestUtils.AssertLoggedMessages(
|
||||||
|
sink.Writes,
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
var httpContext = new DefaultHttpContext();
|
||||||
|
|
||||||
|
// Should not throw
|
||||||
|
ResponseCachingMiddleware.AddResponseCachingFeature(httpContext);
|
||||||
|
|
||||||
|
// Should throw
|
||||||
|
Assert.ThrowsAny<InvalidOperationException>(() => ResponseCachingMiddleware.AddResponseCachingFeature(httpContext));
|
||||||
|
}
|
||||||
|
|
||||||
|
private class FakeResponseFeature : HttpResponseFeature
|
||||||
|
{
|
||||||
|
public override void OnStarting(Func<object, Task> 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<IResponseCachingFeature>() != null;
|
||||||
|
return Task.CompletedTask;
|
||||||
|
},
|
||||||
|
policyProvider: new TestResponseCachingPolicyProvider
|
||||||
|
{
|
||||||
|
AttemptResponseCachingValue = allowResponseCaching,
|
||||||
|
AllowCacheLookupValue = allowCacheLookup,
|
||||||
|
AllowCacheStorageValue = allowCacheStorage
|
||||||
|
});
|
||||||
|
|
||||||
|
var context = new DefaultHttpContext();
|
||||||
|
context.Features.Set<IHttpResponseFeature>(new FakeResponseFeature());
|
||||||
|
await middleware.Invoke(context);
|
||||||
|
|
||||||
|
Assert.True(responseCachingFeatureAdded);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetOrderCasingNormalizedStringValues_NormalizesCasingToUpper()
|
||||||
|
{
|
||||||
|
var uppercaseStrings = new StringValues(new[] { "STRINGA", "STRINGB" });
|
||||||
|
var lowercaseStrings = new StringValues(new[] { "stringA", "stringB" });
|
||||||
|
|
||||||
|
var normalizedStrings = ResponseCachingMiddleware.GetOrderCasingNormalizedStringValues(lowercaseStrings);
|
||||||
|
|
||||||
|
Assert.Equal(uppercaseStrings, normalizedStrings);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetOrderCasingNormalizedStringValues_NormalizesOrder()
|
||||||
|
{
|
||||||
|
var orderedStrings = new StringValues(new[] { "STRINGA", "STRINGB" });
|
||||||
|
var reverseOrderStrings = new StringValues(new[] { "STRINGB", "STRINGA" });
|
||||||
|
|
||||||
|
var normalizedStrings = ResponseCachingMiddleware.GetOrderCasingNormalizedStringValues(reverseOrderStrings);
|
||||||
|
|
||||||
|
Assert.Equal(orderedStrings, normalizedStrings);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetOrderCasingNormalizedStringValues_PreservesCommas()
|
||||||
|
{
|
||||||
|
var originalStrings = new StringValues(new[] { "STRINGA, STRINGB" });
|
||||||
|
|
||||||
|
var normalizedStrings = ResponseCachingMiddleware.GetOrderCasingNormalizedStringValues(originalStrings);
|
||||||
|
|
||||||
|
Assert.Equal(originalStrings, normalizedStrings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,794 @@
|
||||||
|
// 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 Microsoft.Extensions.Logging.Testing;
|
||||||
|
using Microsoft.Net.Http.Headers;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
|
{
|
||||||
|
public class ResponseCachingPolicyProviderTests
|
||||||
|
{
|
||||||
|
public static TheoryData<string> CacheableMethods
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return new TheoryData<string>
|
||||||
|
{
|
||||||
|
HttpMethods.Get,
|
||||||
|
HttpMethods.Head
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(CacheableMethods))]
|
||||||
|
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().AttemptResponseCaching(context));
|
||||||
|
Assert.Empty(sink.Writes);
|
||||||
|
}
|
||||||
|
public static TheoryData<string> NonCacheableMethods
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return new TheoryData<string>
|
||||||
|
{
|
||||||
|
HttpMethods.Post,
|
||||||
|
HttpMethods.Put,
|
||||||
|
HttpMethods.Delete,
|
||||||
|
HttpMethods.Trace,
|
||||||
|
HttpMethods.Connect,
|
||||||
|
HttpMethods.Options,
|
||||||
|
"",
|
||||||
|
null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[MemberData(nameof(NonCacheableMethods))]
|
||||||
|
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().AttemptResponseCaching(context));
|
||||||
|
TestUtils.AssertLoggedMessages(
|
||||||
|
sink.Writes,
|
||||||
|
LoggedMessage.RequestMethodNotCacheable);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
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().AttemptResponseCaching(context));
|
||||||
|
TestUtils.AssertLoggedMessages(
|
||||||
|
sink.Writes,
|
||||||
|
LoggedMessage.RequestWithAuthorizationNotCacheable);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AllowCacheStorage_NoStore_Allowed()
|
||||||
|
{
|
||||||
|
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.True(new ResponseCachingPolicyProvider().AllowCacheLookup(context));
|
||||||
|
Assert.Empty(sink.Writes);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
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().AllowCacheLookup(context));
|
||||||
|
TestUtils.AssertLoggedMessages(
|
||||||
|
sink.Writes,
|
||||||
|
LoggedMessage.RequestWithPragmaNoCacheNotCacheable);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AllowCacheLookup_LegacyDirectives_OverridenByCacheControl()
|
||||||
|
{
|
||||||
|
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 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsResponseCacheable_NoPublic_NotAllowed()
|
||||||
|
{
|
||||||
|
var sink = new TestSink();
|
||||||
|
var context = TestUtils.CreateTestContext(sink);
|
||||||
|
|
||||||
|
Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context));
|
||||||
|
TestUtils.AssertLoggedMessages(
|
||||||
|
sink.Writes,
|
||||||
|
LoggedMessage.ResponseWithoutPublicNotCacheable);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsResponseCacheable_Public_Allowed()
|
||||||
|
{
|
||||||
|
var sink = new TestSink();
|
||||||
|
var context = TestUtils.CreateTestContext(sink);
|
||||||
|
context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
|
||||||
|
{
|
||||||
|
Public = true
|
||||||
|
}.ToString();
|
||||||
|
|
||||||
|
Assert.True(new ResponseCachingPolicyProvider().IsResponseCacheable(context));
|
||||||
|
Assert.Empty(sink.Writes);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsResponseCacheable_NoCache_NotAllowed()
|
||||||
|
{
|
||||||
|
var sink = new TestSink();
|
||||||
|
var context = TestUtils.CreateTestContext(sink);
|
||||||
|
context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
|
||||||
|
{
|
||||||
|
Public = true,
|
||||||
|
NoCache = true
|
||||||
|
}.ToString();
|
||||||
|
|
||||||
|
Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context));
|
||||||
|
TestUtils.AssertLoggedMessages(
|
||||||
|
sink.Writes,
|
||||||
|
LoggedMessage.ResponseWithNoCacheNotCacheable);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsResponseCacheable_ResponseNoStore_NotAllowed()
|
||||||
|
{
|
||||||
|
var sink = new TestSink();
|
||||||
|
var context = TestUtils.CreateTestContext(sink);
|
||||||
|
context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
|
||||||
|
{
|
||||||
|
Public = true,
|
||||||
|
NoStore = true
|
||||||
|
}.ToString();
|
||||||
|
|
||||||
|
Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context));
|
||||||
|
TestUtils.AssertLoggedMessages(
|
||||||
|
sink.Writes,
|
||||||
|
LoggedMessage.ResponseWithNoStoreNotCacheable);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsResponseCacheable_SetCookieHeader_NotAllowed()
|
||||||
|
{
|
||||||
|
var sink = new TestSink();
|
||||||
|
var context = TestUtils.CreateTestContext(sink);
|
||||||
|
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));
|
||||||
|
TestUtils.AssertLoggedMessages(
|
||||||
|
sink.Writes,
|
||||||
|
LoggedMessage.ResponseWithSetCookieNotCacheable);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsResponseCacheable_VaryHeaderByStar_NotAllowed()
|
||||||
|
{
|
||||||
|
var sink = new TestSink();
|
||||||
|
var context = TestUtils.CreateTestContext(sink);
|
||||||
|
context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
|
||||||
|
{
|
||||||
|
Public = true
|
||||||
|
}.ToString();
|
||||||
|
context.HttpContext.Response.Headers[HeaderNames.Vary] = "*";
|
||||||
|
|
||||||
|
Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context));
|
||||||
|
TestUtils.AssertLoggedMessages(
|
||||||
|
sink.Writes,
|
||||||
|
LoggedMessage.ResponseWithVaryStarNotCacheable);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsResponseCacheable_Private_NotAllowed()
|
||||||
|
{
|
||||||
|
var sink = new TestSink();
|
||||||
|
var context = TestUtils.CreateTestContext(sink);
|
||||||
|
context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
|
||||||
|
{
|
||||||
|
Public = true,
|
||||||
|
Private = true
|
||||||
|
}.ToString();
|
||||||
|
|
||||||
|
Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context));
|
||||||
|
TestUtils.AssertLoggedMessages(
|
||||||
|
sink.Writes,
|
||||||
|
LoggedMessage.ResponseWithPrivateNotCacheable);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(StatusCodes.Status200OK)]
|
||||||
|
public void IsResponseCacheable_SuccessStatusCodes_Allowed(int statusCode)
|
||||||
|
{
|
||||||
|
var sink = new TestSink();
|
||||||
|
var context = TestUtils.CreateTestContext(sink);
|
||||||
|
context.HttpContext.Response.StatusCode = statusCode;
|
||||||
|
context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
|
||||||
|
{
|
||||||
|
Public = true
|
||||||
|
}.ToString();
|
||||||
|
|
||||||
|
Assert.True(new ResponseCachingPolicyProvider().IsResponseCacheable(context));
|
||||||
|
Assert.Empty(sink.Writes);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(StatusCodes.Status100Continue)]
|
||||||
|
[InlineData(StatusCodes.Status101SwitchingProtocols)]
|
||||||
|
[InlineData(StatusCodes.Status102Processing)]
|
||||||
|
[InlineData(StatusCodes.Status201Created)]
|
||||||
|
[InlineData(StatusCodes.Status202Accepted)]
|
||||||
|
[InlineData(StatusCodes.Status203NonAuthoritative)]
|
||||||
|
[InlineData(StatusCodes.Status204NoContent)]
|
||||||
|
[InlineData(StatusCodes.Status205ResetContent)]
|
||||||
|
[InlineData(StatusCodes.Status206PartialContent)]
|
||||||
|
[InlineData(StatusCodes.Status207MultiStatus)]
|
||||||
|
[InlineData(StatusCodes.Status208AlreadyReported)]
|
||||||
|
[InlineData(StatusCodes.Status226IMUsed)]
|
||||||
|
[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.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)]
|
||||||
|
[InlineData(StatusCodes.Status502BadGateway)]
|
||||||
|
[InlineData(StatusCodes.Status503ServiceUnavailable)]
|
||||||
|
[InlineData(StatusCodes.Status504GatewayTimeout)]
|
||||||
|
[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();
|
||||||
|
var context = TestUtils.CreateTestContext(sink);
|
||||||
|
context.HttpContext.Response.StatusCode = statusCode;
|
||||||
|
context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
|
||||||
|
{
|
||||||
|
Public = true
|
||||||
|
}.ToString();
|
||||||
|
|
||||||
|
Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context));
|
||||||
|
TestUtils.AssertLoggedMessages(
|
||||||
|
sink.Writes,
|
||||||
|
LoggedMessage.ResponseWithUnsuccessfulStatusCodeNotCacheable);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsResponseCacheable_NoExpiryRequirements_IsAllowed()
|
||||||
|
{
|
||||||
|
var sink = new TestSink();
|
||||||
|
var context = TestUtils.CreateTestContext(sink);
|
||||||
|
context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
|
||||||
|
context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
|
||||||
|
{
|
||||||
|
Public = true
|
||||||
|
}.ToString();
|
||||||
|
|
||||||
|
var utcNow = DateTimeOffset.UtcNow;
|
||||||
|
context.HttpContext.Response.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow);
|
||||||
|
context.ResponseTime = DateTimeOffset.MaxValue;
|
||||||
|
|
||||||
|
Assert.True(new ResponseCachingPolicyProvider().IsResponseCacheable(context));
|
||||||
|
Assert.Empty(sink.Writes);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsResponseCacheable_AtExpiry_NotAllowed()
|
||||||
|
{
|
||||||
|
var sink = new TestSink();
|
||||||
|
var context = TestUtils.CreateTestContext(sink);
|
||||||
|
context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
|
||||||
|
context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
|
||||||
|
{
|
||||||
|
Public = true
|
||||||
|
}.ToString();
|
||||||
|
var utcNow = DateTimeOffset.UtcNow;
|
||||||
|
context.HttpContext.Response.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow);
|
||||||
|
|
||||||
|
context.HttpContext.Response.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow);
|
||||||
|
context.ResponseTime = utcNow;
|
||||||
|
|
||||||
|
Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context));
|
||||||
|
TestUtils.AssertLoggedMessages(
|
||||||
|
sink.Writes,
|
||||||
|
LoggedMessage.ExpirationExpiresExceeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsResponseCacheable_MaxAgeOverridesExpiry_ToAllowed()
|
||||||
|
{
|
||||||
|
var utcNow = DateTimeOffset.UtcNow;
|
||||||
|
var sink = new TestSink();
|
||||||
|
var context = TestUtils.CreateTestContext(sink);
|
||||||
|
context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
|
||||||
|
context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
|
||||||
|
{
|
||||||
|
Public = true,
|
||||||
|
MaxAge = TimeSpan.FromSeconds(10)
|
||||||
|
}.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));
|
||||||
|
Assert.Empty(sink.Writes);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsResponseCacheable_MaxAgeOverridesExpiry_ToNotAllowed()
|
||||||
|
{
|
||||||
|
var utcNow = DateTimeOffset.UtcNow;
|
||||||
|
var sink = new TestSink();
|
||||||
|
var context = TestUtils.CreateTestContext(sink);
|
||||||
|
context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
|
||||||
|
context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
|
||||||
|
{
|
||||||
|
Public = true,
|
||||||
|
MaxAge = TimeSpan.FromSeconds(10)
|
||||||
|
}.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));
|
||||||
|
TestUtils.AssertLoggedMessages(
|
||||||
|
sink.Writes,
|
||||||
|
LoggedMessage.ExpirationMaxAgeExceeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsResponseCacheable_SharedMaxAgeOverridesMaxAge_ToAllowed()
|
||||||
|
{
|
||||||
|
var utcNow = DateTimeOffset.UtcNow;
|
||||||
|
var sink = new TestSink();
|
||||||
|
var context = TestUtils.CreateTestContext(sink);
|
||||||
|
context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
|
||||||
|
context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
|
||||||
|
{
|
||||||
|
Public = true,
|
||||||
|
MaxAge = TimeSpan.FromSeconds(10),
|
||||||
|
SharedMaxAge = TimeSpan.FromSeconds(15)
|
||||||
|
}.ToString();
|
||||||
|
context.HttpContext.Response.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow);
|
||||||
|
context.ResponseTime = utcNow + TimeSpan.FromSeconds(11);
|
||||||
|
|
||||||
|
Assert.True(new ResponseCachingPolicyProvider().IsResponseCacheable(context));
|
||||||
|
Assert.Empty(sink.Writes);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsResponseCacheable_SharedMaxAgeOverridesMaxAge_ToNotAllowed()
|
||||||
|
{
|
||||||
|
var utcNow = DateTimeOffset.UtcNow;
|
||||||
|
var sink = new TestSink();
|
||||||
|
var context = TestUtils.CreateTestContext(sink);
|
||||||
|
context.HttpContext.Response.StatusCode = StatusCodes.Status200OK;
|
||||||
|
context.HttpContext.Response.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
|
||||||
|
{
|
||||||
|
Public = true,
|
||||||
|
MaxAge = TimeSpan.FromSeconds(10),
|
||||||
|
SharedMaxAge = TimeSpan.FromSeconds(5)
|
||||||
|
}.ToString();
|
||||||
|
context.HttpContext.Response.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow);
|
||||||
|
context.ResponseTime = utcNow + TimeSpan.FromSeconds(5);
|
||||||
|
|
||||||
|
Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context));
|
||||||
|
TestUtils.AssertLoggedMessages(
|
||||||
|
sink.Writes,
|
||||||
|
LoggedMessage.ExpirationSharedMaxAgeExceeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsCachedEntryFresh_NoCachedCacheControl_FallsbackToEmptyCacheControl()
|
||||||
|
{
|
||||||
|
var utcNow = DateTimeOffset.UtcNow;
|
||||||
|
var sink = new TestSink();
|
||||||
|
var context = TestUtils.CreateTestContext(sink);
|
||||||
|
context.ResponseTime = DateTimeOffset.MaxValue;
|
||||||
|
context.CachedEntryAge = TimeSpan.MaxValue;
|
||||||
|
context.CachedResponseHeaders = new HeaderDictionary();
|
||||||
|
|
||||||
|
Assert.True(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context));
|
||||||
|
Assert.Empty(sink.Writes);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsCachedEntryFresh_NoExpiryRequirements_IsFresh()
|
||||||
|
{
|
||||||
|
var utcNow = DateTimeOffset.UtcNow;
|
||||||
|
var sink = new TestSink();
|
||||||
|
var context = TestUtils.CreateTestContext(sink);
|
||||||
|
context.ResponseTime = DateTimeOffset.MaxValue;
|
||||||
|
context.CachedEntryAge = TimeSpan.MaxValue;
|
||||||
|
context.CachedResponseHeaders = new HeaderDictionary();
|
||||||
|
context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue()
|
||||||
|
{
|
||||||
|
Public = true
|
||||||
|
}.ToString();
|
||||||
|
|
||||||
|
Assert.True(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context));
|
||||||
|
Assert.Empty(sink.Writes);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsCachedEntryFresh_AtExpiry_IsNotFresh()
|
||||||
|
{
|
||||||
|
var utcNow = DateTimeOffset.UtcNow;
|
||||||
|
var sink = new TestSink();
|
||||||
|
var context = TestUtils.CreateTestContext(sink);
|
||||||
|
context.ResponseTime = utcNow;
|
||||||
|
context.CachedEntryAge = TimeSpan.Zero;
|
||||||
|
context.CachedResponseHeaders = new HeaderDictionary();
|
||||||
|
context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue()
|
||||||
|
{
|
||||||
|
Public = true
|
||||||
|
}.ToString();
|
||||||
|
context.CachedResponseHeaders[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow);
|
||||||
|
|
||||||
|
Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context));
|
||||||
|
TestUtils.AssertLoggedMessages(
|
||||||
|
sink.Writes,
|
||||||
|
LoggedMessage.ExpirationExpiresExceeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsCachedEntryFresh_MaxAgeOverridesExpiry_ToFresh()
|
||||||
|
{
|
||||||
|
var utcNow = DateTimeOffset.UtcNow;
|
||||||
|
var sink = new TestSink();
|
||||||
|
var context = TestUtils.CreateTestContext(sink);
|
||||||
|
context.CachedEntryAge = TimeSpan.FromSeconds(9);
|
||||||
|
context.ResponseTime = utcNow + context.CachedEntryAge;
|
||||||
|
context.CachedResponseHeaders = new HeaderDictionary();
|
||||||
|
context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue()
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsCachedEntryFresh_MaxAgeOverridesExpiry_ToNotFresh()
|
||||||
|
{
|
||||||
|
var utcNow = DateTimeOffset.UtcNow;
|
||||||
|
var sink = new TestSink();
|
||||||
|
var context = TestUtils.CreateTestContext(sink);
|
||||||
|
context.CachedEntryAge = TimeSpan.FromSeconds(10);
|
||||||
|
context.ResponseTime = utcNow + context.CachedEntryAge;
|
||||||
|
context.CachedResponseHeaders = new HeaderDictionary();
|
||||||
|
context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue()
|
||||||
|
{
|
||||||
|
Public = true,
|
||||||
|
MaxAge = TimeSpan.FromSeconds(10)
|
||||||
|
}.ToString();
|
||||||
|
context.HttpContext.Response.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow);
|
||||||
|
|
||||||
|
Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context));
|
||||||
|
TestUtils.AssertLoggedMessages(
|
||||||
|
sink.Writes,
|
||||||
|
LoggedMessage.ExpirationMaxAgeExceeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsCachedEntryFresh_SharedMaxAgeOverridesMaxAge_ToFresh()
|
||||||
|
{
|
||||||
|
var utcNow = DateTimeOffset.UtcNow;
|
||||||
|
var sink = new TestSink();
|
||||||
|
var context = TestUtils.CreateTestContext(sink);
|
||||||
|
context.CachedEntryAge = TimeSpan.FromSeconds(11);
|
||||||
|
context.ResponseTime = utcNow + context.CachedEntryAge;
|
||||||
|
context.CachedResponseHeaders = new HeaderDictionary();
|
||||||
|
context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue()
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsCachedEntryFresh_SharedMaxAgeOverridesMaxAge_ToNotFresh()
|
||||||
|
{
|
||||||
|
var utcNow = DateTimeOffset.UtcNow;
|
||||||
|
var sink = new TestSink();
|
||||||
|
var context = TestUtils.CreateTestContext(sink);
|
||||||
|
context.CachedEntryAge = TimeSpan.FromSeconds(5);
|
||||||
|
context.ResponseTime = utcNow + context.CachedEntryAge;
|
||||||
|
context.CachedResponseHeaders = new HeaderDictionary();
|
||||||
|
context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue()
|
||||||
|
{
|
||||||
|
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(
|
||||||
|
sink.Writes,
|
||||||
|
LoggedMessage.ExpirationSharedMaxAgeExceeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsCachedEntryFresh_MinFreshReducesFreshness_ToNotFresh()
|
||||||
|
{
|
||||||
|
var sink = new TestSink();
|
||||||
|
var context = TestUtils.CreateTestContext(sink);
|
||||||
|
context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
|
||||||
|
{
|
||||||
|
MinFresh = TimeSpan.FromSeconds(2)
|
||||||
|
}.ToString();
|
||||||
|
context.CachedResponseHeaders = new HeaderDictionary();
|
||||||
|
context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue()
|
||||||
|
{
|
||||||
|
MaxAge = TimeSpan.FromSeconds(10),
|
||||||
|
SharedMaxAge = TimeSpan.FromSeconds(5)
|
||||||
|
}.ToString();
|
||||||
|
context.CachedEntryAge = TimeSpan.FromSeconds(3);
|
||||||
|
|
||||||
|
Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context));
|
||||||
|
TestUtils.AssertLoggedMessages(
|
||||||
|
sink.Writes,
|
||||||
|
LoggedMessage.ExpirationMinFreshAdded,
|
||||||
|
LoggedMessage.ExpirationSharedMaxAgeExceeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsCachedEntryFresh_RequestMaxAgeRestrictAge_ToNotFresh()
|
||||||
|
{
|
||||||
|
var sink = new TestSink();
|
||||||
|
var context = TestUtils.CreateTestContext(sink);
|
||||||
|
context.HttpContext.Request.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue()
|
||||||
|
{
|
||||||
|
MaxAge = TimeSpan.FromSeconds(5)
|
||||||
|
}.ToString();
|
||||||
|
context.CachedResponseHeaders = new HeaderDictionary();
|
||||||
|
context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue()
|
||||||
|
{
|
||||||
|
MaxAge = TimeSpan.FromSeconds(10),
|
||||||
|
}.ToString();
|
||||||
|
context.CachedEntryAge = TimeSpan.FromSeconds(5);
|
||||||
|
|
||||||
|
Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context));
|
||||||
|
TestUtils.AssertLoggedMessages(
|
||||||
|
sink.Writes,
|
||||||
|
LoggedMessage.ExpirationMaxAgeExceeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsCachedEntryFresh_MaxStaleOverridesFreshness_ToFresh()
|
||||||
|
{
|
||||||
|
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),
|
||||||
|
}.ToString();
|
||||||
|
context.CachedEntryAge = TimeSpan.FromSeconds(6);
|
||||||
|
|
||||||
|
Assert.True(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context));
|
||||||
|
TestUtils.AssertLoggedMessages(
|
||||||
|
sink.Writes,
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
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(1)
|
||||||
|
}.ToString();
|
||||||
|
context.CachedResponseHeaders = new HeaderDictionary();
|
||||||
|
context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue()
|
||||||
|
{
|
||||||
|
MaxAge = TimeSpan.FromSeconds(5),
|
||||||
|
}.ToString();
|
||||||
|
context.CachedEntryAge = TimeSpan.FromSeconds(6);
|
||||||
|
|
||||||
|
Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context));
|
||||||
|
TestUtils.AssertLoggedMessages(
|
||||||
|
sink.Writes,
|
||||||
|
LoggedMessage.ExpirationMaxAgeExceeded);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void IsCachedEntryFresh_MustRevalidateOverridesRequestMaxStale_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);
|
||||||
|
}
|
||||||
|
|
||||||
|
[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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,849 @@
|
||||||
|
// 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.Net.Http;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Http.Features;
|
||||||
|
using Microsoft.AspNetCore.TestHost;
|
||||||
|
using Microsoft.Net.Http.Headers;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.ResponseCaching.Tests
|
||||||
|
{
|
||||||
|
public class ResponseCachingTests
|
||||||
|
{
|
||||||
|
[Theory]
|
||||||
|
[InlineData("GET")]
|
||||||
|
[InlineData("HEAD")]
|
||||||
|
public async void ServesCachedContent_IfAvailable(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, ""));
|
||||||
|
var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, ""));
|
||||||
|
|
||||||
|
await AssertCachedResponseAsync(initialResponse, subsequentResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("GET")]
|
||||||
|
[InlineData("HEAD")]
|
||||||
|
public async void ServesFreshContent_IfNotAvailable(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, ""));
|
||||||
|
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();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[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();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async void ServesCachedContent_IfVaryHeader_Matches()
|
||||||
|
{
|
||||||
|
var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Response.Headers[HeaderNames.Vary] = HeaderNames.From);
|
||||||
|
|
||||||
|
foreach (var builder in builders)
|
||||||
|
{
|
||||||
|
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 AssertCachedResponseAsync(initialResponse, subsequentResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async void ServesFreshContent_IfVaryHeader_Mismatches()
|
||||||
|
{
|
||||||
|
var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Response.Headers[HeaderNames.Vary] = HeaderNames.From);
|
||||||
|
|
||||||
|
foreach (var builder in builders)
|
||||||
|
{
|
||||||
|
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 AssertFreshResponseAsync(initialResponse, subsequentResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async void ServesCachedContent_IfVaryQueryKeys_Matches()
|
||||||
|
{
|
||||||
|
var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Features.Get<IResponseCachingFeature>().VaryByQueryKeys = new[] { "query" });
|
||||||
|
|
||||||
|
foreach (var builder in builders)
|
||||||
|
{
|
||||||
|
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 AssertCachedResponseAsync(initialResponse, subsequentResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async void ServesCachedContent_IfVaryQueryKeysExplicit_Matches_QueryKeyCaseInsensitive()
|
||||||
|
{
|
||||||
|
var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Features.Get<IResponseCachingFeature>().VaryByQueryKeys = new[] { "QueryA", "queryb" });
|
||||||
|
|
||||||
|
foreach (var builder in builders)
|
||||||
|
{
|
||||||
|
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 AssertCachedResponseAsync(initialResponse, subsequentResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async void ServesCachedContent_IfVaryQueryKeyStar_Matches_QueryKeyCaseInsensitive()
|
||||||
|
{
|
||||||
|
var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Features.Get<IResponseCachingFeature>().VaryByQueryKeys = new[] { "*" });
|
||||||
|
|
||||||
|
foreach (var builder in builders)
|
||||||
|
{
|
||||||
|
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 AssertCachedResponseAsync(initialResponse, subsequentResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async void ServesCachedContent_IfVaryQueryKeyExplicit_Matches_OrderInsensitive()
|
||||||
|
{
|
||||||
|
var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Features.Get<IResponseCachingFeature>().VaryByQueryKeys = new[] { "QueryB", "QueryA" });
|
||||||
|
|
||||||
|
foreach (var builder in builders)
|
||||||
|
{
|
||||||
|
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 AssertCachedResponseAsync(initialResponse, subsequentResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async void ServesCachedContent_IfVaryQueryKeyStar_Matches_OrderInsensitive()
|
||||||
|
{
|
||||||
|
var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Features.Get<IResponseCachingFeature>().VaryByQueryKeys = new[] { "*" });
|
||||||
|
|
||||||
|
foreach (var builder in builders)
|
||||||
|
{
|
||||||
|
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 AssertCachedResponseAsync(initialResponse, subsequentResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async void ServesFreshContent_IfVaryQueryKey_Mismatches()
|
||||||
|
{
|
||||||
|
var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Features.Get<IResponseCachingFeature>().VaryByQueryKeys = new[] { "query" });
|
||||||
|
|
||||||
|
foreach (var builder in builders)
|
||||||
|
{
|
||||||
|
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 AssertFreshResponseAsync(initialResponse, subsequentResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async void ServesFreshContent_IfVaryQueryKeyExplicit_Mismatch_QueryKeyCaseSensitive()
|
||||||
|
{
|
||||||
|
var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Features.Get<IResponseCachingFeature>().VaryByQueryKeys = new[] { "QueryA", "QueryB" });
|
||||||
|
|
||||||
|
foreach (var builder in builders)
|
||||||
|
{
|
||||||
|
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 AssertFreshResponseAsync(initialResponse, subsequentResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async void ServesFreshContent_IfVaryQueryKeyStar_Mismatch_QueryKeyValueCaseSensitive()
|
||||||
|
{
|
||||||
|
var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Features.Get<IResponseCachingFeature>().VaryByQueryKeys = new[] { "*" });
|
||||||
|
|
||||||
|
foreach (var builder in builders)
|
||||||
|
{
|
||||||
|
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 AssertFreshResponseAsync(initialResponse, subsequentResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async void ServesFreshContent_IfRequestRequirements_NotMet()
|
||||||
|
{
|
||||||
|
var builders = TestUtils.CreateBuildersWithResponseCaching();
|
||||||
|
|
||||||
|
foreach (var builder in builders)
|
||||||
|
{
|
||||||
|
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("");
|
||||||
|
|
||||||
|
await AssertFreshResponseAsync(initialResponse, subsequentResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async void Serves504_IfOnlyIfCachedHeader_IsSpecified()
|
||||||
|
{
|
||||||
|
var builders = TestUtils.CreateBuildersWithResponseCaching();
|
||||||
|
|
||||||
|
foreach (var builder in builders)
|
||||||
|
{
|
||||||
|
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 ServesFreshContent_IfSetCookie_IsSpecified()
|
||||||
|
{
|
||||||
|
var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Response.Headers[HeaderNames.SetCookie] = "cookieName=cookieValue");
|
||||||
|
|
||||||
|
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 ServesCachedContent_IfIHttpSendFileFeature_NotUsed()
|
||||||
|
{
|
||||||
|
var builders = TestUtils.CreateBuildersWithResponseCaching(app =>
|
||||||
|
{
|
||||||
|
app.Use(async (context, next) =>
|
||||||
|
{
|
||||||
|
context.Features.Set<IHttpSendFileFeature>(new DummySendFileFeature());
|
||||||
|
await next.Invoke();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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 AssertCachedResponseAsync(initialResponse, subsequentResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async void ServesFreshContent_IfIHttpSendFileFeature_Used()
|
||||||
|
{
|
||||||
|
var builders = TestUtils.CreateBuildersWithResponseCaching(
|
||||||
|
app =>
|
||||||
|
{
|
||||||
|
app.Use(async (context, next) =>
|
||||||
|
{
|
||||||
|
context.Features.Set<IHttpSendFileFeature>(new DummySendFileFeature());
|
||||||
|
await next.Invoke();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
contextAction: async context => await context.Features.Get<IHttpSendFileFeature>().SendFileAsync("dummy", 0, 0, CancellationToken.None));
|
||||||
|
|
||||||
|
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 ServesCachedContent_IfSubsequentRequestContainsNoStore()
|
||||||
|
{
|
||||||
|
var builders = TestUtils.CreateBuildersWithResponseCaching();
|
||||||
|
|
||||||
|
foreach (var builder in builders)
|
||||||
|
{
|
||||||
|
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("");
|
||||||
|
|
||||||
|
await AssertCachedResponseAsync(initialResponse, subsequentResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async void ServesFreshContent_IfInitialRequestContainsNoStore()
|
||||||
|
{
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
NoStore = true
|
||||||
|
};
|
||||||
|
var initialResponse = await client.GetAsync("");
|
||||||
|
var subsequentResponse = await client.GetAsync("");
|
||||||
|
|
||||||
|
await AssertFreshResponseAsync(initialResponse, subsequentResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async void ServesFreshContent_IfInitialResponseContainsNoStore()
|
||||||
|
{
|
||||||
|
var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Response.Headers[HeaderNames.CacheControl] = CacheControlHeaderValue.NoStoreString);
|
||||||
|
|
||||||
|
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()
|
||||||
|
{
|
||||||
|
var builders = TestUtils.CreateBuildersWithResponseCaching();
|
||||||
|
|
||||||
|
foreach (var builder in builders)
|
||||||
|
{
|
||||||
|
using (var server = new TestServer(builder))
|
||||||
|
{
|
||||||
|
var client = server.CreateClient();
|
||||||
|
var initialResponse = await client.GetAsync("");
|
||||||
|
client.DefaultRequestHeaders.IfModifiedSince = 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 builders = TestUtils.CreateBuildersWithResponseCaching();
|
||||||
|
|
||||||
|
foreach (var builder in builders)
|
||||||
|
{
|
||||||
|
using (var server = new TestServer(builder))
|
||||||
|
{
|
||||||
|
var client = server.CreateClient();
|
||||||
|
var initialResponse = await client.GetAsync("");
|
||||||
|
client.DefaultRequestHeaders.IfModifiedSince = DateTimeOffset.MinValue;
|
||||||
|
var subsequentResponse = await client.GetAsync("");
|
||||||
|
|
||||||
|
await AssertCachedResponseAsync(initialResponse, subsequentResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async void Serves304_IfIfNoneMatch_Satisfied()
|
||||||
|
{
|
||||||
|
var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Response.GetTypedHeaders().ETag = new EntityTagHeaderValue("\"E1\""));
|
||||||
|
|
||||||
|
foreach (var builder in builders)
|
||||||
|
{
|
||||||
|
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 builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Response.GetTypedHeaders().ETag = new EntityTagHeaderValue("\"E1\""));
|
||||||
|
|
||||||
|
foreach (var builder in builders)
|
||||||
|
{
|
||||||
|
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 AssertCachedResponseAsync(initialResponse, subsequentResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async void ServesCachedContent_IfBodySize_IsCacheable()
|
||||||
|
{
|
||||||
|
var builders = TestUtils.CreateBuildersWithResponseCaching(options: new ResponseCachingOptions()
|
||||||
|
{
|
||||||
|
MaximumBodySize = 100
|
||||||
|
});
|
||||||
|
|
||||||
|
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 AssertCachedResponseAsync(initialResponse, subsequentResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async void ServesFreshContent_IfBodySize_IsNotCacheable()
|
||||||
|
{
|
||||||
|
var builders = TestUtils.CreateBuildersWithResponseCaching(options: new ResponseCachingOptions()
|
||||||
|
{
|
||||||
|
MaximumBodySize = 1
|
||||||
|
});
|
||||||
|
|
||||||
|
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("/different");
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async void ServesCachedContent_WithoutReplacingCachedVaryBy_OnCacheMiss()
|
||||||
|
{
|
||||||
|
var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Response.Headers[HeaderNames.Vary] = HeaderNames.From);
|
||||||
|
|
||||||
|
foreach (var builder in builders)
|
||||||
|
{
|
||||||
|
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 AssertCachedResponseAsync(initialResponse, subsequentResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async void ServesFreshContent_IfCachedVaryByUpdated_OnCacheMiss()
|
||||||
|
{
|
||||||
|
var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Response.Headers[HeaderNames.Vary] = context.Request.Headers[HeaderNames.Pragma]);
|
||||||
|
|
||||||
|
foreach (var builder in builders)
|
||||||
|
{
|
||||||
|
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 AssertFreshResponseAsync(initialResponse, subsequentResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async void ServesCachedContent_IfCachedVaryByNotUpdated_OnCacheMiss()
|
||||||
|
{
|
||||||
|
var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Response.Headers[HeaderNames.Vary] = context.Request.Headers[HeaderNames.Pragma]);
|
||||||
|
|
||||||
|
foreach (var builder in builders)
|
||||||
|
{
|
||||||
|
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 AssertCachedResponseAsync(initialResponse, subsequentResponse);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task AssertCachedResponseAsync(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 AssertFreshResponseAsync(HttpResponseMessage initialResponse, HttpResponseMessage subsequentResponse)
|
||||||
|
{
|
||||||
|
initialResponse.EnsureSuccessStatusCode();
|
||||||
|
subsequentResponse.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
Assert.False(subsequentResponse.Headers.Contains(HeaderNames.Age));
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<byte[]> Segments { get; set; }
|
||||||
|
internal int SegmentSize { get; set; }
|
||||||
|
internal long Length { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static TheoryData<TestStreamInitInfo> TestStreams
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return new TheoryData<TestStreamInitInfo>
|
||||||
|
{
|
||||||
|
// Partial Segment
|
||||||
|
new TestStreamInitInfo()
|
||||||
|
{
|
||||||
|
Segments = new List<byte[]>(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<byte[]>(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<ArgumentNullException>(() => new SegmentReadStream(null, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Position_ResetToZero_Succeeds()
|
||||||
|
{
|
||||||
|
var stream = new SegmentReadStream(new List<byte[]>(), 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<byte[]>(new[] { new byte[100] }), 100);
|
||||||
|
|
||||||
|
Assert.Throws<ArgumentOutOfRangeException>(() => stream.Position = position);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WriteOperations_Throws()
|
||||||
|
{
|
||||||
|
var stream = new SegmentReadStream(new List<byte[]>(), 0);
|
||||||
|
|
||||||
|
|
||||||
|
Assert.Throws<NotSupportedException>(() => stream.Flush());
|
||||||
|
Assert.Throws<NotSupportedException>(() => stream.Write(new byte[1], 0, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SetLength_Throws()
|
||||||
|
{
|
||||||
|
var stream = new SegmentReadStream(new List<byte[]>(), 0);
|
||||||
|
|
||||||
|
Assert.Throws<NotSupportedException>(() => stream.SetLength(0));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(SeekOrigin.Current)]
|
||||||
|
[InlineData(SeekOrigin.End)]
|
||||||
|
public void Seek_NotBegin_Throws(SeekOrigin origin)
|
||||||
|
{
|
||||||
|
var stream = new SegmentReadStream(new List<byte[]>(), 0);
|
||||||
|
|
||||||
|
Assert.Throws<ArgumentException>(() => 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<byte[]>(), 0);
|
||||||
|
|
||||||
|
Assert.Throws<ArgumentOutOfRangeException>(() => 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]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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<ArgumentOutOfRangeException>(() => new SegmentWriteStream(segmentSize));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ReadAndSeekOperations_Throws()
|
||||||
|
{
|
||||||
|
var stream = new SegmentWriteStream(1);
|
||||||
|
|
||||||
|
Assert.Throws<NotSupportedException>(() => stream.Read(new byte[1], 0, 0));
|
||||||
|
Assert.Throws<NotSupportedException>(() => stream.Position = 0);
|
||||||
|
Assert.Throws<NotSupportedException>(() => stream.Seek(0, SeekOrigin.Begin));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GetSegments_ExtractionDisablesWriting()
|
||||||
|
{
|
||||||
|
var stream = new SegmentWriteStream(1);
|
||||||
|
|
||||||
|
Assert.True(stream.CanWrite);
|
||||||
|
Assert.Empty(stream.GetSegments());
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,396 @@
|
||||||
|
// 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.Net.Http;
|
||||||
|
using System.Text;
|
||||||
|
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.ResponseCaching.Internal;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Microsoft.Extensions.Logging.Testing;
|
||||||
|
using Microsoft.Extensions.ObjectPool;
|
||||||
|
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
|
||||||
|
{
|
||||||
|
internal class TestUtils
|
||||||
|
{
|
||||||
|
static TestUtils()
|
||||||
|
{
|
||||||
|
// Force sharding in tests
|
||||||
|
StreamUtilities.BodySegmentSize = 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TestRequestDelegate(HttpContext context, string guid)
|
||||||
|
{
|
||||||
|
var headers = context.Response.GetTypedHeaders();
|
||||||
|
|
||||||
|
var expires = context.Request.Query["Expires"];
|
||||||
|
if (!string.IsNullOrEmpty(expires))
|
||||||
|
{
|
||||||
|
headers.Expires = DateTimeOffset.Now.AddSeconds(int.Parse(expires));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (headers.CacheControl == 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"] = 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 Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static IResponseCachingKeyProvider CreateTestKeyProvider()
|
||||||
|
{
|
||||||
|
return CreateTestKeyProvider(new ResponseCachingOptions());
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static IResponseCachingKeyProvider CreateTestKeyProvider(ResponseCachingOptions options)
|
||||||
|
{
|
||||||
|
return new ResponseCachingKeyProvider(new DefaultObjectPoolProvider(), Options.Create(options));
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static IEnumerable<IWebHostBuilder> CreateBuildersWithResponseCaching(
|
||||||
|
Action<IApplicationBuilder> configureDelegate = null,
|
||||||
|
ResponseCachingOptions options = null,
|
||||||
|
Action<HttpContext> 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<IWebHostBuilder> CreateBuildersWithResponseCaching(
|
||||||
|
Action<IApplicationBuilder> configureDelegate = null,
|
||||||
|
ResponseCachingOptions options = null,
|
||||||
|
IEnumerable<RequestDelegate> requestDelegates = null)
|
||||||
|
{
|
||||||
|
if (configureDelegate == null)
|
||||||
|
{
|
||||||
|
configureDelegate = app => { };
|
||||||
|
}
|
||||||
|
if (requestDelegates == null)
|
||||||
|
{
|
||||||
|
requestDelegates = new RequestDelegate[]
|
||||||
|
{
|
||||||
|
TestRequestDelegateWriteAsync,
|
||||||
|
TestRequestDelegateWrite
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var requestDelegate in requestDelegates)
|
||||||
|
{
|
||||||
|
// Test with in memory ResponseCache
|
||||||
|
yield return new WebHostBuilder()
|
||||||
|
.ConfigureServices(services =>
|
||||||
|
{
|
||||||
|
services.AddResponseCaching(responseCachingOptions =>
|
||||||
|
{
|
||||||
|
if (options != null)
|
||||||
|
{
|
||||||
|
responseCachingOptions.MaximumBodySize = options.MaximumBodySize;
|
||||||
|
responseCachingOptions.UseCaseSensitivePaths = options.UseCaseSensitivePaths;
|
||||||
|
responseCachingOptions.SystemClock = options.SystemClock;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.Configure(app =>
|
||||||
|
{
|
||||||
|
configureDelegate(app);
|
||||||
|
app.UseResponseCaching();
|
||||||
|
app.Run(requestDelegate);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 => Task.CompletedTask;
|
||||||
|
}
|
||||||
|
if (cache == null)
|
||||||
|
{
|
||||||
|
cache = new TestResponseCache();
|
||||||
|
}
|
||||||
|
if (options == null)
|
||||||
|
{
|
||||||
|
options = new ResponseCachingOptions();
|
||||||
|
}
|
||||||
|
if (keyProvider == null)
|
||||||
|
{
|
||||||
|
keyProvider = new ResponseCachingKeyProvider(new DefaultObjectPoolProvider(), Options.Create(options));
|
||||||
|
}
|
||||||
|
if (policyProvider == null)
|
||||||
|
{
|
||||||
|
policyProvider = new TestResponseCachingPolicyProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ResponseCachingMiddleware(
|
||||||
|
next,
|
||||||
|
Options.Create(options),
|
||||||
|
testSink == null ? (ILoggerFactory)NullLoggerFactory.Instance : new TestLoggerFactory(testSink, true),
|
||||||
|
policyProvider,
|
||||||
|
cache,
|
||||||
|
keyProvider);
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static ResponseCachingContext CreateTestContext()
|
||||||
|
{
|
||||||
|
return new ResponseCachingContext(new DefaultHttpContext(), NullLogger.Instance)
|
||||||
|
{
|
||||||
|
ResponseTime = DateTimeOffset.UtcNow
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static ResponseCachingContext CreateTestContext(ITestSink testSink)
|
||||||
|
{
|
||||||
|
return new ResponseCachingContext(new DefaultHttpContext(), new TestLogger("ResponseCachingTests", testSink, true))
|
||||||
|
{
|
||||||
|
ResponseTime = DateTimeOffset.UtcNow
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void AssertLoggedMessages(IEnumerable<WriteContext> messages, params LoggedMessage[] expectedMessages)
|
||||||
|
{
|
||||||
|
var messageList = messages.ToList();
|
||||||
|
Assert.Equal(messageList.Count, expectedMessages.Length);
|
||||||
|
|
||||||
|
for (var i = 0; i < messageList.Count; i++)
|
||||||
|
{
|
||||||
|
Assert.Equal(expectedMessages[i].EventId, messageList[i].EventId);
|
||||||
|
Assert.Equal(expectedMessages[i].LogLevel, messageList[i].LogLevel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static HttpRequestMessage CreateRequest(string method, string requestUri)
|
||||||
|
{
|
||||||
|
return new HttpRequestMessage(new HttpMethod(method), requestUri);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
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 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);
|
||||||
|
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);
|
||||||
|
internal static LoggedMessage ExpirationInfiniteMaxStaleSatisfied => new LoggedMessage(29, LogLevel.Debug);
|
||||||
|
|
||||||
|
private LoggedMessage(int evenId, LogLevel logLevel)
|
||||||
|
{
|
||||||
|
EventId = evenId;
|
||||||
|
LogLevel = logLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal int EventId { get; }
|
||||||
|
internal LogLevel LogLevel { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class DummySendFileFeature : IHttpSendFileFeature
|
||||||
|
{
|
||||||
|
public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellation)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class TestResponseCachingPolicyProvider : IResponseCachingPolicyProvider
|
||||||
|
{
|
||||||
|
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 AllowCacheLookup(ResponseCachingContext context) => AllowCacheLookupValue;
|
||||||
|
|
||||||
|
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
|
||||||
|
{
|
||||||
|
private readonly string _baseKey;
|
||||||
|
private readonly StringValues _varyKey;
|
||||||
|
|
||||||
|
public TestResponseCachingKeyProvider(string lookupBaseKey = null, StringValues? lookupVaryKey = null)
|
||||||
|
{
|
||||||
|
_baseKey = lookupBaseKey;
|
||||||
|
if (lookupVaryKey.HasValue)
|
||||||
|
{
|
||||||
|
_varyKey = lookupVaryKey.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerable<string> CreateLookupVaryByKeys(ResponseCachingContext context)
|
||||||
|
{
|
||||||
|
foreach (var varyKey in _varyKey)
|
||||||
|
{
|
||||||
|
yield return _baseKey + varyKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public string CreateBaseKey(ResponseCachingContext context)
|
||||||
|
{
|
||||||
|
return _baseKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string CreateStorageVaryByKey(ResponseCachingContext context)
|
||||||
|
{
|
||||||
|
throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class TestResponseCache : IResponseCache
|
||||||
|
{
|
||||||
|
private readonly IDictionary<string, IResponseCacheEntry> _storage = new Dictionary<string, IResponseCacheEntry>();
|
||||||
|
public int GetCount { get; private set; }
|
||||||
|
public int SetCount { get; private set; }
|
||||||
|
|
||||||
|
public IResponseCacheEntry Get(string key)
|
||||||
|
{
|
||||||
|
GetCount++;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return _storage[key];
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<IResponseCacheEntry> 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)
|
||||||
|
{
|
||||||
|
Set(key, entry, validFor);
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class TestClock : ISystemClock
|
||||||
|
{
|
||||||
|
public DateTimeOffset UtcNow { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
<Project>
|
||||||
|
<PropertyGroup>
|
||||||
|
<VersionPrefix>2.1.1</VersionPrefix>
|
||||||
|
<VersionSuffix>rtm</VersionSuffix>
|
||||||
|
<PackageVersion Condition="'$(IsFinalBuild)' == 'true' AND '$(VersionSuffix)' == 'rtm' ">$(VersionPrefix)</PackageVersion>
|
||||||
|
<PackageVersion Condition="'$(IsFinalBuild)' == 'true' AND '$(VersionSuffix)' != 'rtm' ">$(VersionPrefix)-$(VersionSuffix)-final</PackageVersion>
|
||||||
|
<BuildNumber Condition="'$(BuildNumber)' == ''">t000</BuildNumber>
|
||||||
|
<FeatureBranchVersionPrefix Condition="'$(FeatureBranchVersionPrefix)' == ''">a-</FeatureBranchVersionPrefix>
|
||||||
|
<VersionSuffix Condition="'$(VersionSuffix)' != '' And '$(FeatureBranchVersionSuffix)' != ''">$(FeatureBranchVersionPrefix)$(VersionSuffix)-$([System.Text.RegularExpressions.Regex]::Replace('$(FeatureBranchVersionSuffix)', '[^\w-]', '-'))</VersionSuffix>
|
||||||
|
<VersionSuffix Condition="'$(VersionSuffix)' != '' And '$(BuildNumber)' != ''">$(VersionSuffix)-$(BuildNumber)</VersionSuffix>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
Loading…
Reference in New Issue