Merge remote-tracking branch 'ResponseCaching/rybrande/release21ToSrc' into rybrande/Mondo2.1

This commit is contained in:
Ryan Brandenburg 2018-11-21 15:17:21 -08:00
commit c8e271e8ac
58 changed files with 7103 additions and 0 deletions

33
src/ResponseCaching/.gitignore vendored Normal file
View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,7 @@
{
"Default": {
"rules": [
"DefaultCompositeRule"
]
}
}

View File

@ -0,0 +1,9 @@
ASP.NET Core Response Caching
========
AppVeyor: [![AppVeyor](https://ci.appveyor.com/api/projects/status/p52yj0kghdyicvwu/branch/dev?svg=true)](https://ci.appveyor.com/project/aspnetci/ResponseCaching/branch/dev)
Travis: [![Travis](https://travis-ci.org/aspnet/ResponseCaching.svg?branch=dev)](https://travis-ci.org/aspnet/ResponseCaching)
This repo hosts the ASP.NET Core middleware for response caching.
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.

View File

@ -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.

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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.

View File

@ -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>

View File

@ -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();
}
}
}

View File

@ -0,0 +1,7 @@
<Project>
<Import Project="..\Directory.Build.props" />
<ItemGroup>
<PackageReference Include="Internal.AspNetCore.Sdk" PrivateAssets="All" Version="$(InternalAspNetCoreSdkPackageVersion)" />
</ItemGroup>
</Project>

View File

@ -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; }
}
}

View File

@ -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>

View File

@ -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": []
}
]
}

View File

@ -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;
}
}
}
}

View File

@ -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; }
}
}

View File

@ -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; }
}
}

View File

@ -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);
}
}
}

View File

@ -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; }
}
}

View File

@ -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);
}
}

View File

@ -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
{
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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; }
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -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>

View File

@ -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")]

View File

@ -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>();
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}
}

View File

@ -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();
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -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();
}
}
}

View File

@ -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;
}
}
}

View File

@ -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": []
}
]
}

View File

@ -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>

View File

@ -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>

View File

@ -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);
}
}
}

View File

@ -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));
}
}
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -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());
}
}
}
}

View File

@ -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]));
}
}
}
}

View File

@ -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));
}
}
}
}

View File

@ -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; }
}
}

View File

@ -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>