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