├── samples └── OutputCachingSample │ ├── appsettings.json │ ├── appsettings.Development.json │ ├── OutputCachingSample.csproj │ ├── README.md │ ├── Gravatar.cs │ ├── Properties │ └── launchSettings.json │ ├── .vscode │ ├── tasks.json │ └── launch.json │ └── Startup.cs ├── src ├── OutputCacheFeature.cs ├── ISystemClock.cs ├── Serialization │ ├── FormatterEntrySerializerContext.cs │ └── FormatterEntry.cs ├── IOutputCacheFeature.cs ├── SystemClock.cs ├── Streams │ ├── StreamUtilities.cs │ ├── OutputCacheStream.cs │ └── SegmentWriteStream.cs ├── IOutputCacheKeyProvider.cs ├── OutputCacheOptionsSetup.cs ├── StringBuilderExtensions.cs ├── Microsoft.AspNetCore.OutputCaching.csproj ├── OutputCacheApplicationBuilderExtensions.cs ├── OutputCacheEntry.cs ├── Policies │ ├── NoStorePolicy.cs │ ├── NoLookupPolicy.cs │ ├── EnableCachePolicy.cs │ ├── TagsPolicy.cs │ ├── ExpirationPolicy.cs │ ├── LockingPolicy.cs │ ├── CompositePolicy.cs │ ├── TypedPolicy.cs │ ├── NamedPolicy.cs │ ├── VaryByHeaderPolicy.cs │ ├── VaryByQueryPolicy.cs │ ├── OutputCacheConventionBuilderExtensions.cs │ ├── PredicatePolicy.cs │ ├── DefaultPolicy.cs │ └── VaryByValuePolicy.cs ├── CacheVaryByRules.cs ├── IOutputCachePolicy.cs ├── CacheEntryHelpers.cs ├── CachedResponseBody.cs ├── IOutputCacheStore.cs ├── OutputCacheServiceCollectionExtensions.cs ├── OutputCacheAttribute.cs ├── OutputCacheContext.cs ├── OutputCacheEntryFormatter.cs ├── Memory │ └── MemoryOutputCacheStore.cs ├── DispatcherExtensions.cs ├── OutputCacheOptions.cs ├── LoggerExtensions.cs ├── External │ └── TaskToApm.cs ├── Resources.resx ├── OutputCacheKeyProvider.cs ├── OutputCachePolicyBuilder.cs └── OutputCacheMiddleware.cs ├── .github └── workflows │ ├── build.yml │ └── publish.yml ├── LICENSE ├── Microsoft.AspNetCore.OutputCaching.sln ├── README.md └── .gitignore /samples/OutputCachingSample/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /samples/OutputCachingSample/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information", 7 | "Microsoft.AspNetCore.OutputCaching": "Debug" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /samples/OutputCachingSample/OutputCachingSample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | enable 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /samples/OutputCachingSample/README.md: -------------------------------------------------------------------------------- 1 | ASP.NET Core Output Caching Sample 2 | =================================== 3 | 4 | This sample illustrates the usage of ASP.NET Core output caching middleware. The application sends a `Hello World!` message and the current time. A different cache entry is created for each variation of the query string. 5 | 6 | When running the sample, a response will be served from cache when possible and will be stored for up to 10 seconds. 7 | -------------------------------------------------------------------------------- /src/OutputCacheFeature.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | namespace Microsoft.AspNetCore.OutputCaching; 5 | 6 | internal sealed class OutputCacheFeature : IOutputCacheFeature 7 | { 8 | public OutputCacheFeature(OutputCacheContext context) 9 | { 10 | Context = context; 11 | } 12 | 13 | public OutputCacheContext Context { get; } 14 | } 15 | -------------------------------------------------------------------------------- /src/ISystemClock.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | namespace Microsoft.AspNetCore.OutputCaching; 5 | 6 | /// 7 | /// Abstracts the system clock to facilitate testing. 8 | /// 9 | internal interface ISystemClock 10 | { 11 | /// 12 | /// Retrieves the current system time in UTC. 13 | /// 14 | DateTimeOffset UtcNow { get; } 15 | } 16 | -------------------------------------------------------------------------------- /src/Serialization/FormatterEntrySerializerContext.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using System.Text.Json.Serialization; 5 | 6 | namespace Microsoft.AspNetCore.OutputCaching.Serialization; 7 | 8 | [JsonSourceGenerationOptions(WriteIndented = false)] 9 | [JsonSerializable(typeof(FormatterEntry))] 10 | internal partial class FormatterEntrySerializerContext : JsonSerializerContext 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /src/IOutputCacheFeature.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | namespace Microsoft.AspNetCore.OutputCaching; 5 | 6 | /// 7 | /// A feature for configuring additional output cache options on the HTTP response. 8 | /// 9 | public interface IOutputCacheFeature 10 | { 11 | /// 12 | /// Gets the cache context. 13 | /// 14 | OutputCacheContext Context { get; } 15 | } 16 | -------------------------------------------------------------------------------- /src/SystemClock.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | namespace Microsoft.AspNetCore.OutputCaching; 5 | 6 | /// 7 | /// Provides access to the normal system clock. 8 | /// 9 | internal sealed class SystemClock : ISystemClock 10 | { 11 | /// 12 | /// Retrieves the current system time in UTC. 13 | /// 14 | public DateTimeOffset UtcNow => DateTimeOffset.UtcNow; 15 | } 16 | -------------------------------------------------------------------------------- /src/Streams/StreamUtilities.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | namespace Microsoft.AspNetCore.OutputCaching; 5 | 6 | internal static class StreamUtilities 7 | { 8 | /// 9 | /// 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. 10 | /// 11 | // Internal for testing 12 | internal static int BodySegmentSize { get; set; } = 81920; 13 | } 14 | -------------------------------------------------------------------------------- /src/IOutputCacheKeyProvider.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | namespace Microsoft.AspNetCore.OutputCaching; 5 | 6 | internal interface IOutputCacheKeyProvider 7 | { 8 | /// 9 | /// Create a key for storing cached responses. 10 | /// 11 | /// The . 12 | /// The created key. 13 | string CreateStorageKey(OutputCacheContext context); 14 | } 15 | -------------------------------------------------------------------------------- /src/Serialization/FormatterEntry.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | namespace Microsoft.AspNetCore.OutputCaching.Serialization; 5 | internal sealed class FormatterEntry 6 | { 7 | public DateTimeOffset Created { get; set; } 8 | public int StatusCode { get; set; } 9 | public Dictionary Headers { get; set; } = default!; 10 | public List Body { get; set; } = default!; 11 | public string[] Tags { get; set; } = default!; 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | paths-ignore: 7 | - 'doc/**' 8 | - 'readme.md' 9 | 10 | pull_request: 11 | branches: [ main ] 12 | paths-ignore: 13 | - 'doc/**' 14 | - 'readme.md' 15 | 16 | jobs: 17 | build: 18 | 19 | runs-on: ubuntu-latest 20 | env: 21 | DOTNET_NOLOGO: true 22 | DOTNET_CLI_TELEMETRY_OPTOUT: 1 23 | 24 | steps: 25 | - uses: actions/checkout@v2 26 | - name: Setup .NET 6.0 27 | uses: actions/setup-dotnet@v1 28 | with: 29 | dotnet-version: 6.0.* 30 | - name: Build 31 | run: dotnet build --configuration Release 32 | -------------------------------------------------------------------------------- /src/OutputCacheOptionsSetup.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using Microsoft.Extensions.Options; 5 | 6 | namespace Microsoft.AspNetCore.OutputCaching; 7 | 8 | internal sealed class OutputCacheOptionsSetup : IConfigureOptions 9 | { 10 | private readonly IServiceProvider _services; 11 | 12 | public OutputCacheOptionsSetup(IServiceProvider services) 13 | { 14 | _services = services; 15 | } 16 | 17 | public void Configure(OutputCacheOptions options) 18 | { 19 | options.ApplicationServices = _services; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/StringBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using System.Text; 5 | 6 | namespace Microsoft.AspNetCore.OutputCaching; 7 | 8 | internal static class StringBuilderExtensions 9 | { 10 | internal static StringBuilder AppendUpperInvariant(this StringBuilder builder, string? value) 11 | { 12 | if (!string.IsNullOrEmpty(value)) 13 | { 14 | builder.EnsureCapacity(builder.Length + value.Length); 15 | for (var i = 0; i < value.Length; i++) 16 | { 17 | builder.Append(char.ToUpperInvariant(value[i])); 18 | } 19 | } 20 | 21 | return builder; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /samples/OutputCachingSample/Gravatar.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | public static class Gravatar 5 | { 6 | public static async Task WriteGravatar(HttpContext context) 7 | { 8 | const string type = "monsterid"; // identicon, monsterid, wavatar 9 | const int size = 200; 10 | var hash = Guid.NewGuid().ToString("n"); 11 | 12 | context.Response.StatusCode = 200; 13 | context.Response.ContentType = "text/html"; 14 | await context.Response.WriteAsync($""); 15 | await context.Response.WriteAsync($"
Generated at {DateTime.Now:hh:mm:ss.ff}
"); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /samples/OutputCachingSample/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "iisSettings": { 3 | "windowsAuthentication": false, 4 | "anonymousAuthentication": true, 5 | "iisExpress": { 6 | "applicationUrl": "http://localhost:54270/", 7 | "sslPort": 44398 8 | } 9 | }, 10 | "profiles": { 11 | "OutputCachingSample": { 12 | "commandName": "Project", 13 | "launchBrowser": true, 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | }, 17 | "applicationUrl": "https://localhost:5001;http://localhost:5000" 18 | }, 19 | "IIS Express": { 20 | "commandName": "IISExpress", 21 | "launchBrowser": true, 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.OutputCaching.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | true 6 | true 7 | enable 8 | enable 9 | 10 | Preview.OutputCaching 11 | Output Caching - .NET 7 Preview 12 | ASP.NET Core middleware for caching HTTP responses on the server. 13 | README.md 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/OutputCacheApplicationBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using Microsoft.AspNetCore.OutputCaching; 5 | 6 | namespace Microsoft.AspNetCore.Builder; 7 | 8 | /// 9 | /// Extension methods for adding the to an application. 10 | /// 11 | public static class OutputCacheApplicationBuilderExtensions 12 | { 13 | /// 14 | /// Adds the for caching HTTP responses. 15 | /// 16 | /// The . 17 | public static IApplicationBuilder UseOutputCache(this IApplicationBuilder app) 18 | { 19 | ArgumentNullException.ThrowIfNull(app); 20 | 21 | return app.UseMiddleware(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | env: 13 | DOTNET_NOLOGO: true 14 | DOTNET_CLI_TELEMETRY_OPTOUT: 1 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Setup dotnet 6.0 19 | uses: actions/setup-dotnet@v1 20 | with: 21 | dotnet-version: 6.0.* 22 | - name: Build 23 | run: dotnet build --configuration Release 24 | - name: Pack with dotnet 25 | run: | 26 | arrTag=(${GITHUB_REF//\// }) 27 | VERSION="${arrTag[2]}" 28 | VERSION="${VERSION#?}" 29 | echo "$VERSION" 30 | dotnet pack --output artifacts --configuration Release -p:Version=$VERSION 31 | - name: Push with dotnet 32 | run: dotnet nuget push "artifacts/*.nupkg" --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json --skip-duplicate 33 | -------------------------------------------------------------------------------- /src/OutputCacheEntry.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using Microsoft.AspNetCore.Http; 5 | 6 | namespace Microsoft.AspNetCore.OutputCaching; 7 | 8 | internal sealed class OutputCacheEntry 9 | { 10 | /// 11 | /// Gets the created date and time of the cache entry. 12 | /// 13 | public DateTimeOffset Created { get; set; } 14 | 15 | /// 16 | /// Gets the status code of the cache entry. 17 | /// 18 | public int StatusCode { get; set; } 19 | 20 | /// 21 | /// Gets the headers of the cache entry. 22 | /// 23 | public HeaderDictionary Headers { get; set; } = default!; 24 | 25 | /// 26 | /// Gets the body of the cache entry. 27 | /// 28 | public CachedResponseBody Body { get; set; } = default!; 29 | 30 | /// 31 | /// Gets the tags of the cache entry. 32 | /// 33 | public string[] Tags { get; set; } = Array.Empty(); 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) .NET Foundation and Contributors 4 | 5 | All rights reserved. 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /src/Policies/NoStorePolicy.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | namespace Microsoft.AspNetCore.OutputCaching; 5 | 6 | /// 7 | /// A policy that prevents the response from being cached. 8 | /// 9 | internal sealed class NoStorePolicy : IOutputCachePolicy 10 | { 11 | public static NoStorePolicy Instance = new(); 12 | 13 | private NoStorePolicy() 14 | { 15 | } 16 | 17 | /// 18 | ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context, CancellationToken cancellationToken) 19 | { 20 | context.AllowCacheStorage = false; 21 | 22 | return ValueTask.CompletedTask; 23 | } 24 | 25 | /// 26 | ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellationToken) 27 | { 28 | return ValueTask.CompletedTask; 29 | } 30 | 31 | /// 32 | ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken) 33 | { 34 | return ValueTask.CompletedTask; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Policies/NoLookupPolicy.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | namespace Microsoft.AspNetCore.OutputCaching; 5 | 6 | /// 7 | /// A policy that prevents the response from being served from cache. 8 | /// 9 | internal sealed class NoLookupPolicy : IOutputCachePolicy 10 | { 11 | public static NoLookupPolicy Instance = new(); 12 | 13 | private NoLookupPolicy() 14 | { 15 | } 16 | 17 | /// 18 | ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context, CancellationToken cancellationToken) 19 | { 20 | context.AllowCacheLookup = false; 21 | 22 | return ValueTask.CompletedTask; 23 | } 24 | 25 | /// 26 | ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellationToken) 27 | { 28 | return ValueTask.CompletedTask; 29 | } 30 | 31 | /// 32 | ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken) 33 | { 34 | return ValueTask.CompletedTask; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/CacheVaryByRules.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using System.Linq; 5 | using Microsoft.Extensions.Primitives; 6 | 7 | namespace Microsoft.AspNetCore.OutputCaching; 8 | 9 | /// 10 | /// Represents vary-by rules. 11 | /// 12 | public sealed class CacheVaryByRules 13 | { 14 | private Dictionary? _varyByCustom; 15 | 16 | internal bool HasVaryByCustom => _varyByCustom != null && _varyByCustom.Any(); 17 | 18 | /// 19 | /// Gets a dictionary of key-pair values to vary the cache by. 20 | /// 21 | public IDictionary VaryByCustom => _varyByCustom ??= new(); 22 | 23 | /// 24 | /// Gets or sets the list of headers to vary by. 25 | /// 26 | public StringValues Headers { get; set; } 27 | 28 | /// 29 | /// Gets or sets the list of query string keys to vary by. 30 | /// 31 | public StringValues QueryKeys { get; set; } 32 | 33 | /// 34 | /// Gets or sets a prefix to vary by. 35 | /// 36 | public StringValues VaryByPrefix { get; set; } 37 | } 38 | -------------------------------------------------------------------------------- /src/Policies/EnableCachePolicy.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | namespace Microsoft.AspNetCore.OutputCaching; 5 | 6 | /// 7 | /// A policy that enables caching 8 | /// 9 | internal sealed class EnableCachePolicy : IOutputCachePolicy 10 | { 11 | public static readonly EnableCachePolicy Enabled = new(); 12 | public static readonly EnableCachePolicy Disabled = new(); 13 | 14 | private EnableCachePolicy() 15 | { 16 | } 17 | 18 | /// 19 | ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken) 20 | { 21 | context.EnableOutputCaching = this == Enabled; 22 | 23 | return ValueTask.CompletedTask; 24 | } 25 | 26 | /// 27 | ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context, CancellationToken cancellationToken) 28 | { 29 | return ValueTask.CompletedTask; 30 | } 31 | 32 | /// 33 | ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellationToken) 34 | { 35 | return ValueTask.CompletedTask; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /samples/OutputCachingSample/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/OutputCachingSample.csproj", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/OutputCachingSample.csproj", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "--project", 36 | "${workspaceFolder}/OutputCachingSample.csproj" 37 | ], 38 | "problemMatcher": "$msCompile" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /src/Policies/TagsPolicy.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | namespace Microsoft.AspNetCore.OutputCaching; 5 | 6 | /// 7 | /// A policy that defines custom tags on the cache entry. 8 | /// 9 | internal sealed class TagsPolicy : IOutputCachePolicy 10 | { 11 | private readonly string[] _tags; 12 | 13 | /// 14 | /// Creates a new instance. 15 | /// 16 | /// The tags. 17 | public TagsPolicy(params string[] tags) 18 | { 19 | _tags = tags; 20 | } 21 | 22 | /// 23 | ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken) 24 | { 25 | foreach (var tag in _tags) 26 | { 27 | context.Tags.Add(tag); 28 | } 29 | 30 | return ValueTask.CompletedTask; 31 | } 32 | 33 | /// 34 | ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellationToken) 35 | { 36 | return ValueTask.CompletedTask; 37 | } 38 | 39 | /// 40 | ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context, CancellationToken cancellationToken) 41 | { 42 | return ValueTask.CompletedTask; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Policies/ExpirationPolicy.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | namespace Microsoft.AspNetCore.OutputCaching; 5 | 6 | /// 7 | /// A policy that defines a custom expiration duration. 8 | /// 9 | internal sealed class ExpirationPolicy : IOutputCachePolicy 10 | { 11 | private readonly TimeSpan _expiration; 12 | 13 | /// 14 | /// Creates a new instance. 15 | /// 16 | /// The expiration duration. 17 | public ExpirationPolicy(TimeSpan expiration) 18 | { 19 | _expiration = expiration; 20 | } 21 | 22 | /// 23 | ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken) 24 | { 25 | context.ResponseExpirationTimeSpan = _expiration; 26 | 27 | return ValueTask.CompletedTask; 28 | } 29 | 30 | /// 31 | ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellationToken) 32 | { 33 | return ValueTask.CompletedTask; 34 | } 35 | 36 | /// 37 | ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context, CancellationToken cancellationToken) 38 | { 39 | return ValueTask.CompletedTask; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/IOutputCachePolicy.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | namespace Microsoft.AspNetCore.OutputCaching; 5 | 6 | /// 7 | /// An implementation of this interface can update how the current request is cached. 8 | /// 9 | public interface IOutputCachePolicy 10 | { 11 | /// 12 | /// Updates the before the cache middleware is invoked. 13 | /// At that point the cache middleware can still be enabled or disabled for the request. 14 | /// 15 | /// The current request's cache context. 16 | ValueTask CacheRequestAsync(OutputCacheContext context, CancellationToken cancellation); 17 | 18 | /// 19 | /// Updates the before the cached response is used. 20 | /// At that point the freshness of the cached response can be updated. 21 | /// 22 | /// The current request's cache context. 23 | ValueTask ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellation); 24 | 25 | /// 26 | /// Updates the before the response is served and can be cached. 27 | /// At that point cacheability of the response can be updated. 28 | /// 29 | ValueTask ServeResponseAsync(OutputCacheContext context, CancellationToken cancellation); 30 | } 31 | -------------------------------------------------------------------------------- /samples/OutputCachingSample/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | // Use IntelliSense to find out which attributes exist for C# debugging 6 | // Use hover for the description of the existing attributes 7 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 8 | "name": ".NET Core Launch (web)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/bin/Debug/net7.0/OutputCachingSample.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}", 16 | "stopAtEntry": false, 17 | // Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser 18 | "serverReadyAction": { 19 | "action": "openExternally", 20 | "pattern": "\\bNow listening on:\\s+(https?://\\S+)" 21 | }, 22 | "env": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | }, 25 | "sourceFileMap": { 26 | "/Views": "${workspaceFolder}/Views" 27 | } 28 | }, 29 | { 30 | "name": ".NET Core Attach", 31 | "type": "coreclr", 32 | "request": "attach" 33 | } 34 | ] 35 | } -------------------------------------------------------------------------------- /src/Policies/LockingPolicy.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | namespace Microsoft.AspNetCore.OutputCaching; 5 | 6 | /// 7 | /// A policy that changes the locking behavior. 8 | /// 9 | internal sealed class LockingPolicy : IOutputCachePolicy 10 | { 11 | private readonly bool _lockResponse; 12 | 13 | private LockingPolicy(bool lockResponse) 14 | { 15 | _lockResponse = lockResponse; 16 | } 17 | 18 | /// 19 | /// A policy that enables locking. 20 | /// 21 | public static readonly LockingPolicy Enabled = new(true); 22 | 23 | /// 24 | /// A policy that disables locking. 25 | /// 26 | public static readonly LockingPolicy Disabled = new(false); 27 | 28 | /// 29 | ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken) 30 | { 31 | context.AllowLocking = _lockResponse; 32 | 33 | return ValueTask.CompletedTask; 34 | } 35 | 36 | /// 37 | ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellationToken) 38 | { 39 | return ValueTask.CompletedTask; 40 | } 41 | 42 | /// 43 | ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context, CancellationToken cancellationToken) 44 | { 45 | return ValueTask.CompletedTask; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Policies/CompositePolicy.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | namespace Microsoft.AspNetCore.OutputCaching.Policies; 5 | 6 | /// 7 | /// A composite policy. 8 | /// 9 | internal sealed class CompositePolicy : IOutputCachePolicy 10 | { 11 | private readonly IOutputCachePolicy[] _policies; 12 | 13 | /// 14 | /// Creates a new instance of 15 | /// 16 | /// The policies to include. 17 | public CompositePolicy(params IOutputCachePolicy[] policies) 18 | { 19 | _policies = policies; 20 | } 21 | 22 | /// 23 | async ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken) 24 | { 25 | foreach (var policy in _policies) 26 | { 27 | await policy.CacheRequestAsync(context, cancellationToken); 28 | } 29 | } 30 | 31 | /// 32 | async ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellationToken) 33 | { 34 | foreach (var policy in _policies) 35 | { 36 | await policy.ServeFromCacheAsync(context, cancellationToken); 37 | } 38 | } 39 | 40 | /// 41 | async ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context, CancellationToken cancellationToken) 42 | { 43 | foreach (var policy in _policies) 44 | { 45 | await policy.ServeResponseAsync(context, cancellationToken); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/CacheEntryHelpers.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using Microsoft.Extensions.Primitives; 5 | 6 | namespace Microsoft.AspNetCore.OutputCaching; 7 | 8 | internal static class CacheEntryHelpers 9 | { 10 | internal static long EstimateCachedResponseSize(OutputCacheEntry cachedResponse) 11 | { 12 | if (cachedResponse == null) 13 | { 14 | return 0L; 15 | } 16 | 17 | checked 18 | { 19 | // StatusCode 20 | long size = sizeof(int); 21 | 22 | // Headers 23 | if (cachedResponse.Headers != null) 24 | { 25 | foreach (var item in cachedResponse.Headers) 26 | { 27 | size += (item.Key.Length * sizeof(char)) + EstimateStringValuesSize(item.Value); 28 | } 29 | } 30 | 31 | // Body 32 | if (cachedResponse.Body != null) 33 | { 34 | size += cachedResponse.Body.Length; 35 | } 36 | 37 | return size; 38 | } 39 | } 40 | 41 | internal static long EstimateStringValuesSize(StringValues stringValues) 42 | { 43 | checked 44 | { 45 | var size = 0L; 46 | 47 | for (var i = 0; i < stringValues.Count; i++) 48 | { 49 | var stringValue = stringValues[i]; 50 | if (!string.IsNullOrEmpty(stringValue)) 51 | { 52 | size += stringValue.Length * sizeof(char); 53 | } 54 | } 55 | 56 | return size; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/CachedResponseBody.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using System.IO.Pipelines; 5 | 6 | namespace Microsoft.AspNetCore.OutputCaching; 7 | 8 | /// 9 | /// Represents a cached response body. 10 | /// 11 | internal sealed class CachedResponseBody 12 | { 13 | /// 14 | /// Creates a new instance. 15 | /// 16 | /// The segments. 17 | /// The length. 18 | public CachedResponseBody(List segments, long length) 19 | { 20 | ArgumentNullException.ThrowIfNull(segments); 21 | 22 | Segments = segments; 23 | Length = length; 24 | } 25 | 26 | /// 27 | /// Gets the segments of the body. 28 | /// 29 | public List Segments { get; } 30 | 31 | /// 32 | /// Gets the length of the body. 33 | /// 34 | public long Length { get; } 35 | 36 | /// 37 | /// Copies the body to a . 38 | /// 39 | /// The destination 40 | /// The cancellation token. 41 | /// 42 | public async Task CopyToAsync(PipeWriter destination, CancellationToken cancellationToken) 43 | { 44 | ArgumentNullException.ThrowIfNull(destination); 45 | 46 | foreach (var segment in Segments) 47 | { 48 | cancellationToken.ThrowIfCancellationRequested(); 49 | 50 | await destination.WriteAsync(segment, cancellationToken); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Microsoft.AspNetCore.OutputCaching.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.3.32505.426 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.OutputCaching", "src\Microsoft.AspNetCore.OutputCaching.csproj", "{821DCBF8-B767-4933-B6EF-3F576E3560DB}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OutputCachingSample", "samples\OutputCachingSample\OutputCachingSample.csproj", "{EBCCE70C-12E3-46B4-8C9E-6E266B27203E}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {821DCBF8-B767-4933-B6EF-3F576E3560DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {821DCBF8-B767-4933-B6EF-3F576E3560DB}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {821DCBF8-B767-4933-B6EF-3F576E3560DB}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {821DCBF8-B767-4933-B6EF-3F576E3560DB}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {EBCCE70C-12E3-46B4-8C9E-6E266B27203E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {EBCCE70C-12E3-46B4-8C9E-6E266B27203E}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {EBCCE70C-12E3-46B4-8C9E-6E266B27203E}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {EBCCE70C-12E3-46B4-8C9E-6E266B27203E}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {45B6341E-2478-4EB9-811D-F811BBA39F39} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /src/IOutputCacheStore.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | namespace Microsoft.AspNetCore.OutputCaching; 5 | 6 | /// 7 | /// Represents a store for cached responses. 8 | /// 9 | public interface IOutputCacheStore 10 | { 11 | /// 12 | /// Evicts cached responses by tag. 13 | /// 14 | /// The tag to evict. 15 | /// Indicates that the operation should be cancelled. 16 | ValueTask EvictByTagAsync(string tag, CancellationToken cancellationToken); 17 | 18 | /// 19 | /// Gets the cached response for the given key, if it exists. 20 | /// If no cached response exists for the given key, null is returned. 21 | /// 22 | /// The cache key to look up. 23 | /// Indicates that the operation should be cancelled. 24 | /// The response cache entry if it exists; otherwise null. 25 | ValueTask GetAsync(string key, CancellationToken cancellationToken); 26 | 27 | /// 28 | /// Stores the given response in the response cache. 29 | /// 30 | /// The cache key to store the response under. 31 | /// The response cache entry to store. 32 | /// The tags associated with the cache entry to store. 33 | /// The amount of time the entry will be kept in the cache before expiring, relative to now. 34 | /// Indicates that the operation should be cancelled. 35 | ValueTask SetAsync(string key, byte[] value, string[]? tags, TimeSpan validFor, CancellationToken cancellationToken); 36 | } 37 | -------------------------------------------------------------------------------- /src/Policies/TypedPolicy.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using System.Diagnostics.CodeAnalysis; 5 | using Microsoft.Extensions.DependencyInjection; 6 | 7 | namespace Microsoft.AspNetCore.OutputCaching.Policies; 8 | 9 | /// 10 | /// A type base policy. 11 | /// 12 | internal sealed class TypedPolicy : IOutputCachePolicy 13 | { 14 | private IOutputCachePolicy? _instance; 15 | 16 | [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] 17 | private readonly Type _policyType; 18 | 19 | /// 20 | /// Creates a new instance of 21 | /// 22 | /// The type of policy. 23 | public TypedPolicy([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type policyType) 24 | { 25 | ArgumentNullException.ThrowIfNull(policyType); 26 | 27 | _policyType = policyType; 28 | } 29 | 30 | private IOutputCachePolicy? CreatePolicy(OutputCacheContext context) 31 | { 32 | return _instance ??= ActivatorUtilities.CreateInstance(context.Options.ApplicationServices, _policyType) as IOutputCachePolicy; 33 | } 34 | 35 | /// 36 | ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken) 37 | { 38 | return CreatePolicy(context)?.CacheRequestAsync(context, cancellationToken) ?? ValueTask.CompletedTask; 39 | } 40 | 41 | /// 42 | ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellationToken) 43 | { 44 | return CreatePolicy(context)?.ServeFromCacheAsync(context, cancellationToken) ?? ValueTask.CompletedTask; 45 | } 46 | 47 | /// 48 | ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context, CancellationToken cancellationToken) 49 | { 50 | return CreatePolicy(context)?.ServeResponseAsync(context, cancellationToken) ?? ValueTask.CompletedTask; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Policies/NamedPolicy.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | namespace Microsoft.AspNetCore.OutputCaching; 5 | 6 | /// 7 | /// A named policy. 8 | /// 9 | internal sealed class NamedPolicy : IOutputCachePolicy 10 | { 11 | private readonly string _policyName; 12 | 13 | /// 14 | /// Create a new instance. 15 | /// 16 | /// The name of the profile. 17 | public NamedPolicy(string policyName) 18 | { 19 | _policyName = policyName; 20 | } 21 | 22 | /// 23 | ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context, CancellationToken cancellationToken) 24 | { 25 | var policy = GetProfilePolicy(context); 26 | 27 | if (policy == null) 28 | { 29 | return ValueTask.CompletedTask; 30 | } 31 | 32 | return policy.ServeResponseAsync(context, cancellationToken); 33 | } 34 | 35 | /// 36 | ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellationToken) 37 | { 38 | var policy = GetProfilePolicy(context); 39 | 40 | if (policy == null) 41 | { 42 | return ValueTask.CompletedTask; 43 | } 44 | 45 | return policy.ServeFromCacheAsync(context, cancellationToken); 46 | } 47 | 48 | /// 49 | ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken) 50 | { 51 | var policy = GetProfilePolicy(context); 52 | 53 | if (policy == null) 54 | { 55 | return ValueTask.CompletedTask; 56 | } 57 | 58 | return policy.CacheRequestAsync(context, cancellationToken); ; 59 | } 60 | 61 | internal IOutputCachePolicy? GetProfilePolicy(OutputCacheContext context) 62 | { 63 | var policies = context.Options.NamedPolicies; 64 | 65 | return policies != null && policies.TryGetValue(_policyName, out var cacheProfile) 66 | ? cacheProfile 67 | : null; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Policies/VaryByHeaderPolicy.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using Microsoft.Extensions.Primitives; 5 | 6 | namespace Microsoft.AspNetCore.OutputCaching; 7 | 8 | /// 9 | /// When applied, the cached content will be different for every value of the provided headers. 10 | /// 11 | internal sealed class VaryByHeaderPolicy : IOutputCachePolicy 12 | { 13 | private readonly StringValues _headers; 14 | 15 | /// 16 | /// Creates a policy that doesn't vary the cached content based on headers. 17 | /// 18 | public VaryByHeaderPolicy() 19 | { 20 | } 21 | 22 | /// 23 | /// Creates a policy that varies the cached content based on the specified header. 24 | /// 25 | public VaryByHeaderPolicy(string header) 26 | { 27 | ArgumentNullException.ThrowIfNull(header); 28 | 29 | _headers = header; 30 | } 31 | 32 | /// 33 | /// Creates a policy that varies the cached content based on the specified query string keys. 34 | /// 35 | public VaryByHeaderPolicy(params string[] headers) 36 | { 37 | ArgumentNullException.ThrowIfNull(headers); 38 | 39 | _headers = headers; 40 | } 41 | 42 | /// 43 | ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken) 44 | { 45 | // No vary by header? 46 | if (_headers.Count == 0) 47 | { 48 | context.CacheVaryByRules.Headers = _headers; 49 | return ValueTask.CompletedTask; 50 | } 51 | 52 | context.CacheVaryByRules.Headers = StringValues.Concat(context.CacheVaryByRules.Headers, _headers); 53 | 54 | return ValueTask.CompletedTask; 55 | } 56 | 57 | /// 58 | ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellationToken) 59 | { 60 | return ValueTask.CompletedTask; 61 | } 62 | 63 | /// 64 | ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context, CancellationToken cancellationToken) 65 | { 66 | return ValueTask.CompletedTask; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/OutputCacheServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using Microsoft.AspNetCore.OutputCaching; 5 | using Microsoft.AspNetCore.OutputCaching.Memory; 6 | using Microsoft.Extensions.Caching.Memory; 7 | using Microsoft.Extensions.DependencyInjection.Extensions; 8 | using Microsoft.Extensions.ObjectPool; 9 | using Microsoft.Extensions.Options; 10 | 11 | namespace Microsoft.Extensions.DependencyInjection; 12 | 13 | /// 14 | /// Extension methods for the OutputCaching middleware. 15 | /// 16 | public static class OutputCacheServiceCollectionExtensions 17 | { 18 | /// 19 | /// Add output caching services. 20 | /// 21 | /// The for adding services. 22 | /// 23 | public static IServiceCollection AddOutputCache(this IServiceCollection services) 24 | { 25 | ArgumentNullException.ThrowIfNull(services); 26 | 27 | services.AddTransient, OutputCacheOptionsSetup>(); 28 | 29 | services.TryAddSingleton(); 30 | 31 | services.TryAddSingleton(sp => 32 | { 33 | var outputCacheOptions = sp.GetRequiredService>(); 34 | return new MemoryOutputCacheStore(new MemoryCache(new MemoryCacheOptions 35 | { 36 | SizeLimit = outputCacheOptions.Value.SizeLimit 37 | })); 38 | }); 39 | return services; 40 | } 41 | 42 | /// 43 | /// Add output caching services and configure the related options. 44 | /// 45 | /// The for adding services. 46 | /// A delegate to configure the . 47 | /// 48 | public static IServiceCollection AddOutputCache(this IServiceCollection services, Action configureOptions) 49 | { 50 | ArgumentNullException.ThrowIfNull(services); 51 | ArgumentNullException.ThrowIfNull(configureOptions); 52 | 53 | services.Configure(configureOptions); 54 | services.AddOutputCache(); 55 | 56 | return services; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Policies/VaryByQueryPolicy.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using Microsoft.Extensions.Primitives; 5 | 6 | namespace Microsoft.AspNetCore.OutputCaching; 7 | 8 | /// 9 | /// When applied, the cached content will be different for every value of the provided query string keys. 10 | /// It also disables the default behavior which is to vary on all query string keys. 11 | /// 12 | internal sealed class VaryByQueryPolicy : IOutputCachePolicy 13 | { 14 | private readonly StringValues _queryKeys; 15 | 16 | /// 17 | /// Creates a policy that doesn't vary the cached content based on query string. 18 | /// 19 | public VaryByQueryPolicy() 20 | { 21 | } 22 | 23 | /// 24 | /// Creates a policy that varies the cached content based on the specified query string key. 25 | /// 26 | public VaryByQueryPolicy(string queryKey) 27 | { 28 | _queryKeys = queryKey; 29 | } 30 | 31 | /// 32 | /// Creates a policy that varies the cached content based on the specified query string keys. 33 | /// 34 | public VaryByQueryPolicy(params string[] queryKeys) 35 | { 36 | _queryKeys = queryKeys; 37 | } 38 | 39 | /// 40 | ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken) 41 | { 42 | // No vary by query? 43 | if (_queryKeys.Count == 0) 44 | { 45 | context.CacheVaryByRules.QueryKeys = _queryKeys; 46 | return ValueTask.CompletedTask; 47 | } 48 | 49 | // If the current key is "*" (default) replace it 50 | if (context.CacheVaryByRules.QueryKeys.Count == 1 && string.Equals(context.CacheVaryByRules.QueryKeys[0], "*", StringComparison.Ordinal)) 51 | { 52 | context.CacheVaryByRules.QueryKeys = _queryKeys; 53 | return ValueTask.CompletedTask; 54 | } 55 | 56 | context.CacheVaryByRules.QueryKeys = StringValues.Concat(context.CacheVaryByRules.QueryKeys, _queryKeys); 57 | 58 | return ValueTask.CompletedTask; 59 | } 60 | 61 | /// 62 | ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellationToken) 63 | { 64 | return ValueTask.CompletedTask; 65 | } 66 | 67 | /// 68 | ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context, CancellationToken cancellationToken) 69 | { 70 | return ValueTask.CompletedTask; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/OutputCacheAttribute.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | namespace Microsoft.AspNetCore.OutputCaching; 5 | 6 | /// 7 | /// Specifies the parameters necessary for setting appropriate headers in output caching. 8 | /// 9 | /// 10 | /// This attribute requires the output cache middleware. 11 | /// 12 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] 13 | public sealed class OutputCacheAttribute : Attribute 14 | { 15 | // A nullable-int cannot be used as an Attribute parameter. 16 | // Hence this nullable-int is present to back the Duration property. 17 | // The same goes for nullable-ResponseCacheLocation and nullable-bool. 18 | private int? _duration; 19 | private bool? _noCache; 20 | 21 | private IOutputCachePolicy? _builtPolicy; 22 | 23 | /// 24 | /// Gets or sets the duration in seconds for which the response is cached. 25 | /// 26 | public int Duration 27 | { 28 | get => _duration ?? 0; 29 | init => _duration = value; 30 | } 31 | 32 | /// 33 | /// Gets or sets the value which determines whether the reponse should be cached or not. 34 | /// When set to , the response won't be cached. 35 | /// 36 | public bool NoStore 37 | { 38 | get => _noCache ?? false; 39 | init => _noCache = value; 40 | } 41 | 42 | /// 43 | /// Gets or sets the query keys to vary by. 44 | /// 45 | public string[]? VaryByQueryKeys { get; init; } 46 | 47 | /// 48 | /// Gets or sets the headers to vary by. 49 | /// 50 | public string[]? VaryByHeaders { get; init; } 51 | 52 | /// 53 | /// Gets or sets the value of the cache policy name. 54 | /// 55 | public string? PolicyName { get; init; } 56 | 57 | internal IOutputCachePolicy BuildPolicy() 58 | { 59 | if (_builtPolicy != null) 60 | { 61 | return _builtPolicy; 62 | } 63 | 64 | var builder = new OutputCachePolicyBuilder(); 65 | 66 | if (PolicyName != null) 67 | { 68 | builder.AddPolicy(new NamedPolicy(PolicyName)); 69 | } 70 | 71 | if (_noCache != null && _noCache.Value) 72 | { 73 | builder.NoCache(); 74 | } 75 | 76 | if (VaryByQueryKeys != null) 77 | { 78 | builder.VaryByQuery(VaryByQueryKeys); 79 | } 80 | 81 | if (_duration != null) 82 | { 83 | builder.Expire(TimeSpan.FromSeconds(_duration.Value)); 84 | } 85 | 86 | return _builtPolicy = builder.Build(); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Policies/OutputCacheConventionBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using Microsoft.AspNetCore.Builder; 5 | using Microsoft.AspNetCore.OutputCaching; 6 | 7 | namespace Microsoft.Extensions.DependencyInjection; 8 | 9 | /// 10 | /// A set of endpoint extension methods. 11 | /// 12 | public static class OutputCacheConventionBuilderExtensions 13 | { 14 | /// 15 | /// Marks an endpoint to be cached with the default policy. 16 | /// 17 | public static TBuilder CacheOutput(this TBuilder builder) where TBuilder : IEndpointConventionBuilder 18 | { 19 | ArgumentNullException.ThrowIfNull(builder); 20 | 21 | // Enable caching if this method is invoked on an endpoint, extra policies can disable it 22 | 23 | builder.Add(endpointBuilder => 24 | { 25 | endpointBuilder.Metadata.Add(DefaultPolicy.Instance); 26 | }); 27 | return builder; 28 | } 29 | 30 | /// 31 | /// Marks an endpoint to be cached with the specified policy. 32 | /// 33 | public static TBuilder CacheOutput(this TBuilder builder, IOutputCachePolicy policy) where TBuilder : IEndpointConventionBuilder 34 | { 35 | ArgumentNullException.ThrowIfNull(builder); 36 | 37 | // Enable caching if this method is invoked on an endpoint, extra policies can disable it 38 | 39 | builder.Add(endpointBuilder => 40 | { 41 | endpointBuilder.Metadata.Add(policy); 42 | }); 43 | return builder; 44 | } 45 | 46 | /// 47 | /// Marks an endpoint to be cached using the specified policy builder. 48 | /// 49 | public static TBuilder CacheOutput(this TBuilder builder, Action policy) where TBuilder : IEndpointConventionBuilder 50 | { 51 | ArgumentNullException.ThrowIfNull(builder); 52 | 53 | var outputCachePolicyBuilder = new OutputCachePolicyBuilder(); 54 | 55 | policy?.Invoke(outputCachePolicyBuilder); 56 | 57 | builder.Add(endpointBuilder => 58 | { 59 | endpointBuilder.Metadata.Add(outputCachePolicyBuilder.Build()); 60 | }); 61 | 62 | return builder; 63 | } 64 | 65 | /// 66 | /// Marks an endpoint to be cached using a named policy. 67 | /// 68 | public static TBuilder CacheOutput(this TBuilder builder, string policyName) where TBuilder : IEndpointConventionBuilder 69 | { 70 | ArgumentNullException.ThrowIfNull(builder); 71 | 72 | var policy = new NamedPolicy(policyName); 73 | 74 | builder.Add(endpointBuilder => 75 | { 76 | endpointBuilder.Metadata.Add(policy); 77 | }); 78 | 79 | return builder; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/OutputCacheContext.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.Extensions.Logging; 6 | 7 | namespace Microsoft.AspNetCore.OutputCaching; 8 | 9 | /// 10 | /// Represent the current cache context for the request. 11 | /// 12 | public sealed class OutputCacheContext 13 | { 14 | internal OutputCacheContext(HttpContext httpContext, IOutputCacheStore store, OutputCacheOptions options, ILogger logger) 15 | { 16 | HttpContext = httpContext; 17 | Logger = logger; 18 | Store = store; 19 | Options = options; 20 | } 21 | 22 | /// 23 | /// Determines whether the output caching logic should be configured for the incoming HTTP request. 24 | /// 25 | public bool EnableOutputCaching { get; set; } 26 | 27 | /// 28 | /// Determines whether a cache lookup is allowed for the incoming HTTP request. 29 | /// 30 | public bool AllowCacheLookup { get; set; } 31 | 32 | /// 33 | /// Determines whether storage of the response is allowed for the incoming HTTP request. 34 | /// 35 | public bool AllowCacheStorage { get; set; } 36 | 37 | /// 38 | /// Determines whether the request should be locked. 39 | /// 40 | public bool AllowLocking { get; set; } 41 | 42 | /// 43 | /// Gets the . 44 | /// 45 | public HttpContext HttpContext { get; } 46 | 47 | /// 48 | /// Gets the response time. 49 | /// 50 | public DateTimeOffset? ResponseTime { get; internal set; } 51 | 52 | /// 53 | /// Gets the instance. 54 | /// 55 | public CacheVaryByRules CacheVaryByRules { get; set; } = new(); 56 | 57 | /// 58 | /// Gets the tags of the cached response. 59 | /// 60 | public HashSet Tags { get; } = new(); 61 | 62 | /// 63 | /// Gets or sets the amount of time the response should be cached for. 64 | /// 65 | public TimeSpan? ResponseExpirationTimeSpan { get; set; } 66 | 67 | internal string CacheKey { get; set; } = default!; 68 | 69 | internal TimeSpan CachedResponseValidFor { get; set; } 70 | 71 | internal bool IsCacheEntryFresh { get; set; } 72 | 73 | internal TimeSpan CachedEntryAge { get; set; } 74 | 75 | internal OutputCacheEntry CachedResponse { get; set; } = default!; 76 | 77 | internal bool ResponseStarted { get; set; } 78 | 79 | internal Stream OriginalResponseStream { get; set; } = default!; 80 | 81 | internal OutputCacheStream OutputCacheStream { get; set; } = default!; 82 | internal ILogger Logger { get; } 83 | internal OutputCacheOptions Options { get; } 84 | internal IOutputCacheStore Store { get; } 85 | } 86 | -------------------------------------------------------------------------------- /src/OutputCacheEntryFormatter.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using System.Linq; 5 | using System.Text.Json; 6 | using Microsoft.AspNetCore.OutputCaching.Serialization; 7 | 8 | namespace Microsoft.AspNetCore.OutputCaching; 9 | /// 10 | /// Formats instance to match structures supported by the implementations. 11 | /// 12 | internal static class OutputCacheEntryFormatter 13 | { 14 | public static async ValueTask GetAsync(string key, IOutputCacheStore store, CancellationToken cancellationToken) 15 | { 16 | ArgumentNullException.ThrowIfNull(key); 17 | 18 | var content = await store.GetAsync(key, cancellationToken); 19 | 20 | if (content == null) 21 | { 22 | return null; 23 | } 24 | 25 | var formatter = JsonSerializer.Deserialize(content, FormatterEntrySerializerContext.Default.FormatterEntry); 26 | 27 | if (formatter == null) 28 | { 29 | return null; 30 | } 31 | 32 | var outputCacheEntry = new OutputCacheEntry 33 | { 34 | StatusCode = formatter.StatusCode, 35 | Created = formatter.Created, 36 | Tags = formatter.Tags, 37 | Headers = new(), 38 | Body = new CachedResponseBody(formatter.Body, formatter.Body.Sum(x => x.Length)) 39 | }; 40 | 41 | if (formatter.Headers != null) 42 | { 43 | foreach (var header in formatter.Headers) 44 | { 45 | outputCacheEntry.Headers.TryAdd(header.Key, header.Value); 46 | } 47 | } 48 | 49 | return outputCacheEntry; 50 | } 51 | 52 | public static async ValueTask StoreAsync(string key, OutputCacheEntry value, TimeSpan duration, IOutputCacheStore store, CancellationToken cancellationToken) 53 | { 54 | ArgumentNullException.ThrowIfNull(value); 55 | ArgumentNullException.ThrowIfNull(value.Body); 56 | ArgumentNullException.ThrowIfNull(value.Headers); 57 | 58 | var formatterEntry = new FormatterEntry 59 | { 60 | StatusCode = value.StatusCode, 61 | Created = value.Created, 62 | Tags = value.Tags, 63 | Body = value.Body.Segments 64 | }; 65 | 66 | if (value.Headers != null) 67 | { 68 | formatterEntry.Headers = new(); 69 | foreach (var header in value.Headers) 70 | { 71 | formatterEntry.Headers.TryAdd(header.Key, header.Value.ToArray()); 72 | } 73 | } 74 | 75 | using var bufferStream = new MemoryStream(); 76 | 77 | JsonSerializer.Serialize(bufferStream, formatterEntry, FormatterEntrySerializerContext.Default.FormatterEntry); 78 | 79 | await store.SetAsync(key, bufferStream.ToArray(), value.Tags ?? Array.Empty(), duration, cancellationToken); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Policies/PredicatePolicy.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | namespace Microsoft.AspNetCore.OutputCaching.Policies; 5 | 6 | /// 7 | /// A policy that adds a requirement to another policy. 8 | /// 9 | internal sealed class PredicatePolicy : IOutputCachePolicy 10 | { 11 | // TODO: Accept a non async predicate too? 12 | 13 | private readonly Func> _predicate; 14 | private readonly IOutputCachePolicy _policy; 15 | 16 | /// 17 | /// Creates a new instance. 18 | /// 19 | /// The predicate. 20 | /// The policy. 21 | public PredicatePolicy(Func> asyncPredicate, IOutputCachePolicy policy) 22 | { 23 | _predicate = asyncPredicate; 24 | _policy = policy; 25 | } 26 | 27 | /// 28 | ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken) 29 | { 30 | return ExecuteAwaited(static (policy, context, cancellationToken) => policy.CacheRequestAsync(context, cancellationToken), _policy, context, cancellationToken); 31 | } 32 | 33 | /// 34 | ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellationToken) 35 | { 36 | return ExecuteAwaited(static (policy, context, cancellationToken) => policy.ServeFromCacheAsync(context, cancellationToken), _policy, context, cancellationToken); 37 | } 38 | 39 | /// 40 | ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context, CancellationToken cancellationToken) 41 | { 42 | return ExecuteAwaited(static (policy, context, cancellationToken) => policy.ServeResponseAsync(context, cancellationToken), _policy, context, cancellationToken); 43 | } 44 | 45 | private ValueTask ExecuteAwaited(Func action, IOutputCachePolicy policy, OutputCacheContext context, CancellationToken cancellationToken) 46 | { 47 | ArgumentNullException.ThrowIfNull(action); 48 | 49 | if (_predicate == null) 50 | { 51 | return action(policy, context, cancellationToken); 52 | } 53 | 54 | var task = _predicate(context); 55 | 56 | if (task.IsCompletedSuccessfully) 57 | { 58 | if (task.Result) 59 | { 60 | return action(policy, context, cancellationToken); 61 | } 62 | 63 | return ValueTask.CompletedTask; 64 | } 65 | 66 | return Awaited(task); 67 | 68 | async ValueTask Awaited(ValueTask task) 69 | { 70 | if (await task) 71 | { 72 | await action(policy, context, cancellationToken); 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Memory/MemoryOutputCacheStore.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using Microsoft.Extensions.Caching.Memory; 5 | 6 | namespace Microsoft.AspNetCore.OutputCaching.Memory; 7 | 8 | internal sealed class MemoryOutputCacheStore : IOutputCacheStore 9 | { 10 | private readonly IMemoryCache _cache; 11 | private readonly Dictionary> _taggedEntries = new(); 12 | private readonly object _tagsLock = new(); 13 | 14 | internal MemoryOutputCacheStore(IMemoryCache cache) 15 | { 16 | ArgumentNullException.ThrowIfNull(cache); 17 | 18 | _cache = cache; 19 | } 20 | 21 | public ValueTask EvictByTagAsync(string tag, CancellationToken cancellationToken) 22 | { 23 | ArgumentNullException.ThrowIfNull(tag); 24 | 25 | lock (_tagsLock) 26 | { 27 | if (_taggedEntries.TryGetValue(tag, out var keys)) 28 | { 29 | foreach (var key in keys) 30 | { 31 | _cache.Remove(key); 32 | } 33 | 34 | _taggedEntries.Remove(tag); 35 | } 36 | } 37 | 38 | return ValueTask.CompletedTask; 39 | } 40 | 41 | /// 42 | public ValueTask GetAsync(string key, CancellationToken cancellationToken) 43 | { 44 | ArgumentNullException.ThrowIfNull(key); 45 | 46 | var entry = _cache.Get(key) as byte[]; 47 | return ValueTask.FromResult(entry); 48 | } 49 | 50 | /// 51 | public ValueTask SetAsync(string key, byte[] value, string[]? tags, TimeSpan validFor, CancellationToken cancellationToken) 52 | { 53 | ArgumentNullException.ThrowIfNull(key); 54 | ArgumentNullException.ThrowIfNull(value); 55 | 56 | if (tags != null) 57 | { 58 | // Lock with SetEntry() to prevent EvictByTagAsync() from trying to remove a tag whose entry hasn't been added yet. 59 | // It might be acceptable to not lock SetEntry() since in this case Remove(key) would just no-op and the user retry to evict. 60 | 61 | lock (_tagsLock) 62 | { 63 | foreach (var tag in tags) 64 | { 65 | if (!_taggedEntries.TryGetValue(tag, out var keys)) 66 | { 67 | keys = new HashSet(); 68 | _taggedEntries[tag] = keys; 69 | } 70 | 71 | keys.Add(key); 72 | } 73 | 74 | SetEntry(); 75 | } 76 | } 77 | else 78 | { 79 | SetEntry(); 80 | } 81 | 82 | void SetEntry() 83 | { 84 | _cache.Set( 85 | key, 86 | value, 87 | new MemoryCacheEntryOptions 88 | { 89 | AbsoluteExpirationRelativeToNow = validFor, 90 | Size = value.Length 91 | }); 92 | } 93 | 94 | return ValueTask.CompletedTask; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /samples/OutputCachingSample/Startup.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using System.Globalization; 5 | using Microsoft.AspNetCore.OutputCaching; 6 | 7 | var builder = WebApplication.CreateBuilder(args); 8 | 9 | builder.Services.AddOutputCache(options => 10 | { 11 | // Define policies for all requests which are not configured per endpoint or per request 12 | options.AddBasePolicy(builder => builder.With(c => c.HttpContext.Request.Path.StartsWithSegments("/js")).Expire(TimeSpan.FromDays(1))); 13 | options.AddBasePolicy(builder => builder.With(c => c.HttpContext.Request.Path.StartsWithSegments("/js")).NoCache()); 14 | 15 | options.AddPolicy("NoCache", b => b.NoCache()); 16 | }); 17 | 18 | var app = builder.Build(); 19 | 20 | app.UseOutputCache(); 21 | 22 | app.MapGet("/", Gravatar.WriteGravatar); 23 | 24 | app.MapGet("/cached", Gravatar.WriteGravatar).CacheOutput(); 25 | 26 | app.MapGet("/nocache", Gravatar.WriteGravatar).CacheOutput(x => x.NoCache()); 27 | 28 | app.MapGet("/profile", Gravatar.WriteGravatar).CacheOutput("NoCache"); 29 | 30 | app.MapGet("/attribute", [OutputCache(PolicyName = "NoCache")] () => Gravatar.WriteGravatar); 31 | 32 | // Only available in dotnet 7 33 | //var blog = app.MapGroup("blog").CacheOutput(x => x.Tag("blog")); 34 | //blog.MapGet("/", Gravatar.WriteGravatar); 35 | //blog.MapGet("/post/{id}", Gravatar.WriteGravatar).CacheOutput(x => x.Tag("blog", "byid")); // Calling CacheOutput() here overwrites the group's policy 36 | 37 | app.MapPost("/purge/{tag}", async (IOutputCacheStore cache, string tag) => 38 | { 39 | // POST such that the endpoint is not cached itself 40 | 41 | await cache.EvictByTagAsync(tag, default); 42 | }); 43 | 44 | // Cached entries will vary by culture, but any other additional query is ignored and returns the same cached content 45 | app.MapGet("/query", Gravatar.WriteGravatar).CacheOutput(p => p.VaryByQuery("culture")); 46 | 47 | app.MapGet("/vary", Gravatar.WriteGravatar).CacheOutput(c => c.VaryByValue((context) => new KeyValuePair("time", (DateTime.Now.Second % 2).ToString(CultureInfo.InvariantCulture)))); 48 | 49 | long requests = 0; 50 | 51 | // Locking is enabled by default 52 | app.MapGet("/lock", async (context) => 53 | { 54 | await Task.Delay(1000); 55 | await context.Response.WriteAsync($"
{requests++}
"); 56 | }).CacheOutput(p => p.AllowLocking(false).Expire(TimeSpan.FromMilliseconds(1))); 57 | 58 | // Etag 59 | app.MapGet("/etag", async (context) => 60 | { 61 | // If the client sends an If-None-Match header with the etag value, the server 62 | // returns 304 if the cache entry is fresh instead of the full response 63 | 64 | var etag = $"\"{Guid.NewGuid():n}\""; 65 | context.Response.Headers.ETag = etag; 66 | 67 | await Gravatar.WriteGravatar(context); 68 | 69 | var cacheContext = context.Features.Get()?.Context; 70 | 71 | }).CacheOutput(); 72 | 73 | // When the request header If-Modified-Since is provided, return 304 if the cached entry is older 74 | app.MapGet("/ims", Gravatar.WriteGravatar).CacheOutput(); 75 | 76 | await app.RunAsync(); 77 | -------------------------------------------------------------------------------- /src/Policies/DefaultPolicy.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.Extensions.Primitives; 6 | 7 | namespace Microsoft.AspNetCore.OutputCaching; 8 | 9 | /// 10 | /// A policy which caches un-authenticated, GET and HEAD, 200 responses. 11 | /// 12 | internal sealed class DefaultPolicy : IOutputCachePolicy 13 | { 14 | public static readonly DefaultPolicy Instance = new(); 15 | 16 | private DefaultPolicy() 17 | { 18 | } 19 | 20 | /// 21 | ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken) 22 | { 23 | var attemptOutputCaching = AttemptOutputCaching(context); 24 | context.EnableOutputCaching = true; 25 | context.AllowCacheLookup = attemptOutputCaching; 26 | context.AllowCacheStorage = attemptOutputCaching; 27 | context.AllowLocking = true; 28 | 29 | // Vary by any query by default 30 | context.CacheVaryByRules.QueryKeys = "*"; 31 | 32 | return ValueTask.CompletedTask; 33 | } 34 | 35 | /// 36 | ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellationToken) 37 | { 38 | return ValueTask.CompletedTask; 39 | } 40 | 41 | /// 42 | ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context, CancellationToken cancellationToken) 43 | { 44 | var response = context.HttpContext.Response; 45 | 46 | // Verify existence of cookie headers 47 | if (!StringValues.IsNullOrEmpty(response.Headers.SetCookie)) 48 | { 49 | context.Logger.ResponseWithSetCookieNotCacheable(); 50 | context.AllowCacheStorage = false; 51 | return ValueTask.CompletedTask; 52 | } 53 | 54 | // Check response code 55 | if (response.StatusCode != StatusCodes.Status200OK) 56 | { 57 | context.Logger.ResponseWithUnsuccessfulStatusCodeNotCacheable(response.StatusCode); 58 | context.AllowCacheStorage = false; 59 | return ValueTask.CompletedTask; 60 | } 61 | 62 | return ValueTask.CompletedTask; 63 | } 64 | 65 | private static bool AttemptOutputCaching(OutputCacheContext context) 66 | { 67 | // Check if the current request fulfills the requirements to be cached 68 | 69 | var request = context.HttpContext.Request; 70 | 71 | // Verify the method 72 | if (!HttpMethods.IsGet(request.Method) && !HttpMethods.IsHead(request.Method)) 73 | { 74 | context.Logger.RequestMethodNotCacheable(request.Method); 75 | return false; 76 | } 77 | 78 | // Verify existence of authorization headers 79 | if (!StringValues.IsNullOrEmpty(request.Headers.Authorization) || request.HttpContext.User?.Identity?.IsAuthenticated == true) 80 | { 81 | context.Logger.RequestWithAuthorizationNotCacheable(); 82 | return false; 83 | } 84 | 85 | return true; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Policies/VaryByValuePolicy.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using Microsoft.AspNetCore.Http; 5 | 6 | namespace Microsoft.AspNetCore.OutputCaching; 7 | 8 | /// 9 | /// When applied, the cached content will be different for every provided value. 10 | /// 11 | internal sealed class VaryByValuePolicy : IOutputCachePolicy 12 | { 13 | private readonly Action? _varyBy; 14 | private readonly Func? _varyByAsync; 15 | 16 | /// 17 | /// Creates a policy that doesn't vary the cached content based on values. 18 | /// 19 | public VaryByValuePolicy() 20 | { 21 | } 22 | 23 | /// 24 | /// Creates a policy that vary the cached content based on the specified value. 25 | /// 26 | public VaryByValuePolicy(Func varyBy) 27 | { 28 | _varyBy = (context, rules) => rules.VaryByPrefix += varyBy(context); 29 | } 30 | 31 | /// 32 | /// Creates a policy that vary the cached content based on the specified value. 33 | /// 34 | public VaryByValuePolicy(Func> varyBy) 35 | { 36 | _varyByAsync = async (context, rules, token) => rules.VaryByPrefix += await varyBy(context, token); 37 | } 38 | 39 | /// 40 | /// Creates a policy that vary the cached content based on the specified value. 41 | /// 42 | public VaryByValuePolicy(Func> varyBy) 43 | { 44 | _varyBy = (context, rules) => 45 | { 46 | var result = varyBy(context); 47 | rules.VaryByCustom?.TryAdd(result.Key, result.Value); 48 | }; 49 | } 50 | 51 | /// 52 | /// Creates a policy that vary the cached content based on the specified value. 53 | /// 54 | public VaryByValuePolicy(Func>> varyBy) 55 | { 56 | _varyBy = async (context, rules) => 57 | { 58 | var result = await varyBy(context, context.RequestAborted); 59 | rules.VaryByCustom?.TryAdd(result.Key, result.Value); 60 | }; 61 | } 62 | 63 | /// 64 | ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken) 65 | { 66 | _varyBy?.Invoke(context.HttpContext, context.CacheVaryByRules); 67 | 68 | return _varyByAsync?.Invoke(context.HttpContext, context.CacheVaryByRules, context.HttpContext.RequestAborted) ?? ValueTask.CompletedTask; 69 | } 70 | 71 | /// 72 | ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellationToken) 73 | { 74 | return ValueTask.CompletedTask; 75 | } 76 | 77 | /// 78 | ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context, CancellationToken cancellationToken) 79 | { 80 | return ValueTask.CompletedTask; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/DispatcherExtensions.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using System.Collections.Concurrent; 5 | 6 | namespace Microsoft.AspNetCore.OutputCaching; 7 | 8 | internal sealed class WorkDispatcher where TKey : notnull 9 | { 10 | private readonly ConcurrentDictionary> _workers = new(); 11 | 12 | public async Task ScheduleAsync(TKey key, Func> valueFactory) 13 | { 14 | ArgumentNullException.ThrowIfNull(key); 15 | 16 | while (true) 17 | { 18 | if (_workers.TryGetValue(key, out var task)) 19 | { 20 | return await task; 21 | } 22 | 23 | // This is the task that we'll return to all waiters. We'll complete it when the factory is complete 24 | var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); 25 | 26 | if (_workers.TryAdd(key, tcs.Task)) 27 | { 28 | try 29 | { 30 | var value = await valueFactory(key); 31 | tcs.TrySetResult(value); 32 | return await tcs.Task; 33 | } 34 | catch (Exception ex) 35 | { 36 | // Make sure all waiters see the exception 37 | tcs.SetException(ex); 38 | 39 | throw; 40 | } 41 | finally 42 | { 43 | // We remove the entry if the factory failed so it's not a permanent failure 44 | // and future gets can retry (this could be a pluggable policy) 45 | _workers.TryRemove(key, out _); 46 | } 47 | } 48 | } 49 | } 50 | 51 | public async Task ScheduleAsync(TKey key, TState state, Func> valueFactory) 52 | { 53 | ArgumentNullException.ThrowIfNull(key); 54 | 55 | while (true) 56 | { 57 | if (_workers.TryGetValue(key, out var task)) 58 | { 59 | return await task; 60 | } 61 | 62 | // This is the task that we'll return to all waiters. We'll complete it when the factory is complete 63 | var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); 64 | 65 | if (_workers.TryAdd(key, tcs.Task)) 66 | { 67 | try 68 | { 69 | var value = await valueFactory(key, state); 70 | tcs.TrySetResult(value); 71 | return await tcs.Task; 72 | } 73 | catch (Exception ex) 74 | { 75 | // Make sure all waiters see the exception 76 | tcs.SetException(ex); 77 | 78 | throw; 79 | } 80 | finally 81 | { 82 | // We remove the entry if the factory failed so it's not a permanent failure 83 | // and future gets can retry (this could be a pluggable policy) 84 | _workers.TryRemove(key, out _); 85 | } 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/OutputCacheOptions.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using System.ComponentModel; 5 | 6 | namespace Microsoft.AspNetCore.OutputCaching; 7 | 8 | /// 9 | /// Options for configuring the . 10 | /// 11 | public class OutputCacheOptions 12 | { 13 | /// 14 | /// The size limit for the output cache middleware in bytes. The default is set to 100 MB. 15 | /// When this limit is exceeded, no new responses will be cached until older entries are 16 | /// evicted. 17 | /// 18 | public long SizeLimit { get; set; } = 100 * 1024 * 1024; 19 | 20 | /// 21 | /// The largest cacheable size for the response body in bytes. The default is set to 64 MB. 22 | /// If the response body exceeds this limit, it will not be cached by the . 23 | /// 24 | public long MaximumBodySize { get; set; } = 64 * 1024 * 1024; 25 | 26 | /// 27 | /// The duration a response is cached when no specific value is defined by a policy. The default is set to 60 seconds. 28 | /// 29 | public TimeSpan DefaultExpirationTimeSpan { get; set; } = TimeSpan.FromSeconds(60); 30 | 31 | /// 32 | /// true if request paths are case-sensitive; otherwise false. The default is to treat paths as case-insensitive. 33 | /// 34 | public bool UseCaseSensitivePaths { get; set; } 35 | 36 | /// 37 | /// Gets the application . 38 | /// 39 | public IServiceProvider ApplicationServices { get; internal set; } = default!; 40 | 41 | internal Dictionary? NamedPolicies { get; set; } 42 | 43 | internal List? BasePolicies { get; set; } 44 | 45 | /// 46 | /// For testing purposes only. 47 | /// 48 | [EditorBrowsable(EditorBrowsableState.Never)] 49 | internal ISystemClock SystemClock { get; set; } = new SystemClock(); 50 | 51 | /// 52 | /// Defines a which can be referenced by name. 53 | /// 54 | /// The name of the policy. 55 | /// The policy to add 56 | public void AddPolicy(string name, IOutputCachePolicy policy) 57 | { 58 | NamedPolicies ??= new Dictionary(StringComparer.OrdinalIgnoreCase); 59 | NamedPolicies[name] = policy; 60 | } 61 | 62 | /// 63 | /// Defines a which can be referenced by name. 64 | /// 65 | /// The name of the policy. 66 | /// an action on . 67 | public void AddPolicy(string name, Action build) 68 | { 69 | var builder = new OutputCachePolicyBuilder(); 70 | build(builder); 71 | NamedPolicies ??= new Dictionary(StringComparer.OrdinalIgnoreCase); 72 | NamedPolicies[name] = builder.Build(); 73 | } 74 | 75 | /// 76 | /// Adds an instance to base policies. 77 | /// 78 | /// The policy to add 79 | public void AddBasePolicy(IOutputCachePolicy policy) 80 | { 81 | BasePolicies ??= new(); 82 | BasePolicies.Add(policy); 83 | } 84 | 85 | /// 86 | /// Builds and adds an instance to base policies. 87 | /// 88 | /// an action on . 89 | public void AddBasePolicy(Action build) 90 | { 91 | var builder = new OutputCachePolicyBuilder(); 92 | build(builder); 93 | BasePolicies ??= new(); 94 | BasePolicies.Add(builder.Build()); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/LoggerExtensions.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using Microsoft.Extensions.Logging; 5 | using Microsoft.Net.Http.Headers; 6 | 7 | namespace Microsoft.AspNetCore.OutputCaching; 8 | 9 | /// 10 | /// Defines the logger messages produced by output caching 11 | /// 12 | internal static partial class LoggerExtensions 13 | { 14 | [LoggerMessage(1, LogLevel.Debug, "The request cannot be served from cache because it uses the HTTP method: {Method}.", 15 | EventName = "RequestMethodNotCacheable")] 16 | internal static partial void RequestMethodNotCacheable(this ILogger logger, string method); 17 | 18 | [LoggerMessage(2, LogLevel.Debug, "The request cannot be served from cache because it contains an 'Authorization' header.", 19 | EventName = "RequestWithAuthorizationNotCacheable")] 20 | internal static partial void RequestWithAuthorizationNotCacheable(this ILogger logger); 21 | 22 | [LoggerMessage(3, LogLevel.Debug, "Response is not cacheable because it contains a 'SetCookie' header.", EventName = "ResponseWithSetCookieNotCacheable")] 23 | internal static partial void ResponseWithSetCookieNotCacheable(this ILogger logger); 24 | 25 | [LoggerMessage(4, LogLevel.Debug, "Response is not cacheable because its status code {StatusCode} does not indicate success.", 26 | EventName = "ResponseWithUnsuccessfulStatusCodeNotCacheable")] 27 | internal static partial void ResponseWithUnsuccessfulStatusCodeNotCacheable(this ILogger logger, int statusCode); 28 | 29 | [LoggerMessage(5, LogLevel.Debug, "The 'IfNoneMatch' header of the request contains a value of *.", EventName = "NotModifiedIfNoneMatchStar")] 30 | internal static partial void NotModifiedIfNoneMatchStar(this ILogger logger); 31 | 32 | [LoggerMessage(6, LogLevel.Debug, "The ETag {ETag} in the 'IfNoneMatch' header matched the ETag of a cached entry.", 33 | EventName = "NotModifiedIfNoneMatchMatched")] 34 | internal static partial void NotModifiedIfNoneMatchMatched(this ILogger logger, EntityTagHeaderValue etag); 35 | 36 | [LoggerMessage(7, LogLevel.Debug, "The last modified date of {LastModified} is before the date {IfModifiedSince} specified in the 'IfModifiedSince' header.", 37 | EventName = "NotModifiedIfModifiedSinceSatisfied")] 38 | internal static partial void NotModifiedIfModifiedSinceSatisfied(this ILogger logger, DateTimeOffset lastModified, DateTimeOffset ifModifiedSince); 39 | 40 | [LoggerMessage(8, LogLevel.Information, "The content requested has not been modified.", EventName = "NotModifiedServed")] 41 | internal static partial void NotModifiedServed(this ILogger logger); 42 | 43 | [LoggerMessage(9, LogLevel.Information, "Serving response from cache.", EventName = "CachedResponseServed")] 44 | internal static partial void CachedResponseServed(this ILogger logger); 45 | 46 | [LoggerMessage(10, LogLevel.Information, "No cached response available for this request and the 'only-if-cached' cache directive was specified.", 47 | EventName = "GatewayTimeoutServed")] 48 | internal static partial void GatewayTimeoutServed(this ILogger logger); 49 | 50 | [LoggerMessage(11, LogLevel.Information, "No cached response available for this request.", EventName = "NoResponseServed")] 51 | internal static partial void NoResponseServed(this ILogger logger); 52 | 53 | [LoggerMessage(12, LogLevel.Debug, "Vary by rules were updated. Headers: {Headers}, Query keys: {QueryKeys}", EventName = "VaryByRulesUpdated")] 54 | internal static partial void VaryByRulesUpdated(this ILogger logger, string headers, string queryKeys); 55 | 56 | [LoggerMessage(13, LogLevel.Information, "The response has been cached.", EventName = "ResponseCached")] 57 | internal static partial void ResponseCached(this ILogger logger); 58 | 59 | [LoggerMessage(14, LogLevel.Information, "The response could not be cached for this request.", EventName = "ResponseNotCached")] 60 | internal static partial void ResponseNotCached(this ILogger logger); 61 | 62 | [LoggerMessage(15, LogLevel.Warning, "The response could not be cached for this request because the 'Content-Length' did not match the body length.", 63 | EventName = "ResponseContentLengthMismatchNotCached")] 64 | internal static partial void ResponseContentLengthMismatchNotCached(this ILogger logger); 65 | 66 | [LoggerMessage(16, LogLevel.Debug, "The response time of the entry is {ResponseTime} and has exceeded its expiry date.", 67 | EventName = "ExpirationExpiresExceeded")] 68 | internal static partial void ExpirationExpiresExceeded(this ILogger logger, DateTimeOffset responseTime); 69 | 70 | } 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Output Caching for ASP.NET Core 6.0 2 | 3 | A copy of .NET 7.0 Output Caching middleware, targeting .NET 6.0. 4 | 5 | ## Warning !!! 6 | 7 | This package is not supported and might be removed in the future. It's goal is to provide a way to test the Output Caching features that will ship in ASP.NET Core 7.0 but on .NET 6.0. Any improvement made to the official version will be ported here. 8 | 9 | ## Sample usage - Minimal APIs 10 | 11 | #### Program.cs 12 | 13 | ```c# 14 | using System.Globalization; 15 | using Microsoft.AspNetCore.OutputCaching; 16 | 17 | var builder = WebApplication.CreateBuilder(args); 18 | 19 | builder.Services.AddOutputCache(options => 20 | { 21 | // Define policies for all requests which are not configured per endpoint or per request 22 | options.AddBasePolicy(builder => builder.With(c => c.HttpContext.Request.Path.StartsWithSegments("/js")).Expire(TimeSpan.FromDays(1))); 23 | options.AddBasePolicy(builder => builder.With(c => c.HttpContext.Request.Path.StartsWithSegments("/js")).NoCache()); 24 | 25 | options.AddPolicy("NoCache", b => b.NoCache()); 26 | }); 27 | 28 | var app = builder.Build(); 29 | 30 | app.UseOutputCache(); 31 | 32 | app.MapGet("/", Gravatar.WriteGravatar); 33 | 34 | app.MapGet("/cached", Gravatar.WriteGravatar).CacheOutput(); 35 | 36 | app.MapGet("/nocache", Gravatar.WriteGravatar).CacheOutput(x => x.NoCache()); 37 | 38 | app.MapGet("/profile", Gravatar.WriteGravatar).CacheOutput("NoCache"); 39 | 40 | app.MapGet("/attribute", [OutputCache(PolicyName = "NoCache")] () => Gravatar.WriteGravatar); 41 | 42 | // Only available in dotnet 7 43 | //var blog = app.MapGroup("blog").CacheOutput(x => x.Tag("blog")); 44 | //blog.MapGet("/", Gravatar.WriteGravatar); 45 | //blog.MapGet("/post/{id}", Gravatar.WriteGravatar).CacheOutput(x => x.Tag("blog", "byid")); // Calling CacheOutput() here overwrites the group's policy 46 | 47 | app.MapPost("/purge/{tag}", async (IOutputCacheStore cache, string tag) => 48 | { 49 | // POST such that the endpoint is not cached itself 50 | 51 | await cache.EvictByTagAsync(tag, default); 52 | }); 53 | 54 | // Cached entries will vary by culture, but any other additional query is ignored and returns the same cached content 55 | app.MapGet("/query", Gravatar.WriteGravatar).CacheOutput(p => p.VaryByQuery("culture")); 56 | 57 | app.MapGet("/vary", Gravatar.WriteGravatar).CacheOutput(c => c.VaryByValue((context) => new KeyValuePair("time", (DateTime.Now.Second % 2).ToString(CultureInfo.InvariantCulture)))); 58 | 59 | long requests = 0; 60 | 61 | // Locking is enabled by default 62 | app.MapGet("/lock", async (context) => 63 | { 64 | await Task.Delay(1000); 65 | await context.Response.WriteAsync($"
{requests++}
"); 66 | }).CacheOutput(p => p.AllowLocking(false).Expire(TimeSpan.FromMilliseconds(1))); 67 | 68 | // Etag 69 | app.MapGet("/etag", async (context) => 70 | { 71 | // If the client sends an If-None-Match header with the etag value, the server 72 | // returns 304 if the cache entry is fresh instead of the full response 73 | 74 | var etag = $"\"{Guid.NewGuid():n}\""; 75 | context.Response.Headers.ETag = etag; 76 | 77 | await Gravatar.WriteGravatar(context); 78 | 79 | var cacheContext = context.Features.Get()?.Context; 80 | 81 | }).CacheOutput(); 82 | 83 | // When the request header If-Modified-Since is provided, return 304 if the cached entry is older 84 | app.MapGet("/ims", Gravatar.WriteGravatar).CacheOutput(); 85 | 86 | await app.RunAsync(); 87 | 88 | ``` 89 | 90 | ## Sample usage - ASP.NET Core MVC 91 | 92 | Enabling output cache for an MVC action: 93 | 94 | #### Program.cs 95 | 96 | ```c# 97 | var builder = WebApplication.CreateBuilder(args); 98 | 99 | // Add services to the container. 100 | builder.Services.AddControllersWithViews(); 101 | builder.Services.AddOutputCache(); 102 | 103 | var app = builder.Build(); 104 | 105 | // Configure the HTTP request pipeline. 106 | if (!app.Environment.IsDevelopment()) 107 | { 108 | app.UseExceptionHandler("/Home/Error"); 109 | } 110 | app.UseStaticFiles(); 111 | 112 | app.UseRouting(); 113 | 114 | app.UseOutputCache(); 115 | 116 | app.UseAuthorization(); 117 | 118 | app.MapControllerRoute( 119 | name: "default", 120 | pattern: "{controller=Home}/{action=Index}/{id?}"); 121 | 122 | app.Run(); 123 | ``` 124 | 125 | #### HomeController.cs 126 | 127 | ```c# 128 | public class HomeController : Controller 129 | { 130 | [OutputCache(Duration = 5)] 131 | public IActionResult Index() 132 | { 133 | return View(); 134 | } 135 | } 136 | ``` 137 | 138 | ## Sample usage - Razor Page Model 139 | 140 | Enabling output cache for a Razor Page: 141 | 142 | #### Program.cs 143 | 144 | ```c# 145 | var builder = WebApplication.CreateBuilder(args); 146 | 147 | // Add services to the container. 148 | builder.Services.AddRazorPages(); 149 | builder.Services.AddOutputCache(); 150 | 151 | var app = builder.Build(); 152 | 153 | // Configure the HTTP request pipeline. 154 | if (!app.Environment.IsDevelopment()) 155 | { 156 | app.UseExceptionHandler("/Error"); 157 | } 158 | app.UseStaticFiles(); 159 | 160 | app.UseRouting(); 161 | 162 | app.UseOutputCache(); 163 | 164 | app.UseAuthorization(); 165 | 166 | app.MapRazorPages(); 167 | 168 | app.Run(); 169 | ``` 170 | 171 | #### Index.cshtml.cs 172 | 173 | ```c# 174 | using Microsoft.AspNetCore.Mvc.RazorPages; 175 | using Microsoft.AspNetCore.OutputCaching; 176 | 177 | namespace WebApplication4.Pages 178 | { 179 | [OutputCache(Duration = 5)] 180 | public class IndexModel : PageModel 181 | { 182 | public void OnGet() 183 | { 184 | 185 | } 186 | } 187 | } 188 | ``` 189 | -------------------------------------------------------------------------------- /src/External/TaskToApm.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | // Helper methods for using Tasks to implement the APM pattern. 5 | // 6 | // Example usage, wrapping a Task-returning FooAsync method with Begin/EndFoo methods: 7 | // 8 | // public IAsyncResult BeginFoo(..., AsyncCallback callback, object state) => 9 | // TaskToApm.Begin(FooAsync(...), callback, state); 10 | // 11 | // public int EndFoo(IAsyncResult asyncResult) => 12 | // TaskToApm.End(asyncResult); 13 | 14 | #nullable enable 15 | using System.Diagnostics; 16 | 17 | namespace System.Threading.Tasks; 18 | 19 | /// 20 | /// Provides support for efficiently using Tasks to implement the APM (Begin/End) pattern. 21 | /// 22 | internal static class TaskToApm 23 | { 24 | /// 25 | /// Marshals the Task as an IAsyncResult, using the supplied callback and state 26 | /// to implement the APM pattern. 27 | /// 28 | /// The Task to be marshaled. 29 | /// The callback to be invoked upon completion. 30 | /// The state to be stored in the IAsyncResult. 31 | /// An IAsyncResult to represent the task's asynchronous operation. 32 | public static IAsyncResult Begin(Task task, AsyncCallback? callback, object? state) => 33 | new TaskAsyncResult(task, state, callback); 34 | 35 | /// Processes an IAsyncResult returned by Begin. 36 | /// The IAsyncResult to unwrap. 37 | public static void End(IAsyncResult asyncResult) 38 | { 39 | if (asyncResult is TaskAsyncResult twar) 40 | { 41 | twar._task.GetAwaiter().GetResult(); 42 | return; 43 | } 44 | 45 | ArgumentNullException.ThrowIfNull(asyncResult, nameof(asyncResult)); 46 | } 47 | 48 | /// Processes an IAsyncResult returned by Begin. 49 | /// The IAsyncResult to unwrap. 50 | public static TResult End(IAsyncResult asyncResult) 51 | { 52 | if (asyncResult is TaskAsyncResult twar && twar._task is Task task) 53 | { 54 | return task.GetAwaiter().GetResult(); 55 | } 56 | 57 | throw new ArgumentNullException(nameof(asyncResult)); 58 | } 59 | 60 | /// Provides a simple IAsyncResult that wraps a Task. 61 | /// 62 | /// We could use the Task as the IAsyncResult if the Task's AsyncState is the same as the object state, 63 | /// but that's very rare, in particular in a situation where someone cares about allocation, and always 64 | /// using TaskAsyncResult simplifies things and enables additional optimizations. 65 | /// 66 | internal sealed class TaskAsyncResult : IAsyncResult 67 | { 68 | /// The wrapped Task. 69 | internal readonly Task _task; 70 | /// Callback to invoke when the wrapped task completes. 71 | private readonly AsyncCallback? _callback; 72 | 73 | /// Initializes the IAsyncResult with the Task to wrap and the associated object state. 74 | /// The Task to wrap. 75 | /// The new AsyncState value. 76 | /// Callback to invoke when the wrapped task completes. 77 | internal TaskAsyncResult(Task task, object? state, AsyncCallback? callback) 78 | { 79 | Debug.Assert(task != null); 80 | _task = task; 81 | AsyncState = state; 82 | 83 | if (task.IsCompleted) 84 | { 85 | // Synchronous completion. Invoke the callback. No need to store it. 86 | CompletedSynchronously = true; 87 | callback?.Invoke(this); 88 | } 89 | else if (callback != null) 90 | { 91 | // Asynchronous completion, and we have a callback; schedule it. We use OnCompleted rather than ContinueWith in 92 | // order to avoid running synchronously if the task has already completed by the time we get here but still run 93 | // synchronously as part of the task's completion if the task completes after (the more common case). 94 | _callback = callback; 95 | _task.ConfigureAwait(continueOnCapturedContext: false) 96 | .GetAwaiter() 97 | .OnCompleted(InvokeCallback); // allocates a delegate, but avoids a closure 98 | } 99 | } 100 | 101 | /// Invokes the callback. 102 | private void InvokeCallback() 103 | { 104 | Debug.Assert(!CompletedSynchronously); 105 | Debug.Assert(_callback != null); 106 | _callback.Invoke(this); 107 | } 108 | 109 | /// Gets a user-defined object that qualifies or contains information about an asynchronous operation. 110 | public object? AsyncState { get; } 111 | /// Gets a value that indicates whether the asynchronous operation completed synchronously. 112 | /// This is set lazily based on whether the has completed by the time this object is created. 113 | public bool CompletedSynchronously { get; } 114 | /// Gets a value that indicates whether the asynchronous operation has completed. 115 | public bool IsCompleted => _task.IsCompleted; 116 | /// Gets a that is used to wait for an asynchronous operation to complete. 117 | public WaitHandle AsyncWaitHandle => ((IAsyncResult)_task).AsyncWaitHandle; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Streams/OutputCacheStream.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | namespace Microsoft.AspNetCore.OutputCaching; 5 | 6 | internal sealed class OutputCacheStream : Stream 7 | { 8 | private readonly Stream _innerStream; 9 | private readonly long _maxBufferSize; 10 | private readonly int _segmentSize; 11 | private readonly SegmentWriteStream _segmentWriteStream; 12 | private readonly Action _startResponseCallback; 13 | 14 | internal OutputCacheStream(Stream innerStream, long maxBufferSize, int segmentSize, Action startResponseCallback) 15 | { 16 | _innerStream = innerStream; 17 | _maxBufferSize = maxBufferSize; 18 | _segmentSize = segmentSize; 19 | _startResponseCallback = startResponseCallback; 20 | _segmentWriteStream = new SegmentWriteStream(_segmentSize); 21 | } 22 | 23 | internal bool BufferingEnabled { get; private set; } = true; 24 | 25 | public override bool CanRead => _innerStream.CanRead; 26 | 27 | public override bool CanSeek => _innerStream.CanSeek; 28 | 29 | public override bool CanWrite => _innerStream.CanWrite; 30 | 31 | public override long Length => _innerStream.Length; 32 | 33 | public override long Position 34 | { 35 | get { return _innerStream.Position; } 36 | set 37 | { 38 | DisableBuffering(); 39 | _innerStream.Position = value; 40 | } 41 | } 42 | 43 | internal CachedResponseBody GetCachedResponseBody() 44 | { 45 | if (!BufferingEnabled) 46 | { 47 | throw new InvalidOperationException("Buffer stream cannot be retrieved since buffering is disabled."); 48 | } 49 | return new CachedResponseBody(_segmentWriteStream.GetSegments(), _segmentWriteStream.Length); 50 | } 51 | 52 | internal void DisableBuffering() 53 | { 54 | BufferingEnabled = false; 55 | _segmentWriteStream.Dispose(); 56 | } 57 | 58 | public override void SetLength(long value) 59 | { 60 | DisableBuffering(); 61 | _innerStream.SetLength(value); 62 | } 63 | 64 | public override long Seek(long offset, SeekOrigin origin) 65 | { 66 | DisableBuffering(); 67 | return _innerStream.Seek(offset, origin); 68 | } 69 | 70 | public override void Flush() 71 | { 72 | try 73 | { 74 | _startResponseCallback(); 75 | _innerStream.Flush(); 76 | } 77 | catch 78 | { 79 | DisableBuffering(); 80 | throw; 81 | } 82 | } 83 | 84 | public override async Task FlushAsync(CancellationToken cancellationToken) 85 | { 86 | try 87 | { 88 | _startResponseCallback(); 89 | await _innerStream.FlushAsync(cancellationToken); 90 | } 91 | catch 92 | { 93 | DisableBuffering(); 94 | throw; 95 | } 96 | } 97 | 98 | // Underlying stream is write-only, no need to override other read related methods 99 | public override int Read(byte[] buffer, int offset, int count) 100 | => _innerStream.Read(buffer, offset, count); 101 | 102 | public override void Write(byte[] buffer, int offset, int count) 103 | { 104 | try 105 | { 106 | _startResponseCallback(); 107 | _innerStream.Write(buffer, offset, count); 108 | } 109 | catch 110 | { 111 | DisableBuffering(); 112 | throw; 113 | } 114 | 115 | if (BufferingEnabled) 116 | { 117 | if (_segmentWriteStream.Length + count > _maxBufferSize) 118 | { 119 | DisableBuffering(); 120 | } 121 | else 122 | { 123 | _segmentWriteStream.Write(buffer, offset, count); 124 | } 125 | } 126 | } 127 | 128 | public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => 129 | await WriteAsync(buffer.AsMemory(offset, count), cancellationToken); 130 | 131 | public override async ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) 132 | { 133 | try 134 | { 135 | _startResponseCallback(); 136 | await _innerStream.WriteAsync(buffer, cancellationToken); 137 | } 138 | catch 139 | { 140 | DisableBuffering(); 141 | throw; 142 | } 143 | 144 | if (BufferingEnabled) 145 | { 146 | if (_segmentWriteStream.Length + buffer.Length > _maxBufferSize) 147 | { 148 | DisableBuffering(); 149 | } 150 | else 151 | { 152 | await _segmentWriteStream.WriteAsync(buffer, cancellationToken); 153 | } 154 | } 155 | } 156 | 157 | public override void WriteByte(byte value) 158 | { 159 | try 160 | { 161 | _innerStream.WriteByte(value); 162 | } 163 | catch 164 | { 165 | DisableBuffering(); 166 | throw; 167 | } 168 | 169 | if (BufferingEnabled) 170 | { 171 | if (_segmentWriteStream.Length + 1 > _maxBufferSize) 172 | { 173 | DisableBuffering(); 174 | } 175 | else 176 | { 177 | _segmentWriteStream.WriteByte(value); 178 | } 179 | } 180 | } 181 | 182 | public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) 183 | => TaskToApm.Begin(WriteAsync(buffer, offset, count, CancellationToken.None), callback, state); 184 | 185 | public override void EndWrite(IAsyncResult asyncResult) 186 | => TaskToApm.End(asyncResult); 187 | } 188 | -------------------------------------------------------------------------------- /src/Resources.resx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | The type '{0}' is not a valid output policy. 122 | 123 | -------------------------------------------------------------------------------- /src/Streams/SegmentWriteStream.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | namespace Microsoft.AspNetCore.OutputCaching; 5 | 6 | internal sealed class SegmentWriteStream : Stream 7 | { 8 | private readonly List _segments = new(); 9 | private readonly MemoryStream _bufferStream = new(); 10 | private readonly int _segmentSize; 11 | private long _length; 12 | private bool _closed; 13 | private bool _disposed; 14 | 15 | internal SegmentWriteStream(int segmentSize) 16 | { 17 | if (segmentSize <= 0) 18 | { 19 | throw new ArgumentOutOfRangeException(nameof(segmentSize), segmentSize, $"{nameof(segmentSize)} must be greater than 0."); 20 | } 21 | 22 | _segmentSize = segmentSize; 23 | } 24 | 25 | // Extracting the buffered segments closes the stream for writing 26 | internal List GetSegments() 27 | { 28 | if (!_closed) 29 | { 30 | _closed = true; 31 | FinalizeSegments(); 32 | } 33 | return _segments; 34 | } 35 | 36 | public override bool CanRead => false; 37 | 38 | public override bool CanSeek => false; 39 | 40 | public override bool CanWrite => !_closed; 41 | 42 | public override long Length => _length; 43 | 44 | public override long Position 45 | { 46 | get 47 | { 48 | return _length; 49 | } 50 | set 51 | { 52 | throw new NotSupportedException("The stream does not support seeking."); 53 | } 54 | } 55 | 56 | private void DisposeMemoryStream() 57 | { 58 | // Clean up the memory stream 59 | _bufferStream.SetLength(0); 60 | _bufferStream.Capacity = 0; 61 | _bufferStream.Dispose(); 62 | } 63 | 64 | private void FinalizeSegments() 65 | { 66 | // Append any remaining segments 67 | if (_bufferStream.Length > 0) 68 | { 69 | // Add the last segment 70 | _segments.Add(_bufferStream.ToArray()); 71 | } 72 | 73 | DisposeMemoryStream(); 74 | } 75 | 76 | protected override void Dispose(bool disposing) 77 | { 78 | try 79 | { 80 | if (_disposed) 81 | { 82 | return; 83 | } 84 | 85 | if (disposing) 86 | { 87 | _segments.Clear(); 88 | DisposeMemoryStream(); 89 | } 90 | 91 | _disposed = true; 92 | _closed = true; 93 | } 94 | finally 95 | { 96 | base.Dispose(disposing); 97 | } 98 | } 99 | 100 | public override void Flush() 101 | { 102 | if (!CanWrite) 103 | { 104 | throw new ObjectDisposedException("The stream has been closed for writing."); 105 | } 106 | } 107 | 108 | public override int Read(byte[] buffer, int offset, int count) 109 | { 110 | throw new NotSupportedException("The stream does not support reading."); 111 | } 112 | 113 | public override long Seek(long offset, SeekOrigin origin) 114 | { 115 | throw new NotSupportedException("The stream does not support seeking."); 116 | } 117 | 118 | public override void SetLength(long value) 119 | { 120 | throw new NotSupportedException("The stream does not support seeking."); 121 | } 122 | 123 | public override void Write(byte[] buffer, int offset, int count) 124 | { 125 | ArgumentNullException.ThrowIfNull(buffer); 126 | 127 | if (offset < 0) 128 | { 129 | throw new ArgumentOutOfRangeException(nameof(offset), offset, "Non-negative number required."); 130 | } 131 | if (count < 0) 132 | { 133 | throw new ArgumentOutOfRangeException(nameof(count), count, "Non-negative number required."); 134 | } 135 | if (count > buffer.Length - offset) 136 | { 137 | 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."); 138 | } 139 | if (!CanWrite) 140 | { 141 | throw new ObjectDisposedException("The stream has been closed for writing."); 142 | } 143 | 144 | Write(buffer.AsSpan(offset, count)); 145 | } 146 | 147 | public override void Write(ReadOnlySpan buffer) 148 | { 149 | while (!buffer.IsEmpty) 150 | { 151 | if ((int)_bufferStream.Length == _segmentSize) 152 | { 153 | _segments.Add(_bufferStream.ToArray()); 154 | _bufferStream.SetLength(0); 155 | } 156 | 157 | var bytesWritten = Math.Min(buffer.Length, _segmentSize - (int)_bufferStream.Length); 158 | 159 | _bufferStream.Write(buffer[..bytesWritten]); 160 | buffer = buffer[bytesWritten..]; 161 | _length += bytesWritten; 162 | } 163 | } 164 | 165 | public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) 166 | { 167 | Write(buffer, offset, count); 168 | return Task.CompletedTask; 169 | } 170 | 171 | public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken) 172 | { 173 | Write(buffer.Span); 174 | return default; 175 | } 176 | 177 | public override void WriteByte(byte value) 178 | { 179 | if (!CanWrite) 180 | { 181 | throw new ObjectDisposedException("The stream has been closed for writing."); 182 | } 183 | 184 | if ((int)_bufferStream.Length == _segmentSize) 185 | { 186 | _segments.Add(_bufferStream.ToArray()); 187 | _bufferStream.SetLength(0); 188 | } 189 | 190 | _bufferStream.WriteByte(value); 191 | _length++; 192 | } 193 | 194 | public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) 195 | => TaskToApm.Begin(WriteAsync(buffer, offset, count, CancellationToken.None), callback, state); 196 | 197 | public override void EndWrite(IAsyncResult asyncResult) 198 | => TaskToApm.End(asyncResult); 199 | } 200 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | -------------------------------------------------------------------------------- /src/OutputCacheKeyProvider.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using System.Linq; 5 | using System.Text; 6 | using Microsoft.Extensions.ObjectPool; 7 | using Microsoft.Extensions.Options; 8 | using Microsoft.Extensions.Primitives; 9 | 10 | namespace Microsoft.AspNetCore.OutputCaching; 11 | 12 | internal sealed class OutputCacheKeyProvider : IOutputCacheKeyProvider 13 | { 14 | // Use the record separator for delimiting components of the cache key to avoid possible collisions 15 | private const char KeyDelimiter = '\x1e'; 16 | // Use the unit separator for delimiting subcomponents of the cache key to avoid possible collisions 17 | private const char KeySubDelimiter = '\x1f'; 18 | 19 | private readonly ObjectPool _builderPool; 20 | private readonly OutputCacheOptions _options; 21 | 22 | internal OutputCacheKeyProvider(ObjectPoolProvider poolProvider, IOptions options) 23 | { 24 | ArgumentNullException.ThrowIfNull(poolProvider); 25 | ArgumentNullException.ThrowIfNull(options); 26 | 27 | _builderPool = poolProvider.CreateStringBuilderPool(); 28 | _options = options.Value; 29 | } 30 | 31 | // GETSCHEMEHOST:PORT/PATHBASE/PATHHHeaderName=HeaderValueQQueryName=QueryValue1QueryValue2 32 | public string CreateStorageKey(OutputCacheContext context) 33 | { 34 | ArgumentNullException.ThrowIfNull(_builderPool); 35 | 36 | var varyByRules = context.CacheVaryByRules; 37 | if (varyByRules == null) 38 | { 39 | throw new InvalidOperationException($"{nameof(CacheVaryByRules)} must not be null on the {nameof(OutputCacheContext)}"); 40 | } 41 | 42 | var request = context.HttpContext.Request; 43 | var builder = _builderPool.Get(); 44 | 45 | try 46 | { 47 | builder 48 | .AppendUpperInvariant(request.Method) 49 | .Append(KeyDelimiter) 50 | .AppendUpperInvariant(request.Scheme) 51 | .Append(KeyDelimiter) 52 | .AppendUpperInvariant(request.Host.Value); 53 | 54 | if (_options.UseCaseSensitivePaths) 55 | { 56 | builder 57 | .Append(request.PathBase.Value) 58 | .Append(request.Path.Value); 59 | } 60 | else 61 | { 62 | builder 63 | .AppendUpperInvariant(request.PathBase.Value) 64 | .AppendUpperInvariant(request.Path.Value); 65 | } 66 | 67 | // Vary by prefix and custom 68 | var prefixCount = varyByRules?.VaryByPrefix.Count ?? 0; 69 | if (prefixCount > 0) 70 | { 71 | // Append a group separator for the header segment of the cache key 72 | builder.Append(KeyDelimiter) 73 | .Append('C'); 74 | 75 | for (var i = 0; i < prefixCount; i++) 76 | { 77 | var value = varyByRules?.VaryByPrefix[i] ?? string.Empty; 78 | builder.Append(KeyDelimiter).Append(value); 79 | } 80 | } 81 | 82 | // Vary by headers 83 | var headersCount = varyByRules?.Headers.Count ?? 0; 84 | if (headersCount > 0) 85 | { 86 | // Append a group separator for the header segment of the cache key 87 | builder.Append(KeyDelimiter) 88 | .Append('H'); 89 | 90 | var requestHeaders = context.HttpContext.Request.Headers; 91 | for (var i = 0; i < headersCount; i++) 92 | { 93 | var header = varyByRules!.Headers[i] ?? string.Empty; 94 | var headerValues = requestHeaders[header]; 95 | builder.Append(KeyDelimiter) 96 | .Append(header) 97 | .Append('='); 98 | 99 | var headerValuesArray = headerValues.ToArray(); 100 | Array.Sort(headerValuesArray, StringComparer.Ordinal); 101 | 102 | for (var j = 0; j < headerValuesArray.Length; j++) 103 | { 104 | builder.Append(headerValuesArray[j]); 105 | } 106 | } 107 | } 108 | 109 | // Vary by query keys 110 | if (varyByRules?.QueryKeys.Count > 0) 111 | { 112 | // Append a group separator for the query key segment of the cache key 113 | builder.Append(KeyDelimiter) 114 | .Append('Q'); 115 | 116 | if (varyByRules.QueryKeys.Count == 1 && string.Equals(varyByRules.QueryKeys[0], "*", StringComparison.Ordinal) && context.HttpContext.Request.Query.Count > 0) 117 | { 118 | // Vary by all available query keys 119 | var queryArray = context.HttpContext.Request.Query.ToArray(); 120 | // Query keys are aggregated case-insensitively whereas the query values are compared ordinally. 121 | Array.Sort(queryArray, QueryKeyComparer.OrdinalIgnoreCase); 122 | 123 | for (var i = 0; i < queryArray.Length; i++) 124 | { 125 | builder.Append(KeyDelimiter) 126 | .AppendUpperInvariant(queryArray[i].Key) 127 | .Append('='); 128 | 129 | var queryValueArray = queryArray[i].Value.ToArray(); 130 | Array.Sort(queryValueArray, StringComparer.Ordinal); 131 | 132 | for (var j = 0; j < queryValueArray.Length; j++) 133 | { 134 | if (j > 0) 135 | { 136 | builder.Append(KeySubDelimiter); 137 | } 138 | 139 | builder.Append(queryValueArray[j]); 140 | } 141 | } 142 | } 143 | else 144 | { 145 | for (var i = 0; i < varyByRules.QueryKeys.Count; i++) 146 | { 147 | var queryKey = varyByRules.QueryKeys[i] ?? string.Empty; 148 | var queryKeyValues = context.HttpContext.Request.Query[queryKey]; 149 | builder.Append(KeyDelimiter) 150 | .Append(queryKey) 151 | .Append('='); 152 | 153 | var queryValueArray = queryKeyValues.ToArray(); 154 | Array.Sort(queryValueArray, StringComparer.Ordinal); 155 | 156 | for (var j = 0; j < queryValueArray.Length; j++) 157 | { 158 | if (j > 0) 159 | { 160 | builder.Append(KeySubDelimiter); 161 | } 162 | 163 | builder.Append(queryValueArray[j]); 164 | } 165 | } 166 | } 167 | } 168 | 169 | return builder.ToString(); 170 | } 171 | finally 172 | { 173 | _builderPool.Return(builder); 174 | } 175 | } 176 | 177 | private sealed class QueryKeyComparer : IComparer> 178 | { 179 | private readonly StringComparer _stringComparer; 180 | 181 | public static QueryKeyComparer OrdinalIgnoreCase { get; } = new QueryKeyComparer(StringComparer.OrdinalIgnoreCase); 182 | 183 | public QueryKeyComparer(StringComparer stringComparer) 184 | { 185 | _stringComparer = stringComparer; 186 | } 187 | 188 | public int Compare(KeyValuePair x, KeyValuePair y) => _stringComparer.Compare(x.Key, y.Key); 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /src/OutputCachePolicyBuilder.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using System.Diagnostics.CodeAnalysis; 5 | using System.Linq; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.AspNetCore.OutputCaching.Policies; 8 | 9 | namespace Microsoft.AspNetCore.OutputCaching; 10 | 11 | /// 12 | /// Provides helper methods to create custom policies. 13 | /// 14 | public sealed class OutputCachePolicyBuilder 15 | { 16 | private const DynamicallyAccessedMemberTypes ActivatorAccessibility = DynamicallyAccessedMemberTypes.PublicConstructors; 17 | 18 | private IOutputCachePolicy? _builtPolicy; 19 | private readonly List _policies = new(); 20 | private List>>? _requirements; 21 | 22 | /// 23 | /// Creates a new instance. 24 | /// 25 | public OutputCachePolicyBuilder() 26 | { 27 | _builtPolicy = null; 28 | _policies.Add(DefaultPolicy.Instance); 29 | } 30 | 31 | internal OutputCachePolicyBuilder AddPolicy(IOutputCachePolicy policy) 32 | { 33 | _builtPolicy = null; 34 | _policies.Add(policy); 35 | return this; 36 | } 37 | 38 | /// 39 | /// Adds a dynamically resolved policy. 40 | /// 41 | /// The type of policy to add 42 | public OutputCachePolicyBuilder AddPolicy([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type policyType) 43 | { 44 | return AddPolicy(new TypedPolicy(policyType)); 45 | } 46 | 47 | /// 48 | /// Adds a dynamically resolved policy. 49 | /// 50 | /// The policy type. 51 | public OutputCachePolicyBuilder AddPolicy<[DynamicallyAccessedMembers(ActivatorAccessibility)] T>() where T : IOutputCachePolicy 52 | { 53 | return AddPolicy(typeof(T)); 54 | } 55 | 56 | /// 57 | /// Adds a requirement to the current policy. 58 | /// 59 | /// The predicate applied to the policy. 60 | public OutputCachePolicyBuilder With(Func> predicate) 61 | { 62 | ArgumentNullException.ThrowIfNull(predicate); 63 | 64 | _builtPolicy = null; 65 | _requirements ??= new(); 66 | _requirements.Add(predicate); 67 | return this; 68 | } 69 | 70 | /// 71 | /// Adds a requirement to the current policy. 72 | /// 73 | /// The predicate applied to the policy. 74 | public OutputCachePolicyBuilder With(Func predicate) 75 | { 76 | ArgumentNullException.ThrowIfNull(predicate); 77 | 78 | _builtPolicy = null; 79 | _requirements ??= new(); 80 | _requirements.Add((c, t) => Task.FromResult(predicate(c))); 81 | return this; 82 | } 83 | 84 | /// 85 | /// Adds a policy to vary the cached responses by query strings. 86 | /// 87 | /// The query keys to vary the cached responses by. Leave empty to ignore all query strings. 88 | /// 89 | /// By default all query keys vary the cache entries. However when specific query keys are specified only these are then taken into account. 90 | /// 91 | public OutputCachePolicyBuilder VaryByQuery(params string[] queryKeys) 92 | { 93 | ArgumentNullException.ThrowIfNull(queryKeys); 94 | 95 | return AddPolicy(new VaryByQueryPolicy(queryKeys)); 96 | } 97 | 98 | /// 99 | /// Adds a policy to vary the cached responses by header. 100 | /// 101 | /// The headers to vary the cached responses by. 102 | public OutputCachePolicyBuilder VaryByHeader(params string[] headers) 103 | { 104 | ArgumentNullException.ThrowIfNull(headers); 105 | 106 | return AddPolicy(new VaryByHeaderPolicy(headers)); 107 | } 108 | 109 | /// 110 | /// Adds a policy to vary the cached responses by custom values. 111 | /// 112 | /// The value to vary the cached responses by. 113 | public OutputCachePolicyBuilder VaryByValue(Func> varyBy) 114 | { 115 | ArgumentNullException.ThrowIfNull(varyBy); 116 | 117 | return AddPolicy(new VaryByValuePolicy(varyBy)); 118 | } 119 | 120 | /// 121 | /// Adds a policy to vary the cached responses by custom key/value. 122 | /// 123 | /// The key/value to vary the cached responses by. 124 | public OutputCachePolicyBuilder VaryByValue(Func>> varyBy) 125 | { 126 | ArgumentNullException.ThrowIfNull(varyBy); 127 | 128 | return AddPolicy(new VaryByValuePolicy(varyBy)); 129 | } 130 | 131 | /// 132 | /// Adds a policy to vary the cached responses by custom values. 133 | /// 134 | /// The value to vary the cached responses by. 135 | public OutputCachePolicyBuilder VaryByValue(Func varyBy) 136 | { 137 | ArgumentNullException.ThrowIfNull(varyBy); 138 | 139 | return AddPolicy(new VaryByValuePolicy(varyBy)); 140 | } 141 | 142 | /// 143 | /// Adds a policy to vary the cached responses by custom key/value. 144 | /// 145 | /// The key/value to vary the cached responses by. 146 | public OutputCachePolicyBuilder VaryByValue(Func> varyBy) 147 | { 148 | ArgumentNullException.ThrowIfNull(varyBy); 149 | 150 | return AddPolicy(new VaryByValuePolicy(varyBy)); 151 | } 152 | 153 | /// 154 | /// Adds a policy to tag the cached response. 155 | /// 156 | /// The tags to add to the cached reponse. 157 | public OutputCachePolicyBuilder Tag(params string[] tags) 158 | { 159 | ArgumentNullException.ThrowIfNull(tags); 160 | 161 | return AddPolicy(new TagsPolicy(tags)); 162 | } 163 | 164 | /// 165 | /// Adds a policy to change the cached response expiration. 166 | /// 167 | /// The expiration of the cached reponse. 168 | public OutputCachePolicyBuilder Expire(TimeSpan expiration) 169 | { 170 | return AddPolicy(new ExpirationPolicy(expiration)); 171 | } 172 | 173 | /// 174 | /// Adds a policy to change the request locking strategy. 175 | /// 176 | /// Whether the request should be locked. 177 | public OutputCachePolicyBuilder AllowLocking(bool lockResponse = true) 178 | { 179 | return AddPolicy(lockResponse ? LockingPolicy.Enabled : LockingPolicy.Disabled); 180 | } 181 | 182 | /// 183 | /// Clears the current policies. 184 | /// 185 | /// It also removed the default cache policy. 186 | public OutputCachePolicyBuilder Clear() 187 | { 188 | _builtPolicy = null; 189 | if (_requirements != null) 190 | { 191 | _requirements.Clear(); 192 | } 193 | _policies.Clear(); 194 | return this; 195 | } 196 | 197 | /// 198 | /// Clears the policies and adds one preventing any caching logic to happen. 199 | /// 200 | /// 201 | /// The cache key will never be computed. 202 | /// 203 | public OutputCachePolicyBuilder NoCache() 204 | { 205 | _policies.Clear(); 206 | return AddPolicy(EnableCachePolicy.Disabled); 207 | } 208 | 209 | /// 210 | /// Creates the . 211 | /// 212 | /// The instance. 213 | internal IOutputCachePolicy Build() 214 | { 215 | if (_builtPolicy != null) 216 | { 217 | return _builtPolicy; 218 | } 219 | 220 | var policies = _policies.Count == 1 221 | ? _policies[0] 222 | : new CompositePolicy(_policies.ToArray()) 223 | ; 224 | 225 | // If the policy was built with requirements, wrap it 226 | if (_requirements != null && _requirements.Any()) 227 | { 228 | policies = new PredicatePolicy(async c => 229 | { 230 | foreach (var r in _requirements) 231 | { 232 | if (!await r(c, c.HttpContext.RequestAborted)) 233 | { 234 | return false; 235 | } 236 | } 237 | 238 | return true; 239 | }, policies); 240 | } 241 | 242 | return _builtPolicy = policies; 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /src/OutputCacheMiddleware.cs: -------------------------------------------------------------------------------- 1 | // Licensed to the .NET Foundation under one or more agreements. 2 | // The .NET Foundation licenses this file to you under the MIT license. 3 | 4 | using System.Linq; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.Extensions.Logging; 7 | using Microsoft.Extensions.ObjectPool; 8 | using Microsoft.Extensions.Options; 9 | using Microsoft.Extensions.Primitives; 10 | using Microsoft.Net.Http.Headers; 11 | 12 | namespace Microsoft.AspNetCore.OutputCaching; 13 | 14 | /// 15 | /// Enable HTTP response caching. 16 | /// 17 | internal sealed class OutputCacheMiddleware 18 | { 19 | // see https://tools.ietf.org/html/rfc7232#section-4.1 20 | private static readonly string[] HeadersToIncludeIn304 = 21 | new[] { "Cache-Control", "Content-Location", "Date", "ETag", "Expires", "Vary" }; 22 | 23 | private readonly RequestDelegate _next; 24 | private readonly OutputCacheOptions _options; 25 | private readonly ILogger _logger; 26 | private readonly IOutputCacheStore _store; 27 | private readonly IOutputCacheKeyProvider _keyProvider; 28 | private readonly WorkDispatcher _outputCacheEntryDispatcher; 29 | private readonly WorkDispatcher _requestDispatcher; 30 | 31 | /// 32 | /// Creates a new . 33 | /// 34 | /// The representing the next middleware in the pipeline. 35 | /// The options for this middleware. 36 | /// The used for logging. 37 | /// The store. 38 | /// The used for creating instances. 39 | public OutputCacheMiddleware( 40 | RequestDelegate next, 41 | IOptions options, 42 | ILoggerFactory loggerFactory, 43 | IOutputCacheStore outputCache, 44 | ObjectPoolProvider poolProvider 45 | ) 46 | : this( 47 | next, 48 | options, 49 | loggerFactory, 50 | outputCache, 51 | new OutputCacheKeyProvider(poolProvider, options)) 52 | { } 53 | 54 | // for testing 55 | internal OutputCacheMiddleware( 56 | RequestDelegate next, 57 | IOptions options, 58 | ILoggerFactory loggerFactory, 59 | IOutputCacheStore cache, 60 | IOutputCacheKeyProvider keyProvider) 61 | { 62 | ArgumentNullException.ThrowIfNull(next); 63 | ArgumentNullException.ThrowIfNull(options); 64 | ArgumentNullException.ThrowIfNull(loggerFactory); 65 | ArgumentNullException.ThrowIfNull(cache); 66 | ArgumentNullException.ThrowIfNull(keyProvider); 67 | 68 | _next = next; 69 | _options = options.Value; 70 | _logger = loggerFactory.CreateLogger(); 71 | _store = cache; 72 | _keyProvider = keyProvider; 73 | _outputCacheEntryDispatcher = new(); 74 | _requestDispatcher = new(); 75 | } 76 | 77 | /// 78 | /// Invokes the logic of the middleware. 79 | /// 80 | /// The . 81 | /// A that completes when the middleware has completed processing. 82 | public Task Invoke(HttpContext httpContext) 83 | { 84 | // Skip the middleware if there is no policy for the current request 85 | if (!TryGetRequestPolicies(httpContext, out var policies)) 86 | { 87 | return _next(httpContext); 88 | } 89 | 90 | return InvokeAwaited(httpContext, policies); 91 | } 92 | 93 | private async Task InvokeAwaited(HttpContext httpContext, IReadOnlyList policies) 94 | { 95 | var context = new OutputCacheContext(httpContext, _store, _options, _logger); 96 | 97 | // Add IOutputCacheFeature 98 | AddOutputCacheFeature(context); 99 | 100 | try 101 | { 102 | foreach (var policy in policies) 103 | { 104 | await policy.CacheRequestAsync(context, httpContext.RequestAborted); 105 | } 106 | 107 | // Should we attempt any caching logic? 108 | if (context.EnableOutputCaching) 109 | { 110 | // Can this request be served from cache? 111 | if (context.AllowCacheLookup) 112 | { 113 | if (await TryServeFromCacheAsync(context, policies)) 114 | { 115 | return; 116 | } 117 | } 118 | 119 | // Should we store the response to this request? 120 | if (context.AllowCacheStorage) 121 | { 122 | // It is also a pre-condition to reponse locking 123 | 124 | var executed = false; 125 | 126 | if (context.AllowLocking) 127 | { 128 | var cacheEntry = await _requestDispatcher.ScheduleAsync(context.CacheKey, key => ExecuteResponseAsync()); 129 | 130 | // The current request was processed, nothing more to do 131 | if (executed) 132 | { 133 | return; 134 | } 135 | 136 | // If the result was processed by another request, try to serve it from cache entry (no lookup) 137 | if (await TryServeCachedResponseAsync(context, cacheEntry, policies)) 138 | { 139 | return; 140 | } 141 | 142 | // If the cache entry couldn't be served, continue to processing the request as usual 143 | } 144 | 145 | await ExecuteResponseAsync(); 146 | 147 | async Task ExecuteResponseAsync() 148 | { 149 | // Hook up to listen to the response stream 150 | ShimResponseStream(context); 151 | 152 | try 153 | { 154 | await _next(httpContext); 155 | 156 | // The next middleware might change the policy 157 | foreach (var policy in policies) 158 | { 159 | await policy.ServeResponseAsync(context, httpContext.RequestAborted); 160 | } 161 | 162 | // If there was no response body, check the response headers now. We can cache things like redirects. 163 | StartResponse(context); 164 | 165 | // Finalize the cache entry 166 | await FinalizeCacheBodyAsync(context); 167 | 168 | executed = true; 169 | } 170 | finally 171 | { 172 | UnshimResponseStream(context); 173 | } 174 | 175 | return context.CachedResponse; 176 | } 177 | 178 | return; 179 | } 180 | } 181 | 182 | await _next(httpContext); 183 | } 184 | finally 185 | { 186 | RemoveOutputCacheFeature(httpContext); 187 | } 188 | } 189 | 190 | internal bool TryGetRequestPolicies(HttpContext httpContext, out IReadOnlyList policies) 191 | { 192 | policies = Array.Empty(); 193 | List? result = null; 194 | 195 | if (_options.BasePolicies != null) 196 | { 197 | result = new(); 198 | result.AddRange(_options.BasePolicies); 199 | } 200 | 201 | var metadata = httpContext.GetEndpoint()?.Metadata; 202 | 203 | var policy = metadata?.GetMetadata(); 204 | 205 | if (policy != null) 206 | { 207 | result ??= new(); 208 | result.Add(policy); 209 | } 210 | 211 | var attribute = metadata?.GetMetadata(); 212 | 213 | if (attribute != null) 214 | { 215 | result ??= new(); 216 | result.Add(attribute.BuildPolicy()); 217 | } 218 | 219 | if (result != null) 220 | { 221 | policies = result; 222 | return true; 223 | } 224 | 225 | return false; 226 | } 227 | 228 | internal async Task TryServeCachedResponseAsync(OutputCacheContext context, OutputCacheEntry? cacheEntry, IReadOnlyList policies) 229 | { 230 | if (cacheEntry == null) 231 | { 232 | return false; 233 | } 234 | 235 | context.CachedResponse = cacheEntry; 236 | context.ResponseTime = _options.SystemClock.UtcNow; 237 | var cacheEntryAge = context.ResponseTime.Value - context.CachedResponse.Created; 238 | context.CachedEntryAge = cacheEntryAge > TimeSpan.Zero ? cacheEntryAge : TimeSpan.Zero; 239 | 240 | foreach (var policy in policies) 241 | { 242 | await policy.ServeFromCacheAsync(context, context.HttpContext.RequestAborted); 243 | } 244 | 245 | context.IsCacheEntryFresh = true; 246 | 247 | // Validate expiration 248 | if (context.CachedEntryAge <= TimeSpan.Zero) 249 | { 250 | context.Logger.ExpirationExpiresExceeded(context.ResponseTime!.Value); 251 | context.IsCacheEntryFresh = false; 252 | } 253 | 254 | if (context.IsCacheEntryFresh) 255 | { 256 | var cachedResponseHeaders = context.CachedResponse.Headers; 257 | 258 | // Check conditional request rules 259 | if (ContentIsNotModified(context)) 260 | { 261 | _logger.NotModifiedServed(); 262 | context.HttpContext.Response.StatusCode = StatusCodes.Status304NotModified; 263 | 264 | if (cachedResponseHeaders != null) 265 | { 266 | foreach (var key in HeadersToIncludeIn304) 267 | { 268 | if (cachedResponseHeaders.TryGetValue(key, out var values)) 269 | { 270 | context.HttpContext.Response.Headers[key] = values; 271 | } 272 | } 273 | } 274 | } 275 | else 276 | { 277 | var response = context.HttpContext.Response; 278 | // Copy the cached status code and response headers 279 | response.StatusCode = context.CachedResponse.StatusCode; 280 | foreach (var header in context.CachedResponse.Headers) 281 | { 282 | response.Headers[header.Key] = header.Value; 283 | } 284 | 285 | // Note: int64 division truncates result and errors may be up to 1 second. This reduction in 286 | // accuracy of age calculation is considered appropriate since it is small compared to clock 287 | // skews and the "Age" header is an estimate of the real age of cached content. 288 | response.Headers.Age = HeaderUtilities.FormatNonNegativeInt64(context.CachedEntryAge.Ticks / TimeSpan.TicksPerSecond); 289 | 290 | // Copy the cached response body 291 | var body = context.CachedResponse.Body; 292 | if (body.Length > 0) 293 | { 294 | try 295 | { 296 | await body.CopyToAsync(response.BodyWriter, context.HttpContext.RequestAborted); 297 | } 298 | catch (OperationCanceledException) 299 | { 300 | context.HttpContext.Abort(); 301 | } 302 | } 303 | _logger.CachedResponseServed(); 304 | } 305 | return true; 306 | } 307 | 308 | return false; 309 | } 310 | 311 | internal async Task TryServeFromCacheAsync(OutputCacheContext cacheContext, IReadOnlyList policies) 312 | { 313 | CreateCacheKey(cacheContext); 314 | 315 | // Locking cache lookups by default 316 | // TODO: should it be part of the cache implementations or can we assume all caches would benefit from it? 317 | // It makes sense for caches that use IO (disk, network) or need to deserialize the state but could also be a global option 318 | 319 | var cacheEntry = await _outputCacheEntryDispatcher.ScheduleAsync(cacheContext.CacheKey, cacheContext, static async (key, cacheContext) => await OutputCacheEntryFormatter.GetAsync(key, cacheContext.Store, cacheContext.HttpContext.RequestAborted)); 320 | 321 | if (await TryServeCachedResponseAsync(cacheContext, cacheEntry, policies)) 322 | { 323 | return true; 324 | } 325 | 326 | if (HeaderUtilities.ContainsCacheDirective(cacheContext.HttpContext.Request.Headers.CacheControl, CacheControlHeaderValue.OnlyIfCachedString)) 327 | { 328 | _logger.GatewayTimeoutServed(); 329 | cacheContext.HttpContext.Response.StatusCode = StatusCodes.Status504GatewayTimeout; 330 | return true; 331 | } 332 | 333 | _logger.NoResponseServed(); 334 | return false; 335 | } 336 | 337 | internal void CreateCacheKey(OutputCacheContext context) 338 | { 339 | if (!string.IsNullOrEmpty(context.CacheKey)) 340 | { 341 | return; 342 | } 343 | 344 | var varyHeaders = context.CacheVaryByRules.Headers; 345 | var varyQueryKeys = context.CacheVaryByRules.QueryKeys; 346 | var varyByCustomKeys = context.CacheVaryByRules.VaryByCustom; 347 | var varyByPrefix = context.CacheVaryByRules.VaryByPrefix; 348 | 349 | // Check if any vary rules exist 350 | if (!StringValues.IsNullOrEmpty(varyHeaders) || !StringValues.IsNullOrEmpty(varyQueryKeys) || !StringValues.IsNullOrEmpty(varyByPrefix) || varyByCustomKeys?.Count > 0) 351 | { 352 | // Normalize order and casing of vary by rules 353 | var normalizedVaryHeaders = GetOrderCasingNormalizedStringValues(varyHeaders); 354 | var normalizedVaryQueryKeys = GetOrderCasingNormalizedStringValues(varyQueryKeys); 355 | var normalizedVaryByCustom = GetOrderCasingNormalizedDictionary(varyByCustomKeys); 356 | 357 | // Update vary rules with normalized values 358 | context.CacheVaryByRules = new CacheVaryByRules 359 | { 360 | VaryByPrefix = varyByPrefix + normalizedVaryByCustom, 361 | Headers = normalizedVaryHeaders, 362 | QueryKeys = normalizedVaryQueryKeys 363 | }; 364 | 365 | // TODO: Add same condition on LogLevel in Response Caching 366 | // Always overwrite the CachedVaryByRules to update the expiry information 367 | if (_logger.IsEnabled(LogLevel.Debug)) 368 | { 369 | _logger.VaryByRulesUpdated(normalizedVaryHeaders.ToString(), normalizedVaryQueryKeys.ToString()); 370 | } 371 | } 372 | 373 | context.CacheKey = _keyProvider.CreateStorageKey(context); 374 | } 375 | 376 | /// 377 | /// Finalize cache headers. 378 | /// 379 | /// 380 | internal void FinalizeCacheHeaders(OutputCacheContext context) 381 | { 382 | if (context.AllowCacheStorage) 383 | { 384 | // Create the cache entry now 385 | var response = context.HttpContext.Response; 386 | var headers = response.Headers; 387 | 388 | context.CachedResponseValidFor = context.ResponseExpirationTimeSpan ?? _options.DefaultExpirationTimeSpan; 389 | 390 | // Setting the date on the raw response headers. 391 | headers.Date = HeaderUtilities.FormatDate(context.ResponseTime!.Value); 392 | 393 | // Store the response on the state 394 | context.CachedResponse = new OutputCacheEntry 395 | { 396 | Created = context.ResponseTime!.Value, 397 | StatusCode = response.StatusCode, 398 | Headers = new HeaderDictionary(), 399 | Tags = context.Tags.ToArray() 400 | }; 401 | 402 | foreach (var header in headers) 403 | { 404 | if (!string.Equals(header.Key, HeaderNames.Age, StringComparison.OrdinalIgnoreCase)) 405 | { 406 | context.CachedResponse.Headers[header.Key] = header.Value; 407 | } 408 | } 409 | 410 | return; 411 | } 412 | 413 | context.OutputCacheStream.DisableBuffering(); 414 | } 415 | 416 | /// 417 | /// Stores the response body 418 | /// 419 | internal async ValueTask FinalizeCacheBodyAsync(OutputCacheContext context) 420 | { 421 | if (context.AllowCacheStorage && context.OutputCacheStream.BufferingEnabled) 422 | { 423 | // If AllowCacheLookup is false, the cache key was not created 424 | CreateCacheKey(context); 425 | 426 | var contentLength = context.HttpContext.Response.ContentLength; 427 | var cachedResponseBody = context.OutputCacheStream.GetCachedResponseBody(); 428 | if (!contentLength.HasValue || contentLength == cachedResponseBody.Length 429 | || (cachedResponseBody.Length == 0 430 | && HttpMethods.IsHead(context.HttpContext.Request.Method))) 431 | { 432 | var response = context.HttpContext.Response; 433 | // Add a content-length if required 434 | if (!response.ContentLength.HasValue && StringValues.IsNullOrEmpty(response.Headers.TransferEncoding)) 435 | { 436 | context.CachedResponse.Headers.ContentLength = cachedResponseBody.Length; 437 | } 438 | 439 | context.CachedResponse.Body = cachedResponseBody; 440 | _logger.ResponseCached(); 441 | 442 | if (string.IsNullOrEmpty(context.CacheKey)) 443 | { 444 | throw new InvalidOperationException("Cache key must be defined"); 445 | } 446 | 447 | await OutputCacheEntryFormatter.StoreAsync(context.CacheKey, context.CachedResponse, context.CachedResponseValidFor, _store, context.HttpContext.RequestAborted); 448 | } 449 | else 450 | { 451 | _logger.ResponseContentLengthMismatchNotCached(); 452 | } 453 | } 454 | else 455 | { 456 | _logger.ResponseNotCached(); 457 | } 458 | } 459 | 460 | /// 461 | /// Mark the response as started and set the response time if no response was started yet. 462 | /// 463 | /// 464 | /// true if the response was not started before this call; otherwise false. 465 | private bool OnStartResponse(OutputCacheContext context) 466 | { 467 | if (!context.ResponseStarted) 468 | { 469 | context.ResponseStarted = true; 470 | context.ResponseTime = _options.SystemClock.UtcNow; 471 | 472 | return true; 473 | } 474 | return false; 475 | } 476 | 477 | internal void StartResponse(OutputCacheContext context) 478 | { 479 | if (OnStartResponse(context)) 480 | { 481 | FinalizeCacheHeaders(context); 482 | } 483 | } 484 | 485 | internal static void AddOutputCacheFeature(OutputCacheContext context) 486 | { 487 | if (context.HttpContext.Features.Get() != null) 488 | { 489 | throw new InvalidOperationException($"Another instance of {nameof(OutputCacheFeature)} already exists. Only one instance of {nameof(OutputCacheMiddleware)} can be configured for an application."); 490 | } 491 | 492 | context.HttpContext.Features.Set(new OutputCacheFeature(context)); 493 | } 494 | 495 | internal void ShimResponseStream(OutputCacheContext context) 496 | { 497 | // Shim response stream 498 | context.OriginalResponseStream = context.HttpContext.Response.Body; 499 | context.OutputCacheStream = new OutputCacheStream( 500 | context.OriginalResponseStream, 501 | _options.MaximumBodySize, 502 | StreamUtilities.BodySegmentSize, 503 | () => StartResponse(context)); 504 | context.HttpContext.Response.Body = context.OutputCacheStream; 505 | } 506 | 507 | internal static void RemoveOutputCacheFeature(HttpContext context) => 508 | context.Features.Set(null); 509 | 510 | internal static void UnshimResponseStream(OutputCacheContext context) 511 | { 512 | // Unshim response stream 513 | context.HttpContext.Response.Body = context.OriginalResponseStream; 514 | 515 | // Remove IOutputCachingFeature 516 | RemoveOutputCacheFeature(context.HttpContext); 517 | } 518 | 519 | internal static bool ContentIsNotModified(OutputCacheContext context) 520 | { 521 | var cachedResponseHeaders = context.CachedResponse.Headers; 522 | var ifNoneMatchHeader = context.HttpContext.Request.Headers.IfNoneMatch; 523 | 524 | if (!StringValues.IsNullOrEmpty(ifNoneMatchHeader)) 525 | { 526 | if (ifNoneMatchHeader.Count == 1 && StringSegment.Equals(ifNoneMatchHeader[0], EntityTagHeaderValue.Any.Tag, StringComparison.OrdinalIgnoreCase)) 527 | { 528 | context.Logger.NotModifiedIfNoneMatchStar(); 529 | return true; 530 | } 531 | 532 | if (!StringValues.IsNullOrEmpty(cachedResponseHeaders[HeaderNames.ETag]) 533 | && EntityTagHeaderValue.TryParse(cachedResponseHeaders[HeaderNames.ETag].ToString(), out var eTag) 534 | && EntityTagHeaderValue.TryParseList(ifNoneMatchHeader, out var ifNoneMatchEtags)) 535 | { 536 | for (var i = 0; i < ifNoneMatchEtags?.Count; i++) 537 | { 538 | var requestETag = ifNoneMatchEtags[i]; 539 | if (eTag.Compare(requestETag, useStrongComparison: false)) 540 | { 541 | context.Logger.NotModifiedIfNoneMatchMatched(requestETag); 542 | return true; 543 | } 544 | } 545 | } 546 | } 547 | else 548 | { 549 | var ifModifiedSince = context.HttpContext.Request.Headers.IfModifiedSince; 550 | if (!StringValues.IsNullOrEmpty(ifModifiedSince)) 551 | { 552 | if (!HeaderUtilities.TryParseDate(cachedResponseHeaders[HeaderNames.LastModified].ToString(), out var modified) && 553 | !HeaderUtilities.TryParseDate(cachedResponseHeaders[HeaderNames.Date].ToString(), out modified)) 554 | { 555 | return false; 556 | } 557 | 558 | if (HeaderUtilities.TryParseDate(ifModifiedSince.ToString(), out var modifiedSince) && 559 | modified <= modifiedSince) 560 | { 561 | context.Logger.NotModifiedIfModifiedSinceSatisfied(modified, modifiedSince); 562 | return true; 563 | } 564 | } 565 | } 566 | 567 | return false; 568 | } 569 | 570 | // Normalize order and casing 571 | internal static StringValues GetOrderCasingNormalizedStringValues(StringValues stringValues) 572 | { 573 | if (stringValues.Count == 0) 574 | { 575 | return StringValues.Empty; 576 | } 577 | else if (stringValues.Count == 1) 578 | { 579 | return new StringValues(stringValues.ToString().ToUpperInvariant()); 580 | } 581 | else 582 | { 583 | var originalArray = stringValues.ToArray(); 584 | var newArray = new string[originalArray.Length]; 585 | 586 | for (var i = 0; i < originalArray.Length; i++) 587 | { 588 | newArray[i] = originalArray[i]!.ToUpperInvariant(); 589 | } 590 | 591 | // Since the casing has already been normalized, use Ordinal comparison 592 | Array.Sort(newArray, StringComparer.Ordinal); 593 | 594 | return new StringValues(newArray); 595 | } 596 | } 597 | 598 | internal static StringValues GetOrderCasingNormalizedDictionary(IDictionary? dictionary) 599 | { 600 | const char KeySubDelimiter = '\x1f'; 601 | 602 | if (dictionary == null || dictionary.Count == 0) 603 | { 604 | return StringValues.Empty; 605 | } 606 | 607 | var newArray = new string[dictionary.Count]; 608 | 609 | var i = 0; 610 | foreach (var (key, value) in dictionary) 611 | { 612 | newArray[i++] = $"{key.ToUpperInvariant()}{KeySubDelimiter}{value}"; 613 | } 614 | 615 | // Since the casing has already been normalized, use Ordinal comparison 616 | Array.Sort(newArray, StringComparer.Ordinal); 617 | 618 | return new StringValues(newArray); 619 | } 620 | } 621 | --------------------------------------------------------------------------------