From ecb85f0cdbfe18c3e97e7aada5307eebb25963fa Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Mon, 4 Apr 2022 16:06:59 -0700 Subject: [PATCH 01/48] Create output caching middleware --- AspNetCore.sln | 85 ++ eng/ProjectReferences.props | 2 + eng/SharedFramework.Local.props | 2 + src/Analyzers/Analyzers.slnf | 1 + src/Components/Components.slnf | 1 + src/Framework/Framework.slnf | 2 + src/Framework/test/TestData.cs | 4 + src/Identity/Identity.slnf | 1 + .../Properties/launchSettings.json | 2 +- .../Properties/launchSettings.json | 2 +- .../Properties/launchSettings.json | 2 +- .../Properties/launchSettings.json | 2 +- .../Properties/launchSettings.json | 2 +- .../Properties/launchSettings.json | 2 +- .../Properties/launchSettings.json | 2 +- src/Middleware/Middleware.slnf | 4 + .../src/CachedResponseBody.cs | 41 + .../src/CachedVaryByRules.cs | 18 + .../src/IOutputCache.cs | 29 + .../src/IOutputCachingContext.cs | 61 ++ .../src/IOutputCachingFeature.cs | 14 + .../src/IOutputCachingPolicy.cs | 14 + .../src/IOutputCachingPolicyProvider.cs | 31 + .../src/IPoliciesMetadata.cs | 9 + ...pNetCore.OutputCaching.Abstractions.csproj | 19 + .../src/OutputCacheEntry.cs | 18 + .../src/PublicAPI.Shipped.txt | 1 + .../src/PublicAPI.Unshipped.txt | 0 .../OutputCaching/OutputCaching.slnf | 10 + .../OutputCachingSample/.vscode/launch.json | 35 + .../OutputCachingSample/.vscode/tasks.json | 41 + .../samples/OutputCachingSample/Gravatar.cs | 17 + .../OutputCachingSample.csproj | 20 + .../Properties/launchSettings.json | 27 + .../samples/OutputCachingSample/README.md | 6 + .../samples/OutputCachingSample/Startup.cs | 78 ++ .../appsettings.Development.json | 10 + .../OutputCachingSample/appsettings.json | 10 + .../OutputCaching/src/CacheEntryHelpers.cs | 59 ++ .../OutputCaching/src/DispatcherExtensions.cs | 89 ++ src/Middleware/OutputCaching/src/FastGuid.cs | 73 ++ .../Interfaces/IOutputCachingKeyProvider.cs | 14 + .../src/Interfaces/ISystemClock.cs | 15 + .../OutputCaching/src/LoggerExtensions.cs | 123 +++ .../src/Memory/MemoryCachedResponse.cs | 18 + .../src/Memory/MemoryOutputCache.cs | 89 ++ .../Microsoft.AspNetCore.OutputCaching.csproj | 27 + .../OutputCaching/src/OutputCacheAttribute.cs | 82 ++ .../OutputCaching/src/OutputCachingContext.cs | 148 +++ .../src/OutputCachingExtensions.cs | 23 + .../OutputCaching/src/OutputCachingFeature.cs | 17 + .../src/OutputCachingKeyProvider.cs | 190 ++++ .../src/OutputCachingMiddleware.cs | 545 ++++++++++ .../OutputCaching/src/OutputCachingOptions.cs | 49 + .../src/OutputCachingPolicyProvider.cs | 102 ++ .../src/OutputCachingServicesExtensions.cs | 56 + .../src/Policies/CompositePolicy.cs | 38 + .../src/Policies/DefaultOutputCachePolicy.cs | 62 ++ .../src/Policies/EnableCachingPolicy.cs | 29 + .../src/Policies/ExpirationPolicy.cs | 34 + .../src/Policies/LockingPolicy.cs | 34 + .../src/Policies/NoStorePolicy.cs | 27 + .../src/Policies/OutputCachePolicyBuilder.cs | 182 ++++ .../src/Policies/PoliciesMetadata.cs | 9 + .../src/Policies/PolicyExtensions.cs | 65 ++ .../src/Policies/PredicatePolicy.cs | 57 + .../src/Policies/ProfilePolicy.cs | 65 ++ .../src/Policies/ResponseCachingPolicy.cs | 275 +++++ .../OutputCaching/src/Policies/TagsPolicy.cs | 37 + .../src/Policies/VaryByQueryPolicy.cs | 69 ++ .../src/Policies/VaryByValuePolicy.cs | 77 ++ .../src/Properties/AssemblyInfo.cs | 6 + .../OutputCaching/src/PublicAPI.Shipped.txt | 1 + .../OutputCaching/src/PublicAPI.Unshipped.txt | 1 + .../src/Streams/OutputCachingStream.cs | 187 ++++ .../src/Streams/SegmentWriteStream.cs | 199 ++++ .../src/Streams/StreamUtilities.cs | 13 + .../src/StringBuilderExtensions.cs | 23 + .../OutputCaching/src/SystemClock.cs | 15 + src/Middleware/OutputCaching/startvs.cmd | 3 + .../test/CachedResponseBodyTests.cs | 122 +++ ...ft.AspNetCore.ResponseCaching.Tests.csproj | 18 + .../test/ResponseCachingFeatureTests.cs | 54 + .../test/ResponseCachingKeyProviderTests.cs | 214 ++++ .../test/ResponseCachingMiddlewareTests.cs | 966 +++++++++++++++++ .../ResponseCachingPolicyProviderTests.cs | 790 ++++++++++++++ .../test/ResponseCachingTests.cs | 984 ++++++++++++++++++ .../test/SegmentWriteStreamTests.cs | 105 ++ .../OutputCaching/test/TestDocument.txt | 1 + .../OutputCaching/test/TestUtils.cs | 403 +++++++ .../src/Filters/IOutputCacheFilter.cs | 11 + .../Mvc.Core/src/Filters/OutputCacheFilter.cs | 59 ++ .../src/Microsoft.AspNetCore.Mvc.Core.csproj | 3 +- src/Mvc/Mvc.Core/src/OutputCacheAttribute.cs | 88 ++ src/Mvc/Mvc.Core/src/Resources.resx | 5 +- src/Mvc/Mvc.slnf | 2 + .../MvcSandbox/Controllers/HomeController.cs | 1 + src/Mvc/samples/MvcSandbox/MvcSandbox.csproj | 3 +- .../samples/MvcSandbox/Pages/PagesHome.cshtml | 3 +- .../MvcSandbox/Views/Home/Index.cshtml | 1 + src/Shared/TaskToApm.cs | 2 +- 101 files changed, 7675 insertions(+), 12 deletions(-) create mode 100644 src/Middleware/OutputCaching.Abstractions/src/CachedResponseBody.cs create mode 100644 src/Middleware/OutputCaching.Abstractions/src/CachedVaryByRules.cs create mode 100644 src/Middleware/OutputCaching.Abstractions/src/IOutputCache.cs create mode 100644 src/Middleware/OutputCaching.Abstractions/src/IOutputCachingContext.cs create mode 100644 src/Middleware/OutputCaching.Abstractions/src/IOutputCachingFeature.cs create mode 100644 src/Middleware/OutputCaching.Abstractions/src/IOutputCachingPolicy.cs create mode 100644 src/Middleware/OutputCaching.Abstractions/src/IOutputCachingPolicyProvider.cs create mode 100644 src/Middleware/OutputCaching.Abstractions/src/IPoliciesMetadata.cs create mode 100644 src/Middleware/OutputCaching.Abstractions/src/Microsoft.AspNetCore.OutputCaching.Abstractions.csproj create mode 100644 src/Middleware/OutputCaching.Abstractions/src/OutputCacheEntry.cs create mode 100644 src/Middleware/OutputCaching.Abstractions/src/PublicAPI.Shipped.txt create mode 100644 src/Middleware/OutputCaching.Abstractions/src/PublicAPI.Unshipped.txt create mode 100644 src/Middleware/OutputCaching/OutputCaching.slnf create mode 100644 src/Middleware/OutputCaching/samples/OutputCachingSample/.vscode/launch.json create mode 100644 src/Middleware/OutputCaching/samples/OutputCachingSample/.vscode/tasks.json create mode 100644 src/Middleware/OutputCaching/samples/OutputCachingSample/Gravatar.cs create mode 100644 src/Middleware/OutputCaching/samples/OutputCachingSample/OutputCachingSample.csproj create mode 100644 src/Middleware/OutputCaching/samples/OutputCachingSample/Properties/launchSettings.json create mode 100644 src/Middleware/OutputCaching/samples/OutputCachingSample/README.md create mode 100644 src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs create mode 100644 src/Middleware/OutputCaching/samples/OutputCachingSample/appsettings.Development.json create mode 100644 src/Middleware/OutputCaching/samples/OutputCachingSample/appsettings.json create mode 100644 src/Middleware/OutputCaching/src/CacheEntryHelpers.cs create mode 100644 src/Middleware/OutputCaching/src/DispatcherExtensions.cs create mode 100644 src/Middleware/OutputCaching/src/FastGuid.cs create mode 100644 src/Middleware/OutputCaching/src/Interfaces/IOutputCachingKeyProvider.cs create mode 100644 src/Middleware/OutputCaching/src/Interfaces/ISystemClock.cs create mode 100644 src/Middleware/OutputCaching/src/LoggerExtensions.cs create mode 100644 src/Middleware/OutputCaching/src/Memory/MemoryCachedResponse.cs create mode 100644 src/Middleware/OutputCaching/src/Memory/MemoryOutputCache.cs create mode 100644 src/Middleware/OutputCaching/src/Microsoft.AspNetCore.OutputCaching.csproj create mode 100644 src/Middleware/OutputCaching/src/OutputCacheAttribute.cs create mode 100644 src/Middleware/OutputCaching/src/OutputCachingContext.cs create mode 100644 src/Middleware/OutputCaching/src/OutputCachingExtensions.cs create mode 100644 src/Middleware/OutputCaching/src/OutputCachingFeature.cs create mode 100644 src/Middleware/OutputCaching/src/OutputCachingKeyProvider.cs create mode 100644 src/Middleware/OutputCaching/src/OutputCachingMiddleware.cs create mode 100644 src/Middleware/OutputCaching/src/OutputCachingOptions.cs create mode 100644 src/Middleware/OutputCaching/src/OutputCachingPolicyProvider.cs create mode 100644 src/Middleware/OutputCaching/src/OutputCachingServicesExtensions.cs create mode 100644 src/Middleware/OutputCaching/src/Policies/CompositePolicy.cs create mode 100644 src/Middleware/OutputCaching/src/Policies/DefaultOutputCachePolicy.cs create mode 100644 src/Middleware/OutputCaching/src/Policies/EnableCachingPolicy.cs create mode 100644 src/Middleware/OutputCaching/src/Policies/ExpirationPolicy.cs create mode 100644 src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs create mode 100644 src/Middleware/OutputCaching/src/Policies/NoStorePolicy.cs create mode 100644 src/Middleware/OutputCaching/src/Policies/OutputCachePolicyBuilder.cs create mode 100644 src/Middleware/OutputCaching/src/Policies/PoliciesMetadata.cs create mode 100644 src/Middleware/OutputCaching/src/Policies/PolicyExtensions.cs create mode 100644 src/Middleware/OutputCaching/src/Policies/PredicatePolicy.cs create mode 100644 src/Middleware/OutputCaching/src/Policies/ProfilePolicy.cs create mode 100644 src/Middleware/OutputCaching/src/Policies/ResponseCachingPolicy.cs create mode 100644 src/Middleware/OutputCaching/src/Policies/TagsPolicy.cs create mode 100644 src/Middleware/OutputCaching/src/Policies/VaryByQueryPolicy.cs create mode 100644 src/Middleware/OutputCaching/src/Policies/VaryByValuePolicy.cs create mode 100644 src/Middleware/OutputCaching/src/Properties/AssemblyInfo.cs create mode 100644 src/Middleware/OutputCaching/src/PublicAPI.Shipped.txt create mode 100644 src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt create mode 100644 src/Middleware/OutputCaching/src/Streams/OutputCachingStream.cs create mode 100644 src/Middleware/OutputCaching/src/Streams/SegmentWriteStream.cs create mode 100644 src/Middleware/OutputCaching/src/Streams/StreamUtilities.cs create mode 100644 src/Middleware/OutputCaching/src/StringBuilderExtensions.cs create mode 100644 src/Middleware/OutputCaching/src/SystemClock.cs create mode 100644 src/Middleware/OutputCaching/startvs.cmd create mode 100644 src/Middleware/OutputCaching/test/CachedResponseBodyTests.cs create mode 100644 src/Middleware/OutputCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests.csproj create mode 100644 src/Middleware/OutputCaching/test/ResponseCachingFeatureTests.cs create mode 100644 src/Middleware/OutputCaching/test/ResponseCachingKeyProviderTests.cs create mode 100644 src/Middleware/OutputCaching/test/ResponseCachingMiddlewareTests.cs create mode 100644 src/Middleware/OutputCaching/test/ResponseCachingPolicyProviderTests.cs create mode 100644 src/Middleware/OutputCaching/test/ResponseCachingTests.cs create mode 100644 src/Middleware/OutputCaching/test/SegmentWriteStreamTests.cs create mode 100644 src/Middleware/OutputCaching/test/TestDocument.txt create mode 100644 src/Middleware/OutputCaching/test/TestUtils.cs create mode 100644 src/Mvc/Mvc.Core/src/Filters/IOutputCacheFilter.cs create mode 100644 src/Mvc/Mvc.Core/src/Filters/OutputCacheFilter.cs create mode 100644 src/Mvc/Mvc.Core/src/OutputCacheAttribute.cs diff --git a/AspNetCore.sln b/AspNetCore.sln index 3dba71ff9f45..8019cd3e265c 100644 --- a/AspNetCore.sln +++ b/AspNetCore.sln @@ -1654,6 +1654,20 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Compon EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Components.SdkAnalyzers.Tests", "src\Tools\SDK-Analyzers\Components\test\Microsoft.AspNetCore.Components.SdkAnalyzers.Tests.csproj", "{DC349A25-0DBF-4468-99E1-B95C22D3A7EF}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "OutputCaching", "OutputCaching", "{AA5ABFBC-177C-421E-B743-005E0FD1248B}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "OutputCaching.Abstractions", "OutputCaching.Abstractions", "{B034044C-C1AF-4077-B413-35F0A9CF8042}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.OutputCaching.Abstractions", "src\Middleware\OutputCaching.Abstractions\src\Microsoft.AspNetCore.OutputCaching.Abstractions.csproj", "{7ABE16D3-E0A6-43B8-8E7A-A8C5A4857384}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.OutputCaching", "src\Middleware\OutputCaching\src\Microsoft.AspNetCore.OutputCaching.csproj", "{5D5A3B60-A014-447C-9126-B1FA6C821C8D}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{B5AC1D8B-9D43-4261-AE0F-6B7574656F2C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.ResponseCaching.Tests", "src\Middleware\OutputCaching\test\Microsoft.AspNetCore.ResponseCaching.Tests.csproj", "{6EBAD7A2-0FA8-4314-89F5-54009E86EE2F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OutputCachingSample", "src\Middleware\OutputCaching\samples\OutputCachingSample\OutputCachingSample.csproj", "{C3FFA4E4-0E7E-4866-A15F-034245BFD800}" +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "LinkabilityChecker", "LinkabilityChecker", "{94F95276-7CDF-44A8-B159-D09702EF6794}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LinkabilityChecker", "src\Tools\LinkabilityChecker\LinkabilityChecker.csproj", "{EA7D844B-C180-41C7-9D55-273AD88BF71F}" @@ -9957,6 +9971,70 @@ Global {DC349A25-0DBF-4468-99E1-B95C22D3A7EF}.Release|x64.Build.0 = Release|Any CPU {DC349A25-0DBF-4468-99E1-B95C22D3A7EF}.Release|x86.ActiveCfg = Release|Any CPU {DC349A25-0DBF-4468-99E1-B95C22D3A7EF}.Release|x86.Build.0 = Release|Any CPU + {7ABE16D3-E0A6-43B8-8E7A-A8C5A4857384}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7ABE16D3-E0A6-43B8-8E7A-A8C5A4857384}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7ABE16D3-E0A6-43B8-8E7A-A8C5A4857384}.Debug|arm64.ActiveCfg = Debug|Any CPU + {7ABE16D3-E0A6-43B8-8E7A-A8C5A4857384}.Debug|arm64.Build.0 = Debug|Any CPU + {7ABE16D3-E0A6-43B8-8E7A-A8C5A4857384}.Debug|x64.ActiveCfg = Debug|Any CPU + {7ABE16D3-E0A6-43B8-8E7A-A8C5A4857384}.Debug|x64.Build.0 = Debug|Any CPU + {7ABE16D3-E0A6-43B8-8E7A-A8C5A4857384}.Debug|x86.ActiveCfg = Debug|Any CPU + {7ABE16D3-E0A6-43B8-8E7A-A8C5A4857384}.Debug|x86.Build.0 = Debug|Any CPU + {7ABE16D3-E0A6-43B8-8E7A-A8C5A4857384}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7ABE16D3-E0A6-43B8-8E7A-A8C5A4857384}.Release|Any CPU.Build.0 = Release|Any CPU + {7ABE16D3-E0A6-43B8-8E7A-A8C5A4857384}.Release|arm64.ActiveCfg = Release|Any CPU + {7ABE16D3-E0A6-43B8-8E7A-A8C5A4857384}.Release|arm64.Build.0 = Release|Any CPU + {7ABE16D3-E0A6-43B8-8E7A-A8C5A4857384}.Release|x64.ActiveCfg = Release|Any CPU + {7ABE16D3-E0A6-43B8-8E7A-A8C5A4857384}.Release|x64.Build.0 = Release|Any CPU + {7ABE16D3-E0A6-43B8-8E7A-A8C5A4857384}.Release|x86.ActiveCfg = Release|Any CPU + {7ABE16D3-E0A6-43B8-8E7A-A8C5A4857384}.Release|x86.Build.0 = Release|Any CPU + {5D5A3B60-A014-447C-9126-B1FA6C821C8D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5D5A3B60-A014-447C-9126-B1FA6C821C8D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5D5A3B60-A014-447C-9126-B1FA6C821C8D}.Debug|arm64.ActiveCfg = Debug|Any CPU + {5D5A3B60-A014-447C-9126-B1FA6C821C8D}.Debug|arm64.Build.0 = Debug|Any CPU + {5D5A3B60-A014-447C-9126-B1FA6C821C8D}.Debug|x64.ActiveCfg = Debug|Any CPU + {5D5A3B60-A014-447C-9126-B1FA6C821C8D}.Debug|x64.Build.0 = Debug|Any CPU + {5D5A3B60-A014-447C-9126-B1FA6C821C8D}.Debug|x86.ActiveCfg = Debug|Any CPU + {5D5A3B60-A014-447C-9126-B1FA6C821C8D}.Debug|x86.Build.0 = Debug|Any CPU + {5D5A3B60-A014-447C-9126-B1FA6C821C8D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5D5A3B60-A014-447C-9126-B1FA6C821C8D}.Release|Any CPU.Build.0 = Release|Any CPU + {5D5A3B60-A014-447C-9126-B1FA6C821C8D}.Release|arm64.ActiveCfg = Release|Any CPU + {5D5A3B60-A014-447C-9126-B1FA6C821C8D}.Release|arm64.Build.0 = Release|Any CPU + {5D5A3B60-A014-447C-9126-B1FA6C821C8D}.Release|x64.ActiveCfg = Release|Any CPU + {5D5A3B60-A014-447C-9126-B1FA6C821C8D}.Release|x64.Build.0 = Release|Any CPU + {5D5A3B60-A014-447C-9126-B1FA6C821C8D}.Release|x86.ActiveCfg = Release|Any CPU + {5D5A3B60-A014-447C-9126-B1FA6C821C8D}.Release|x86.Build.0 = Release|Any CPU + {6EBAD7A2-0FA8-4314-89F5-54009E86EE2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6EBAD7A2-0FA8-4314-89F5-54009E86EE2F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6EBAD7A2-0FA8-4314-89F5-54009E86EE2F}.Debug|arm64.ActiveCfg = Debug|Any CPU + {6EBAD7A2-0FA8-4314-89F5-54009E86EE2F}.Debug|arm64.Build.0 = Debug|Any CPU + {6EBAD7A2-0FA8-4314-89F5-54009E86EE2F}.Debug|x64.ActiveCfg = Debug|Any CPU + {6EBAD7A2-0FA8-4314-89F5-54009E86EE2F}.Debug|x64.Build.0 = Debug|Any CPU + {6EBAD7A2-0FA8-4314-89F5-54009E86EE2F}.Debug|x86.ActiveCfg = Debug|Any CPU + {6EBAD7A2-0FA8-4314-89F5-54009E86EE2F}.Debug|x86.Build.0 = Debug|Any CPU + {6EBAD7A2-0FA8-4314-89F5-54009E86EE2F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6EBAD7A2-0FA8-4314-89F5-54009E86EE2F}.Release|Any CPU.Build.0 = Release|Any CPU + {6EBAD7A2-0FA8-4314-89F5-54009E86EE2F}.Release|arm64.ActiveCfg = Release|Any CPU + {6EBAD7A2-0FA8-4314-89F5-54009E86EE2F}.Release|arm64.Build.0 = Release|Any CPU + {6EBAD7A2-0FA8-4314-89F5-54009E86EE2F}.Release|x64.ActiveCfg = Release|Any CPU + {6EBAD7A2-0FA8-4314-89F5-54009E86EE2F}.Release|x64.Build.0 = Release|Any CPU + {6EBAD7A2-0FA8-4314-89F5-54009E86EE2F}.Release|x86.ActiveCfg = Release|Any CPU + {6EBAD7A2-0FA8-4314-89F5-54009E86EE2F}.Release|x86.Build.0 = Release|Any CPU + {C3FFA4E4-0E7E-4866-A15F-034245BFD800}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C3FFA4E4-0E7E-4866-A15F-034245BFD800}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C3FFA4E4-0E7E-4866-A15F-034245BFD800}.Debug|arm64.ActiveCfg = Debug|Any CPU + {C3FFA4E4-0E7E-4866-A15F-034245BFD800}.Debug|arm64.Build.0 = Debug|Any CPU + {C3FFA4E4-0E7E-4866-A15F-034245BFD800}.Debug|x64.ActiveCfg = Debug|Any CPU + {C3FFA4E4-0E7E-4866-A15F-034245BFD800}.Debug|x64.Build.0 = Debug|Any CPU + {C3FFA4E4-0E7E-4866-A15F-034245BFD800}.Debug|x86.ActiveCfg = Debug|Any CPU + {C3FFA4E4-0E7E-4866-A15F-034245BFD800}.Debug|x86.Build.0 = Debug|Any CPU + {C3FFA4E4-0E7E-4866-A15F-034245BFD800}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C3FFA4E4-0E7E-4866-A15F-034245BFD800}.Release|Any CPU.Build.0 = Release|Any CPU + {C3FFA4E4-0E7E-4866-A15F-034245BFD800}.Release|arm64.ActiveCfg = Release|Any CPU + {C3FFA4E4-0E7E-4866-A15F-034245BFD800}.Release|arm64.Build.0 = Release|Any CPU + {C3FFA4E4-0E7E-4866-A15F-034245BFD800}.Release|x64.ActiveCfg = Release|Any CPU + {C3FFA4E4-0E7E-4866-A15F-034245BFD800}.Release|x64.Build.0 = Release|Any CPU + {C3FFA4E4-0E7E-4866-A15F-034245BFD800}.Release|x86.ActiveCfg = Release|Any CPU + {C3FFA4E4-0E7E-4866-A15F-034245BFD800}.Release|x86.Build.0 = Release|Any CPU {EA7D844B-C180-41C7-9D55-273AD88BF71F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {EA7D844B-C180-41C7-9D55-273AD88BF71F}.Debug|Any CPU.Build.0 = Debug|Any CPU {EA7D844B-C180-41C7-9D55-273AD88BF71F}.Debug|arm64.ActiveCfg = Debug|Any CPU @@ -11064,6 +11142,13 @@ Global {CC45FA2D-128B-485D-BA6D-DFD9735CB3C3} = {6C06163A-80E9-49C1-817C-B391852BA563} {825BCF97-67A9-4834-B3A8-C3DC97A90E41} = {CC45FA2D-128B-485D-BA6D-DFD9735CB3C3} {DC349A25-0DBF-4468-99E1-B95C22D3A7EF} = {CC45FA2D-128B-485D-BA6D-DFD9735CB3C3} + {AA5ABFBC-177C-421E-B743-005E0FD1248B} = {E5963C9F-20A6-4385-B364-814D2581FADF} + {B034044C-C1AF-4077-B413-35F0A9CF8042} = {E5963C9F-20A6-4385-B364-814D2581FADF} + {7ABE16D3-E0A6-43B8-8E7A-A8C5A4857384} = {B034044C-C1AF-4077-B413-35F0A9CF8042} + {5D5A3B60-A014-447C-9126-B1FA6C821C8D} = {AA5ABFBC-177C-421E-B743-005E0FD1248B} + {B5AC1D8B-9D43-4261-AE0F-6B7574656F2C} = {AA5ABFBC-177C-421E-B743-005E0FD1248B} + {6EBAD7A2-0FA8-4314-89F5-54009E86EE2F} = {AA5ABFBC-177C-421E-B743-005E0FD1248B} + {C3FFA4E4-0E7E-4866-A15F-034245BFD800} = {B5AC1D8B-9D43-4261-AE0F-6B7574656F2C} {94F95276-7CDF-44A8-B159-D09702EF6794} = {0B200A66-B809-4ED3-A790-CB1C2E80975E} {EA7D844B-C180-41C7-9D55-273AD88BF71F} = {94F95276-7CDF-44A8-B159-D09702EF6794} {7A331A1C-E2C4-4E37-B0A0-B5AA10661229} = {8DAC59BE-CB96-4F04-909C-56C22E7665EB} diff --git a/eng/ProjectReferences.props b/eng/ProjectReferences.props index d59faa1b0daa..c3592ae84466 100644 --- a/eng/ProjectReferences.props +++ b/eng/ProjectReferences.props @@ -89,6 +89,8 @@ + + diff --git a/eng/SharedFramework.Local.props b/eng/SharedFramework.Local.props index f32bf007b8a4..0d4de82d7166 100644 --- a/eng/SharedFramework.Local.props +++ b/eng/SharedFramework.Local.props @@ -77,6 +77,8 @@ + + diff --git a/src/Analyzers/Analyzers.slnf b/src/Analyzers/Analyzers.slnf index bfb010ea2d1c..5f65fd7ed785 100644 --- a/src/Analyzers/Analyzers.slnf +++ b/src/Analyzers/Analyzers.slnf @@ -42,6 +42,7 @@ "src\\Mvc\\Mvc.ViewFeatures\\src\\Microsoft.AspNetCore.Mvc.ViewFeatures.csproj", "src\\Razor\\Razor\\src\\Microsoft.AspNetCore.Razor.csproj", "src\\Razor\\Razor.Runtime\\src\\Microsoft.AspNetCore.Razor.Runtime.csproj", + "src\\Middleware\\OutputCaching.Abstractions\\src\\Microsoft.AspNetCore.OutputCaching.Abstractions.csproj", "src\\Middleware\\ResponseCaching.Abstractions\\src\\Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj", "src\\Http\\Routing\\src\\Microsoft.AspNetCore.Routing.csproj", "src\\Http\\Routing.Abstractions\\src\\Microsoft.AspNetCore.Routing.Abstractions.csproj", diff --git a/src/Components/Components.slnf b/src/Components/Components.slnf index d867f6dc47cf..acf537d4ad4e 100644 --- a/src/Components/Components.slnf +++ b/src/Components/Components.slnf @@ -91,6 +91,7 @@ "src\\Middleware\\HttpOverrides\\src\\Microsoft.AspNetCore.HttpOverrides.csproj", "src\\Middleware\\HttpsPolicy\\src\\Microsoft.AspNetCore.HttpsPolicy.csproj", "src\\Middleware\\Localization\\src\\Microsoft.AspNetCore.Localization.csproj", + "src\\Middleware\\OutputCaching.Abstractions\\src\\Microsoft.AspNetCore.OutputCaching.Abstractions.csproj", "src\\Middleware\\ResponseCaching.Abstractions\\src\\Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj", "src\\Middleware\\ResponseCompression\\src\\Microsoft.AspNetCore.ResponseCompression.csproj", "src\\Middleware\\StaticFiles\\src\\Microsoft.AspNetCore.StaticFiles.csproj", diff --git a/src/Framework/Framework.slnf b/src/Framework/Framework.slnf index e24240682606..847e9c2db095 100644 --- a/src/Framework/Framework.slnf +++ b/src/Framework/Framework.slnf @@ -56,6 +56,8 @@ "src\\Middleware\\HttpsPolicy\\src\\Microsoft.AspNetCore.HttpsPolicy.csproj", "src\\Middleware\\Localization.Routing\\src\\Microsoft.AspNetCore.Localization.Routing.csproj", "src\\Middleware\\Localization\\src\\Microsoft.AspNetCore.Localization.csproj", + "src\\Middleware\\OutputCaching.Abstractions\\src\\Microsoft.AspNetCore.OutputCaching.Abstractions.csproj", + "src\\Middleware\\OutputCaching\\src\\Microsoft.AspNetCore.OutputCaching.csproj", "src\\Middleware\\ResponseCaching.Abstractions\\src\\Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj", "src\\Middleware\\ResponseCaching\\src\\Microsoft.AspNetCore.ResponseCaching.csproj", "src\\Middleware\\ResponseCompression\\src\\Microsoft.AspNetCore.ResponseCompression.csproj", diff --git a/src/Framework/test/TestData.cs b/src/Framework/test/TestData.cs index 6b3978eb4f52..619b75d63c52 100644 --- a/src/Framework/test/TestData.cs +++ b/src/Framework/test/TestData.cs @@ -73,6 +73,8 @@ static TestData() "Microsoft.AspNetCore.Mvc.RazorPages", "Microsoft.AspNetCore.Mvc.TagHelpers", "Microsoft.AspNetCore.Mvc.ViewFeatures", + "Microsoft.AspNetCore.OutputCaching", + "Microsoft.AspNetCore.OutputCaching.Abstractions", "Microsoft.AspNetCore.Razor", "Microsoft.AspNetCore.Razor.Runtime", "Microsoft.AspNetCore.ResponseCaching", @@ -208,6 +210,8 @@ static TestData() { "Microsoft.AspNetCore.Mvc.RazorPages", "7.0.0.0" }, { "Microsoft.AspNetCore.Mvc.TagHelpers", "7.0.0.0" }, { "Microsoft.AspNetCore.Mvc.ViewFeatures", "7.0.0.0" }, + { "Microsoft.AspNetCore.OutputCaching", "7.0.0.0" }, + { "Microsoft.AspNetCore.OutputCaching.Abstractions", "7.0.0.0" }, { "Microsoft.AspNetCore.Razor", "7.0.0.0" }, { "Microsoft.AspNetCore.Razor.Runtime", "7.0.0.0" }, { "Microsoft.AspNetCore.ResponseCaching", "7.0.0.0" }, diff --git a/src/Identity/Identity.slnf b/src/Identity/Identity.slnf index cd63efdba11c..8f46593f2c67 100644 --- a/src/Identity/Identity.slnf +++ b/src/Identity/Identity.slnf @@ -69,6 +69,7 @@ "src\\Mvc\\Mvc.ViewFeatures\\src\\Microsoft.AspNetCore.Mvc.ViewFeatures.csproj", "src\\Razor\\Razor\\src\\Microsoft.AspNetCore.Razor.csproj", "src\\Razor\\Razor.Runtime\\src\\Microsoft.AspNetCore.Razor.Runtime.csproj", + "src\\Middleware\\OutputCaching.Abstractions\\src\\Microsoft.AspNetCore.OutputCaching.Abstractions.csproj", "src\\Middleware\\ResponseCaching.Abstractions\\src\\Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj", "src\\Http\\Routing\\src\\Microsoft.AspNetCore.Routing.csproj", "src\\Http\\Routing.Abstractions\\src\\Microsoft.AspNetCore.Routing.Abstractions.csproj", diff --git a/src/Middleware/CORS/test/testassets/CorsMiddlewareWebSite/Properties/launchSettings.json b/src/Middleware/CORS/test/testassets/CorsMiddlewareWebSite/Properties/launchSettings.json index 485cac49a974..3528609bc2ff 100644 --- a/src/Middleware/CORS/test/testassets/CorsMiddlewareWebSite/Properties/launchSettings.json +++ b/src/Middleware/CORS/test/testassets/CorsMiddlewareWebSite/Properties/launchSettings.json @@ -9,4 +9,4 @@ "applicationUrl": "https://localhost:61226;http://localhost:61227" } } -} \ No newline at end of file +} diff --git a/src/Middleware/Diagnostics/test/testassets/DatabaseErrorPageSample/Properties/launchSettings.json b/src/Middleware/Diagnostics/test/testassets/DatabaseErrorPageSample/Properties/launchSettings.json index 3fbc5274ba38..b2929adc68de 100644 --- a/src/Middleware/Diagnostics/test/testassets/DatabaseErrorPageSample/Properties/launchSettings.json +++ b/src/Middleware/Diagnostics/test/testassets/DatabaseErrorPageSample/Properties/launchSettings.json @@ -9,4 +9,4 @@ "applicationUrl": "https://localhost:61218;http://localhost:61219" } } -} \ No newline at end of file +} diff --git a/src/Middleware/Diagnostics/test/testassets/DeveloperExceptionPageSample/Properties/launchSettings.json b/src/Middleware/Diagnostics/test/testassets/DeveloperExceptionPageSample/Properties/launchSettings.json index 8f4f5d821bdc..919228c3341c 100644 --- a/src/Middleware/Diagnostics/test/testassets/DeveloperExceptionPageSample/Properties/launchSettings.json +++ b/src/Middleware/Diagnostics/test/testassets/DeveloperExceptionPageSample/Properties/launchSettings.json @@ -9,4 +9,4 @@ "applicationUrl": "https://localhost:61228;http://localhost:61229" } } -} \ No newline at end of file +} diff --git a/src/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/Properties/launchSettings.json b/src/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/Properties/launchSettings.json index c69c9dd556ac..86c5743f7e60 100644 --- a/src/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/Properties/launchSettings.json +++ b/src/Middleware/Diagnostics/test/testassets/ExceptionHandlerSample/Properties/launchSettings.json @@ -9,4 +9,4 @@ "applicationUrl": "https://localhost:61220;http://localhost:61221" } } -} \ No newline at end of file +} diff --git a/src/Middleware/Diagnostics/test/testassets/StatusCodePagesSample/Properties/launchSettings.json b/src/Middleware/Diagnostics/test/testassets/StatusCodePagesSample/Properties/launchSettings.json index f27a4afd7135..97b0bd2ba905 100644 --- a/src/Middleware/Diagnostics/test/testassets/StatusCodePagesSample/Properties/launchSettings.json +++ b/src/Middleware/Diagnostics/test/testassets/StatusCodePagesSample/Properties/launchSettings.json @@ -9,4 +9,4 @@ "applicationUrl": "https://localhost:61230;http://localhost:61231" } } -} \ No newline at end of file +} diff --git a/src/Middleware/Diagnostics/test/testassets/WelcomePageSample/Properties/launchSettings.json b/src/Middleware/Diagnostics/test/testassets/WelcomePageSample/Properties/launchSettings.json index b82719da65c1..10feca9e72b6 100644 --- a/src/Middleware/Diagnostics/test/testassets/WelcomePageSample/Properties/launchSettings.json +++ b/src/Middleware/Diagnostics/test/testassets/WelcomePageSample/Properties/launchSettings.json @@ -9,4 +9,4 @@ "applicationUrl": "https://localhost:61223;http://localhost:61225" } } -} \ No newline at end of file +} diff --git a/src/Middleware/Localization/testassets/LocalizationWebsite/Properties/launchSettings.json b/src/Middleware/Localization/testassets/LocalizationWebsite/Properties/launchSettings.json index 3b71d90077f8..7494427a696b 100644 --- a/src/Middleware/Localization/testassets/LocalizationWebsite/Properties/launchSettings.json +++ b/src/Middleware/Localization/testassets/LocalizationWebsite/Properties/launchSettings.json @@ -9,4 +9,4 @@ "applicationUrl": "https://localhost:61222;http://localhost:61224" } } -} \ No newline at end of file +} diff --git a/src/Middleware/Middleware.slnf b/src/Middleware/Middleware.slnf index 2321ac94ea79..b466b64275d6 100644 --- a/src/Middleware/Middleware.slnf +++ b/src/Middleware/Middleware.slnf @@ -76,6 +76,10 @@ "src\\Middleware\\MiddlewareAnalysis\\samples\\MiddlewareAnalysisSample\\MiddlewareAnalysisSample.csproj", "src\\Middleware\\MiddlewareAnalysis\\src\\Microsoft.AspNetCore.MiddlewareAnalysis.csproj", "src\\Middleware\\MiddlewareAnalysis\\test\\Microsoft.AspNetCore.MiddlewareAnalysis.Tests.csproj", + "src\\Middleware\\OutputCaching.Abstractions\\src\\Microsoft.AspNetCore.OutputCaching.Abstractions.csproj", + "src\\Middleware\\OutputCaching\\samples\\OutputCachingSample\\OutputCachingSample.csproj", + "src\\Middleware\\OutputCaching\\src\\Microsoft.AspNetCore.OutputCaching.csproj", + "src\\Middleware\\OutputCaching\\test\\Microsoft.AspNetCore.OutputCaching.Tests.csproj", "src\\Middleware\\RateLimiting\\src\\Microsoft.AspNetCore.RateLimiting.csproj", "src\\Middleware\\RateLimiting\\test\\Microsoft.AspNetCore.RateLimiting.Tests.csproj", "src\\Middleware\\ResponseCaching.Abstractions\\src\\Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj", diff --git a/src/Middleware/OutputCaching.Abstractions/src/CachedResponseBody.cs b/src/Middleware/OutputCaching.Abstractions/src/CachedResponseBody.cs new file mode 100644 index 000000000000..727606e3e79e --- /dev/null +++ b/src/Middleware/OutputCaching.Abstractions/src/CachedResponseBody.cs @@ -0,0 +1,41 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO.Pipelines; + +namespace Microsoft.AspNetCore.OutputCaching; + +public class CachedResponseBody +{ + public CachedResponseBody(List segments, long length) + { + Segments = segments; + Length = length; + } + + public List Segments { get; } + + public long Length { get; } + + public async Task CopyToAsync(PipeWriter destination, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(destination, nameof(destination)); + + foreach (var segment in Segments) + { + cancellationToken.ThrowIfCancellationRequested(); + + Copy(segment, destination); + + await destination.FlushAsync(cancellationToken); + } + } + + private static void Copy(byte[] segment, PipeWriter destination) + { + var span = destination.GetSpan(segment.Length); + + segment.CopyTo(span); + destination.Advance(segment.Length); + } +} diff --git a/src/Middleware/OutputCaching.Abstractions/src/CachedVaryByRules.cs b/src/Middleware/OutputCaching.Abstractions/src/CachedVaryByRules.cs new file mode 100644 index 000000000000..de63617b4111 --- /dev/null +++ b/src/Middleware/OutputCaching.Abstractions/src/CachedVaryByRules.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.OutputCaching; + +public class CachedVaryByRules +{ + public Dictionary VaryByCustom { get; } = new(StringComparer.OrdinalIgnoreCase); + + public StringValues Headers { get; set; } + + public StringValues QueryKeys { get; set; } + + // Normalized version of VaryByCustom + public StringValues VaryByPrefix { get; set; } +} diff --git a/src/Middleware/OutputCaching.Abstractions/src/IOutputCache.cs b/src/Middleware/OutputCaching.Abstractions/src/IOutputCache.cs new file mode 100644 index 000000000000..8556cacf6e63 --- /dev/null +++ b/src/Middleware/OutputCaching.Abstractions/src/IOutputCache.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.OutputCaching; + +public interface IOutputCacheStore +{ + /// + /// Evicts cache entries by tag. + /// + /// The tag to evict. + ValueTask EvictByTagAsync(string tag); + + /// + /// Gets the cached response for the given key, if it exists. + /// If no cached response exists for the given key, null is returned. + /// + /// The cache key to look up. + /// The response cache entry if it exists; otherwise null. + ValueTask GetAsync(string key); + + /// + /// Stores the given response in the response cache. + /// + /// The cache key to store the response under. + /// The response cache entry to store. + /// The amount of time the entry will be kept in the cache before expiring, relative to now. + ValueTask SetAsync(string key, OutputCacheEntry entry, TimeSpan validFor); +} diff --git a/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingContext.cs b/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingContext.cs new file mode 100644 index 000000000000..9ef3c9f3ad32 --- /dev/null +++ b/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingContext.cs @@ -0,0 +1,61 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.OutputCaching; + +public interface IOutputCachingContext +{ + TimeSpan? CachedEntryAge { get; } + HttpContext HttpContext { get; } + DateTimeOffset? ResponseTime { get; } + DateTimeOffset? ResponseDate { get; } + DateTimeOffset? ResponseExpires { get; } + TimeSpan? ResponseSharedMaxAge { get; } + TimeSpan? ResponseMaxAge { get; } + /// + /// The custom expiration timespan for the response + /// + public TimeSpan? ResponseExpirationTimeSpan { get; set; } + IHeaderDictionary CachedResponseHeaders { get; } + CachedVaryByRules CachedVaryByRules { get; } + HashSet Tags { get; } + ILogger Logger { get; } + + /// + /// Determine whether the output caching logic should is configured for the incoming HTTP request. + /// + bool EnableOutputCaching { get; set; } + + /// + /// Determine whether the output caching logic should be attempted for the incoming HTTP request. + /// + bool AttemptResponseCaching { get; set; } + + /// + /// Determine whether a cache lookup is allowed for the incoming HTTP request. + /// + bool AllowCacheLookup { get; set; } + + /// + /// Determine whether storage of the response is allowed for the incoming HTTP request. + /// + bool AllowCacheStorage { get; set; } + + /// + /// Determine whether request should be locked. + /// + bool AllowLocking { get; set; } + + /// + /// Determine whether the response received by the middleware can be cached for future requests. + /// + bool IsResponseCacheable { get; set; } + + /// + /// Determine whether the response retrieved from the response cache is fresh and can be served. + /// + bool IsCacheEntryFresh { get; set; } +} diff --git a/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingFeature.cs b/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingFeature.cs new file mode 100644 index 000000000000..da551035d2da --- /dev/null +++ b/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingFeature.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.OutputCaching; + +/// +/// A feature for configuring additional output cache options on the HTTP response. +/// +public interface IOutputCachingFeature +{ + IOutputCachingContext Context { get; } + + List Policies { get; } +} diff --git a/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingPolicy.cs b/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingPolicy.cs new file mode 100644 index 000000000000..4805a680479d --- /dev/null +++ b/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingPolicy.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.OutputCaching; + +/// +/// An implementation of this interface can update how the current request is cached. +/// +public interface IOutputCachingPolicy +{ + Task OnRequestAsync(IOutputCachingContext context); + Task OnServeFromCacheAsync(IOutputCachingContext context); + Task OnServeResponseAsync(IOutputCachingContext context); +} diff --git a/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingPolicyProvider.cs b/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingPolicyProvider.cs new file mode 100644 index 000000000000..23a8f6de2a39 --- /dev/null +++ b/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingPolicyProvider.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.OutputCaching; + +/// +/// An implementation of this interface can update how the current request is cached. +/// +public interface IOutputCachingPolicyProvider +{ + /// + /// Determine whether the response caching logic should be attempted for the incoming HTTP request. + /// + /// The . + /// true if response caching logic should be attempted; otherwise false. + Task OnRequestAsync(IOutputCachingContext context); + + /// + /// Determine whether the response retrieved from the response cache is fresh and can be served. + /// + /// The . + /// true if cache lookup for this cache entry is allowed; otherwise false. + Task OnServeFromCacheAsync(IOutputCachingContext context); + + /// + /// Determine whether the response can be cached for future requests. + /// + /// The . + /// true if cache lookup for this request is allowed; otherwise false. + Task OnServeResponseAsync(IOutputCachingContext context); +} diff --git a/src/Middleware/OutputCaching.Abstractions/src/IPoliciesMetadata.cs b/src/Middleware/OutputCaching.Abstractions/src/IPoliciesMetadata.cs new file mode 100644 index 000000000000..5ca892772aae --- /dev/null +++ b/src/Middleware/OutputCaching.Abstractions/src/IPoliciesMetadata.cs @@ -0,0 +1,9 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.OutputCaching; + +public interface IPoliciesMetadata +{ + List Policies { get; } +} diff --git a/src/Middleware/OutputCaching.Abstractions/src/Microsoft.AspNetCore.OutputCaching.Abstractions.csproj b/src/Middleware/OutputCaching.Abstractions/src/Microsoft.AspNetCore.OutputCaching.Abstractions.csproj new file mode 100644 index 000000000000..ec50117005e0 --- /dev/null +++ b/src/Middleware/OutputCaching.Abstractions/src/Microsoft.AspNetCore.OutputCaching.Abstractions.csproj @@ -0,0 +1,19 @@ + + + + ASP.NET Core output caching middleware abstractions and feature interface definitions. + $(DefaultNetCoreTargetFramework) + true + true + aspnetcore;cache;caching + false + enable + false + + + + + + + + diff --git a/src/Middleware/OutputCaching.Abstractions/src/OutputCacheEntry.cs b/src/Middleware/OutputCaching.Abstractions/src/OutputCacheEntry.cs new file mode 100644 index 000000000000..7cc3f64d559c --- /dev/null +++ b/src/Middleware/OutputCaching.Abstractions/src/OutputCacheEntry.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.OutputCaching; + +public class OutputCacheEntry +{ + public DateTimeOffset Created { get; set; } + + public int StatusCode { get; set; } + + public IHeaderDictionary Headers { get; set; } = default!; + + public CachedResponseBody Body { get; set; } = default!; + public string[]? Tags { get; set; } +} diff --git a/src/Middleware/OutputCaching.Abstractions/src/PublicAPI.Shipped.txt b/src/Middleware/OutputCaching.Abstractions/src/PublicAPI.Shipped.txt new file mode 100644 index 000000000000..91b0e1a43b98 --- /dev/null +++ b/src/Middleware/OutputCaching.Abstractions/src/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable \ No newline at end of file diff --git a/src/Middleware/OutputCaching.Abstractions/src/PublicAPI.Unshipped.txt b/src/Middleware/OutputCaching.Abstractions/src/PublicAPI.Unshipped.txt new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/Middleware/OutputCaching/OutputCaching.slnf b/src/Middleware/OutputCaching/OutputCaching.slnf new file mode 100644 index 000000000000..324e210c9722 --- /dev/null +++ b/src/Middleware/OutputCaching/OutputCaching.slnf @@ -0,0 +1,10 @@ +{ + "solution": { + "path": "..\\..\\..\\AspNetCore.sln", + "projects": [ + "src\\Middleware\\OutputCaching.Abstractions\\src\\Microsoft.AspNetCore.OutputCaching.Abstractions.csproj", + "src\\Middleware\\OutputCaching\\samples\\OutputCachingSample\\OutputCachingSample.csproj", + "src\\Middleware\\OutputCaching\\src\\Microsoft.AspNetCore.OutputCaching.csproj" + ] + } +} \ No newline at end of file diff --git a/src/Middleware/OutputCaching/samples/OutputCachingSample/.vscode/launch.json b/src/Middleware/OutputCaching/samples/OutputCachingSample/.vscode/launch.json new file mode 100644 index 000000000000..93e3163e870f --- /dev/null +++ b/src/Middleware/OutputCaching/samples/OutputCachingSample/.vscode/launch.json @@ -0,0 +1,35 @@ +{ + "version": "0.2.0", + "configurations": [ + { + // Use IntelliSense to find out which attributes exist for C# debugging + // Use hover for the description of the existing attributes + // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md + "name": ".NET Core Launch (web)", + "type": "coreclr", + "request": "launch", + "preLaunchTask": "build", + // If you have changed target frameworks, make sure to update the program path. + "program": "${workspaceFolder}/bin/Debug/net7.0/OutputCachingSample.dll", + "args": [], + "cwd": "${workspaceFolder}", + "stopAtEntry": false, + // Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser + "serverReadyAction": { + "action": "openExternally", + "pattern": "\\bNow listening on:\\s+(https?://\\S+)" + }, + "env": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "sourceFileMap": { + "/Views": "${workspaceFolder}/Views" + } + }, + { + "name": ".NET Core Attach", + "type": "coreclr", + "request": "attach" + } + ] +} \ No newline at end of file diff --git a/src/Middleware/OutputCaching/samples/OutputCachingSample/.vscode/tasks.json b/src/Middleware/OutputCaching/samples/OutputCachingSample/.vscode/tasks.json new file mode 100644 index 000000000000..5b41ee7a098c --- /dev/null +++ b/src/Middleware/OutputCaching/samples/OutputCachingSample/.vscode/tasks.json @@ -0,0 +1,41 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "build", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "${workspaceFolder}/OutputCachingSample.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "publish", + "command": "dotnet", + "type": "process", + "args": [ + "publish", + "${workspaceFolder}/OutputCachingSample.csproj", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" + }, + { + "label": "watch", + "command": "dotnet", + "type": "process", + "args": [ + "watch", + "run", + "--project", + "${workspaceFolder}/OutputCachingSample.csproj" + ], + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/src/Middleware/OutputCaching/samples/OutputCachingSample/Gravatar.cs b/src/Middleware/OutputCaching/samples/OutputCachingSample/Gravatar.cs new file mode 100644 index 000000000000..08eccafbb9e6 --- /dev/null +++ b/src/Middleware/OutputCaching/samples/OutputCachingSample/Gravatar.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +public static class Gravatar +{ + public static async Task WriteGravatar(HttpContext context) + { + const string type = "monsterid"; // identicon, monsterid, wavatar + const int size = 200; + var hash = Guid.NewGuid().ToString("n"); + + context.Response.StatusCode = 200; + context.Response.ContentType = "text/html"; + await context.Response.WriteAsync($""); + await context.Response.WriteAsync($"
Generated at {DateTime.Now:hh:mm:ss.ff}
"); + } +} diff --git a/src/Middleware/OutputCaching/samples/OutputCachingSample/OutputCachingSample.csproj b/src/Middleware/OutputCaching/samples/OutputCachingSample/OutputCachingSample.csproj new file mode 100644 index 000000000000..8b0e88781139 --- /dev/null +++ b/src/Middleware/OutputCaching/samples/OutputCachingSample/OutputCachingSample.csproj @@ -0,0 +1,20 @@ + + + + $(DefaultNetCoreTargetFramework) + enable + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Middleware/OutputCaching/samples/OutputCachingSample/Properties/launchSettings.json b/src/Middleware/OutputCaching/samples/OutputCachingSample/Properties/launchSettings.json new file mode 100644 index 000000000000..b544fed19552 --- /dev/null +++ b/src/Middleware/OutputCaching/samples/OutputCachingSample/Properties/launchSettings.json @@ -0,0 +1,27 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:54270/", + "sslPort": 44398 + } + }, + "profiles": { + "OutputCachingSample": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:5001;http://localhost:5000" + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/src/Middleware/OutputCaching/samples/OutputCachingSample/README.md b/src/Middleware/OutputCaching/samples/OutputCachingSample/README.md new file mode 100644 index 000000000000..f88770eaaf01 --- /dev/null +++ b/src/Middleware/OutputCaching/samples/OutputCachingSample/README.md @@ -0,0 +1,6 @@ +ASP.NET Core Output Caching Sample +=================================== + +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. + +When running the sample, a response will be served from cache when possible and will be stored for up to 10 seconds. diff --git a/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs b/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs new file mode 100644 index 000000000000..0b8409be09c2 --- /dev/null +++ b/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs @@ -0,0 +1,78 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.OutputCaching; +using Microsoft.AspNetCore.OutputCaching.Policies; +using Microsoft.Net.Http.Headers; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddOutputCaching(options => +{ + // options.Policies.Clear(); + options.Policies.Add(new EnableCachingPolicy()); + + options.Profiles["NoCache"] = new OutputCachePolicyBuilder().NoStore().Build(); +}); + +var app = builder.Build(); + +app.UseOutputCaching(); + +app.MapGet("/", Gravatar.WriteGravatar).OutputCache(x => x.Tag("home")); + +app.MapGet("/a", Gravatar.WriteGravatar).OutputCache(); + +app.MapGet("/b", Gravatar.WriteGravatar); + +app.MapGet("/nocache", Gravatar.WriteGravatar).OutputCache(x => x.NoStore()); + +app.MapGet("/profile", Gravatar.WriteGravatar).OutputCache(x => x.Profile("NoCache")); + +app.MapGet("/attribute", [OutputCache(Profile = "NoCache")] (c) => Gravatar.WriteGravatar(c)); + +app.MapPost("/purge/{tag}", async (IOutputCacheStore cache, string tag) => +{ + // POST such that the endpoint is not cached itself + + await cache.EvictByTagAsync(tag); +}); + +// Cached entries will vary by culture, but any other additional query is ignored and returns the same cached content +app.MapGet("/query", Gravatar.WriteGravatar).OutputCache(p => p.VaryByQuery("culture")); + +app.MapGet("/vary", Gravatar.WriteGravatar).OutputCache(c => c.VaryByValue(() => ("time", (DateTime.Now.Second % 2).ToString()))); + +long requests = 0; + +// Locking is enabled by default +app.MapGet("/lock", async (context) => +{ + await Task.Delay(1000); + await context.Response.WriteAsync($"
{requests++}
"); +}).OutputCache(p => p.Lock(false).Expires(TimeSpan.FromMilliseconds(1))); + +// Cached because Response Caching policy and contains "Cache-Control: public" +app.MapGet("/headers", async context => +{ + // From a browser this endpoint won't be cached because of max-age: 0 + context.Response.Headers.CacheControl = CacheControlHeaderValue.PublicString; + await context.Response.WriteAsync("Headers " + DateTime.UtcNow.ToString("o")); +}).OutputCache(new ResponseCachingPolicy()); + +// Etag +app.MapGet("/etag", async (context) => +{ + // If the client sends an If-None-Match header with the etag value, the server + // returns 304 if the cache entry is fresh instead of the full response + + var etag = $"\"{Guid.NewGuid().ToString("n")}\""; + context.Response.Headers.ETag = etag; + + await context.Response.WriteAsync("Hello"); +}).OutputCache(); + +// When the request header If-Modified-Since is provided, return 304 if the cached entry is older +app.MapGet("/ims", Gravatar.WriteGravatar).OutputCache(); + +await app.RunAsync(); diff --git a/src/Middleware/OutputCaching/samples/OutputCachingSample/appsettings.Development.json b/src/Middleware/OutputCaching/samples/OutputCachingSample/appsettings.Development.json new file mode 100644 index 000000000000..4e8090a0eea5 --- /dev/null +++ b/src/Middleware/OutputCaching/samples/OutputCachingSample/appsettings.Development.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information", + "Microsoft.AspNetCore.OutputCaching": "Debug" + } + } +} diff --git a/src/Middleware/OutputCaching/samples/OutputCachingSample/appsettings.json b/src/Middleware/OutputCaching/samples/OutputCachingSample/appsettings.json new file mode 100644 index 000000000000..d9d9a9bff6fd --- /dev/null +++ b/src/Middleware/OutputCaching/samples/OutputCachingSample/appsettings.json @@ -0,0 +1,10 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/src/Middleware/OutputCaching/src/CacheEntryHelpers.cs b/src/Middleware/OutputCaching/src/CacheEntryHelpers.cs new file mode 100644 index 000000000000..e7fdcf1bcbf3 --- /dev/null +++ b/src/Middleware/OutputCaching/src/CacheEntryHelpers.cs @@ -0,0 +1,59 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.OutputCaching; + +internal static class CacheEntryHelpers +{ + internal static long EstimateCachedResponseSize(OutputCacheEntry cachedResponse) + { + if (cachedResponse == null) + { + return 0L; + } + + checked + { + // StatusCode + long size = sizeof(int); + + // Headers + if (cachedResponse.Headers != null) + { + foreach (var item in cachedResponse.Headers) + { + size += (item.Key.Length * sizeof(char)) + EstimateStringValuesSize(item.Value); + } + } + + // Body + if (cachedResponse.Body != null) + { + size += cachedResponse.Body.Length; + } + + return size; + } + } + + internal static long EstimateStringValuesSize(StringValues stringValues) + { + checked + { + var size = 0L; + + for (var i = 0; i < stringValues.Count; i++) + { + var stringValue = stringValues[i]; + if (!string.IsNullOrEmpty(stringValue)) + { + size += stringValue.Length * sizeof(char); + } + } + + return size; + } + } +} diff --git a/src/Middleware/OutputCaching/src/DispatcherExtensions.cs b/src/Middleware/OutputCaching/src/DispatcherExtensions.cs new file mode 100644 index 000000000000..ba2bb433d1da --- /dev/null +++ b/src/Middleware/OutputCaching/src/DispatcherExtensions.cs @@ -0,0 +1,89 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; + +namespace Microsoft.AspNetCore.OutputCaching; + +internal class WorkDispatcher where TKey : notnull +{ + private readonly ConcurrentDictionary> _workers = new(); + + public async Task ScheduleAsync(TKey key, Func> valueFactory) + { + ArgumentNullException.ThrowIfNull(key, nameof(key)); + + while (true) + { + if (_workers.TryGetValue(key, out var task)) + { + return await task; + } + + // This is the task that we'll return to all waiters. We'll complete it when the factory is complete + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + if (_workers.TryAdd(key, tcs.Task)) + { + try + { + var value = await valueFactory(key); + tcs.TrySetResult(value); + return await tcs.Task; + } + catch (Exception ex) + { + // Make sure all waiters see the exception + tcs.SetException(ex); + + throw; + } + finally + { + // We remove the entry if the factory failed so it's not a permanent failure + // and future gets can retry (this could be a pluggable policy) + _workers.TryRemove(key, out _); + } + } + } + } + + public async Task ScheduleAsync(TKey key, TState state, Func> valueFactory) + { + ArgumentNullException.ThrowIfNull(key, nameof(key)); + + while (true) + { + if (_workers.TryGetValue(key, out var task)) + { + return await task; + } + + // This is the task that we'll return to all waiters. We'll complete it when the factory is complete + var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + if (_workers.TryAdd(key, tcs.Task)) + { + try + { + var value = await valueFactory(key, state); + tcs.TrySetResult(value); + return await tcs.Task; + } + catch (Exception ex) + { + // Make sure all waiters see the exception + tcs.SetException(ex); + + throw; + } + finally + { + // We remove the entry if the factory failed so it's not a permanent failure + // and future gets can retry (this could be a pluggable policy) + _workers.TryRemove(key, out _); + } + } + } + } +} diff --git a/src/Middleware/OutputCaching/src/FastGuid.cs b/src/Middleware/OutputCaching/src/FastGuid.cs new file mode 100644 index 000000000000..9589996d3cf9 --- /dev/null +++ b/src/Middleware/OutputCaching/src/FastGuid.cs @@ -0,0 +1,73 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.OutputCaching; + +internal class FastGuid +{ + // Base32 encoding - in ascii sort order for easy text based sorting + private static readonly char[] s_encode32Chars = "0123456789ABCDEFGHIJKLMNOPQRSTUV".ToCharArray(); + // Global ID + private static long NextId = InitializeNextId(); + + // Instance components + private string? _idString; + + internal long IdValue { get; } + + internal string IdString + { + get + { + if (_idString == null) + { + _idString = GenerateGuidString(this); + } + return _idString; + } + } + + // Static constructor to initialize global components + private static long InitializeNextId() + { + var guidBytes = Guid.NewGuid().ToByteArray(); + + // Use the first 4 bytes from the Guid to initialize global ID + return + guidBytes[0] << 32 | + guidBytes[1] << 40 | + guidBytes[2] << 48 | + guidBytes[3] << 56; + } + + internal FastGuid(long id) + { + IdValue = id; + } + + internal static FastGuid NewGuid() + { + return new FastGuid(Interlocked.Increment(ref NextId)); + } + + private static string GenerateGuidString(FastGuid guid) + { + return string.Create(13, guid.IdValue, (buffer, value) => + { + char[] encode32Chars = s_encode32Chars; + buffer[12] = encode32Chars[value & 31]; + buffer[11] = encode32Chars[(value >> 5) & 31]; + buffer[10] = encode32Chars[(value >> 10) & 31]; + buffer[9] = encode32Chars[(value >> 15) & 31]; + buffer[8] = encode32Chars[(value >> 20) & 31]; + buffer[7] = encode32Chars[(value >> 25) & 31]; + buffer[6] = encode32Chars[(value >> 30) & 31]; + buffer[5] = encode32Chars[(value >> 35) & 31]; + buffer[4] = encode32Chars[(value >> 40) & 31]; + buffer[3] = encode32Chars[(value >> 45) & 31]; + buffer[2] = encode32Chars[(value >> 50) & 31]; + buffer[1] = encode32Chars[(value >> 55) & 31]; + buffer[0] = encode32Chars[(value >> 60) & 31]; + }); + } +} diff --git a/src/Middleware/OutputCaching/src/Interfaces/IOutputCachingKeyProvider.cs b/src/Middleware/OutputCaching/src/Interfaces/IOutputCachingKeyProvider.cs new file mode 100644 index 000000000000..d5cf14de996a --- /dev/null +++ b/src/Middleware/OutputCaching/src/Interfaces/IOutputCachingKeyProvider.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.OutputCaching; + +internal interface IOutputCachingKeyProvider +{ + /// + /// Create a vary key for storing cached responses. + /// + /// The . + /// The created vary key. + string CreateStorageVaryByKey(OutputCachingContext context); +} diff --git a/src/Middleware/OutputCaching/src/Interfaces/ISystemClock.cs b/src/Middleware/OutputCaching/src/Interfaces/ISystemClock.cs new file mode 100644 index 000000000000..330e860028cf --- /dev/null +++ b/src/Middleware/OutputCaching/src/Interfaces/ISystemClock.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.OutputCaching; + +/// +/// Abstracts the system clock to facilitate testing. +/// +internal interface ISystemClock +{ + /// + /// Retrieves the current system time in UTC. + /// + DateTime UtcNow { get; } +} diff --git a/src/Middleware/OutputCaching/src/LoggerExtensions.cs b/src/Middleware/OutputCaching/src/LoggerExtensions.cs new file mode 100644 index 000000000000..eee6d87602fd --- /dev/null +++ b/src/Middleware/OutputCaching/src/LoggerExtensions.cs @@ -0,0 +1,123 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.OutputCaching; + +/// +/// Defines *all* the logger messages produced by response caching +/// +internal static partial class LoggerExtensions +{ + [LoggerMessage(1, LogLevel.Debug, "The request cannot be served from cache because it uses the HTTP method: {Method}.", + EventName = "RequestMethodNotCacheable")] + internal static partial void RequestMethodNotCacheable(this ILogger logger, string method); + + [LoggerMessage(2, LogLevel.Debug, "The request cannot be served from cache because it contains an 'Authorization' header.", + EventName = "RequestWithAuthorizationNotCacheable")] + internal static partial void RequestWithAuthorizationNotCacheable(this ILogger logger); + + [LoggerMessage(3, LogLevel.Debug, "The request cannot be served from cache because it contains a 'no-cache' cache directive.", + EventName = "RequestWithNoCacheNotCacheable")] + internal static partial void RequestWithNoCacheNotCacheable(this ILogger logger); + + [LoggerMessage(4, LogLevel.Debug, "The request cannot be served from cache because it contains a 'no-cache' pragma directive.", + EventName = "RequestWithPragmaNoCacheNotCacheable")] + internal static partial void RequestWithPragmaNoCacheNotCacheable(this ILogger logger); + + [LoggerMessage(5, LogLevel.Debug, "Adding a minimum freshness requirement of {Duration} specified by the 'min-fresh' cache directive.", + EventName = "LogRequestMethodNotCacheable")] + internal static partial void ExpirationMinFreshAdded(this ILogger logger, TimeSpan duration); + + [LoggerMessage(6, LogLevel.Debug, "The age of the entry is {Age} and has exceeded the maximum age for shared caches of {SharedMaxAge} specified by the 's-maxage' cache directive.", + EventName = "ExpirationSharedMaxAgeExceeded")] + internal static partial void ExpirationSharedMaxAgeExceeded(this ILogger logger, TimeSpan age, TimeSpan sharedMaxAge); + + [LoggerMessage(7, LogLevel.Debug, "The age of the entry is {Age} and has exceeded the maximum age of {MaxAge} specified by the 'max-age' cache directive. " + + "It must be revalidated because the 'must-revalidate' or 'proxy-revalidate' cache directive is specified.", + EventName = "ExpirationMustRevalidate")] + internal static partial void ExpirationMustRevalidate(this ILogger logger, TimeSpan age, TimeSpan maxAge); + + [LoggerMessage(8, LogLevel.Debug, "The age of the entry is {Age} and has exceeded the maximum age of {MaxAge} specified by the 'max-age' cache directive. " + + "However, it satisfied the maximum stale allowance of {MaxStale} specified by the 'max-stale' cache directive.", + EventName = "ExpirationMaxStaleSatisfied")] + internal static partial void ExpirationMaxStaleSatisfied(this ILogger logger, TimeSpan age, TimeSpan maxAge, TimeSpan maxStale); + + [LoggerMessage(9, LogLevel.Debug, "The age of the entry is {Age} and has exceeded the maximum age of {MaxAge} specified by the 'max-age' cache directive.", EventName = "ExpirationMaxAgeExceeded")] + internal static partial void ExpirationMaxAgeExceeded(this ILogger logger, TimeSpan age, TimeSpan maxAge); + + [LoggerMessage(10, LogLevel.Debug, "The response time of the entry is {ResponseTime} and has exceeded the expiry date of {Expired} specified by the 'Expires' header.", + EventName = "ExpirationExpiresExceeded")] + internal static partial void ExpirationExpiresExceeded(this ILogger logger, DateTimeOffset responseTime, DateTimeOffset expired); + + [LoggerMessage(11, LogLevel.Debug, "Response is not cacheable because it does not contain the 'public' cache directive.", + EventName = "ResponseWithoutPublicNotCacheable")] + internal static partial void ResponseWithoutPublicNotCacheable(this ILogger logger); + + [LoggerMessage(12, LogLevel.Debug, "Response is not cacheable because it or its corresponding request contains a 'no-store' cache directive.", + EventName = "ResponseWithNoStoreNotCacheable")] + internal static partial void ResponseWithNoStoreNotCacheable(this ILogger logger); + [LoggerMessage(13, LogLevel.Debug, "Response is not cacheable because it contains a 'no-cache' cache directive.", + EventName = "ResponseWithNoCacheNotCacheable")] + internal static partial void ResponseWithNoCacheNotCacheable(this ILogger logger); + + [LoggerMessage(14, LogLevel.Debug, "Response is not cacheable because it contains a 'SetCookie' header.", EventName = "ResponseWithSetCookieNotCacheable")] + internal static partial void ResponseWithSetCookieNotCacheable(this ILogger logger); + + [LoggerMessage(15, LogLevel.Debug, "Response is not cacheable because it contains a '.Vary' header with a value of *.", + EventName = "ResponseWithVaryStarNotCacheable")] + internal static partial void ResponseWithVaryStarNotCacheable(this ILogger logger); + + [LoggerMessage(16, LogLevel.Debug, "Response is not cacheable because it contains the 'private' cache directive.", + EventName = "ResponseWithPrivateNotCacheable")] + internal static partial void ResponseWithPrivateNotCacheable(this ILogger logger); + + [LoggerMessage(17, LogLevel.Debug, "Response is not cacheable because its status code {StatusCode} does not indicate success.", + EventName = "ResponseWithUnsuccessfulStatusCodeNotCacheable")] + internal static partial void ResponseWithUnsuccessfulStatusCodeNotCacheable(this ILogger logger, int statusCode); + + [LoggerMessage(18, LogLevel.Debug, "The 'IfNoneMatch' header of the request contains a value of *.", EventName = "ExpirationExpiresExceeded")] + internal static partial void NotModifiedIfNoneMatchStar(this ILogger logger); + + [LoggerMessage(19, LogLevel.Debug, "The ETag {ETag} in the 'IfNoneMatch' header matched the ETag of a cached entry.", + EventName = "NotModifiedIfNoneMatchMatched")] + internal static partial void NotModifiedIfNoneMatchMatched(this ILogger logger, EntityTagHeaderValue etag); + + [LoggerMessage(20, LogLevel.Debug, "The last modified date of {LastModified} is before the date {IfModifiedSince} specified in the 'IfModifiedSince' header.", + EventName = "NotModifiedIfModifiedSinceSatisfied")] + internal static partial void NotModifiedIfModifiedSinceSatisfied(this ILogger logger, DateTimeOffset lastModified, DateTimeOffset ifModifiedSince); + + [LoggerMessage(21, LogLevel.Information, "The content requested has not been modified.", EventName = "NotModifiedServed")] + internal static partial void NotModifiedServed(this ILogger logger); + + [LoggerMessage(22, LogLevel.Information, "Serving response from cache.", EventName = "CachedResponseServed")] + internal static partial void CachedResponseServed(this ILogger logger); + + [LoggerMessage(23, LogLevel.Information, "No cached response available for this request and the 'only-if-cached' cache directive was specified.", + EventName = "GatewayTimeoutServed")] + internal static partial void GatewayTimeoutServed(this ILogger logger); + + [LoggerMessage(24, LogLevel.Information, "No cached response available for this request.", EventName = "NoResponseServed")] + internal static partial void NoResponseServed(this ILogger logger); + + [LoggerMessage(25, LogLevel.Debug, "Vary by rules were updated. Headers: {Headers}, Query keys: {QueryKeys}", EventName = "VaryByRulesUpdated")] + internal static partial void VaryByRulesUpdated(this ILogger logger, string headers, string queryKeys); + + [LoggerMessage(26, LogLevel.Information, "The response has been cached.", EventName = "ResponseCached")] + internal static partial void ResponseCached(this ILogger logger); + + [LoggerMessage(27, LogLevel.Information, "The response could not be cached for this request.", EventName = "ResponseNotCached")] + internal static partial void LogResponseNotCached(this ILogger logger); + + [LoggerMessage(28, LogLevel.Warning, "The response could not be cached for this request because the 'Content-Length' did not match the body length.", + EventName = "responseContentLengthMismatchNotCached")] + internal static partial void ResponseContentLengthMismatchNotCached(this ILogger logger); + + [LoggerMessage(29, LogLevel.Debug, + "The age of the entry is {Age} and has exceeded the maximum age of {MaxAge} specified by the 'max-age' cache directive. " + + "However, the 'max-stale' cache directive was specified without an assigned value and a stale response of any age is accepted.", + EventName = "ExpirationInfiniteMaxStaleSatisfied")] + internal static partial void ExpirationInfiniteMaxStaleSatisfied(this ILogger logger, TimeSpan age, TimeSpan maxAge); +} diff --git a/src/Middleware/OutputCaching/src/Memory/MemoryCachedResponse.cs b/src/Middleware/OutputCaching/src/Memory/MemoryCachedResponse.cs new file mode 100644 index 000000000000..c34d0e9a0f88 --- /dev/null +++ b/src/Middleware/OutputCaching/src/Memory/MemoryCachedResponse.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.OutputCaching.Memory; + +internal class MemoryCachedResponse +{ + public DateTimeOffset Created { get; set; } + + public int StatusCode { get; set; } + + public IHeaderDictionary Headers { get; set; } = new HeaderDictionary(); + + public CachedResponseBody Body { get; set; } = default!; + public string[] Tags { get; set; } = default!; +} diff --git a/src/Middleware/OutputCaching/src/Memory/MemoryOutputCache.cs b/src/Middleware/OutputCaching/src/Memory/MemoryOutputCache.cs new file mode 100644 index 000000000000..5712c3a388fd --- /dev/null +++ b/src/Middleware/OutputCaching/src/Memory/MemoryOutputCache.cs @@ -0,0 +1,89 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Concurrent; +using System.Linq; +using Microsoft.Extensions.Caching.Memory; + +namespace Microsoft.AspNetCore.OutputCaching.Memory; + +internal sealed class MemoryOutputCacheStore : IOutputCacheStore +{ + private readonly IMemoryCache _cache; + private readonly ConcurrentDictionary> _taggedEntries = new(); + + internal MemoryOutputCacheStore(IMemoryCache cache) + { + ArgumentNullException.ThrowIfNull(cache, nameof(cache)); + + _cache = cache; + } + + public ValueTask EvictByTagAsync(string tag) + { + if (_taggedEntries.TryGetValue(tag, out var keys)) + { + foreach (var key in keys) + { + _cache.Remove(key); + } + } + + return ValueTask.CompletedTask; + } + + public ValueTask GetAsync(string key) + { + var entry = _cache.Get(key); + + if (entry is MemoryCachedResponse memoryCachedResponse) + { + var outputCacheEntry = new OutputCacheEntry + { + Created = memoryCachedResponse.Created, + StatusCode = memoryCachedResponse.StatusCode, + Headers = memoryCachedResponse.Headers, + Body = memoryCachedResponse.Body, + }; + + outputCacheEntry.Tags = memoryCachedResponse.Tags.ToArray(); + + return ValueTask.FromResult(outputCacheEntry); + } + + return ValueTask.FromResult(default(OutputCacheEntry)); + } + + public ValueTask SetAsync(string key, OutputCacheEntry cachedResponse, TimeSpan validFor) + { + foreach (var tag in cachedResponse.Tags) + { + var keys = _taggedEntries.GetOrAdd(tag, _ => new HashSet()); + + // Copy the list of tags to prevent locking + + var local = new HashSet(keys); + local.Add(key); + + _taggedEntries[tag] = local; + } + + _cache.Set( + key, + new MemoryCachedResponse + { + Created = cachedResponse.Created, + StatusCode = cachedResponse.StatusCode, + Headers = cachedResponse.Headers, + Body = cachedResponse.Body, + Tags = cachedResponse.Tags + }, + new MemoryCacheEntryOptions + { + AbsoluteExpirationRelativeToNow = validFor, + Size = CacheEntryHelpers.EstimateCachedResponseSize(cachedResponse) + }); + + return ValueTask.CompletedTask; + } +} diff --git a/src/Middleware/OutputCaching/src/Microsoft.AspNetCore.OutputCaching.csproj b/src/Middleware/OutputCaching/src/Microsoft.AspNetCore.OutputCaching.csproj new file mode 100644 index 000000000000..9b8baac0274b --- /dev/null +++ b/src/Middleware/OutputCaching/src/Microsoft.AspNetCore.OutputCaching.csproj @@ -0,0 +1,27 @@ + + + + ASP.NET Core middleware for caching HTTP responses on the server. + $(DefaultNetCoreTargetFramework) + true + true + true + aspnetcore;cache;caching + false + enable + false + + + + + + + + + + + + + + + diff --git a/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs b/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs new file mode 100644 index 000000000000..5b71f7d1b3d5 --- /dev/null +++ b/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs @@ -0,0 +1,82 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.OutputCaching; + +/// +/// Specifies the parameters necessary for setting appropriate headers in output caching. +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] +public class OutputCacheAttribute : Attribute, IPoliciesMetadata +{ + // A nullable-int cannot be used as an Attribute parameter. + // Hence this nullable-int is present to back the Duration property. + // The same goes for nullable-ResponseCacheLocation and nullable-bool. + private int? _duration; + private bool? _noStore; + + private List? _policies; + + /// + /// Gets or sets the duration in seconds for which the response is cached. + /// + public int Duration + { + get => _duration ?? 0; + set => _duration = value; + } + + /// + /// Gets or sets the value which determines whether the data should be stored or not. + /// When set to , the response won't be cached. + /// + public bool NoStore + { + get => _noStore ?? false; + set => _noStore = value; + } + + /// + /// Gets or sets the query keys to vary by. + /// + /// + /// requires the output cache middleware. + /// + public string[]? VaryByQueryKeys { get; set; } + + /// + /// Gets or sets the value of the cache profile name. + /// + public string? Profile { get; set; } + + public List Policies => _policies ??= GetPolicies(); + + private List GetPolicies() + { + var policies = new List(5); + + policies.Add(EnableCachingPolicy.Instance); + + if (_noStore != null && _noStore.Value) + { + policies.Add(new NoStorePolicy()); + } + + if (Profile != null) + { + policies.Add(new ProfilePolicy(Profile)); + } + + if (VaryByQueryKeys != null) + { + policies.Add(new VaryByQueryPolicy(VaryByQueryKeys)); + } + + if (_duration != null) + { + policies.Add(new ExpirationPolicy(TimeSpan.FromSeconds(_duration.Value))); + } + + return policies; + } +} diff --git a/src/Middleware/OutputCaching/src/OutputCachingContext.cs b/src/Middleware/OutputCaching/src/OutputCachingContext.cs new file mode 100644 index 000000000000..eb5a967e4e98 --- /dev/null +++ b/src/Middleware/OutputCaching/src/OutputCachingContext.cs @@ -0,0 +1,148 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#nullable disable + +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.OutputCaching; + +public class OutputCachingContext : IOutputCachingContext +{ + private DateTimeOffset? _responseDate; + private bool _parsedResponseDate; + private DateTimeOffset? _responseExpires; + private bool _parsedResponseExpires; + private TimeSpan? _responseSharedMaxAge; + private bool _parsedResponseSharedMaxAge; + private TimeSpan? _responseMaxAge; + private bool _parsedResponseMaxAge; + + internal OutputCachingContext(HttpContext httpContext, ILogger logger) + { + HttpContext = httpContext; + Logger = logger; + } + + /// + /// Determine whether the output caching logic should is configured for the incoming HTTP request. + /// + public bool EnableOutputCaching { get; set; } + + /// + /// Determine whether the response caching logic should be attempted for the incoming HTTP request. + /// + public bool AttemptResponseCaching { get; set; } + + /// + /// Determine whether a cache lookup is allowed for the incoming HTTP request. + /// + public bool AllowCacheLookup { get; set; } + + /// + /// Determine whether storage of the response is allowed for the incoming HTTP request. + /// + public bool AllowCacheStorage { get; set; } + + /// + /// Determine whether request should be locked. + /// + public bool AllowLocking { get; set; } + + /// + /// Determine whether the response received by the middleware can be cached for future requests. + /// + public bool IsResponseCacheable { get; set; } + + /// + /// Determine whether the response retrieved from the response cache is fresh and can be served. + /// + public bool IsCacheEntryFresh { get; set; } + + public HttpContext HttpContext { get; } + + public DateTimeOffset? ResponseTime { get; internal set; } + + public TimeSpan? CachedEntryAge { get; internal set; } + + public CachedVaryByRules CachedVaryByRules { get; set; } = new(); + public HashSet Tags { get; } = new(); + + public ILogger Logger { get; } + + internal string CacheKey { get; set; } + + internal TimeSpan CachedResponseValidFor { get; set; } + + internal OutputCacheEntry CachedResponse { get; set; } + + internal bool ResponseStarted { get; set; } + + internal Stream OriginalResponseStream { get; set; } + + internal OutputCachingStream OutputCachingStream { get; set; } + + public IHeaderDictionary CachedResponseHeaders { get; set; } + + public TimeSpan? ResponseExpirationTimeSpan { get; set; } + + public DateTimeOffset? ResponseDate + { + get + { + if (!_parsedResponseDate) + { + _parsedResponseDate = true; + _responseDate = HeaderUtilities.TryParseDate(HttpContext.Response.Headers.Date.ToString(), out var date) ? date : null; + } + return _responseDate; + } + set + { + // Don't reparse the response date again if it's explicitly set + _parsedResponseDate = true; + _responseDate = value; + } + } + + public DateTimeOffset? ResponseExpires + { + get + { + if (!_parsedResponseExpires) + { + _parsedResponseExpires = true; + _responseExpires = HeaderUtilities.TryParseDate(HttpContext.Response.Headers.Expires.ToString(), out var expires) ? expires : null; + } + return _responseExpires; + } + } + + public TimeSpan? ResponseSharedMaxAge + { + get + { + if (!_parsedResponseSharedMaxAge) + { + _parsedResponseSharedMaxAge = true; + HeaderUtilities.TryParseSeconds(HttpContext.Response.Headers.CacheControl, CacheControlHeaderValue.SharedMaxAgeString, out _responseSharedMaxAge); + } + return _responseSharedMaxAge; + } + } + + public TimeSpan? ResponseMaxAge + { + get + { + if (!_parsedResponseMaxAge) + { + _parsedResponseMaxAge = true; + HeaderUtilities.TryParseSeconds(HttpContext.Response.Headers.CacheControl, CacheControlHeaderValue.MaxAgeString, out _responseMaxAge); + } + return _responseMaxAge; + } + } +} diff --git a/src/Middleware/OutputCaching/src/OutputCachingExtensions.cs b/src/Middleware/OutputCaching/src/OutputCachingExtensions.cs new file mode 100644 index 000000000000..8da224853995 --- /dev/null +++ b/src/Middleware/OutputCaching/src/OutputCachingExtensions.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.OutputCaching; + +namespace Microsoft.AspNetCore.Builder; + +/// +/// Extension methods for adding the to an application. +/// +public static class OutputCachingExtensions +{ + /// + /// Adds the for caching HTTP responses. + /// + /// The . + public static IApplicationBuilder UseOutputCaching(this IApplicationBuilder app) + { + ArgumentNullException.ThrowIfNull(app, nameof(app)); + + return app.UseMiddleware(); + } +} diff --git a/src/Middleware/OutputCaching/src/OutputCachingFeature.cs b/src/Middleware/OutputCaching/src/OutputCachingFeature.cs new file mode 100644 index 000000000000..74e80cabaad1 --- /dev/null +++ b/src/Middleware/OutputCaching/src/OutputCachingFeature.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.OutputCaching; + +/// Default implementation for +public class OutputCachingFeature : IOutputCachingFeature +{ + public OutputCachingFeature(IOutputCachingContext context) + { + Context = context; + } + + public IOutputCachingContext Context { get; } + + public List Policies { get; } = new(); +} diff --git a/src/Middleware/OutputCaching/src/OutputCachingKeyProvider.cs b/src/Middleware/OutputCaching/src/OutputCachingKeyProvider.cs new file mode 100644 index 000000000000..c0eb13593079 --- /dev/null +++ b/src/Middleware/OutputCaching/src/OutputCachingKeyProvider.cs @@ -0,0 +1,190 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using System.Text; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.OutputCaching; + +internal sealed class OutputCachingKeyProvider : IOutputCachingKeyProvider +{ + // Use the record separator for delimiting components of the cache key to avoid possible collisions + private const char KeyDelimiter = '\x1e'; + // Use the unit separator for delimiting subcomponents of the cache key to avoid possible collisions + private const char KeySubDelimiter = '\x1f'; + + private readonly ObjectPool _builderPool; + private readonly OutputCachingOptions _options; + + internal OutputCachingKeyProvider(ObjectPoolProvider poolProvider, IOptions options) + { + ArgumentNullException.ThrowIfNull(poolProvider, nameof(poolProvider)); + ArgumentNullException.ThrowIfNull(options, nameof(options)); + + _builderPool = poolProvider.CreateStringBuilderPool(); + _options = options.Value; + } + + // GETSCHEMEHOST:PORT/PATHBASE/PATHHHeaderName=HeaderValueQQueryName=QueryValue1QueryValue2 + public string CreateStorageVaryByKey(OutputCachingContext context) + { + ArgumentNullException.ThrowIfNull(_builderPool, nameof(context)); + + var varyByRules = context.CachedVaryByRules; + if (varyByRules == null) + { + throw new InvalidOperationException($"{nameof(CachedVaryByRules)} must not be null on the {nameof(OutputCachingContext)}"); + } + + var request = context.HttpContext.Request; + var builder = _builderPool.Get(); + + try + { + builder + .AppendUpperInvariant(request.Method) + .Append(KeyDelimiter) + .AppendUpperInvariant(request.Scheme) + .Append(KeyDelimiter) + .AppendUpperInvariant(request.Host.Value); + + if (_options.UseCaseSensitivePaths) + { + builder + .Append(request.PathBase.Value) + .Append(request.Path.Value); + } + else + { + builder + .AppendUpperInvariant(request.PathBase.Value) + .AppendUpperInvariant(request.Path.Value); + } + + // Vary by prefix and custom + var prefixCount = varyByRules?.VaryByPrefix.Count ?? 0; + if (prefixCount > 0) + { + // Append a group separator for the header segment of the cache key + builder.Append(KeyDelimiter) + .Append('C'); + + for (var i = 0; i < prefixCount; i++) + { + var value = varyByRules?.VaryByPrefix[i] ?? string.Empty; + builder.Append(KeyDelimiter).Append(value); + } + } + + // Vary by headers + var headersCount = varyByRules?.Headers.Count ?? 0; + if (headersCount > 0) + { + // Append a group separator for the header segment of the cache key + builder.Append(KeyDelimiter) + .Append('H'); + + var requestHeaders = context.HttpContext.Request.Headers; + for (var i = 0; i < headersCount; i++) + { + var header = varyByRules!.Headers[i] ?? string.Empty; + var headerValues = requestHeaders[header]; + builder.Append(KeyDelimiter) + .Append(header) + .Append('='); + + var headerValuesArray = headerValues.ToArray(); + Array.Sort(headerValuesArray, StringComparer.Ordinal); + + for (var j = 0; j < headerValuesArray.Length; j++) + { + builder.Append(headerValuesArray[j]); + } + } + } + + // Vary by query keys + if (varyByRules?.QueryKeys.Count > 0) + { + // Append a group separator for the query key segment of the cache key + builder.Append(KeyDelimiter) + .Append('Q'); + + if (varyByRules.QueryKeys.Count == 1 && string.Equals(varyByRules.QueryKeys[0], "*", StringComparison.Ordinal)) + { + // Vary by all available query keys + var queryArray = context.HttpContext.Request.Query.ToArray(); + // Query keys are aggregated case-insensitively whereas the query values are compared ordinally. + Array.Sort(queryArray, QueryKeyComparer.OrdinalIgnoreCase); + + for (var i = 0; i < queryArray.Length; i++) + { + builder.Append(KeyDelimiter) + .AppendUpperInvariant(queryArray[i].Key) + .Append('='); + + var queryValueArray = queryArray[i].Value.ToArray(); + Array.Sort(queryValueArray, StringComparer.Ordinal); + + for (var j = 0; j < queryValueArray.Length; j++) + { + if (j > 0) + { + builder.Append(KeySubDelimiter); + } + + builder.Append(queryValueArray[j]); + } + } + } + else + { + for (var i = 0; i < varyByRules.QueryKeys.Count; i++) + { + var queryKey = varyByRules.QueryKeys[i] ?? string.Empty; + var queryKeyValues = context.HttpContext.Request.Query[queryKey]; + builder.Append(KeyDelimiter) + .Append(queryKey) + .Append('='); + + var queryValueArray = queryKeyValues.ToArray(); + Array.Sort(queryValueArray, StringComparer.Ordinal); + + for (var j = 0; j < queryValueArray.Length; j++) + { + if (j > 0) + { + builder.Append(KeySubDelimiter); + } + + builder.Append(queryValueArray[j]); + } + } + } + } + + return builder.ToString(); + } + finally + { + _builderPool.Return(builder); + } + } + + private class QueryKeyComparer : IComparer> + { + private readonly StringComparer _stringComparer; + + public static QueryKeyComparer OrdinalIgnoreCase { get; } = new QueryKeyComparer(StringComparer.OrdinalIgnoreCase); + + public QueryKeyComparer(StringComparer stringComparer) + { + _stringComparer = stringComparer; + } + + public int Compare(KeyValuePair x, KeyValuePair y) => _stringComparer.Compare(x.Key, y.Key); + } +} diff --git a/src/Middleware/OutputCaching/src/OutputCachingMiddleware.cs b/src/Middleware/OutputCaching/src/OutputCachingMiddleware.cs new file mode 100644 index 000000000000..1451965b1248 --- /dev/null +++ b/src/Middleware/OutputCaching/src/OutputCachingMiddleware.cs @@ -0,0 +1,545 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.OutputCaching; + +/// +/// Enable HTTP response caching. +/// +public class OutputCachingMiddleware +{ + // see https://tools.ietf.org/html/rfc7232#section-4.1 + private static readonly string[] HeadersToIncludeIn304 = + new[] { "Cache-Control", "Content-Location", "Date", "ETag", "Expires", "Vary" }; + + private readonly RequestDelegate _next; + private readonly OutputCachingOptions _options; + private readonly ILogger _logger; + private readonly IOutputCachingPolicyProvider _policyProvider; + private readonly IOutputCacheStore _cache; + private readonly IOutputCachingKeyProvider _keyProvider; + private readonly WorkDispatcher _outputCacheEntryDispatcher; + private readonly WorkDispatcher _requestDispatcher; + + /// + /// Creates a new . + /// + /// The representing the next middleware in the pipeline. + /// The options for this middleware. + /// The used for logging. + /// The used for creating instances. + public OutputCachingMiddleware( + RequestDelegate next, + IOptions options, + ILoggerFactory loggerFactory, + IOutputCacheStore outputCache, + ObjectPoolProvider poolProvider + ) + : this( + next, + options, + loggerFactory, + new OutputCachingPolicyProvider(options), + outputCache, + new OutputCachingKeyProvider(poolProvider, options)) + { } + + // for testing + internal OutputCachingMiddleware( + RequestDelegate next, + IOptions options, + ILoggerFactory loggerFactory, + IOutputCachingPolicyProvider policyProvider, + IOutputCacheStore cache, + IOutputCachingKeyProvider keyProvider) + { + ArgumentNullException.ThrowIfNull(next, nameof(next)); + ArgumentNullException.ThrowIfNull(options, nameof(options)); + ArgumentNullException.ThrowIfNull(loggerFactory, nameof(loggerFactory)); + ArgumentNullException.ThrowIfNull(policyProvider, nameof(policyProvider)); + ArgumentNullException.ThrowIfNull(cache, nameof(cache)); + ArgumentNullException.ThrowIfNull(keyProvider, nameof(keyProvider)); + + _next = next; + _options = options.Value; + _logger = loggerFactory.CreateLogger(); + _policyProvider = policyProvider; + _cache = cache; + _keyProvider = keyProvider; + _outputCacheEntryDispatcher = new(); + _requestDispatcher = new(); + } + + /// + /// Invokes the logic of the middleware. + /// + /// The . + /// A that completes when the middleware has completed processing. + public async Task Invoke(HttpContext httpContext) + { + var context = new OutputCachingContext(httpContext, _logger); + + // Add IOutputCachingFeature + AddOutputCachingFeature(context); + + try + { + await _policyProvider.OnRequestAsync(context); + + // Should we attempt any caching logic? + if (context.EnableOutputCaching && context.AttemptResponseCaching) + { + // Can this request be served from cache? + if (context.AllowCacheLookup) + { + CreateCacheKey(context); + + // Locking cache lookups by default + // TODO: should it be part of the cache implementations or can we assume all caches would benefit from it? + // It makes sense for caches that use IO (disk, network) or need to deserialize the state but could also be a global option + + var cacheEntry = await _outputCacheEntryDispatcher.ScheduleAsync(context.CacheKey, _cache, static async (key, cache) => await cache.GetAsync(key)); + + if (await TryServeFromCacheAsync(context, cacheEntry)) + { + return; + } + } + + // Should we store the response to this request? + if (context.AllowCacheStorage) + { + // It is also a pre-condition to reponse locking + + while (true) + { + var executed = false; + + if (context.AllowLocking) + { + var cacheEntry = await _requestDispatcher.ScheduleAsync(context.CacheKey, key => ExecuteResponseAsync()); + + // If the result was processed by another request, serve it from cache + if (!executed) + { + if (await TryServeFromCacheAsync(context, cacheEntry)) + { + return; + } + } + } + else + { + await ExecuteResponseAsync(); + } + + async Task ExecuteResponseAsync() + { + // Hook up to listen to the response stream + ShimResponseStream(context); + + try + { + await _next(httpContext); + + // The next middleware might change the policy + await _policyProvider.OnServeResponseAsync(context); + + // If there was no response body, check the response headers now. We can cache things like redirects. + StartResponse(context); + + // Finalize the cache entry + await FinalizeCacheBodyAsync(context); + + executed = true; + } + finally + { + UnshimResponseStream(context); + } + + return context.CachedResponse; + } + + return; + } + } + } + + await _next(httpContext); + } + finally + { + RemoveOutputCachingFeature(httpContext); + } + } + + internal async Task TryServeCachedResponseAsync(OutputCachingContext context, OutputCacheEntry cachedResponse) + { + context.CachedResponse = cachedResponse; + context.CachedResponseHeaders = cachedResponse.Headers; + context.ResponseTime = _options.SystemClock.UtcNow; + var cachedEntryAge = context.ResponseTime.Value - context.CachedResponse.Created; + context.CachedEntryAge = cachedEntryAge > TimeSpan.Zero ? cachedEntryAge : TimeSpan.Zero; + + await _policyProvider.OnServeFromCacheAsync(context); + + if (context.IsCacheEntryFresh) + { + // Check conditional request rules + if (ContentIsNotModified(context)) + { + _logger.NotModifiedServed(); + context.HttpContext.Response.StatusCode = StatusCodes.Status304NotModified; + + if (context.CachedResponseHeaders != null) + { + foreach (var key in HeadersToIncludeIn304) + { + if (context.CachedResponseHeaders.TryGetValue(key, out var values)) + { + context.HttpContext.Response.Headers[key] = values; + } + } + } + } + else + { + var response = context.HttpContext.Response; + // Copy the cached status code and response headers + response.StatusCode = context.CachedResponse.StatusCode; + foreach (var header in context.CachedResponse.Headers) + { + response.Headers[header.Key] = header.Value; + } + + // Note: int64 division truncates result and errors may be up to 1 second. This reduction in + // accuracy of age calculation is considered appropriate since it is small compared to clock + // skews and the "Age" header is an estimate of the real age of cached content. + response.Headers.Age = HeaderUtilities.FormatNonNegativeInt64(context.CachedEntryAge.Value.Ticks / TimeSpan.TicksPerSecond); + + // Copy the cached response body + var body = context.CachedResponse.Body; + if (body.Length > 0) + { + try + { + await body.CopyToAsync(response.BodyWriter, context.HttpContext.RequestAborted); + } + catch (OperationCanceledException) + { + context.HttpContext.Abort(); + } + } + _logger.CachedResponseServed(); + } + return true; + } + + return false; + } + + internal async Task TryServeFromCacheAsync(OutputCachingContext context, OutputCacheEntry? cacheEntry) + { + if (cacheEntry != null) + { + if (await TryServeCachedResponseAsync(context, cacheEntry)) + { + return true; + } + } + + if (HeaderUtilities.ContainsCacheDirective(context.HttpContext.Request.Headers.CacheControl, CacheControlHeaderValue.OnlyIfCachedString)) + { + _logger.GatewayTimeoutServed(); + context.HttpContext.Response.StatusCode = StatusCodes.Status504GatewayTimeout; + return true; + } + + _logger.NoResponseServed(); + return false; + } + + private void CreateCacheKey(OutputCachingContext context) + { + var varyHeaders = new StringValues(context.HttpContext.Response.Headers.GetCommaSeparatedValues(HeaderNames.Vary)); + var varyQueryKeys = context.CachedVaryByRules.QueryKeys; + var varyByCustomKeys = context.CachedVaryByRules.VaryByCustom; + var varyByPrefix = context.CachedVaryByRules.VaryByPrefix; + + // Check if any vary rules exist + if (!StringValues.IsNullOrEmpty(varyHeaders) || !StringValues.IsNullOrEmpty(varyQueryKeys) || !StringValues.IsNullOrEmpty(varyByPrefix) || varyByCustomKeys.Count > 0) + { + // Normalize order and casing of vary by rules + var normalizedVaryHeaders = GetOrderCasingNormalizedStringValues(varyHeaders); + var normalizedVaryQueryKeys = GetOrderCasingNormalizedStringValues(varyQueryKeys); + var normalizedVaryByCustom = GetOrderCasingNormalizedDictionary(varyByCustomKeys); + + // Update vary rules with normalized values + context.CachedVaryByRules = new CachedVaryByRules + { + VaryByPrefix = varyByPrefix + normalizedVaryByCustom, + Headers = normalizedVaryHeaders, + QueryKeys = normalizedVaryQueryKeys + }; + + // TODO: Add same condition on LogLevel in Response Caching + // Always overwrite the CachedVaryByRules to update the expiry information + if (_logger.IsEnabled(LogLevel.Debug)) + { + _logger.VaryByRulesUpdated(normalizedVaryHeaders.ToString(), normalizedVaryQueryKeys.ToString()); + } + } + + context.CacheKey = _keyProvider.CreateStorageVaryByKey(context); + } + + /// + /// Finalize cache headers. + /// + /// + private void FinalizeCacheHeaders(OutputCachingContext context) + { + if (context.IsResponseCacheable) + { + // Create the cache entry now + var response = context.HttpContext.Response; + var headers = response.Headers; + + context.CachedResponseValidFor = context.ResponseSharedMaxAge ?? + context.ResponseMaxAge ?? + (context.ResponseExpires - context.ResponseTime!.Value) ?? + context.ResponseExpirationTimeSpan ?? _options.DefaultExpirationTimeSpan; + + // Ensure date header is set + if (!context.ResponseDate.HasValue) + { + context.ResponseDate = context.ResponseTime!.Value; + // Setting the date on the raw response headers. + headers.Date = HeaderUtilities.FormatDate(context.ResponseDate.Value); + } + + // Store the response on the state + context.CachedResponse = new OutputCacheEntry + { + Created = context.ResponseDate.Value, + StatusCode = response.StatusCode, + Headers = new HeaderDictionary(), + Tags = context.Tags.ToArray() + }; + + foreach (var header in headers) + { + if (!string.Equals(header.Key, HeaderNames.Age, StringComparison.OrdinalIgnoreCase)) + { + context.CachedResponse.Headers[header.Key] = header.Value; + } + } + + return; + } + + context.OutputCachingStream.DisableBuffering(); + } + + /// + /// Stores the response body + /// + internal async ValueTask FinalizeCacheBodyAsync(OutputCachingContext context) + { + if (context.IsResponseCacheable && context.OutputCachingStream.BufferingEnabled) + { + var contentLength = context.HttpContext.Response.ContentLength; + var cachedResponseBody = context.OutputCachingStream.GetCachedResponseBody(); + if (!contentLength.HasValue || contentLength == cachedResponseBody.Length + || (cachedResponseBody.Length == 0 + && HttpMethods.IsHead(context.HttpContext.Request.Method))) + { + var response = context.HttpContext.Response; + // Add a content-length if required + if (!response.ContentLength.HasValue && StringValues.IsNullOrEmpty(response.Headers.TransferEncoding)) + { + context.CachedResponse.Headers.ContentLength = cachedResponseBody.Length; + } + + context.CachedResponse.Body = cachedResponseBody; + _logger.ResponseCached(); + + if (string.IsNullOrEmpty(context.CacheKey)) + { + throw new InvalidOperationException("Cache key must be defined"); + } + + await _cache.SetAsync(context.CacheKey, context.CachedResponse, context.CachedResponseValidFor); + } + else + { + _logger.ResponseContentLengthMismatchNotCached(); + } + } + else + { + _logger.LogResponseNotCached(); + } + } + + /// + /// Mark the response as started and set the response time if no response was started yet. + /// + /// + /// true if the response was not started before this call; otherwise false. + private bool OnStartResponse(OutputCachingContext context) + { + if (!context.ResponseStarted) + { + context.ResponseStarted = true; + context.ResponseTime = _options.SystemClock.UtcNow; + + return true; + } + return false; + } + + internal void StartResponse(OutputCachingContext context) + { + if (OnStartResponse(context)) + { + FinalizeCacheHeaders(context); + } + } + + internal static void AddOutputCachingFeature(OutputCachingContext context) + { + if (context.HttpContext.Features.Get() != null) + { + throw new InvalidOperationException($"Another instance of {nameof(OutputCachingFeature)} already exists. Only one instance of {nameof(OutputCachingMiddleware)} can be configured for an application."); + } + + context.HttpContext.Features.Set(new OutputCachingFeature(context)); + } + + internal void ShimResponseStream(OutputCachingContext context) + { + // Shim response stream + context.OriginalResponseStream = context.HttpContext.Response.Body; + context.OutputCachingStream = new OutputCachingStream( + context.OriginalResponseStream, + _options.MaximumBodySize, + StreamUtilities.BodySegmentSize, + () => StartResponse(context)); + context.HttpContext.Response.Body = context.OutputCachingStream; + } + + internal static void RemoveOutputCachingFeature(HttpContext context) => + context.Features.Set(null); + + internal static void UnshimResponseStream(OutputCachingContext context) + { + // Unshim response stream + context.HttpContext.Response.Body = context.OriginalResponseStream; + + // Remove IOutputCachingFeature + RemoveOutputCachingFeature(context.HttpContext); + } + + internal static bool ContentIsNotModified(OutputCachingContext context) + { + var cachedResponseHeaders = context.CachedResponseHeaders; + var ifNoneMatchHeader = context.HttpContext.Request.Headers.IfNoneMatch; + + if (!StringValues.IsNullOrEmpty(ifNoneMatchHeader)) + { + if (ifNoneMatchHeader.Count == 1 && StringSegment.Equals(ifNoneMatchHeader[0], EntityTagHeaderValue.Any.Tag, StringComparison.OrdinalIgnoreCase)) + { + context.Logger.NotModifiedIfNoneMatchStar(); + return true; + } + + if (!StringValues.IsNullOrEmpty(cachedResponseHeaders.ETag) + && EntityTagHeaderValue.TryParse(cachedResponseHeaders.ETag.ToString(), out var eTag) + && EntityTagHeaderValue.TryParseList(ifNoneMatchHeader, out var ifNoneMatchEtags)) + { + for (var i = 0; i < ifNoneMatchEtags.Count; i++) + { + var requestETag = ifNoneMatchEtags[i]; + if (eTag.Compare(requestETag, useStrongComparison: false)) + { + context.Logger.NotModifiedIfNoneMatchMatched(requestETag); + return true; + } + } + } + } + else + { + var ifModifiedSince = context.HttpContext.Request.Headers.IfModifiedSince; + if (!StringValues.IsNullOrEmpty(ifModifiedSince)) + { + if (!HeaderUtilities.TryParseDate(cachedResponseHeaders.LastModified.ToString(), out var modified) && + !HeaderUtilities.TryParseDate(cachedResponseHeaders.Date.ToString(), out modified)) + { + return false; + } + + if (HeaderUtilities.TryParseDate(ifModifiedSince.ToString(), out var modifiedSince) && + modified <= modifiedSince) + { + context.Logger.NotModifiedIfModifiedSinceSatisfied(modified, modifiedSince); + return true; + } + } + } + + return false; + } + + // Normalize order and casing + internal static StringValues GetOrderCasingNormalizedStringValues(StringValues stringValues) + { + if (stringValues.Count == 1) + { + return new StringValues(stringValues.ToString().ToUpperInvariant()); + } + else + { + var originalArray = stringValues.ToArray(); + var newArray = new string[originalArray.Length]; + + for (var i = 0; i < originalArray.Length; i++) + { + newArray[i] = originalArray[i]!.ToUpperInvariant(); + } + + // Since the casing has already been normalized, use Ordinal comparison + Array.Sort(newArray, StringComparer.Ordinal); + + return new StringValues(newArray); + } + } + + internal static StringValues GetOrderCasingNormalizedDictionary(Dictionary dictionary) + { + const char KeySubDelimiter = '\x1f'; + + var newArray = new string[dictionary.Count]; + + var i = 0; + foreach (var (key, value) in dictionary) + { + newArray[i++] = $"{key.ToUpperInvariant()}{KeySubDelimiter}{value}"; + } + + // Since the casing has already been normalized, use Ordinal comparison + Array.Sort(newArray, StringComparer.Ordinal); + + return new StringValues(newArray); + } +} diff --git a/src/Middleware/OutputCaching/src/OutputCachingOptions.cs b/src/Middleware/OutputCaching/src/OutputCachingOptions.cs new file mode 100644 index 000000000000..e26688d54ee4 --- /dev/null +++ b/src/Middleware/OutputCaching/src/OutputCachingOptions.cs @@ -0,0 +1,49 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel; + +namespace Microsoft.AspNetCore.OutputCaching; + +/// +/// Options for configuring the . +/// +public class OutputCachingOptions +{ + /// + /// The size limit for the response cache middleware in bytes. The default is set to 100 MB. + /// When this limit is exceeded, no new responses will be cached until older entries are + /// evicted. + /// + public long SizeLimit { get; set; } = 100 * 1024 * 1024; + + /// + /// The largest cacheable size for the response body in bytes. The default is set to 64 MB. + /// If the response body exceeds this limit, it will not be cached by the . + /// + public long MaximumBodySize { get; set; } = 64 * 1024 * 1024; + + /// + /// The duration a response is cached when no specific value is defined by a policy. The default is set to 60 seconds. + /// + public TimeSpan DefaultExpirationTimeSpan { get; set; } = TimeSpan.FromSeconds(60); + + /// + /// true if request paths are case-sensitive; otherwise false. The default is to treat paths as case-insensitive. + /// + public bool UseCaseSensitivePaths { get; set; } + + public List Policies { get; } = new() { new DefaultOutputCachePolicy() }; + + /// + /// Gets a Dictionary of policy names, which are pre-defined settings for + /// output caching. + /// + public IDictionary Profiles { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); + + /// + /// For testing purposes only. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + internal ISystemClock SystemClock { get; set; } = new SystemClock(); +} diff --git a/src/Middleware/OutputCaching/src/OutputCachingPolicyProvider.cs b/src/Middleware/OutputCaching/src/OutputCachingPolicyProvider.cs new file mode 100644 index 000000000000..108486296549 --- /dev/null +++ b/src/Middleware/OutputCaching/src/OutputCachingPolicyProvider.cs @@ -0,0 +1,102 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.OutputCaching; + +internal sealed class OutputCachingPolicyProvider : IOutputCachingPolicyProvider +{ + private readonly OutputCachingOptions options; + + public OutputCachingPolicyProvider(IOptions options) + { + this.options = options.Value; + } + + public async Task OnRequestAsync(IOutputCachingContext context) + { + foreach (var policy in options.Policies) + { + await policy.OnRequestAsync(context); + } + + var policiesMedata = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata(); + + if (policiesMedata != null) + { + // TODO: Log only? + + if (context.HttpContext.Response.HasStarted) + { + throw new InvalidOperationException("Can't define output caching policies after headers have been sent to client."); + } + + foreach (var policy in policiesMedata.Policies) + { + await policy.OnRequestAsync(context); + } + } + } + + public async Task OnServeFromCacheAsync(IOutputCachingContext context) + { + foreach (var policy in options.Policies) + { + await policy.OnServeFromCacheAsync(context); + } + + // Apply response policies defined on the feature, e.g. from action attributes + + var responsePolicies = context.HttpContext.Features.Get()?.Policies; + + if (responsePolicies != null) + { + foreach (var policy in responsePolicies) + { + await policy.OnServeFromCacheAsync(context); + } + } + + var policiesMedata = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata(); + + if (policiesMedata != null) + { + foreach (var policy in policiesMedata.Policies) + { + await policy.OnServeFromCacheAsync(context); + } + } + } + + public async Task OnServeResponseAsync(IOutputCachingContext context) + { + foreach (var policy in options.Policies) + { + await policy.OnServeResponseAsync(context); + } + + // Apply response policies defined on the feature, e.g. from action attributes + + var responsePolicies = context.HttpContext.Features.Get()?.Policies; + + if (responsePolicies != null) + { + foreach (var policy in responsePolicies) + { + await policy.OnServeResponseAsync(context); + } + } + + var policiesMedata = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata(); + + if (policiesMedata != null) + { + foreach (var policy in policiesMedata.Policies) + { + await policy.OnServeResponseAsync(context); + } + } + } +} diff --git a/src/Middleware/OutputCaching/src/OutputCachingServicesExtensions.cs b/src/Middleware/OutputCaching/src/OutputCachingServicesExtensions.cs new file mode 100644 index 000000000000..eba05ea1150f --- /dev/null +++ b/src/Middleware/OutputCaching/src/OutputCachingServicesExtensions.cs @@ -0,0 +1,56 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.OutputCaching; +using Microsoft.AspNetCore.OutputCaching.Memory; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Options; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Extension methods for the OutputCaching middleware. +/// +public static class OutputCachingServicesExtensions +{ + /// + /// Add output caching services. + /// + /// The for adding services. + /// + public static IServiceCollection AddOutputCaching(this IServiceCollection services) + { + ArgumentNullException.ThrowIfNull(nameof(services)); + + services.TryAddSingleton(); + + services.TryAddSingleton(sp => + { + var outputCacheOptions = sp.GetRequiredService>(); + return new MemoryOutputCacheStore(new MemoryCache(new MemoryCacheOptions + { + SizeLimit = outputCacheOptions.Value.SizeLimit + })); + }); + return services; + } + + /// + /// Add output caching services and configure the related options. + /// + /// The for adding services. + /// A delegate to configure the . + /// + public static IServiceCollection AddOutputCaching(this IServiceCollection services, Action configureOptions) + { + ArgumentNullException.ThrowIfNull(services, nameof(services)); + ArgumentNullException.ThrowIfNull(configureOptions, nameof(configureOptions)); + + services.Configure(configureOptions); + services.AddOutputCaching(); + + return services; + } +} diff --git a/src/Middleware/OutputCaching/src/Policies/CompositePolicy.cs b/src/Middleware/OutputCaching/src/Policies/CompositePolicy.cs new file mode 100644 index 000000000000..9206a8837f33 --- /dev/null +++ b/src/Middleware/OutputCaching/src/Policies/CompositePolicy.cs @@ -0,0 +1,38 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.OutputCaching.Policies; + +public class CompositePolicy : IOutputCachingPolicy +{ + private readonly IOutputCachingPolicy[] _policies; + + public CompositePolicy(params IOutputCachingPolicy[] policies!!) + { + _policies = policies; + } + + public async Task OnRequestAsync(IOutputCachingContext context) + { + foreach (var policy in _policies) + { + await policy.OnRequestAsync(context); + } + } + + public async Task OnServeFromCacheAsync(IOutputCachingContext context) + { + foreach (var policy in _policies) + { + await policy.OnServeFromCacheAsync(context); + } + } + + public async Task OnServeResponseAsync(IOutputCachingContext context) + { + foreach (var policy in _policies) + { + await policy.OnServeResponseAsync(context); + } + } +} diff --git a/src/Middleware/OutputCaching/src/Policies/DefaultOutputCachePolicy.cs b/src/Middleware/OutputCaching/src/Policies/DefaultOutputCachePolicy.cs new file mode 100644 index 000000000000..a973aacb08f5 --- /dev/null +++ b/src/Middleware/OutputCaching/src/Policies/DefaultOutputCachePolicy.cs @@ -0,0 +1,62 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.OutputCaching; + +/// +/// Default policy. +/// +public class DefaultOutputCachePolicy : IOutputCachingPolicy +{ + public Task OnRequestAsync(IOutputCachingContext context) + { + context.AttemptResponseCaching = AttemptOutputCaching(context); + context.AllowCacheLookup = true; + context.AllowCacheStorage = true; + context.AllowLocking = true; + context.IsResponseCacheable = true; + + // Vary by any query by default + context.CachedVaryByRules.QueryKeys = "*"; + + return Task.CompletedTask; + } + + public Task OnServeFromCacheAsync(IOutputCachingContext context) + { + context.IsCacheEntryFresh = true; + return Task.CompletedTask; + } + + public Task OnServeResponseAsync(IOutputCachingContext context) + { + context.IsResponseCacheable = true; + return Task.CompletedTask; + } + + private static bool AttemptOutputCaching(IOutputCachingContext context) + { + // TODO: Should it come from options such that it can be changed without a custom default policy? + + var request = context.HttpContext.Request; + + // Verify the method + if (!HttpMethods.IsGet(request.Method) && !HttpMethods.IsHead(request.Method)) + { + context.Logger.RequestMethodNotCacheable(request.Method); + return false; + } + + // Verify existence of authorization headers + if (!StringValues.IsNullOrEmpty(request.Headers.Authorization) || request.HttpContext.User?.Identity?.IsAuthenticated == true) + { + context.Logger.RequestWithAuthorizationNotCacheable(); + return false; + } + + return true; + } +} diff --git a/src/Middleware/OutputCaching/src/Policies/EnableCachingPolicy.cs b/src/Middleware/OutputCaching/src/Policies/EnableCachingPolicy.cs new file mode 100644 index 000000000000..6ddf21d11550 --- /dev/null +++ b/src/Middleware/OutputCaching/src/Policies/EnableCachingPolicy.cs @@ -0,0 +1,29 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.OutputCaching; + +/// +/// A policy that enables caching +/// +public class EnableCachingPolicy : IOutputCachingPolicy +{ + public static EnableCachingPolicy Instance = new EnableCachingPolicy(); + + public Task OnRequestAsync(IOutputCachingContext context) + { + context.EnableOutputCaching = true; + + return Task.CompletedTask; + } + + public Task OnServeResponseAsync(IOutputCachingContext context) + { + return Task.CompletedTask; + } + + public Task OnServeFromCacheAsync(IOutputCachingContext context) + { + return Task.CompletedTask; + } +} diff --git a/src/Middleware/OutputCaching/src/Policies/ExpirationPolicy.cs b/src/Middleware/OutputCaching/src/Policies/ExpirationPolicy.cs new file mode 100644 index 000000000000..425a2a113abd --- /dev/null +++ b/src/Middleware/OutputCaching/src/Policies/ExpirationPolicy.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.OutputCaching; + +/// +/// A policy that defines a custom expiration timespan. +/// +public class ExpirationPolicy : IOutputCachingPolicy +{ + private readonly TimeSpan _expiration; + + public ExpirationPolicy(TimeSpan expiration) + { + _expiration = expiration; + } + + public Task OnRequestAsync(IOutputCachingContext context) + { + context.ResponseExpirationTimeSpan = _expiration; + + return Task.CompletedTask; + } + + public Task OnServeFromCacheAsync(IOutputCachingContext context) + { + return Task.CompletedTask; + } + + public Task OnServeResponseAsync(IOutputCachingContext context) + { + return Task.CompletedTask; + } +} diff --git a/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs b/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs new file mode 100644 index 000000000000..275599167cf0 --- /dev/null +++ b/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.OutputCaching; + +/// +/// A policy that defines a custom expiration timespan. +/// +public class LockingPolicy : IOutputCachingPolicy +{ + private readonly bool _lockResponse; + + public LockingPolicy(bool lockResponse) + { + _lockResponse = lockResponse; + } + + public Task OnRequestAsync(IOutputCachingContext context) + { + context.AllowLocking = _lockResponse; + + return Task.CompletedTask; + } + + public Task OnServeFromCacheAsync(IOutputCachingContext context) + { + return Task.CompletedTask; + } + + public Task OnServeResponseAsync(IOutputCachingContext context) + { + return Task.CompletedTask; + } +} diff --git a/src/Middleware/OutputCaching/src/Policies/NoStorePolicy.cs b/src/Middleware/OutputCaching/src/Policies/NoStorePolicy.cs new file mode 100644 index 000000000000..1ee6ba1ef0fd --- /dev/null +++ b/src/Middleware/OutputCaching/src/Policies/NoStorePolicy.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.OutputCaching; + +/// +/// A policy that prevents caching +/// +public class NoStorePolicy : IOutputCachingPolicy +{ + public Task OnServeResponseAsync(IOutputCachingContext context) + { + context.IsResponseCacheable = false; + + return Task.CompletedTask; + } + + public Task OnServeFromCacheAsync(IOutputCachingContext context) + { + return Task.CompletedTask; + } + + public Task OnRequestAsync(IOutputCachingContext context) + { + return Task.CompletedTask; + } +} diff --git a/src/Middleware/OutputCaching/src/Policies/OutputCachePolicyBuilder.cs b/src/Middleware/OutputCaching/src/Policies/OutputCachePolicyBuilder.cs new file mode 100644 index 000000000000..55e595f99b91 --- /dev/null +++ b/src/Middleware/OutputCaching/src/Policies/OutputCachePolicyBuilder.cs @@ -0,0 +1,182 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.OutputCaching.Policies; + +namespace Microsoft.AspNetCore.OutputCaching; + +public class OutputCachePolicyBuilder +{ + private List Policies { get; } = new(); + private List>> Requirements { get; } = new(); + + public OutputCachePolicyBuilder When(Func> predicate) + { + Requirements.Add(predicate); + return this; + } + + public OutputCachePolicyBuilder Path(PathString pathBase) + { + ArgumentNullException.ThrowIfNull(pathBase, nameof(pathBase)); + + Requirements.Add(context => + { + var match = context.HttpContext.Request.Path.StartsWithSegments(pathBase); + return Task.FromResult(match); + }); + return this; + } + + public OutputCachePolicyBuilder Path(params PathString[] pathBases) + { + ArgumentNullException.ThrowIfNull(pathBases, nameof(pathBases)); + + Requirements.Add(context => + { + var match = pathBases.Any(x => context.HttpContext.Request.Path.StartsWithSegments(x)); + return Task.FromResult(match); + }); + return this; + } + + public OutputCachePolicyBuilder Method(string method) + { + ArgumentNullException.ThrowIfNull(method, nameof(method)); + + Requirements.Add(context => + { + var upperMethod = method.ToUpperInvariant(); + var match = context.HttpContext.Request.Method.ToUpperInvariant() == upperMethod; + return Task.FromResult(match); + }); + return this; + } + + public OutputCachePolicyBuilder Method(params string[] methods) + { + ArgumentNullException.ThrowIfNull(methods, nameof(methods)); + + Requirements.Add(context => + { + var upperMethods = methods.Select(m => m.ToUpperInvariant()).ToArray(); + var match = methods.Any(m => context.HttpContext.Request.Method.ToUpperInvariant() == m); + return Task.FromResult(match); + }); + return this; + } + + public OutputCachePolicyBuilder VaryByQuery(params string[] queryKeys) + { + ArgumentNullException.ThrowIfNull(queryKeys, nameof(queryKeys)); + + Policies.Add(new VaryByQueryPolicy(queryKeys)); + return this; + } + + public OutputCachePolicyBuilder VaryByValue(Func> varyBy) + { + ArgumentNullException.ThrowIfNull(varyBy, nameof(varyBy)); + + Policies.Add(new VaryByValuePolicy(varyBy)); + return this; + } + + public OutputCachePolicyBuilder VaryByValue(Func> varyBy) + { + ArgumentNullException.ThrowIfNull(varyBy, nameof(varyBy)); + + Policies.Add(new VaryByValuePolicy(varyBy)); + return this; + } + + public OutputCachePolicyBuilder VaryByValue(Func varyBy) + { + ArgumentNullException.ThrowIfNull(varyBy, nameof(varyBy)); + + Policies.Add(new VaryByValuePolicy(varyBy)); + return this; + } + + public OutputCachePolicyBuilder VaryByValue(Func<(string, string)> varyBy) + { + ArgumentNullException.ThrowIfNull(varyBy, nameof(varyBy)); + + Policies.Add(new VaryByValuePolicy(varyBy)); + return this; + } + + public OutputCachePolicyBuilder Profile(string profileName) + { + ArgumentNullException.ThrowIfNull(profileName, nameof(profileName)); + + Policies.Add(new ProfilePolicy(profileName)); + + return this; + } + + public OutputCachePolicyBuilder Tag(params string[] tags) + { + ArgumentNullException.ThrowIfNull(tags, nameof(tags)); + + Policies.Add(new TagsPolicy(tags)); + return this; + } + + public OutputCachePolicyBuilder Expires(TimeSpan expiration) + { + Policies.Add(new ExpirationPolicy(expiration)); + return this; + } + + public OutputCachePolicyBuilder Lock(bool lockResponse = true) + { + Policies.Add(new LockingPolicy(lockResponse)); + return this; + } + + public OutputCachePolicyBuilder Clear() + { + Requirements.Clear(); + Policies.Clear(); + return this; + } + + public OutputCachePolicyBuilder NoStore() + { + Policies.Add(new NoStorePolicy()); + return this; + } + + /// + /// Builds a new from the definitions + /// in this instance. + /// + /// + /// A new built from the definitions in this instance. + /// + public IOutputCachingPolicy Build() + { + var policies = new CompositePolicy(Policies.ToArray()); + + if (Requirements.Any()) + { + return new PredicatePolicy(async c => + { + foreach (var r in Requirements) + { + if (!await r(c)) + { + return false; + } + } + + return true; + }, policies); + } + + return policies; + } +} diff --git a/src/Middleware/OutputCaching/src/Policies/PoliciesMetadata.cs b/src/Middleware/OutputCaching/src/Policies/PoliciesMetadata.cs new file mode 100644 index 000000000000..f38d84795da8 --- /dev/null +++ b/src/Middleware/OutputCaching/src/Policies/PoliciesMetadata.cs @@ -0,0 +1,9 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.OutputCaching.Policies; + +internal sealed class PoliciesMetadata : IPoliciesMetadata +{ + public List Policies { get; } = new(); +} diff --git a/src/Middleware/OutputCaching/src/Policies/PolicyExtensions.cs b/src/Middleware/OutputCaching/src/Policies/PolicyExtensions.cs new file mode 100644 index 000000000000..8bfc3cbe8994 --- /dev/null +++ b/src/Middleware/OutputCaching/src/Policies/PolicyExtensions.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Builder; + +namespace Microsoft.AspNetCore.OutputCaching.Policies; +public static class PolicyExtensions +{ + public static TBuilder OutputCache(this TBuilder builder) where TBuilder : IEndpointConventionBuilder + { + ArgumentNullException.ThrowIfNull(builder, nameof(builder)); + + var policiesMetadata = new PoliciesMetadata(); + + // Enable caching if this method is invoked on an endpoint, extra policies can disable it + policiesMetadata.Policies.Add(EnableCachingPolicy.Instance); + + builder.Add(endpointBuilder => + { + endpointBuilder.Metadata.Add(policiesMetadata); + }); + return builder; + } + + public static TBuilder OutputCache(this TBuilder builder, params IOutputCachingPolicy[] items) where TBuilder : IEndpointConventionBuilder + { + ArgumentNullException.ThrowIfNull(builder, nameof(builder)); + ArgumentNullException.ThrowIfNull(items, nameof(items)); + + var policiesMetadata = new PoliciesMetadata(); + + // Enable caching if this method is invoked on an endpoint, extra policies can disable it + policiesMetadata.Policies.Add(EnableCachingPolicy.Instance); + + policiesMetadata.Policies.AddRange(items); + + builder.Add(endpointBuilder => + { + endpointBuilder.Metadata.Add(policiesMetadata); + }); + return builder; + } + + public static TBuilder OutputCache(this TBuilder builder, Action policy) where TBuilder : IEndpointConventionBuilder + { + ArgumentNullException.ThrowIfNull(builder, nameof(builder)); + + var outputCachePolicyBuilder = new OutputCachePolicyBuilder(); + policy?.Invoke(outputCachePolicyBuilder); + + var policiesMetadata = new PoliciesMetadata(); + + // Enable caching if this method is invoked on an endpoint, extra policies can disable it + policiesMetadata.Policies.Add(EnableCachingPolicy.Instance); + + policiesMetadata.Policies.Add(outputCachePolicyBuilder.Build()); + + builder.Add(endpointBuilder => + { + endpointBuilder.Metadata.Add(policiesMetadata); + }); + + return builder; + } +} diff --git a/src/Middleware/OutputCaching/src/Policies/PredicatePolicy.cs b/src/Middleware/OutputCaching/src/Policies/PredicatePolicy.cs new file mode 100644 index 000000000000..6594ab1829a0 --- /dev/null +++ b/src/Middleware/OutputCaching/src/Policies/PredicatePolicy.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.OutputCaching.Policies; +public class PredicatePolicy : IOutputCachingPolicy +{ + // TODO: Accept a non async predicate too? + + private readonly Func> _predicate; + private readonly IOutputCachingPolicy _policy; + + public PredicatePolicy(Func> predicate, IOutputCachingPolicy policy) + { + _predicate = predicate; + _policy = policy; + } + + public Task OnRequestAsync(IOutputCachingContext context) + { + if (_predicate == null) + { + return _policy.OnRequestAsync(context); + } + + var task = _predicate(context); + + if (task.IsCompletedSuccessfully) + { + if (task.Result) + { + return _policy.OnRequestAsync(context); + } + + return Task.CompletedTask; + } + + return Awaited(task, _policy, context); + + async static Task Awaited(Task task, IOutputCachingPolicy policy, IOutputCachingContext context) + { + if (await task) + { + await policy.OnRequestAsync(context); + } + } + } + + public Task OnServeFromCacheAsync(IOutputCachingContext context) + { + return Task.CompletedTask; + } + + public Task OnServeResponseAsync(IOutputCachingContext context) + { + return Task.CompletedTask; + } +} diff --git a/src/Middleware/OutputCaching/src/Policies/ProfilePolicy.cs b/src/Middleware/OutputCaching/src/Policies/ProfilePolicy.cs new file mode 100644 index 000000000000..9513dd176f49 --- /dev/null +++ b/src/Middleware/OutputCaching/src/Policies/ProfilePolicy.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.OutputCaching; + +/// +/// A policy that prevents caching +/// +public class ProfilePolicy : IOutputCachingPolicy +{ + private readonly string _profileName; + + public ProfilePolicy(string profileName) + { + _profileName = profileName; + } + + public Task OnServeResponseAsync(IOutputCachingContext context) + { + var policy = GetProfilePolicy(context); + + if (policy == null) + { + return Task.CompletedTask; + } + + return policy.OnServeResponseAsync(context); + } + + public Task OnServeFromCacheAsync(IOutputCachingContext context) + { + var policy = GetProfilePolicy(context); + + if (policy == null) + { + return Task.CompletedTask; + } + + return policy.OnServeFromCacheAsync(context); + } + + public Task OnRequestAsync(IOutputCachingContext context) + { + var policy = GetProfilePolicy(context); + + if (policy == null) + { + return Task.CompletedTask; + } + + return policy.OnRequestAsync(context); ; + } + + internal IOutputCachingPolicy GetProfilePolicy(IOutputCachingContext context) + { + var options = context.HttpContext.RequestServices.GetRequiredService>(); + + return options.Value.Profiles.TryGetValue(_profileName, out var cacheProfile) + ? cacheProfile + : null; + } +} diff --git a/src/Middleware/OutputCaching/src/Policies/ResponseCachingPolicy.cs b/src/Middleware/OutputCaching/src/Policies/ResponseCachingPolicy.cs new file mode 100644 index 000000000000..bf03ca9c8f6e --- /dev/null +++ b/src/Middleware/OutputCaching/src/Policies/ResponseCachingPolicy.cs @@ -0,0 +1,275 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.OutputCaching; + +/// +/// Provides the policy implemented by Response caching +/// +public class ResponseCachingPolicy : IOutputCachingPolicy +{ + public Task OnRequestAsync(IOutputCachingContext context) + { + context.AttemptResponseCaching = AttemptOutputCaching(context); + context.AllowCacheLookup = AllowCacheLookup(context); + context.AllowCacheStorage = AllowCacheStorage(context); + context.AllowLocking = true; + + // Vary by any query by default + context.CachedVaryByRules.QueryKeys = "*"; + + return Task.CompletedTask; + } + + public Task OnServeResponseAsync(IOutputCachingContext context) + { + context.IsResponseCacheable = IsResponseCacheable(context); + + return Task.CompletedTask; + } + + public Task OnServeFromCacheAsync(IOutputCachingContext context) + { + context.IsCacheEntryFresh = IsCachedEntryFresh(context); + + return Task.CompletedTask; + } + + internal static bool AttemptOutputCaching(IOutputCachingContext context) + { + var request = context.HttpContext.Request; + + // Verify the method + if (!HttpMethods.IsGet(request.Method) && !HttpMethods.IsHead(request.Method)) + { + context.Logger.RequestMethodNotCacheable(request.Method); + return false; + } + + // Verify existence of authorization headers + if (!StringValues.IsNullOrEmpty(request.Headers.Authorization)) + { + context.Logger.RequestWithAuthorizationNotCacheable(); + return false; + } + + return true; + } + + internal static bool IsResponseCacheable(IOutputCachingContext context) + { + var responseCacheControlHeader = context.HttpContext.Response.Headers.CacheControl; + + // Only cache pages explicitly marked with public + if (!HeaderUtilities.ContainsCacheDirective(responseCacheControlHeader, CacheControlHeaderValue.PublicString)) + { + context.Logger.ResponseWithoutPublicNotCacheable(); + return false; + } + + // Check response no-store + if (HeaderUtilities.ContainsCacheDirective(responseCacheControlHeader, CacheControlHeaderValue.NoStoreString)) + { + context.Logger.ResponseWithNoStoreNotCacheable(); + return false; + } + + // Check no-cache + if (HeaderUtilities.ContainsCacheDirective(responseCacheControlHeader, CacheControlHeaderValue.NoCacheString)) + { + context.Logger.ResponseWithNoCacheNotCacheable(); + return false; + } + + var response = context.HttpContext.Response; + + // Do not cache responses with Set-Cookie headers + if (!StringValues.IsNullOrEmpty(response.Headers.SetCookie)) + { + context.Logger.ResponseWithSetCookieNotCacheable(); + return false; + } + + // Do not cache responses varying by * + var varyHeader = response.Headers.Vary; + if (varyHeader.Count == 1 && string.Equals(varyHeader, "*", StringComparison.OrdinalIgnoreCase)) + { + context.Logger.ResponseWithVaryStarNotCacheable(); + return false; + } + + // Check private + if (HeaderUtilities.ContainsCacheDirective(responseCacheControlHeader, CacheControlHeaderValue.PrivateString)) + { + context.Logger.ResponseWithPrivateNotCacheable(); + return false; + } + + // Check response code + if (response.StatusCode != StatusCodes.Status200OK) + { + context.Logger.ResponseWithUnsuccessfulStatusCodeNotCacheable(response.StatusCode); + return false; + } + + // Check response freshness + if (!context.ResponseDate.HasValue) + { + if (!context.ResponseSharedMaxAge.HasValue && + !context.ResponseMaxAge.HasValue && + context.ResponseTime!.Value >= context.ResponseExpires) + { + context.Logger.ExpirationExpiresExceeded(context.ResponseTime.Value, context.ResponseExpires.Value); + return false; + } + } + else + { + var age = context.ResponseTime!.Value - context.ResponseDate.Value; + + // Validate shared max age + if (age >= context.ResponseSharedMaxAge) + { + context.Logger.ExpirationSharedMaxAgeExceeded(age, context.ResponseSharedMaxAge.Value); + return false; + } + else if (!context.ResponseSharedMaxAge.HasValue) + { + // Validate max age + if (age >= context.ResponseMaxAge) + { + context.Logger.ExpirationMaxAgeExceeded(age, context.ResponseMaxAge.Value); + return false; + } + else if (!context.ResponseMaxAge.HasValue) + { + // Validate expiration + if (context.ResponseTime.Value >= context.ResponseExpires) + { + context.Logger.ExpirationExpiresExceeded(context.ResponseTime.Value, context.ResponseExpires.Value); + return false; + } + } + } + } + + return true; + } + + internal static bool IsCachedEntryFresh(IOutputCachingContext context) + { + var age = context.CachedEntryAge!.Value; + var cachedCacheControlHeaders = context.CachedResponseHeaders.CacheControl; + var requestCacheControlHeaders = context.HttpContext.Request.Headers.CacheControl; + + // Add min-fresh requirements + if (HeaderUtilities.TryParseSeconds(requestCacheControlHeaders, CacheControlHeaderValue.MinFreshString, out var minFresh)) + { + age += minFresh.Value; + context.Logger.ExpirationMinFreshAdded(minFresh.Value); + } + + // Validate shared max age, this overrides any max age settings for shared caches + TimeSpan? cachedSharedMaxAge; + HeaderUtilities.TryParseSeconds(cachedCacheControlHeaders, CacheControlHeaderValue.SharedMaxAgeString, out cachedSharedMaxAge); + + if (age >= cachedSharedMaxAge) + { + // shared max age implies must revalidate + context.Logger.ExpirationSharedMaxAgeExceeded(age, cachedSharedMaxAge.Value); + return false; + } + else if (!cachedSharedMaxAge.HasValue) + { + TimeSpan? requestMaxAge; + HeaderUtilities.TryParseSeconds(requestCacheControlHeaders, CacheControlHeaderValue.MaxAgeString, out requestMaxAge); + + TimeSpan? cachedMaxAge; + HeaderUtilities.TryParseSeconds(cachedCacheControlHeaders, CacheControlHeaderValue.MaxAgeString, out cachedMaxAge); + + var lowestMaxAge = cachedMaxAge < requestMaxAge ? cachedMaxAge : requestMaxAge ?? cachedMaxAge; + // Validate max age + if (age >= lowestMaxAge) + { + // Must revalidate or proxy revalidate + if (HeaderUtilities.ContainsCacheDirective(cachedCacheControlHeaders, CacheControlHeaderValue.MustRevalidateString) + || HeaderUtilities.ContainsCacheDirective(cachedCacheControlHeaders, CacheControlHeaderValue.ProxyRevalidateString)) + { + context.Logger.ExpirationMustRevalidate(age, lowestMaxAge.Value); + return false; + } + + TimeSpan? requestMaxStale; + var maxStaleExist = HeaderUtilities.ContainsCacheDirective(requestCacheControlHeaders, CacheControlHeaderValue.MaxStaleString); + HeaderUtilities.TryParseSeconds(requestCacheControlHeaders, CacheControlHeaderValue.MaxStaleString, out requestMaxStale); + + // Request allows stale values with no age limit + if (maxStaleExist && !requestMaxStale.HasValue) + { + context.Logger.ExpirationInfiniteMaxStaleSatisfied(age, lowestMaxAge.Value); + return true; + } + + // Request allows stale values with age limit + if (requestMaxStale.HasValue && age - lowestMaxAge < requestMaxStale) + { + context.Logger.ExpirationMaxStaleSatisfied(age, lowestMaxAge.Value, requestMaxStale.Value); + return true; + } + + context.Logger.ExpirationMaxAgeExceeded(age, lowestMaxAge.Value); + return false; + } + else if (!cachedMaxAge.HasValue && !requestMaxAge.HasValue) + { + // Validate expiration + DateTimeOffset expires; + if (HeaderUtilities.TryParseDate(context.CachedResponseHeaders.Expires.ToString(), out expires) && + context.ResponseTime!.Value >= expires) + { + context.Logger.ExpirationExpiresExceeded(context.ResponseTime.Value, expires); + return false; + } + } + } + + return true; + } + + internal static bool AllowCacheLookup(IOutputCachingContext context) + { + var requestHeaders = context.HttpContext.Request.Headers; + var cacheControl = requestHeaders.CacheControl; + + // Verify request cache-control parameters + if (!StringValues.IsNullOrEmpty(cacheControl)) + { + if (HeaderUtilities.ContainsCacheDirective(cacheControl, CacheControlHeaderValue.NoCacheString)) + { + context.Logger.RequestWithNoCacheNotCacheable(); + return false; + } + } + else + { + // Support for legacy HTTP 1.0 cache directive + if (HeaderUtilities.ContainsCacheDirective(requestHeaders.Pragma, CacheControlHeaderValue.NoCacheString)) + { + context.Logger.RequestWithPragmaNoCacheNotCacheable(); + return false; + } + } + + return true; + } + + internal static bool AllowCacheStorage(IOutputCachingContext context) + { + // Check request no-store + return !HeaderUtilities.ContainsCacheDirective(context.HttpContext.Request.Headers.CacheControl, CacheControlHeaderValue.NoStoreString); + } +} diff --git a/src/Middleware/OutputCaching/src/Policies/TagsPolicy.cs b/src/Middleware/OutputCaching/src/Policies/TagsPolicy.cs new file mode 100644 index 000000000000..0a41cd05da5e --- /dev/null +++ b/src/Middleware/OutputCaching/src/Policies/TagsPolicy.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.OutputCaching; + +/// +/// A policy that defines custom tags on the cache entry. +/// +public class TagsPolicy : IOutputCachingPolicy +{ + private readonly string[] _tags; + + public TagsPolicy(params string[] tags) + { + _tags = tags; + } + + public Task OnRequestAsync(IOutputCachingContext context) + { + foreach (var tag in _tags) + { + context.Tags.Add(tag); + } + + return Task.CompletedTask; + } + + public Task OnServeFromCacheAsync(IOutputCachingContext context) + { + return Task.CompletedTask; + } + + public Task OnServeResponseAsync(IOutputCachingContext context) + { + return Task.CompletedTask; + } +} diff --git a/src/Middleware/OutputCaching/src/Policies/VaryByQueryPolicy.cs b/src/Middleware/OutputCaching/src/Policies/VaryByQueryPolicy.cs new file mode 100644 index 000000000000..fa54ae4bd540 --- /dev/null +++ b/src/Middleware/OutputCaching/src/Policies/VaryByQueryPolicy.cs @@ -0,0 +1,69 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.OutputCaching; + +/// +/// When applied, the cached content will be different for every value of the provided query string keys. +/// It also disables the default behavior which is to vary on all query string keys. +/// +public class VaryByQueryPolicy : IOutputCachingPolicy +{ + private StringValues _queryKeys { get; set; } + + /// + /// Creates a policy that doesn't vary the cached content based on query string. + /// + public VaryByQueryPolicy() + { + } + + /// + /// Creates a policy that vary the cached content based on the specified query string key. + /// + public VaryByQueryPolicy(string queryKey) + { + _queryKeys = queryKey; + } + + /// + /// Creates a policy that vary the cached content based on the specified query string keys. + /// + public VaryByQueryPolicy(params string[] queryKeys) + { + _queryKeys = queryKeys; + } + + public Task OnRequestAsync(IOutputCachingContext context) + { + // No vary by query? + if (_queryKeys.Count == 0) + { + context.CachedVaryByRules.QueryKeys = _queryKeys; + return Task.CompletedTask; + } + + // If the current key is "*" (default) replace it + if (context.CachedVaryByRules.QueryKeys.Count == 1 && string.Equals(context.CachedVaryByRules.QueryKeys[0], "*", StringComparison.Ordinal)) + { + context.CachedVaryByRules.QueryKeys = _queryKeys; + return Task.CompletedTask; + } + + context.CachedVaryByRules.QueryKeys = StringValues.Concat(context.CachedVaryByRules.QueryKeys, _queryKeys); + + return Task.CompletedTask; + } + + public Task OnServeFromCacheAsync(IOutputCachingContext context) + { + return Task.CompletedTask; + } + + public Task OnServeResponseAsync(IOutputCachingContext context) + { + return Task.CompletedTask; + } +} diff --git a/src/Middleware/OutputCaching/src/Policies/VaryByValuePolicy.cs b/src/Middleware/OutputCaching/src/Policies/VaryByValuePolicy.cs new file mode 100644 index 000000000000..5d0591869c3a --- /dev/null +++ b/src/Middleware/OutputCaching/src/Policies/VaryByValuePolicy.cs @@ -0,0 +1,77 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.OutputCaching; + +/// +/// When applied, the cached content will be different for every provided value. +/// +public class VaryByValuePolicy : IOutputCachingPolicy +{ + private readonly Action _varyBy; + private readonly Func _varyByAsync; + + /// + /// Creates a policy that doesn't vary the cached content based on values. + /// + public VaryByValuePolicy() + { + } + + /// + /// Creates a policy that vary the cached content based on the specified value. + /// + public VaryByValuePolicy(Func varyBy) + { + _varyBy = (c) => c.VaryByPrefix = c.VaryByPrefix + varyBy(); + } + + /// + /// Creates a policy that vary the cached content based on the specified value. + /// + public VaryByValuePolicy(Func> varyBy) + { + _varyByAsync = async (c) => c.VaryByPrefix = c.VaryByPrefix + await varyBy(); + } + + /// + /// Creates a policy that vary the cached content based on the specified value. + /// + public VaryByValuePolicy(Func<(string, string)> varyBy) + { + _varyBy = (c) => + { + var result = varyBy(); + c.VaryByCustom.TryAdd(result.Item1, result.Item2); + }; + } + + /// + /// Creates a policy that vary the cached content based on the specified value. + /// + public VaryByValuePolicy(Func> varyBy) + { + _varyBy = async (c) => + { + var result = await varyBy(); + c.VaryByCustom.TryAdd(result.Item1, result.Item2); + }; + } + + public Task OnRequestAsync(IOutputCachingContext context) + { + _varyBy?.Invoke(context.CachedVaryByRules); + + return _varyByAsync?.Invoke(context.CachedVaryByRules) ?? Task.CompletedTask; + } + + public Task OnServeFromCacheAsync(IOutputCachingContext context) + { + return Task.CompletedTask; + } + + public Task OnServeResponseAsync(IOutputCachingContext context) + { + return Task.CompletedTask; + } +} diff --git a/src/Middleware/OutputCaching/src/Properties/AssemblyInfo.cs b/src/Middleware/OutputCaching/src/Properties/AssemblyInfo.cs new file mode 100644 index 000000000000..a185ed9b3c63 --- /dev/null +++ b/src/Middleware/OutputCaching/src/Properties/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.OutputCaching.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Middleware/OutputCaching/src/PublicAPI.Shipped.txt b/src/Middleware/OutputCaching/src/PublicAPI.Shipped.txt new file mode 100644 index 000000000000..7dc5c58110bf --- /dev/null +++ b/src/Middleware/OutputCaching/src/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt b/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt new file mode 100644 index 000000000000..7dc5c58110bf --- /dev/null +++ b/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/Middleware/OutputCaching/src/Streams/OutputCachingStream.cs b/src/Middleware/OutputCaching/src/Streams/OutputCachingStream.cs new file mode 100644 index 000000000000..3759cabbe40b --- /dev/null +++ b/src/Middleware/OutputCaching/src/Streams/OutputCachingStream.cs @@ -0,0 +1,187 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.OutputCaching; + +internal sealed class OutputCachingStream : Stream +{ + private readonly Stream _innerStream; + private readonly long _maxBufferSize; + private readonly int _segmentSize; + private readonly SegmentWriteStream _segmentWriteStream; + private readonly Action _startResponseCallback; + + internal OutputCachingStream(Stream innerStream, long maxBufferSize, int segmentSize, Action startResponseCallback) + { + _innerStream = innerStream; + _maxBufferSize = maxBufferSize; + _segmentSize = segmentSize; + _startResponseCallback = startResponseCallback; + _segmentWriteStream = new SegmentWriteStream(_segmentSize); + } + + internal bool BufferingEnabled { get; private set; } = true; + + public override bool CanRead => _innerStream.CanRead; + + public override bool CanSeek => _innerStream.CanSeek; + + public override bool CanWrite => _innerStream.CanWrite; + + public override long Length => _innerStream.Length; + + public override long Position + { + get { return _innerStream.Position; } + set + { + DisableBuffering(); + _innerStream.Position = value; + } + } + + internal CachedResponseBody GetCachedResponseBody() + { + if (!BufferingEnabled) + { + throw new InvalidOperationException("Buffer stream cannot be retrieved since buffering is disabled."); + } + return new CachedResponseBody(_segmentWriteStream.GetSegments(), _segmentWriteStream.Length); + } + + internal void DisableBuffering() + { + BufferingEnabled = false; + _segmentWriteStream.Dispose(); + } + + public override void SetLength(long value) + { + DisableBuffering(); + _innerStream.SetLength(value); + } + + public override long Seek(long offset, SeekOrigin origin) + { + DisableBuffering(); + return _innerStream.Seek(offset, origin); + } + + public override void Flush() + { + try + { + _startResponseCallback(); + _innerStream.Flush(); + } + catch + { + DisableBuffering(); + throw; + } + } + + public override async Task FlushAsync(CancellationToken cancellationToken) + { + try + { + _startResponseCallback(); + await _innerStream.FlushAsync(cancellationToken); + } + catch + { + DisableBuffering(); + throw; + } + } + + // Underlying stream is write-only, no need to override other read related methods + public override int Read(byte[] buffer, int offset, int count) + => _innerStream.Read(buffer, offset, count); + + public override void Write(byte[] buffer, int offset, int count) + { + try + { + _startResponseCallback(); + _innerStream.Write(buffer, offset, count); + } + catch + { + DisableBuffering(); + throw; + } + + if (BufferingEnabled) + { + if (_segmentWriteStream.Length + count > _maxBufferSize) + { + DisableBuffering(); + } + else + { + _segmentWriteStream.Write(buffer, offset, count); + } + } + } + + public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => + await WriteAsync(buffer.AsMemory(offset, count), cancellationToken); + + public override async ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken = default) + { + try + { + _startResponseCallback(); + await _innerStream.WriteAsync(buffer, cancellationToken); + } + catch + { + DisableBuffering(); + throw; + } + + if (BufferingEnabled) + { + if (_segmentWriteStream.Length + buffer.Length > _maxBufferSize) + { + DisableBuffering(); + } + else + { + await _segmentWriteStream.WriteAsync(buffer, cancellationToken); + } + } + } + + public override void WriteByte(byte value) + { + try + { + _innerStream.WriteByte(value); + } + catch + { + DisableBuffering(); + throw; + } + + if (BufferingEnabled) + { + if (_segmentWriteStream.Length + 1 > _maxBufferSize) + { + DisableBuffering(); + } + else + { + _segmentWriteStream.WriteByte(value); + } + } + } + + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) + => TaskToApm.Begin(WriteAsync(buffer, offset, count, CancellationToken.None), callback, state); + + public override void EndWrite(IAsyncResult asyncResult) + => TaskToApm.End(asyncResult); +} diff --git a/src/Middleware/OutputCaching/src/Streams/SegmentWriteStream.cs b/src/Middleware/OutputCaching/src/Streams/SegmentWriteStream.cs new file mode 100644 index 000000000000..4d8a845418e8 --- /dev/null +++ b/src/Middleware/OutputCaching/src/Streams/SegmentWriteStream.cs @@ -0,0 +1,199 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.OutputCaching; + +internal sealed class SegmentWriteStream : Stream +{ + private readonly List _segments = new List(); + private readonly MemoryStream _bufferStream = new MemoryStream(); + private readonly int _segmentSize; + private long _length; + private bool _closed; + private bool _disposed; + + internal SegmentWriteStream(int segmentSize) + { + if (segmentSize <= 0) + { + throw new ArgumentOutOfRangeException(nameof(segmentSize), segmentSize, $"{nameof(segmentSize)} must be greater than 0."); + } + + _segmentSize = segmentSize; + } + + // Extracting the buffered segments closes the stream for writing + internal List GetSegments() + { + if (!_closed) + { + _closed = true; + FinalizeSegments(); + } + return _segments; + } + + public override bool CanRead => false; + + public override bool CanSeek => false; + + public override bool CanWrite => !_closed; + + public override long Length => _length; + + public override long Position + { + get + { + return _length; + } + set + { + throw new NotSupportedException("The stream does not support seeking."); + } + } + + private void DisposeMemoryStream() + { + // Clean up the memory stream + _bufferStream.SetLength(0); + _bufferStream.Capacity = 0; + _bufferStream.Dispose(); + } + + private void FinalizeSegments() + { + // Append any remaining segments + if (_bufferStream.Length > 0) + { + // Add the last segment + _segments.Add(_bufferStream.ToArray()); + } + + DisposeMemoryStream(); + } + + protected override void Dispose(bool disposing) + { + try + { + if (_disposed) + { + return; + } + + if (disposing) + { + _segments.Clear(); + DisposeMemoryStream(); + } + + _disposed = true; + _closed = true; + } + finally + { + base.Dispose(disposing); + } + } + + public override void Flush() + { + if (!CanWrite) + { + throw new ObjectDisposedException("The stream has been closed for writing."); + } + } + + public override int Read(byte[] buffer, int offset, int count) + { + throw new NotSupportedException("The stream does not support reading."); + } + + public override long Seek(long offset, SeekOrigin origin) + { + throw new NotSupportedException("The stream does not support seeking."); + } + + public override void SetLength(long value) + { + throw new NotSupportedException("The stream does not support seeking."); + } + + public override void Write(byte[] buffer, int offset, int count) + { + ArgumentNullException.ThrowIfNull(buffer, nameof(buffer)); + + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset), offset, "Non-negative number required."); + } + if (count < 0) + { + throw new ArgumentOutOfRangeException(nameof(count), count, "Non-negative number required."); + } + if (count > buffer.Length - offset) + { + throw new ArgumentException("Offset and length were out of bounds for the array or count is greater than the number of elements from index to the end of the source collection."); + } + if (!CanWrite) + { + throw new ObjectDisposedException("The stream has been closed for writing."); + } + + Write(buffer.AsSpan(offset, count)); + } + + public override void Write(ReadOnlySpan buffer) + { + while (!buffer.IsEmpty) + { + if ((int)_bufferStream.Length == _segmentSize) + { + _segments.Add(_bufferStream.ToArray()); + _bufferStream.SetLength(0); + } + + var bytesWritten = Math.Min(buffer.Length, _segmentSize - (int)_bufferStream.Length); + + _bufferStream.Write(buffer.Slice(0, bytesWritten)); + buffer = buffer.Slice(bytesWritten); + _length += bytesWritten; + } + } + + public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) + { + Write(buffer, offset, count); + return Task.CompletedTask; + } + + public override ValueTask WriteAsync(ReadOnlyMemory buffer, CancellationToken cancellationToken) + { + Write(buffer.Span); + return default; + } + + public override void WriteByte(byte value) + { + if (!CanWrite) + { + throw new ObjectDisposedException("The stream has been closed for writing."); + } + + if ((int)_bufferStream.Length == _segmentSize) + { + _segments.Add(_bufferStream.ToArray()); + _bufferStream.SetLength(0); + } + + _bufferStream.WriteByte(value); + _length++; + } + + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback? callback, object? state) + => TaskToApm.Begin(WriteAsync(buffer, offset, count, CancellationToken.None), callback, state); + + public override void EndWrite(IAsyncResult asyncResult) + => TaskToApm.End(asyncResult); +} diff --git a/src/Middleware/OutputCaching/src/Streams/StreamUtilities.cs b/src/Middleware/OutputCaching/src/Streams/StreamUtilities.cs new file mode 100644 index 000000000000..2b9fb359c7b4 --- /dev/null +++ b/src/Middleware/OutputCaching/src/Streams/StreamUtilities.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.OutputCaching; + +internal static class StreamUtilities +{ + /// + /// The segment size for buffering the response body in bytes. The default is set to 80 KB (81920 Bytes) to avoid allocations on the LOH. + /// + // Internal for testing + internal static int BodySegmentSize { get; set; } = 81920; +} diff --git a/src/Middleware/OutputCaching/src/StringBuilderExtensions.cs b/src/Middleware/OutputCaching/src/StringBuilderExtensions.cs new file mode 100644 index 000000000000..6835e9af6c89 --- /dev/null +++ b/src/Middleware/OutputCaching/src/StringBuilderExtensions.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text; + +namespace Microsoft.AspNetCore.OutputCaching; + +internal static class StringBuilderExtensions +{ + internal static StringBuilder AppendUpperInvariant(this StringBuilder builder, string? value) + { + if (!string.IsNullOrEmpty(value)) + { + builder.EnsureCapacity(builder.Length + value.Length); + for (var i = 0; i < value.Length; i++) + { + builder.Append(char.ToUpperInvariant(value[i])); + } + } + + return builder; + } +} diff --git a/src/Middleware/OutputCaching/src/SystemClock.cs b/src/Middleware/OutputCaching/src/SystemClock.cs new file mode 100644 index 000000000000..00d9f03e58b3 --- /dev/null +++ b/src/Middleware/OutputCaching/src/SystemClock.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.OutputCaching; + +/// +/// Provides access to the normal system clock. +/// +internal sealed class SystemClock : ISystemClock +{ + /// + /// Retrieves the current system time in UTC. + /// + public DateTime UtcNow => DateTime.UtcNow; +} diff --git a/src/Middleware/OutputCaching/startvs.cmd b/src/Middleware/OutputCaching/startvs.cmd new file mode 100644 index 000000000000..5c25af9f6400 --- /dev/null +++ b/src/Middleware/OutputCaching/startvs.cmd @@ -0,0 +1,3 @@ +@ECHO OFF + +%~dp0..\..\..\startvs.cmd %~dp0OutputCaching.slnf diff --git a/src/Middleware/OutputCaching/test/CachedResponseBodyTests.cs b/src/Middleware/OutputCaching/test/CachedResponseBodyTests.cs new file mode 100644 index 000000000000..173380c71c55 --- /dev/null +++ b/src/Middleware/OutputCaching/test/CachedResponseBodyTests.cs @@ -0,0 +1,122 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +//using System.Buffers; +//using System.Diagnostics; +//using System.IO.Pipelines; + +//namespace Microsoft.AspNetCore.ResponseCaching.Tests; + +//public class CachedResponseBodyTests +//{ +// private readonly int _timeout = Debugger.IsAttached ? -1 : 5000; + +// [Fact] +// public void GetSegments() +// { +// var segments = new List(); +// var body = new CachedResponseBody(segments, 0); + +// Assert.Same(segments, body.Segments); +// } + +// [Fact] +// public void GetLength() +// { +// var segments = new List(); +// var body = new CachedResponseBody(segments, 42); + +// Assert.Equal(42, body.Length); +// } + +// [Fact] +// public async Task Copy_DoNothingWhenNoSegments() +// { +// var segments = new List(); +// var receivedSegments = new List(); +// var body = new CachedResponseBody(segments, 0); + +// var pipe = new Pipe(); +// using var cts = new CancellationTokenSource(_timeout); + +// var receiverTask = ReceiveDataAsync(pipe.Reader, receivedSegments, cts.Token); +// var copyTask = body.CopyToAsync(pipe.Writer, cts.Token).ContinueWith(_ => pipe.Writer.CompleteAsync()); + +// await Task.WhenAll(receiverTask, copyTask); + +// Assert.Empty(receivedSegments); +// } + +// [Fact] +// public async Task Copy_SingleSegment() +// { +// var segments = new List +// { +// new byte[] { 1 } +// }; +// var receivedSegments = new List(); +// var body = new CachedResponseBody(segments, 0); + +// var pipe = new Pipe(); + +// using var cts = new CancellationTokenSource(_timeout); + +// var receiverTask = ReceiveDataAsync(pipe.Reader, receivedSegments, cts.Token); +// var copyTask = CopyDataAsync(body, pipe.Writer, cts.Token); + +// await Task.WhenAll(receiverTask, copyTask); + +// Assert.Equal(segments, receivedSegments); +// } + +// [Fact] +// public async Task Copy_MultipleSegments() +// { +// var segments = new List +// { +// new byte[] { 1 }, +// new byte[] { 2, 3 } +// }; +// var receivedSegments = new List(); +// var body = new CachedResponseBody(segments, 0); + +// var pipe = new Pipe(); + +// using var cts = new CancellationTokenSource(_timeout); + +// var receiverTask = ReceiveDataAsync(pipe.Reader, receivedSegments, cts.Token); +// var copyTask = CopyDataAsync(body, pipe.Writer, cts.Token); + +// await Task.WhenAll(receiverTask, copyTask); + +// Assert.Equal(new byte[] { 1, 2, 3 }, receivedSegments.SelectMany(x => x).ToArray()); +// } + +// static async Task CopyDataAsync(CachedResponseBody body, PipeWriter writer, CancellationToken cancellationToken) +// { +// await body.CopyToAsync(writer, cancellationToken); +// await writer.CompleteAsync(); +// } + +// static async Task ReceiveDataAsync(PipeReader reader, List receivedSegments, CancellationToken cancellationToken) +// { +// while (true) +// { +// var result = await reader.ReadAsync(cancellationToken); +// var buffer = result.Buffer; + +// foreach (var memory in buffer) +// { +// receivedSegments.Add(memory.ToArray()); +// } + +// reader.AdvanceTo(buffer.End, buffer.End); + +// if (result.IsCompleted) +// { +// break; +// } +// } +// await reader.CompleteAsync(); +// } +//} diff --git a/src/Middleware/OutputCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests.csproj b/src/Middleware/OutputCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests.csproj new file mode 100644 index 000000000000..dd7bcb50d1df --- /dev/null +++ b/src/Middleware/OutputCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests.csproj @@ -0,0 +1,18 @@ + + + $(DefaultNetCoreTargetFramework) + + + + + PreserveNewest + PreserveNewest + + + + + + + + + diff --git a/src/Middleware/OutputCaching/test/ResponseCachingFeatureTests.cs b/src/Middleware/OutputCaching/test/ResponseCachingFeatureTests.cs new file mode 100644 index 000000000000..d5f52327ab06 --- /dev/null +++ b/src/Middleware/OutputCaching/test/ResponseCachingFeatureTests.cs @@ -0,0 +1,54 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +//namespace Microsoft.AspNetCore.ResponseCaching.Tests; + +//public class ResponseCachingFeatureTests +//{ +// public static TheoryData ValidNullOrEmptyVaryRules +// { +// get +// { +// return new TheoryData +// { +// null, +// new string[0], +// new string[] { null }, +// new string[] { string.Empty } +// }; +// } +// } + +// [Theory] +// [MemberData(nameof(ValidNullOrEmptyVaryRules))] +// public void VaryByQueryKeys_Set_ValidEmptyValues_Succeeds(string[] value) +// { +// // Does not throw +// new ResponseCachingFeature().VaryByQueryKeys = value; +// } + +// public static TheoryData InvalidVaryRules +// { +// get +// { +// return new TheoryData +// { +// new string[] { null, null }, +// new string[] { null, string.Empty }, +// new string[] { string.Empty, null }, +// new string[] { string.Empty, "Valid" }, +// new string[] { "Valid", string.Empty }, +// new string[] { null, "Valid" }, +// new string[] { "Valid", null } +// }; +// } +// } + +// [Theory] +// [MemberData(nameof(InvalidVaryRules))] +// public void VaryByQueryKeys_Set_InValidEmptyValues_Throws(string[] value) +// { +// // Throws +// Assert.Throws(() => new ResponseCachingFeature().VaryByQueryKeys = value); +// } +//} diff --git a/src/Middleware/OutputCaching/test/ResponseCachingKeyProviderTests.cs b/src/Middleware/OutputCaching/test/ResponseCachingKeyProviderTests.cs new file mode 100644 index 000000000000..dc3d73c6096f --- /dev/null +++ b/src/Middleware/OutputCaching/test/ResponseCachingKeyProviderTests.cs @@ -0,0 +1,214 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +//using Microsoft.AspNetCore.Http; + +//namespace Microsoft.AspNetCore.ResponseCaching.Tests; + +//public class ResponseCachingKeyProviderTests +//{ +// private static readonly char KeyDelimiter = '\x1e'; +// private static readonly char KeySubDelimiter = '\x1f'; + +// [Fact] +// public void ResponseCachingKeyProvider_CreateStorageBaseKey_IncludesOnlyNormalizedMethodSchemeHostPortAndPath() +// { +// var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); +// var context = TestUtils.CreateTestContext(); +// context.HttpContext.Request.Method = "head"; +// context.HttpContext.Request.Path = "/path/subpath"; +// context.HttpContext.Request.Scheme = "https"; +// context.HttpContext.Request.Host = new HostString("example.com", 80); +// context.HttpContext.Request.PathBase = "/pathBase"; +// context.HttpContext.Request.QueryString = new QueryString("?query.Key=a&query.Value=b"); + +// Assert.Equal($"HEAD{KeyDelimiter}HTTPS{KeyDelimiter}EXAMPLE.COM:80/PATHBASE/PATH/SUBPATH", cacheKeyProvider.CreateBaseKey(context)); +// } + +// [Fact] +// public void ResponseCachingKeyProvider_CreateStorageBaseKey_CaseInsensitivePath_NormalizesPath() +// { +// var cacheKeyProvider = TestUtils.CreateTestKeyProvider(new ResponseCachingOptions() +// { +// UseCaseSensitivePaths = false +// }); +// var context = TestUtils.CreateTestContext(); +// context.HttpContext.Request.Method = HttpMethods.Get; +// context.HttpContext.Request.Path = "/Path"; + +// Assert.Equal($"{HttpMethods.Get}{KeyDelimiter}{KeyDelimiter}/PATH", cacheKeyProvider.CreateBaseKey(context)); +// } + +// [Fact] +// public void ResponseCachingKeyProvider_CreateStorageBaseKey_CaseSensitivePath_PreservesPathCase() +// { +// var cacheKeyProvider = TestUtils.CreateTestKeyProvider(new ResponseCachingOptions() +// { +// UseCaseSensitivePaths = true +// }); +// var context = TestUtils.CreateTestContext(); +// context.HttpContext.Request.Method = HttpMethods.Get; +// context.HttpContext.Request.Path = "/Path"; + +// Assert.Equal($"{HttpMethods.Get}{KeyDelimiter}{KeyDelimiter}/Path", cacheKeyProvider.CreateBaseKey(context)); +// } + +// [Fact] +// public void ResponseCachingKeyProvider_CreateStorageVaryByKey_Throws_IfVaryByRulesIsNull() +// { +// var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); +// var context = TestUtils.CreateTestContext(); + +// Assert.Throws(() => cacheKeyProvider.CreateStorageVaryByKey(context)); +// } + +// [Fact] +// public void ResponseCachingKeyProvider_CreateStorageVaryKey_ReturnsCachedVaryByGuid_IfVaryByRulesIsEmpty() +// { +// var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); +// var context = TestUtils.CreateTestContext(); +// context.CachedVaryByRules = new CachedVaryByRules() +// { +// VaryByKeyPrefix = FastGuid.NewGuid().IdString +// }; + +// Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}", cacheKeyProvider.CreateStorageVaryByKey(context)); +// } + +// [Fact] +// public void ResponseCachingKeyProvider_CreateStorageVaryKey_IncludesListedHeadersOnly() +// { +// var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); +// var context = TestUtils.CreateTestContext(); +// context.HttpContext.Request.Headers["HeaderA"] = "ValueA"; +// context.HttpContext.Request.Headers["HeaderB"] = "ValueB"; +// context.CachedVaryByRules = new CachedVaryByRules() +// { +// Headers = new string[] { "HeaderA", "HeaderC" } +// }; + +// Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=", +// cacheKeyProvider.CreateStorageVaryByKey(context)); +// } + +// [Fact] +// public void ResponseCachingKeyProvider_CreateStorageVaryKey_HeaderValuesAreSorted() +// { +// var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); +// var context = TestUtils.CreateTestContext(); +// context.HttpContext.Request.Headers["HeaderA"] = "ValueB"; +// context.HttpContext.Request.Headers.Append("HeaderA", "ValueA"); +// context.CachedVaryByRules = new CachedVaryByRules() +// { +// Headers = new string[] { "HeaderA", "HeaderC" } +// }; + +// Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueAValueB{KeyDelimiter}HeaderC=", +// cacheKeyProvider.CreateStorageVaryByKey(context)); +// } + +// [Fact] +// public void ResponseCachingKeyProvider_CreateStorageVaryKey_IncludesListedQueryKeysOnly() +// { +// var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); +// var context = TestUtils.CreateTestContext(); +// context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueA&QueryB=ValueB"); +// context.CachedVaryByRules = new CachedVaryByRules() +// { +// VaryByKeyPrefix = FastGuid.NewGuid().IdString, +// QueryKeys = new string[] { "QueryA", "QueryC" } +// }; + +// Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}QueryA=ValueA{KeyDelimiter}QueryC=", +// cacheKeyProvider.CreateStorageVaryByKey(context)); +// } + +// [Fact] +// public void ResponseCachingKeyProvider_CreateStorageVaryKey_IncludesQueryKeys_QueryKeyCaseInsensitive_UseQueryKeyCasing() +// { +// var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); +// var context = TestUtils.CreateTestContext(); +// context.HttpContext.Request.QueryString = new QueryString("?queryA=ValueA&queryB=ValueB"); +// context.CachedVaryByRules = new CachedVaryByRules() +// { +// VaryByKeyPrefix = FastGuid.NewGuid().IdString, +// QueryKeys = new string[] { "QueryA", "QueryC" } +// }; + +// Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}QueryA=ValueA{KeyDelimiter}QueryC=", +// cacheKeyProvider.CreateStorageVaryByKey(context)); +// } + +// [Fact] +// public void ResponseCachingKeyProvider_CreateStorageVaryKey_IncludesAllQueryKeysGivenAsterisk() +// { +// var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); +// var context = TestUtils.CreateTestContext(); +// context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueA&QueryB=ValueB"); +// context.CachedVaryByRules = new CachedVaryByRules() +// { +// VaryByKeyPrefix = FastGuid.NewGuid().IdString, +// QueryKeys = new string[] { "*" } +// }; + +// // To support case insensitivity, all query keys are converted to upper case. +// // Explicit query keys uses the casing specified in the setting. +// Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}QUERYA=ValueA{KeyDelimiter}QUERYB=ValueB", +// cacheKeyProvider.CreateStorageVaryByKey(context)); +// } + +// [Fact] +// public void ResponseCachingKeyProvider_CreateStorageVaryKey_QueryKeysValuesNotConsolidated() +// { +// var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); +// var context = TestUtils.CreateTestContext(); +// context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueA&QueryA=ValueB"); +// context.CachedVaryByRules = new CachedVaryByRules() +// { +// VaryByKeyPrefix = FastGuid.NewGuid().IdString, +// QueryKeys = new string[] { "*" } +// }; + +// // To support case insensitivity, all query keys are converted to upper case. +// // Explicit query keys uses the casing specified in the setting. +// Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}QUERYA=ValueA{KeySubDelimiter}ValueB", +// cacheKeyProvider.CreateStorageVaryByKey(context)); +// } + +// [Fact] +// public void ResponseCachingKeyProvider_CreateStorageVaryKey_QueryKeysValuesAreSorted() +// { +// var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); +// var context = TestUtils.CreateTestContext(); +// context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueB&QueryA=ValueA"); +// context.CachedVaryByRules = new CachedVaryByRules() +// { +// VaryByKeyPrefix = FastGuid.NewGuid().IdString, +// QueryKeys = new string[] { "*" } +// }; + +// // To support case insensitivity, all query keys are converted to upper case. +// // Explicit query keys uses the casing specified in the setting. +// Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}QUERYA=ValueA{KeySubDelimiter}ValueB", +// cacheKeyProvider.CreateStorageVaryByKey(context)); +// } + +// [Fact] +// public void ResponseCachingKeyProvider_CreateStorageVaryKey_IncludesListedHeadersAndQueryKeys() +// { +// var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); +// var context = TestUtils.CreateTestContext(); +// context.HttpContext.Request.Headers["HeaderA"] = "ValueA"; +// context.HttpContext.Request.Headers["HeaderB"] = "ValueB"; +// context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueA&QueryB=ValueB"); +// context.CachedVaryByRules = new CachedVaryByRules() +// { +// VaryByKeyPrefix = FastGuid.NewGuid().IdString, +// Headers = new string[] { "HeaderA", "HeaderC" }, +// QueryKeys = new string[] { "QueryA", "QueryC" } +// }; + +// Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC={KeyDelimiter}Q{KeyDelimiter}QueryA=ValueA{KeyDelimiter}QueryC=", +// cacheKeyProvider.CreateStorageVaryByKey(context)); +// } +//} diff --git a/src/Middleware/OutputCaching/test/ResponseCachingMiddlewareTests.cs b/src/Middleware/OutputCaching/test/ResponseCachingMiddlewareTests.cs new file mode 100644 index 000000000000..8315d2e47af2 --- /dev/null +++ b/src/Middleware/OutputCaching/test/ResponseCachingMiddlewareTests.cs @@ -0,0 +1,966 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +//using Microsoft.AspNetCore.Http; +//using Microsoft.AspNetCore.Http.Features; +//using Microsoft.Extensions.Caching.Memory; +//using Microsoft.Extensions.Logging.Testing; +//using Microsoft.Extensions.Primitives; +//using Microsoft.Net.Http.Headers; + +//namespace Microsoft.AspNetCore.ResponseCaching.Tests; + +//public class ResponseCachingMiddlewareTests +//{ +// [Fact] +// public async Task TryServeFromCacheAsync_OnlyIfCached_Serves504() +// { +// var cache = new TestResponseCache(); +// var sink = new TestSink(); +// var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider()); +// var context = TestUtils.CreateTestContext(); +// context.HttpContext.Request.Headers.CacheControl = new CacheControlHeaderValue() +// { +// OnlyIfCached = true +// }.ToString(); + +// Assert.True(await middleware.TryServeFromCacheAsync(context)); +// Assert.Equal(StatusCodes.Status504GatewayTimeout, context.HttpContext.Response.StatusCode); +// TestUtils.AssertLoggedMessages( +// sink.Writes, +// LoggedMessage.GatewayTimeoutServed); +// } + +// [Fact] +// public async Task TryServeFromCacheAsync_CachedResponseNotFound_Fails() +// { +// var cache = new TestResponseCache(); +// var sink = new TestSink(); +// var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider("BaseKey")); +// var context = TestUtils.CreateTestContext(); + +// Assert.False(await middleware.TryServeFromCacheAsync(context)); +// Assert.Equal(1, cache.GetCount); +// TestUtils.AssertLoggedMessages( +// sink.Writes, +// LoggedMessage.NoResponseServed); +// } + +// [Fact] +// public async Task TryServeFromCacheAsync_CachedResponseFound_Succeeds() +// { +// var cache = new TestResponseCache(); +// var sink = new TestSink(); +// var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider("BaseKey")); +// var context = TestUtils.CreateTestContext(); + +// cache.Set( +// "BaseKey", +// new CachedResponse() +// { +// Headers = new HeaderDictionary(), +// Body = new CachedResponseBody(new List(0), 0) +// }, +// TimeSpan.Zero); + +// Assert.True(await middleware.TryServeFromCacheAsync(context)); +// Assert.Equal(1, cache.GetCount); +// TestUtils.AssertLoggedMessages( +// sink.Writes, +// LoggedMessage.CachedResponseServed); +// } + +// [Fact] +// public async Task TryServeFromCacheAsync_CachedResponseFound_OverwritesExistingHeaders() +// { +// var cache = new TestResponseCache(); +// var sink = new TestSink(); +// var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider("BaseKey")); +// var context = TestUtils.CreateTestContext(); + +// context.HttpContext.Response.Headers["MyHeader"] = "OldValue"; +// cache.Set( +// "BaseKey", +// new CachedResponse() +// { +// Headers = new HeaderDictionary() +// { +// { "MyHeader", "NewValue" } +// }, +// Body = new CachedResponseBody(new List(0), 0) +// }, +// TimeSpan.Zero); + +// Assert.True(await middleware.TryServeFromCacheAsync(context)); +// Assert.Equal("NewValue", context.HttpContext.Response.Headers["MyHeader"]); +// Assert.Equal(1, cache.GetCount); +// TestUtils.AssertLoggedMessages( +// sink.Writes, +// LoggedMessage.CachedResponseServed); +// } + +// [Fact] +// public async Task TryServeFromCacheAsync_VaryByRuleFound_CachedResponseNotFound_Fails() +// { +// var cache = new TestResponseCache(); +// var sink = new TestSink(); +// var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider("BaseKey", "VaryKey")); +// var context = TestUtils.CreateTestContext(); + +// cache.Set( +// "BaseKey", +// new CachedVaryByRules(), +// TimeSpan.Zero); + +// Assert.False(await middleware.TryServeFromCacheAsync(context)); +// Assert.Equal(2, cache.GetCount); +// TestUtils.AssertLoggedMessages( +// sink.Writes, +// LoggedMessage.NoResponseServed); +// } + +// [Fact] +// public async Task TryServeFromCacheAsync_VaryByRuleFound_CachedResponseFound_Succeeds() +// { +// var cache = new TestResponseCache(); +// var sink = new TestSink(); +// var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider("BaseKey", new[] { "VaryKey", "VaryKey2" })); +// var context = TestUtils.CreateTestContext(); + +// cache.Set( +// "BaseKey", +// new CachedVaryByRules(), +// TimeSpan.Zero); +// cache.Set( +// "BaseKeyVaryKey2", +// new CachedResponse() +// { +// Headers = new HeaderDictionary(), +// Body = new CachedResponseBody(new List(0), 0) +// }, +// TimeSpan.Zero); + +// Assert.True(await middleware.TryServeFromCacheAsync(context)); +// Assert.Equal(3, cache.GetCount); +// TestUtils.AssertLoggedMessages( +// sink.Writes, +// LoggedMessage.CachedResponseServed); +// } + +// [Fact] +// public async Task TryServeFromCacheAsync_CachedResponseFound_Serves304IfPossible() +// { +// var cache = new TestResponseCache(); +// var sink = new TestSink(); +// var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider("BaseKey")); +// var context = TestUtils.CreateTestContext(); +// context.HttpContext.Request.Headers.IfNoneMatch = "*"; + +// cache.Set( +// "BaseKey", +// new CachedResponse() +// { +// Body = new CachedResponseBody(new List(0), 0) +// }, +// TimeSpan.Zero); + +// Assert.True(await middleware.TryServeFromCacheAsync(context)); +// Assert.Equal(1, cache.GetCount); +// TestUtils.AssertLoggedMessages( +// sink.Writes, +// LoggedMessage.NotModifiedServed); +// } + +// [Fact] +// public void ContentIsNotModified_NotConditionalRequest_False() +// { +// var sink = new TestSink(); +// var context = TestUtils.CreateTestContext(sink); +// context.CachedResponseHeaders = new HeaderDictionary(); + +// Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context)); +// Assert.Empty(sink.Writes); +// } + +// [Fact] +// public void ContentIsNotModified_IfModifiedSince_FallsbackToDateHeader() +// { +// var utcNow = DateTimeOffset.UtcNow; +// var sink = new TestSink(); +// var context = TestUtils.CreateTestContext(sink); +// context.CachedResponseHeaders = new HeaderDictionary(); + +// context.HttpContext.Request.Headers.IfModifiedSince = HeaderUtilities.FormatDate(utcNow); + +// // Verify modifications in the past succeeds +// context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10)); +// Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context)); +// Assert.Single(sink.Writes); + +// // Verify modifications at present succeeds +// context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow); +// Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context)); +// Assert.Equal(2, sink.Writes.Count); + +// // Verify modifications in the future fails +// context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); +// Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context)); + +// // Verify logging +// TestUtils.AssertLoggedMessages( +// sink.Writes, +// LoggedMessage.NotModifiedIfModifiedSinceSatisfied, +// LoggedMessage.NotModifiedIfModifiedSinceSatisfied); +// } + +// [Fact] +// public void ContentIsNotModified_IfModifiedSince_LastModifiedOverridesDateHeader() +// { +// var utcNow = DateTimeOffset.UtcNow; +// var sink = new TestSink(); +// var context = TestUtils.CreateTestContext(sink); +// context.CachedResponseHeaders = new HeaderDictionary(); + +// context.HttpContext.Request.Headers.IfModifiedSince = HeaderUtilities.FormatDate(utcNow); + +// // Verify modifications in the past succeeds +// context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); +// context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10)); +// Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context)); +// Assert.Single(sink.Writes); + +// // Verify modifications at present +// context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); +// context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow); +// Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context)); +// Assert.Equal(2, sink.Writes.Count); + +// // Verify modifications in the future fails +// context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10)); +// context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); +// Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context)); + +// // Verify logging +// TestUtils.AssertLoggedMessages( +// sink.Writes, +// LoggedMessage.NotModifiedIfModifiedSinceSatisfied, +// LoggedMessage.NotModifiedIfModifiedSinceSatisfied); +// } + +// [Fact] +// public void ContentIsNotModified_IfNoneMatch_Overrides_IfModifiedSince_ToTrue() +// { +// var utcNow = DateTimeOffset.UtcNow; +// var sink = new TestSink(); +// var context = TestUtils.CreateTestContext(sink); +// context.CachedResponseHeaders = new HeaderDictionary(); + +// // This would fail the IfModifiedSince checks +// context.HttpContext.Request.Headers.IfModifiedSince = HeaderUtilities.FormatDate(utcNow); +// context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); + +// context.HttpContext.Request.Headers.IfNoneMatch = EntityTagHeaderValue.Any.ToString(); +// Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context)); +// TestUtils.AssertLoggedMessages( +// sink.Writes, +// LoggedMessage.NotModifiedIfNoneMatchStar); +// } + +// [Fact] +// public void ContentIsNotModified_IfNoneMatch_Overrides_IfModifiedSince_ToFalse() +// { +// var utcNow = DateTimeOffset.UtcNow; +// var sink = new TestSink(); +// var context = TestUtils.CreateTestContext(sink); +// context.CachedResponseHeaders = new HeaderDictionary(); + +// // This would pass the IfModifiedSince checks +// context.HttpContext.Request.Headers.IfModifiedSince = HeaderUtilities.FormatDate(utcNow); +// context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10)); + +// context.HttpContext.Request.Headers.IfNoneMatch = "\"E1\""; +// Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context)); +// Assert.Empty(sink.Writes); +// } + +// [Fact] +// public void ContentIsNotModified_IfNoneMatch_AnyWithoutETagInResponse_False() +// { +// var sink = new TestSink(); +// var context = TestUtils.CreateTestContext(sink); +// context.CachedResponseHeaders = new HeaderDictionary(); +// context.HttpContext.Request.Headers.IfNoneMatch = "\"E1\""; + +// Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context)); +// Assert.Empty(sink.Writes); +// } + +// public static TheoryData EquivalentWeakETags +// { +// get +// { +// return new TheoryData +// { +// { new EntityTagHeaderValue("\"tag\""), new EntityTagHeaderValue("\"tag\"") }, +// { new EntityTagHeaderValue("\"tag\"", true), new EntityTagHeaderValue("\"tag\"") }, +// { new EntityTagHeaderValue("\"tag\""), new EntityTagHeaderValue("\"tag\"", true) }, +// { new EntityTagHeaderValue("\"tag\"", true), new EntityTagHeaderValue("\"tag\"", true) } +// }; +// } +// } + +// [Theory] +// [MemberData(nameof(EquivalentWeakETags))] +// public void ContentIsNotModified_IfNoneMatch_ExplicitWithMatch_True(EntityTagHeaderValue responseETag, EntityTagHeaderValue requestETag) +// { +// var sink = new TestSink(); +// var context = TestUtils.CreateTestContext(sink); +// context.CachedResponseHeaders = new HeaderDictionary(); +// context.CachedResponseHeaders[HeaderNames.ETag] = responseETag.ToString(); +// context.HttpContext.Request.Headers.IfNoneMatch = requestETag.ToString(); + +// Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context)); +// TestUtils.AssertLoggedMessages( +// sink.Writes, +// LoggedMessage.NotModifiedIfNoneMatchMatched); +// } + +// [Fact] +// public void ContentIsNotModified_IfNoneMatch_ExplicitWithoutMatch_False() +// { +// var sink = new TestSink(); +// var context = TestUtils.CreateTestContext(sink); +// context.CachedResponseHeaders = new HeaderDictionary(); +// context.CachedResponseHeaders[HeaderNames.ETag] = "\"E2\""; +// context.HttpContext.Request.Headers.IfNoneMatch = "\"E1\""; + +// Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context)); +// Assert.Empty(sink.Writes); +// } + +// [Fact] +// public void ContentIsNotModified_IfNoneMatch_MatchesAtLeastOneValue_True() +// { +// var sink = new TestSink(); +// var context = TestUtils.CreateTestContext(sink); +// context.CachedResponseHeaders = new HeaderDictionary(); +// context.CachedResponseHeaders[HeaderNames.ETag] = "\"E2\""; +// context.HttpContext.Request.Headers.IfNoneMatch = new string[] { "\"E0\", \"E1\"", "\"E1\", \"E2\"" }; + +// Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context)); +// TestUtils.AssertLoggedMessages( +// sink.Writes, +// LoggedMessage.NotModifiedIfNoneMatchMatched); +// } + +// [Fact] +// public void StartResponsegAsync_IfAllowResponseCaptureIsTrue_SetsResponseTime() +// { +// var clock = new TestClock +// { +// UtcNow = DateTimeOffset.UtcNow +// }; +// var middleware = TestUtils.CreateTestMiddleware(options: new ResponseCachingOptions +// { +// SystemClock = clock +// }); +// var context = TestUtils.CreateTestContext(); +// context.ResponseTime = null; + +// middleware.StartResponse(context); + +// Assert.Equal(clock.UtcNow, context.ResponseTime); +// } + +// [Fact] +// public void StartResponseAsync_IfAllowResponseCaptureIsTrue_SetsResponseTimeOnlyOnce() +// { +// var clock = new TestClock +// { +// UtcNow = DateTimeOffset.UtcNow +// }; +// var middleware = TestUtils.CreateTestMiddleware(options: new ResponseCachingOptions +// { +// SystemClock = clock +// }); +// var context = TestUtils.CreateTestContext(); +// var initialTime = clock.UtcNow; +// context.ResponseTime = null; + +// middleware.StartResponse(context); +// Assert.Equal(initialTime, context.ResponseTime); + +// clock.UtcNow += TimeSpan.FromSeconds(10); + +// middleware.StartResponse(context); +// Assert.NotEqual(clock.UtcNow, context.ResponseTime); +// Assert.Equal(initialTime, context.ResponseTime); +// } + +// [Fact] +// public void FinalizeCacheHeadersAsync_UpdateShouldCacheResponse_IfResponseCacheable() +// { +// var sink = new TestSink(); +// var middleware = TestUtils.CreateTestMiddleware(testSink: sink, policyProvider: new ResponseCachingPolicyProvider()); +// var context = TestUtils.CreateTestContext(); + +// context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() +// { +// Public = true +// }.ToString(); + +// Assert.False(context.ShouldCacheResponse); + +// middleware.FinalizeCacheHeaders(context); + +// Assert.True(context.ShouldCacheResponse); +// Assert.Empty(sink.Writes); +// } + +// [Fact] +// public void FinalizeCacheHeadersAsync_DoNotUpdateShouldCacheResponse_IfResponseIsNotCacheable() +// { +// var sink = new TestSink(); +// var middleware = TestUtils.CreateTestMiddleware(testSink: sink, policyProvider: new ResponseCachingPolicyProvider()); +// var context = TestUtils.CreateTestContext(); + +// middleware.ShimResponseStream(context); + +// middleware.FinalizeCacheHeaders(context); + +// Assert.False(context.ShouldCacheResponse); +// Assert.Empty(sink.Writes); +// } + +// [Fact] +// public void FinalizeCacheHeadersAsync_DefaultResponseValidity_Is10Seconds() +// { +// var sink = new TestSink(); +// var middleware = TestUtils.CreateTestMiddleware(testSink: sink); +// var context = TestUtils.CreateTestContext(); + +// middleware.FinalizeCacheHeaders(context); + +// Assert.Equal(TimeSpan.FromSeconds(10), context.CachedResponseValidFor); +// Assert.Empty(sink.Writes); +// } + +// [Fact] +// public void FinalizeCacheHeadersAsync_ResponseValidity_UseExpiryIfAvailable() +// { +// var clock = new TestClock +// { +// UtcNow = DateTimeOffset.MinValue +// }; +// var sink = new TestSink(); +// var middleware = TestUtils.CreateTestMiddleware(testSink: sink, options: new ResponseCachingOptions +// { +// SystemClock = clock +// }); +// var context = TestUtils.CreateTestContext(); + +// context.ResponseTime = clock.UtcNow; +// context.HttpContext.Response.Headers.Expires = HeaderUtilities.FormatDate(clock.UtcNow + TimeSpan.FromSeconds(11)); + +// middleware.FinalizeCacheHeaders(context); + +// Assert.Equal(TimeSpan.FromSeconds(11), context.CachedResponseValidFor); +// Assert.Empty(sink.Writes); +// } + +// [Fact] +// public void FinalizeCacheHeadersAsync_ResponseValidity_UseMaxAgeIfAvailable() +// { +// var clock = new TestClock +// { +// UtcNow = DateTimeOffset.UtcNow +// }; +// var sink = new TestSink(); +// var middleware = TestUtils.CreateTestMiddleware(testSink: sink, options: new ResponseCachingOptions +// { +// SystemClock = clock +// }); +// var context = TestUtils.CreateTestContext(); + +// context.ResponseTime = clock.UtcNow; +// context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() +// { +// MaxAge = TimeSpan.FromSeconds(12) +// }.ToString(); + +// context.HttpContext.Response.Headers.Expires = HeaderUtilities.FormatDate(clock.UtcNow + TimeSpan.FromSeconds(11)); + +// middleware.FinalizeCacheHeaders(context); + +// Assert.Equal(TimeSpan.FromSeconds(12), context.CachedResponseValidFor); +// Assert.Empty(sink.Writes); +// } + +// [Fact] +// public void FinalizeCacheHeadersAsync_ResponseValidity_UseSharedMaxAgeIfAvailable() +// { +// var clock = new TestClock +// { +// UtcNow = DateTimeOffset.UtcNow +// }; +// var sink = new TestSink(); +// var middleware = TestUtils.CreateTestMiddleware(testSink: sink, options: new ResponseCachingOptions +// { +// SystemClock = clock +// }); +// var context = TestUtils.CreateTestContext(); + +// context.ResponseTime = clock.UtcNow; +// context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() +// { +// MaxAge = TimeSpan.FromSeconds(12), +// SharedMaxAge = TimeSpan.FromSeconds(13) +// }.ToString(); +// context.HttpContext.Response.Headers.Expires = HeaderUtilities.FormatDate(clock.UtcNow + TimeSpan.FromSeconds(11)); + +// middleware.FinalizeCacheHeaders(context); + +// Assert.Equal(TimeSpan.FromSeconds(13), context.CachedResponseValidFor); +// Assert.Empty(sink.Writes); +// } + +// [Fact] +// public void FinalizeCacheHeadersAsync_UpdateCachedVaryByRules_IfNotEquivalentToPrevious() +// { +// var cache = new TestResponseCache(); +// var sink = new TestSink(); +// var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); +// var context = TestUtils.CreateTestContext(); + +// context.HttpContext.Response.Headers.Vary = new StringValues(new[] { "headerA", "HEADERB", "HEADERc" }); +// context.HttpContext.Features.Set(new ResponseCachingFeature() +// { +// VaryByQueryKeys = new StringValues(new[] { "queryB", "QUERYA" }) +// }); +// var cachedVaryByRules = new CachedVaryByRules() +// { +// Headers = new StringValues(new[] { "HeaderA", "HeaderB" }), +// QueryKeys = new StringValues(new[] { "QueryA", "QueryB" }) +// }; +// context.CachedVaryByRules = cachedVaryByRules; + +// middleware.FinalizeCacheHeaders(context); + +// Assert.Equal(1, cache.SetCount); +// Assert.NotSame(cachedVaryByRules, context.CachedVaryByRules); +// TestUtils.AssertLoggedMessages( +// sink.Writes, +// LoggedMessage.VaryByRulesUpdated); +// } + +// [Fact] +// public void FinalizeCacheHeadersAsync_UpdateCachedVaryByRules_IfEquivalentToPrevious() +// { +// var cache = new TestResponseCache(); +// var sink = new TestSink(); +// var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); +// var context = TestUtils.CreateTestContext(); + +// context.HttpContext.Response.Headers.Vary = new StringValues(new[] { "headerA", "HEADERB" }); +// context.HttpContext.Features.Set(new ResponseCachingFeature() +// { +// VaryByQueryKeys = new StringValues(new[] { "queryB", "QUERYA" }) +// }); +// var cachedVaryByRules = new CachedVaryByRules() +// { +// VaryByKeyPrefix = FastGuid.NewGuid().IdString, +// Headers = new StringValues(new[] { "HEADERA", "HEADERB" }), +// QueryKeys = new StringValues(new[] { "QUERYA", "QUERYB" }) +// }; +// context.CachedVaryByRules = cachedVaryByRules; + +// middleware.FinalizeCacheHeaders(context); + +// // An update to the cache is always made but the entry should be the same +// Assert.Equal(1, cache.SetCount); +// Assert.Same(cachedVaryByRules, context.CachedVaryByRules); +// TestUtils.AssertLoggedMessages( +// sink.Writes, +// LoggedMessage.VaryByRulesUpdated); +// } + +// public static TheoryData NullOrEmptyVaryRules +// { +// get +// { +// return new TheoryData +// { +// default(StringValues), +// StringValues.Empty, +// new StringValues((string)null), +// new StringValues(string.Empty), +// new StringValues((string[])null), +// new StringValues(new string[0]), +// new StringValues(new string[] { null }), +// new StringValues(new string[] { string.Empty }) +// }; +// } +// } + +// [Theory] +// [MemberData(nameof(NullOrEmptyVaryRules))] +// public void FinalizeCacheHeadersAsync_UpdateCachedVaryByRules_NullOrEmptyRules(StringValues vary) +// { +// var cache = new TestResponseCache(); +// var sink = new TestSink(); +// var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); +// var context = TestUtils.CreateTestContext(); + +// context.HttpContext.Response.Headers.Vary = vary; +// context.HttpContext.Features.Set(new ResponseCachingFeature() +// { +// VaryByQueryKeys = vary +// }); + +// middleware.FinalizeCacheHeaders(context); + +// // Vary rules should not be updated +// Assert.Equal(0, cache.SetCount); +// Assert.Empty(sink.Writes); +// } + +// [Fact] +// public void FinalizeCacheHeadersAsync_AddsDate_IfNoneSpecified() +// { +// var utcNow = DateTimeOffset.UtcNow; +// var sink = new TestSink(); +// var middleware = TestUtils.CreateTestMiddleware(testSink: sink); +// var context = TestUtils.CreateTestContext(); +// // ResponseTime is the actual value that's used to set the Date header in FinalizeCacheHeadersAsync +// context.ResponseTime = utcNow; + +// Assert.True(StringValues.IsNullOrEmpty(context.HttpContext.Response.Headers.Date)); + +// middleware.FinalizeCacheHeaders(context); + +// Assert.Equal(HeaderUtilities.FormatDate(utcNow), context.HttpContext.Response.Headers.Date); +// Assert.Empty(sink.Writes); +// } + +// [Fact] +// public void FinalizeCacheHeadersAsync_DoNotAddDate_IfSpecified() +// { +// var utcNow = DateTimeOffset.MinValue; +// var sink = new TestSink(); +// var middleware = TestUtils.CreateTestMiddleware(testSink: sink); +// var context = TestUtils.CreateTestContext(); + +// context.HttpContext.Response.Headers.Date = HeaderUtilities.FormatDate(utcNow); +// context.ResponseTime = utcNow + TimeSpan.FromSeconds(10); + +// Assert.Equal(HeaderUtilities.FormatDate(utcNow), context.HttpContext.Response.Headers.Date); + +// middleware.FinalizeCacheHeaders(context); + +// Assert.Equal(HeaderUtilities.FormatDate(utcNow), context.HttpContext.Response.Headers.Date); +// Assert.Empty(sink.Writes); +// } + +// [Fact] +// public void FinalizeCacheHeadersAsync_StoresCachedResponse_InState() +// { +// var sink = new TestSink(); +// var middleware = TestUtils.CreateTestMiddleware(testSink: sink); +// var context = TestUtils.CreateTestContext(); + +// Assert.Null(context.CachedResponse); + +// middleware.FinalizeCacheHeaders(context); + +// Assert.NotNull(context.CachedResponse); +// Assert.Empty(sink.Writes); +// } + +// [Fact] +// public void FinalizeCacheHeadersAsync_SplitsVaryHeaderByCommas() +// { +// var sink = new TestSink(); +// var middleware = TestUtils.CreateTestMiddleware(testSink: sink); +// var context = TestUtils.CreateTestContext(); + +// context.HttpContext.Response.Headers.Vary = "HeaderB, heaDera"; + +// middleware.FinalizeCacheHeaders(context); + +// Assert.Equal(new StringValues(new[] { "HEADERA", "HEADERB" }), context.CachedVaryByRules.Headers); +// TestUtils.AssertLoggedMessages( +// sink.Writes, +// LoggedMessage.VaryByRulesUpdated); +// } + +// [Fact] +// public async Task FinalizeCacheBody_Cache_IfContentLengthMatches() +// { +// var cache = new TestResponseCache(); +// var sink = new TestSink(); +// var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); +// var context = TestUtils.CreateTestContext(); + +// context.ShouldCacheResponse = true; +// middleware.ShimResponseStream(context); +// context.HttpContext.Response.ContentLength = 20; + +// await context.HttpContext.Response.WriteAsync(new string('0', 20)); + +// context.CachedResponse = new CachedResponse(); +// context.BaseKey = "BaseKey"; +// context.CachedResponseValidFor = TimeSpan.FromSeconds(10); + +// middleware.FinalizeCacheBody(context); + +// Assert.Equal(1, cache.SetCount); +// TestUtils.AssertLoggedMessages( +// sink.Writes, +// LoggedMessage.ResponseCached); +// } + +// [Theory] +// [InlineData("GET")] +// [InlineData("HEAD")] +// public async Task FinalizeCacheBody_DoNotCache_IfContentLengthMismatches(string method) +// { +// var cache = new TestResponseCache(); +// var sink = new TestSink(); +// var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); +// var context = TestUtils.CreateTestContext(); + +// context.ShouldCacheResponse = true; +// middleware.ShimResponseStream(context); +// context.HttpContext.Response.ContentLength = 9; +// context.HttpContext.Request.Method = method; + +// await context.HttpContext.Response.WriteAsync(new string('0', 10)); + +// context.CachedResponse = new CachedResponse(); +// context.BaseKey = "BaseKey"; +// context.CachedResponseValidFor = TimeSpan.FromSeconds(10); + +// middleware.FinalizeCacheBody(context); + +// Assert.Equal(0, cache.SetCount); +// TestUtils.AssertLoggedMessages( +// sink.Writes, +// LoggedMessage.ResponseContentLengthMismatchNotCached); +// } + +// [Theory] +// [InlineData(false)] +// [InlineData(true)] +// public async Task FinalizeCacheBody_RequestHead_Cache_IfContentLengthPresent_AndBodyAbsentOrOfSameLength(bool includeBody) +// { +// var cache = new TestResponseCache(); +// var sink = new TestSink(); +// var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); +// var context = TestUtils.CreateTestContext(); + +// context.ShouldCacheResponse = true; +// middleware.ShimResponseStream(context); +// context.HttpContext.Response.ContentLength = 10; +// context.HttpContext.Request.Method = "HEAD"; + +// if (includeBody) +// { +// // A response to HEAD should not include a body, but it may be present +// await context.HttpContext.Response.WriteAsync(new string('0', 10)); +// } + +// context.CachedResponse = new CachedResponse(); +// context.BaseKey = "BaseKey"; +// context.CachedResponseValidFor = TimeSpan.FromSeconds(10); + +// middleware.FinalizeCacheBody(context); + +// Assert.Equal(1, cache.SetCount); +// TestUtils.AssertLoggedMessages( +// sink.Writes, +// LoggedMessage.ResponseCached); +// } + +// [Fact] +// public async Task FinalizeCacheBody_Cache_IfContentLengthAbsent() +// { +// var cache = new TestResponseCache(); +// var sink = new TestSink(); +// var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); +// var context = TestUtils.CreateTestContext(); + +// context.ShouldCacheResponse = true; +// middleware.ShimResponseStream(context); + +// await context.HttpContext.Response.WriteAsync(new string('0', 10)); + +// context.CachedResponse = new CachedResponse() +// { +// Headers = new HeaderDictionary() +// }; +// context.BaseKey = "BaseKey"; +// context.CachedResponseValidFor = TimeSpan.FromSeconds(10); + +// middleware.FinalizeCacheBody(context); + +// Assert.Equal(1, cache.SetCount); +// TestUtils.AssertLoggedMessages( +// sink.Writes, +// LoggedMessage.ResponseCached); +// } + +// [Fact] +// public async Task FinalizeCacheBody_DoNotCache_IfShouldCacheResponseFalse() +// { +// var cache = new TestResponseCache(); +// var sink = new TestSink(); +// var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); +// var context = TestUtils.CreateTestContext(); + +// middleware.ShimResponseStream(context); +// await context.HttpContext.Response.WriteAsync(new string('0', 10)); +// context.ShouldCacheResponse = false; + +// middleware.FinalizeCacheBody(context); + +// Assert.Equal(0, cache.SetCount); +// TestUtils.AssertLoggedMessages( +// sink.Writes, +// LoggedMessage.ResponseNotCached); +// } + +// [Fact] +// public async Task FinalizeCacheBody_DoNotCache_IfBufferingDisabled() +// { +// var cache = new TestResponseCache(); +// var sink = new TestSink(); +// var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); +// var context = TestUtils.CreateTestContext(); + +// context.ShouldCacheResponse = true; +// middleware.ShimResponseStream(context); +// await context.HttpContext.Response.WriteAsync(new string('0', 10)); + +// context.ResponseCachingStream.DisableBuffering(); + +// middleware.FinalizeCacheBody(context); + +// Assert.Equal(0, cache.SetCount); +// TestUtils.AssertLoggedMessages( +// sink.Writes, +// LoggedMessage.ResponseNotCached); +// } + +// [Fact] +// public async Task FinalizeCacheBody_DoNotCache_IfSizeTooBig() +// { +// var sink = new TestSink(); +// var middleware = TestUtils.CreateTestMiddleware( +// testSink: sink, +// keyProvider: new TestResponseCachingKeyProvider("BaseKey"), +// cache: new MemoryResponseCache(new MemoryCache(new MemoryCacheOptions +// { +// SizeLimit = 100 +// }))); +// var context = TestUtils.CreateTestContext(); + +// context.ShouldCacheResponse = true; +// middleware.ShimResponseStream(context); + +// await context.HttpContext.Response.WriteAsync(new string('0', 101)); + +// context.CachedResponse = new CachedResponse() { Headers = new HeaderDictionary() }; +// context.CachedResponseValidFor = TimeSpan.FromSeconds(10); + +// middleware.FinalizeCacheBody(context); + +// // The response cached message will be logged but the adding of the entry will no-op +// TestUtils.AssertLoggedMessages( +// sink.Writes, +// LoggedMessage.ResponseCached); + +// // The entry cannot be retrieved +// Assert.False(await middleware.TryServeFromCacheAsync(context)); +// } + +// [Fact] +// public void AddResponseCachingFeature_SecondInvocation_Throws() +// { +// var httpContext = new DefaultHttpContext(); + +// // Should not throw +// ResponseCachingMiddleware.AddResponseCachingFeature(httpContext); + +// // Should throw +// Assert.ThrowsAny(() => ResponseCachingMiddleware.AddResponseCachingFeature(httpContext)); +// } + +// private class FakeResponseFeature : HttpResponseFeature +// { +// public override void OnStarting(Func callback, object state) { } +// } + +// [Theory] +// // If allowResponseCaching is false, other settings will not matter but are included for completeness +// [InlineData(false, false, false)] +// [InlineData(false, false, true)] +// [InlineData(false, true, false)] +// [InlineData(false, true, true)] +// [InlineData(true, false, false)] +// [InlineData(true, false, true)] +// [InlineData(true, true, false)] +// [InlineData(true, true, true)] +// public async Task Invoke_AddsResponseCachingFeature_Always(bool allowResponseCaching, bool allowCacheLookup, bool allowCacheStorage) +// { +// var responseCachingFeatureAdded = false; +// var middleware = TestUtils.CreateTestMiddleware(next: httpContext => +// { +// responseCachingFeatureAdded = httpContext.Features.Get() != null; +// return Task.CompletedTask; +// }, +// policyProvider: new TestResponseCachingPolicyProvider +// { +// AttemptResponseCachingValue = allowResponseCaching, +// AllowCacheLookupValue = allowCacheLookup, +// AllowCacheStorageValue = allowCacheStorage +// }); + +// var context = new DefaultHttpContext(); +// context.Features.Set(new FakeResponseFeature()); +// await middleware.Invoke(context); + +// Assert.True(responseCachingFeatureAdded); +// } + +// [Fact] +// public void GetOrderCasingNormalizedStringValues_NormalizesCasingToUpper() +// { +// var uppercaseStrings = new StringValues(new[] { "STRINGA", "STRINGB" }); +// var lowercaseStrings = new StringValues(new[] { "stringA", "stringB" }); + +// var normalizedStrings = ResponseCachingMiddleware.GetOrderCasingNormalizedStringValues(lowercaseStrings); + +// Assert.Equal(uppercaseStrings, normalizedStrings); +// } + +// [Fact] +// public void GetOrderCasingNormalizedStringValues_NormalizesOrder() +// { +// var orderedStrings = new StringValues(new[] { "STRINGA", "STRINGB" }); +// var reverseOrderStrings = new StringValues(new[] { "STRINGB", "STRINGA" }); + +// var normalizedStrings = ResponseCachingMiddleware.GetOrderCasingNormalizedStringValues(reverseOrderStrings); + +// Assert.Equal(orderedStrings, normalizedStrings); +// } + +// [Fact] +// public void GetOrderCasingNormalizedStringValues_PreservesCommas() +// { +// var originalStrings = new StringValues(new[] { "STRINGA, STRINGB" }); + +// var normalizedStrings = ResponseCachingMiddleware.GetOrderCasingNormalizedStringValues(originalStrings); + +// Assert.Equal(originalStrings, normalizedStrings); +// } +//} diff --git a/src/Middleware/OutputCaching/test/ResponseCachingPolicyProviderTests.cs b/src/Middleware/OutputCaching/test/ResponseCachingPolicyProviderTests.cs new file mode 100644 index 000000000000..f75b3b482154 --- /dev/null +++ b/src/Middleware/OutputCaching/test/ResponseCachingPolicyProviderTests.cs @@ -0,0 +1,790 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +//using Microsoft.AspNetCore.Http; +//using Microsoft.Extensions.Logging.Testing; +//using Microsoft.Net.Http.Headers; + +//namespace Microsoft.AspNetCore.ResponseCaching.Tests; + +//public class ResponseCachingPolicyProviderTests +//{ +// public static TheoryData CacheableMethods +// { +// get +// { +// return new TheoryData +// { +// HttpMethods.Get, +// HttpMethods.Head +// }; +// } +// } + +// [Theory] +// [MemberData(nameof(CacheableMethods))] +// public void AttemptResponseCaching_CacheableMethods_Allowed(string method) +// { +// var sink = new TestSink(); +// var context = TestUtils.CreateTestContext(sink); +// context.HttpContext.Request.Method = method; + +// Assert.True(new ResponseCachingPolicyProvider().AttemptResponseCaching(context)); +// Assert.Empty(sink.Writes); +// } +// public static TheoryData NonCacheableMethods +// { +// get +// { +// return new TheoryData +// { +// HttpMethods.Post, +// HttpMethods.Put, +// HttpMethods.Delete, +// HttpMethods.Trace, +// HttpMethods.Connect, +// HttpMethods.Options, +// "", +// null +// }; +// } +// } + +// [Theory] +// [MemberData(nameof(NonCacheableMethods))] +// public void AttemptResponseCaching_UncacheableMethods_NotAllowed(string method) +// { +// var sink = new TestSink(); +// var context = TestUtils.CreateTestContext(sink); +// context.HttpContext.Request.Method = method; + +// Assert.False(new ResponseCachingPolicyProvider().AttemptResponseCaching(context)); +// TestUtils.AssertLoggedMessages( +// sink.Writes, +// LoggedMessage.RequestMethodNotCacheable); +// } + +// [Fact] +// public void AttemptResponseCaching_AuthorizationHeaders_NotAllowed() +// { +// var sink = new TestSink(); +// var context = TestUtils.CreateTestContext(sink); +// context.HttpContext.Request.Method = HttpMethods.Get; +// context.HttpContext.Request.Headers.Authorization = "Placeholder"; + +// Assert.False(new ResponseCachingPolicyProvider().AttemptResponseCaching(context)); +// TestUtils.AssertLoggedMessages( +// sink.Writes, +// LoggedMessage.RequestWithAuthorizationNotCacheable); +// } + +// [Fact] +// public void AllowCacheStorage_NoStore_Allowed() +// { +// var sink = new TestSink(); +// var context = TestUtils.CreateTestContext(sink); +// context.HttpContext.Request.Method = HttpMethods.Get; +// context.HttpContext.Request.Headers.CacheControl = new CacheControlHeaderValue() +// { +// NoStore = true +// }.ToString(); + +// Assert.True(new ResponseCachingPolicyProvider().AllowCacheLookup(context)); +// Assert.Empty(sink.Writes); +// } + +// [Fact] +// public void AllowCacheLookup_NoCache_NotAllowed() +// { +// var sink = new TestSink(); +// var context = TestUtils.CreateTestContext(sink); +// context.HttpContext.Request.Method = HttpMethods.Get; +// context.HttpContext.Request.Headers.CacheControl = new CacheControlHeaderValue() +// { +// NoCache = true +// }.ToString(); + +// Assert.False(new ResponseCachingPolicyProvider().AllowCacheLookup(context)); +// TestUtils.AssertLoggedMessages( +// sink.Writes, +// LoggedMessage.RequestWithNoCacheNotCacheable); +// } + +// [Fact] +// public void AllowCacheLookup_LegacyDirectives_NotAllowed() +// { +// var sink = new TestSink(); +// var context = TestUtils.CreateTestContext(sink); +// context.HttpContext.Request.Method = HttpMethods.Get; +// context.HttpContext.Request.Headers.Pragma = "no-cache"; + +// Assert.False(new ResponseCachingPolicyProvider().AllowCacheLookup(context)); +// TestUtils.AssertLoggedMessages( +// sink.Writes, +// LoggedMessage.RequestWithPragmaNoCacheNotCacheable); +// } + +// [Fact] +// public void AllowCacheLookup_LegacyDirectives_OverridenByCacheControl() +// { +// var sink = new TestSink(); +// var context = TestUtils.CreateTestContext(sink); +// context.HttpContext.Request.Method = HttpMethods.Get; +// context.HttpContext.Request.Headers.Pragma = "no-cache"; +// context.HttpContext.Request.Headers.CacheControl = "max-age=10"; + +// Assert.True(new ResponseCachingPolicyProvider().AllowCacheLookup(context)); +// Assert.Empty(sink.Writes); +// } + +// [Fact] +// public void AllowCacheStorage_NoStore_NotAllowed() +// { +// var sink = new TestSink(); +// var context = TestUtils.CreateTestContext(sink); +// context.HttpContext.Request.Method = HttpMethods.Get; +// context.HttpContext.Request.Headers.CacheControl = new CacheControlHeaderValue() +// { +// NoStore = true +// }.ToString(); + +// Assert.False(new ResponseCachingPolicyProvider().AllowCacheStorage(context)); +// Assert.Empty(sink.Writes); +// } + +// [Fact] +// public void IsResponseCacheable_NoPublic_NotAllowed() +// { +// var sink = new TestSink(); +// var context = TestUtils.CreateTestContext(sink); + +// Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); +// TestUtils.AssertLoggedMessages( +// sink.Writes, +// LoggedMessage.ResponseWithoutPublicNotCacheable); +// } + +// [Fact] +// public void IsResponseCacheable_Public_Allowed() +// { +// var sink = new TestSink(); +// var context = TestUtils.CreateTestContext(sink); +// context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() +// { +// Public = true +// }.ToString(); + +// Assert.True(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); +// Assert.Empty(sink.Writes); +// } + +// [Fact] +// public void IsResponseCacheable_NoCache_NotAllowed() +// { +// var sink = new TestSink(); +// var context = TestUtils.CreateTestContext(sink); +// context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() +// { +// Public = true, +// NoCache = true +// }.ToString(); + +// Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); +// TestUtils.AssertLoggedMessages( +// sink.Writes, +// LoggedMessage.ResponseWithNoCacheNotCacheable); +// } + +// [Fact] +// public void IsResponseCacheable_ResponseNoStore_NotAllowed() +// { +// var sink = new TestSink(); +// var context = TestUtils.CreateTestContext(sink); +// context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() +// { +// Public = true, +// NoStore = true +// }.ToString(); + +// Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); +// TestUtils.AssertLoggedMessages( +// sink.Writes, +// LoggedMessage.ResponseWithNoStoreNotCacheable); +// } + +// [Fact] +// public void IsResponseCacheable_SetCookieHeader_NotAllowed() +// { +// var sink = new TestSink(); +// var context = TestUtils.CreateTestContext(sink); +// context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() +// { +// Public = true +// }.ToString(); +// context.HttpContext.Response.Headers.SetCookie = "cookieName=cookieValue"; + +// Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); +// TestUtils.AssertLoggedMessages( +// sink.Writes, +// LoggedMessage.ResponseWithSetCookieNotCacheable); +// } + +// [Fact] +// public void IsResponseCacheable_VaryHeaderByStar_NotAllowed() +// { +// var sink = new TestSink(); +// var context = TestUtils.CreateTestContext(sink); +// context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() +// { +// Public = true +// }.ToString(); +// context.HttpContext.Response.Headers.Vary = "*"; + +// Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); +// TestUtils.AssertLoggedMessages( +// sink.Writes, +// LoggedMessage.ResponseWithVaryStarNotCacheable); +// } + +// [Fact] +// public void IsResponseCacheable_Private_NotAllowed() +// { +// var sink = new TestSink(); +// var context = TestUtils.CreateTestContext(sink); +// context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() +// { +// Public = true, +// Private = true +// }.ToString(); + +// Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); +// TestUtils.AssertLoggedMessages( +// sink.Writes, +// LoggedMessage.ResponseWithPrivateNotCacheable); +// } + +// [Theory] +// [InlineData(StatusCodes.Status200OK)] +// public void IsResponseCacheable_SuccessStatusCodes_Allowed(int statusCode) +// { +// var sink = new TestSink(); +// var context = TestUtils.CreateTestContext(sink); +// context.HttpContext.Response.StatusCode = statusCode; +// context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() +// { +// Public = true +// }.ToString(); + +// Assert.True(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); +// Assert.Empty(sink.Writes); +// } + +// [Theory] +// [InlineData(StatusCodes.Status100Continue)] +// [InlineData(StatusCodes.Status101SwitchingProtocols)] +// [InlineData(StatusCodes.Status102Processing)] +// [InlineData(StatusCodes.Status201Created)] +// [InlineData(StatusCodes.Status202Accepted)] +// [InlineData(StatusCodes.Status203NonAuthoritative)] +// [InlineData(StatusCodes.Status204NoContent)] +// [InlineData(StatusCodes.Status205ResetContent)] +// [InlineData(StatusCodes.Status206PartialContent)] +// [InlineData(StatusCodes.Status207MultiStatus)] +// [InlineData(StatusCodes.Status208AlreadyReported)] +// [InlineData(StatusCodes.Status226IMUsed)] +// [InlineData(StatusCodes.Status300MultipleChoices)] +// [InlineData(StatusCodes.Status301MovedPermanently)] +// [InlineData(StatusCodes.Status302Found)] +// [InlineData(StatusCodes.Status303SeeOther)] +// [InlineData(StatusCodes.Status304NotModified)] +// [InlineData(StatusCodes.Status305UseProxy)] +// [InlineData(StatusCodes.Status306SwitchProxy)] +// [InlineData(StatusCodes.Status307TemporaryRedirect)] +// [InlineData(StatusCodes.Status308PermanentRedirect)] +// [InlineData(StatusCodes.Status400BadRequest)] +// [InlineData(StatusCodes.Status401Unauthorized)] +// [InlineData(StatusCodes.Status402PaymentRequired)] +// [InlineData(StatusCodes.Status403Forbidden)] +// [InlineData(StatusCodes.Status404NotFound)] +// [InlineData(StatusCodes.Status405MethodNotAllowed)] +// [InlineData(StatusCodes.Status406NotAcceptable)] +// [InlineData(StatusCodes.Status407ProxyAuthenticationRequired)] +// [InlineData(StatusCodes.Status408RequestTimeout)] +// [InlineData(StatusCodes.Status409Conflict)] +// [InlineData(StatusCodes.Status410Gone)] +// [InlineData(StatusCodes.Status411LengthRequired)] +// [InlineData(StatusCodes.Status412PreconditionFailed)] +// [InlineData(StatusCodes.Status413RequestEntityTooLarge)] +// [InlineData(StatusCodes.Status414RequestUriTooLong)] +// [InlineData(StatusCodes.Status415UnsupportedMediaType)] +// [InlineData(StatusCodes.Status416RequestedRangeNotSatisfiable)] +// [InlineData(StatusCodes.Status417ExpectationFailed)] +// [InlineData(StatusCodes.Status418ImATeapot)] +// [InlineData(StatusCodes.Status419AuthenticationTimeout)] +// [InlineData(StatusCodes.Status421MisdirectedRequest)] +// [InlineData(StatusCodes.Status422UnprocessableEntity)] +// [InlineData(StatusCodes.Status423Locked)] +// [InlineData(StatusCodes.Status424FailedDependency)] +// [InlineData(StatusCodes.Status426UpgradeRequired)] +// [InlineData(StatusCodes.Status428PreconditionRequired)] +// [InlineData(StatusCodes.Status429TooManyRequests)] +// [InlineData(StatusCodes.Status431RequestHeaderFieldsTooLarge)] +// [InlineData(StatusCodes.Status451UnavailableForLegalReasons)] +// [InlineData(StatusCodes.Status500InternalServerError)] +// [InlineData(StatusCodes.Status501NotImplemented)] +// [InlineData(StatusCodes.Status502BadGateway)] +// [InlineData(StatusCodes.Status503ServiceUnavailable)] +// [InlineData(StatusCodes.Status504GatewayTimeout)] +// [InlineData(StatusCodes.Status505HttpVersionNotsupported)] +// [InlineData(StatusCodes.Status506VariantAlsoNegotiates)] +// [InlineData(StatusCodes.Status507InsufficientStorage)] +// [InlineData(StatusCodes.Status508LoopDetected)] +// [InlineData(StatusCodes.Status510NotExtended)] +// [InlineData(StatusCodes.Status511NetworkAuthenticationRequired)] +// public void IsResponseCacheable_NonSuccessStatusCodes_NotAllowed(int statusCode) +// { +// var sink = new TestSink(); +// var context = TestUtils.CreateTestContext(sink); +// context.HttpContext.Response.StatusCode = statusCode; +// context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() +// { +// Public = true +// }.ToString(); + +// Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); +// TestUtils.AssertLoggedMessages( +// sink.Writes, +// LoggedMessage.ResponseWithUnsuccessfulStatusCodeNotCacheable); +// } + +// [Fact] +// public void IsResponseCacheable_NoExpiryRequirements_IsAllowed() +// { +// var sink = new TestSink(); +// var context = TestUtils.CreateTestContext(sink); +// context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; +// context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() +// { +// Public = true +// }.ToString(); + +// var utcNow = DateTimeOffset.UtcNow; +// context.HttpContext.Response.Headers.Date = HeaderUtilities.FormatDate(utcNow); +// context.ResponseTime = DateTimeOffset.MaxValue; + +// Assert.True(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); +// Assert.Empty(sink.Writes); +// } + +// [Fact] +// public void IsResponseCacheable_AtExpiry_NotAllowed() +// { +// var sink = new TestSink(); +// var context = TestUtils.CreateTestContext(sink); +// context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; +// context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() +// { +// Public = true +// }.ToString(); +// var utcNow = DateTimeOffset.UtcNow; +// context.HttpContext.Response.Headers.Expires = HeaderUtilities.FormatDate(utcNow); + +// context.HttpContext.Response.Headers.Date = HeaderUtilities.FormatDate(utcNow); +// context.ResponseTime = utcNow; + +// Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); +// TestUtils.AssertLoggedMessages( +// sink.Writes, +// LoggedMessage.ExpirationExpiresExceeded); +// } + +// [Fact] +// public void IsResponseCacheable_MaxAgeOverridesExpiry_ToAllowed() +// { +// var utcNow = DateTimeOffset.UtcNow; +// var sink = new TestSink(); +// var context = TestUtils.CreateTestContext(sink); +// context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; +// context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() +// { +// Public = true, +// MaxAge = TimeSpan.FromSeconds(10) +// }.ToString(); +// context.HttpContext.Response.Headers.Expires = HeaderUtilities.FormatDate(utcNow); +// context.HttpContext.Response.Headers.Date = HeaderUtilities.FormatDate(utcNow); +// context.ResponseTime = utcNow + TimeSpan.FromSeconds(9); + +// Assert.True(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); +// Assert.Empty(sink.Writes); +// } + +// [Fact] +// public void IsResponseCacheable_MaxAgeOverridesExpiry_ToNotAllowed() +// { +// var utcNow = DateTimeOffset.UtcNow; +// var sink = new TestSink(); +// var context = TestUtils.CreateTestContext(sink); +// context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; +// context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() +// { +// Public = true, +// MaxAge = TimeSpan.FromSeconds(10) +// }.ToString(); +// context.HttpContext.Response.Headers.Expires = HeaderUtilities.FormatDate(utcNow); +// context.HttpContext.Response.Headers.Date = HeaderUtilities.FormatDate(utcNow); +// context.ResponseTime = utcNow + TimeSpan.FromSeconds(10); + +// Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); +// TestUtils.AssertLoggedMessages( +// sink.Writes, +// LoggedMessage.ExpirationMaxAgeExceeded); +// } + +// [Fact] +// public void IsResponseCacheable_SharedMaxAgeOverridesMaxAge_ToAllowed() +// { +// var utcNow = DateTimeOffset.UtcNow; +// var sink = new TestSink(); +// var context = TestUtils.CreateTestContext(sink); +// context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; +// context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() +// { +// Public = true, +// MaxAge = TimeSpan.FromSeconds(10), +// SharedMaxAge = TimeSpan.FromSeconds(15) +// }.ToString(); +// context.HttpContext.Response.Headers.Date = HeaderUtilities.FormatDate(utcNow); +// context.ResponseTime = utcNow + TimeSpan.FromSeconds(11); + +// Assert.True(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); +// Assert.Empty(sink.Writes); +// } + +// [Fact] +// public void IsResponseCacheable_SharedMaxAgeOverridesMaxAge_ToNotAllowed() +// { +// var utcNow = DateTimeOffset.UtcNow; +// var sink = new TestSink(); +// var context = TestUtils.CreateTestContext(sink); +// context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; +// context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() +// { +// Public = true, +// MaxAge = TimeSpan.FromSeconds(10), +// SharedMaxAge = TimeSpan.FromSeconds(5) +// }.ToString(); +// context.HttpContext.Response.Headers.Date = HeaderUtilities.FormatDate(utcNow); +// context.ResponseTime = utcNow + TimeSpan.FromSeconds(5); + +// Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); +// TestUtils.AssertLoggedMessages( +// sink.Writes, +// LoggedMessage.ExpirationSharedMaxAgeExceeded); +// } + +// [Fact] +// public void IsCachedEntryFresh_NoCachedCacheControl_FallsbackToEmptyCacheControl() +// { +// var utcNow = DateTimeOffset.UtcNow; +// var sink = new TestSink(); +// var context = TestUtils.CreateTestContext(sink); +// context.ResponseTime = DateTimeOffset.MaxValue; +// context.CachedEntryAge = TimeSpan.MaxValue; +// context.CachedResponseHeaders = new HeaderDictionary(); + +// Assert.True(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); +// Assert.Empty(sink.Writes); +// } + +// [Fact] +// public void IsCachedEntryFresh_NoExpiryRequirements_IsFresh() +// { +// var utcNow = DateTimeOffset.UtcNow; +// var sink = new TestSink(); +// var context = TestUtils.CreateTestContext(sink); +// context.ResponseTime = DateTimeOffset.MaxValue; +// context.CachedEntryAge = TimeSpan.MaxValue; +// context.CachedResponseHeaders = new HeaderDictionary(); +// context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() +// { +// Public = true +// }.ToString(); + +// Assert.True(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); +// Assert.Empty(sink.Writes); +// } + +// [Fact] +// public void IsCachedEntryFresh_AtExpiry_IsNotFresh() +// { +// var utcNow = DateTimeOffset.UtcNow; +// var sink = new TestSink(); +// var context = TestUtils.CreateTestContext(sink); +// context.ResponseTime = utcNow; +// context.CachedEntryAge = TimeSpan.Zero; +// context.CachedResponseHeaders = new HeaderDictionary(); +// context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() +// { +// Public = true +// }.ToString(); +// context.CachedResponseHeaders[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow); + +// Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); +// TestUtils.AssertLoggedMessages( +// sink.Writes, +// LoggedMessage.ExpirationExpiresExceeded); +// } + +// [Fact] +// public void IsCachedEntryFresh_MaxAgeOverridesExpiry_ToFresh() +// { +// var utcNow = DateTimeOffset.UtcNow; +// var sink = new TestSink(); +// var context = TestUtils.CreateTestContext(sink); +// context.CachedEntryAge = TimeSpan.FromSeconds(9); +// context.ResponseTime = utcNow + context.CachedEntryAge; +// context.CachedResponseHeaders = new HeaderDictionary(); +// context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() +// { +// Public = true, +// MaxAge = TimeSpan.FromSeconds(10) +// }.ToString(); +// context.HttpContext.Response.Headers.Expires = HeaderUtilities.FormatDate(utcNow); + +// Assert.True(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); +// Assert.Empty(sink.Writes); +// } + +// [Fact] +// public void IsCachedEntryFresh_MaxAgeOverridesExpiry_ToNotFresh() +// { +// var utcNow = DateTimeOffset.UtcNow; +// var sink = new TestSink(); +// var context = TestUtils.CreateTestContext(sink); +// context.CachedEntryAge = TimeSpan.FromSeconds(10); +// context.ResponseTime = utcNow + context.CachedEntryAge; +// context.CachedResponseHeaders = new HeaderDictionary(); +// context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() +// { +// Public = true, +// MaxAge = TimeSpan.FromSeconds(10) +// }.ToString(); +// context.HttpContext.Response.Headers.Expires = HeaderUtilities.FormatDate(utcNow); + +// Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); +// TestUtils.AssertLoggedMessages( +// sink.Writes, +// LoggedMessage.ExpirationMaxAgeExceeded); +// } + +// [Fact] +// public void IsCachedEntryFresh_SharedMaxAgeOverridesMaxAge_ToFresh() +// { +// var utcNow = DateTimeOffset.UtcNow; +// var sink = new TestSink(); +// var context = TestUtils.CreateTestContext(sink); +// context.CachedEntryAge = TimeSpan.FromSeconds(11); +// context.ResponseTime = utcNow + context.CachedEntryAge; +// context.CachedResponseHeaders = new HeaderDictionary(); +// context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() +// { +// Public = true, +// MaxAge = TimeSpan.FromSeconds(10), +// SharedMaxAge = TimeSpan.FromSeconds(15) +// }.ToString(); +// context.CachedResponseHeaders[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow); + +// Assert.True(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); +// Assert.Empty(sink.Writes); +// } + +// [Fact] +// public void IsCachedEntryFresh_SharedMaxAgeOverridesMaxAge_ToNotFresh() +// { +// var utcNow = DateTimeOffset.UtcNow; +// var sink = new TestSink(); +// var context = TestUtils.CreateTestContext(sink); +// context.CachedEntryAge = TimeSpan.FromSeconds(5); +// context.ResponseTime = utcNow + context.CachedEntryAge; +// context.CachedResponseHeaders = new HeaderDictionary(); +// context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() +// { +// Public = true, +// MaxAge = TimeSpan.FromSeconds(10), +// SharedMaxAge = TimeSpan.FromSeconds(5) +// }.ToString(); +// context.CachedResponseHeaders[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow); + +// Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); +// TestUtils.AssertLoggedMessages( +// sink.Writes, +// LoggedMessage.ExpirationSharedMaxAgeExceeded); +// } + +// [Fact] +// public void IsCachedEntryFresh_MinFreshReducesFreshness_ToNotFresh() +// { +// var sink = new TestSink(); +// var context = TestUtils.CreateTestContext(sink); +// context.HttpContext.Request.Headers.CacheControl = new CacheControlHeaderValue() +// { +// MinFresh = TimeSpan.FromSeconds(2) +// }.ToString(); +// context.CachedResponseHeaders = new HeaderDictionary(); +// context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() +// { +// MaxAge = TimeSpan.FromSeconds(10), +// SharedMaxAge = TimeSpan.FromSeconds(5) +// }.ToString(); +// context.CachedEntryAge = TimeSpan.FromSeconds(3); + +// Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); +// TestUtils.AssertLoggedMessages( +// sink.Writes, +// LoggedMessage.ExpirationMinFreshAdded, +// LoggedMessage.ExpirationSharedMaxAgeExceeded); +// } + +// [Fact] +// public void IsCachedEntryFresh_RequestMaxAgeRestrictAge_ToNotFresh() +// { +// var sink = new TestSink(); +// var context = TestUtils.CreateTestContext(sink); +// context.HttpContext.Request.Headers.CacheControl = new CacheControlHeaderValue() +// { +// MaxAge = TimeSpan.FromSeconds(5) +// }.ToString(); +// context.CachedResponseHeaders = new HeaderDictionary(); +// context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() +// { +// MaxAge = TimeSpan.FromSeconds(10), +// }.ToString(); +// context.CachedEntryAge = TimeSpan.FromSeconds(5); + +// Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); +// TestUtils.AssertLoggedMessages( +// sink.Writes, +// LoggedMessage.ExpirationMaxAgeExceeded); +// } + +// [Fact] +// public void IsCachedEntryFresh_MaxStaleOverridesFreshness_ToFresh() +// { +// var sink = new TestSink(); +// var context = TestUtils.CreateTestContext(sink); +// context.HttpContext.Request.Headers.CacheControl = new CacheControlHeaderValue() +// { +// MaxAge = TimeSpan.FromSeconds(5), +// MaxStale = true, // This value must be set to true in order to specify MaxStaleLimit +// MaxStaleLimit = TimeSpan.FromSeconds(2) +// }.ToString(); +// context.CachedResponseHeaders = new HeaderDictionary(); +// context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() +// { +// MaxAge = TimeSpan.FromSeconds(5), +// }.ToString(); +// context.CachedEntryAge = TimeSpan.FromSeconds(6); + +// Assert.True(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); +// TestUtils.AssertLoggedMessages( +// sink.Writes, +// LoggedMessage.ExpirationMaxStaleSatisfied); +// } + +// [Fact] +// public void IsCachedEntryFresh_MaxStaleInfiniteOverridesFreshness_ToFresh() +// { +// var sink = new TestSink(); +// var context = TestUtils.CreateTestContext(sink); +// context.HttpContext.Request.Headers.CacheControl = new CacheControlHeaderValue() +// { +// MaxAge = TimeSpan.FromSeconds(5), +// MaxStale = true // No value specified means a MaxStaleLimit of infinity +// }.ToString(); +// context.CachedResponseHeaders = new HeaderDictionary(); +// context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() +// { +// MaxAge = TimeSpan.FromSeconds(5), +// }.ToString(); +// context.CachedEntryAge = TimeSpan.FromSeconds(6); + +// Assert.True(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); +// TestUtils.AssertLoggedMessages( +// sink.Writes, +// LoggedMessage.ExpirationInfiniteMaxStaleSatisfied); +// } + +// [Fact] +// public void IsCachedEntryFresh_MaxStaleOverridesFreshness_ButStillNotFresh() +// { +// var sink = new TestSink(); +// var context = TestUtils.CreateTestContext(sink); +// context.HttpContext.Request.Headers.CacheControl = new CacheControlHeaderValue() +// { +// MaxAge = TimeSpan.FromSeconds(5), +// MaxStale = true, // This value must be set to true in order to specify MaxStaleLimit +// MaxStaleLimit = TimeSpan.FromSeconds(1) +// }.ToString(); +// context.CachedResponseHeaders = new HeaderDictionary(); +// context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() +// { +// MaxAge = TimeSpan.FromSeconds(5), +// }.ToString(); +// context.CachedEntryAge = TimeSpan.FromSeconds(6); + +// Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); +// TestUtils.AssertLoggedMessages( +// sink.Writes, +// LoggedMessage.ExpirationMaxAgeExceeded); +// } + +// [Fact] +// public void IsCachedEntryFresh_MustRevalidateOverridesRequestMaxStale_ToNotFresh() +// { +// var sink = new TestSink(); +// var context = TestUtils.CreateTestContext(sink); +// context.HttpContext.Request.Headers.CacheControl = new CacheControlHeaderValue() +// { +// MaxAge = TimeSpan.FromSeconds(5), +// MaxStale = true, // This value must be set to true in order to specify MaxStaleLimit +// MaxStaleLimit = TimeSpan.FromSeconds(2) +// }.ToString(); +// context.CachedResponseHeaders = new HeaderDictionary(); +// context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() +// { +// MaxAge = TimeSpan.FromSeconds(5), +// MustRevalidate = true +// }.ToString(); +// context.CachedEntryAge = TimeSpan.FromSeconds(6); + +// Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); +// TestUtils.AssertLoggedMessages( +// sink.Writes, +// LoggedMessage.ExpirationMustRevalidate); +// } + +// [Fact] +// public void IsCachedEntryFresh_ProxyRevalidateOverridesRequestMaxStale_ToNotFresh() +// { +// var sink = new TestSink(); +// var context = TestUtils.CreateTestContext(sink); +// context.HttpContext.Request.Headers.CacheControl = new CacheControlHeaderValue() +// { +// MaxAge = TimeSpan.FromSeconds(5), +// MaxStale = true, // This value must be set to true in order to specify MaxStaleLimit +// MaxStaleLimit = TimeSpan.FromSeconds(2) +// }.ToString(); +// context.CachedResponseHeaders = new HeaderDictionary(); +// context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() +// { +// MaxAge = TimeSpan.FromSeconds(5), +// MustRevalidate = true +// }.ToString(); +// context.CachedEntryAge = TimeSpan.FromSeconds(6); + +// Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); +// TestUtils.AssertLoggedMessages( +// sink.Writes, +// LoggedMessage.ExpirationMustRevalidate); +// } +//} diff --git a/src/Middleware/OutputCaching/test/ResponseCachingTests.cs b/src/Middleware/OutputCaching/test/ResponseCachingTests.cs new file mode 100644 index 000000000000..0622854d79e8 --- /dev/null +++ b/src/Middleware/OutputCaching/test/ResponseCachingTests.cs @@ -0,0 +1,984 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +//using System.Net.Http; +//using Microsoft.AspNetCore.Http; +//using Microsoft.AspNetCore.TestHost; +//using Microsoft.Net.Http.Headers; + +//namespace Microsoft.AspNetCore.ResponseCaching.Tests; + +//public class ResponseCachingTests +//{ +// [Theory] +// [InlineData("GET")] +// [InlineData("HEAD")] +// public async Task ServesCachedContent_IfAvailable(string method) +// { +// var builders = TestUtils.CreateBuildersWithResponseCaching(); + +// foreach (var builder in builders) +// { +// using var host = builder.Build(); + +// await host.StartAsync(); + +// using (var server = host.GetTestServer()) +// { +// var client = server.CreateClient(); +// var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); +// var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); + +// await AssertCachedResponseAsync(initialResponse, subsequentResponse); +// } +// } +// } + +// [Theory] +// [InlineData("GET")] +// [InlineData("HEAD")] +// public async Task ServesFreshContent_IfNotAvailable(string method) +// { +// var builders = TestUtils.CreateBuildersWithResponseCaching(); + +// foreach (var builder in builders) +// { +// using var host = builder.Build(); + +// await host.StartAsync(); + +// using (var server = host.GetTestServer()) +// { +// var client = server.CreateClient(); +// var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); +// var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, "different")); + +// await AssertFreshResponseAsync(initialResponse, subsequentResponse); +// } +// } +// } + +// [Fact] +// public async Task ServesFreshContent_Post() +// { +// var builders = TestUtils.CreateBuildersWithResponseCaching(); + +// foreach (var builder in builders) +// { +// using var host = builder.Build(); + +// await host.StartAsync(); + +// using (var server = host.GetTestServer()) +// { +// var client = server.CreateClient(); +// var initialResponse = await client.PostAsync("", new StringContent(string.Empty)); +// var subsequentResponse = await client.PostAsync("", new StringContent(string.Empty)); + +// await AssertFreshResponseAsync(initialResponse, subsequentResponse); +// } +// } +// } + +// [Fact] +// public async Task ServesFreshContent_Head_Get() +// { +// var builders = TestUtils.CreateBuildersWithResponseCaching(); + +// foreach (var builder in builders) +// { +// using var host = builder.Build(); + +// await host.StartAsync(); + +// using (var server = host.GetTestServer()) +// { +// var client = server.CreateClient(); +// var subsequentResponse = await client.SendAsync(new HttpRequestMessage(HttpMethod.Head, "")); +// var initialResponse = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "")); + +// await AssertFreshResponseAsync(initialResponse, subsequentResponse); +// } +// } +// } + +// [Fact] +// public async Task ServesFreshContent_Get_Head() +// { +// var builders = TestUtils.CreateBuildersWithResponseCaching(); + +// foreach (var builder in builders) +// { +// using var host = builder.Build(); + +// await host.StartAsync(); + +// using (var server = host.GetTestServer()) +// { +// var client = server.CreateClient(); +// var initialResponse = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "")); +// var subsequentResponse = await client.SendAsync(new HttpRequestMessage(HttpMethod.Head, "")); + +// await AssertFreshResponseAsync(initialResponse, subsequentResponse); +// } +// } +// } + +// [Theory] +// [InlineData("GET")] +// [InlineData("HEAD")] +// public async Task ServesFreshContent_If_CacheControlNoCache(string method) +// { +// var builders = TestUtils.CreateBuildersWithResponseCaching(); + +// foreach (var builder in builders) +// { +// using var host = builder.Build(); + +// await host.StartAsync(); + +// using (var server = host.GetTestServer()) +// { +// var client = server.CreateClient(); + +// var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); + +// // verify the response is cached +// var cachedResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); +// await AssertCachedResponseAsync(initialResponse, cachedResponse); + +// // assert cached response no longer served +// client.DefaultRequestHeaders.CacheControl = +// new System.Net.Http.Headers.CacheControlHeaderValue { NoCache = true }; +// var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); + +// await AssertFreshResponseAsync(initialResponse, subsequentResponse); +// } +// } +// } + +// [Theory] +// [InlineData("GET")] +// [InlineData("HEAD")] +// public async Task ServesFreshContent_If_PragmaNoCache(string method) +// { +// var builders = TestUtils.CreateBuildersWithResponseCaching(); + +// foreach (var builder in builders) +// { +// using var host = builder.Build(); + +// await host.StartAsync(); + +// using (var server = host.GetTestServer()) +// { +// var client = server.CreateClient(); + +// var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); + +// // verify the response is cached +// var cachedResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); +// await AssertCachedResponseAsync(initialResponse, cachedResponse); + +// // assert cached response no longer served +// client.DefaultRequestHeaders.Pragma.Clear(); +// client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("no-cache")); +// var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); + +// await AssertFreshResponseAsync(initialResponse, subsequentResponse); +// } +// } +// } + +// [Theory] +// [InlineData("GET")] +// [InlineData("HEAD")] +// public async Task ServesCachedContent_If_PathCasingDiffers(string method) +// { +// var builders = TestUtils.CreateBuildersWithResponseCaching(); + +// foreach (var builder in builders) +// { +// using var host = builder.Build(); + +// await host.StartAsync(); + +// using (var server = host.GetTestServer()) +// { +// var client = server.CreateClient(); +// var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, "path")); +// var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, "PATH")); + +// await AssertCachedResponseAsync(initialResponse, subsequentResponse); +// } +// } +// } + +// [Theory] +// [InlineData("GET")] +// [InlineData("HEAD")] +// public async Task ServesFreshContent_If_ResponseExpired(string method) +// { +// var builders = TestUtils.CreateBuildersWithResponseCaching(); + +// foreach (var builder in builders) +// { +// using var host = builder.Build(); + +// await host.StartAsync(); + +// using (var server = host.GetTestServer()) +// { +// var client = server.CreateClient(); +// var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, "?Expires=0")); +// var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); + +// await AssertFreshResponseAsync(initialResponse, subsequentResponse); +// } +// } +// } + +// [Theory] +// [InlineData("GET")] +// [InlineData("HEAD")] +// public async Task ServesFreshContent_If_Authorization_HeaderExists(string method) +// { +// var builders = TestUtils.CreateBuildersWithResponseCaching(); + +// foreach (var builder in builders) +// { +// using var host = builder.Build(); + +// await host.StartAsync(); + +// using (var server = host.GetTestServer()) +// { +// var client = server.CreateClient(); +// client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("abc"); +// var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); +// var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); + +// await AssertFreshResponseAsync(initialResponse, subsequentResponse); +// } +// } +// } + +// [Fact] +// public async Task ServesCachedContent_IfVaryHeader_Matches() +// { +// var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Response.Headers.Vary = HeaderNames.From); + +// foreach (var builder in builders) +// { +// using var host = builder.Build(); + +// await host.StartAsync(); + +// using (var server = host.GetTestServer()) +// { +// var client = server.CreateClient(); +// client.DefaultRequestHeaders.From = "user@example.com"; +// var initialResponse = await client.GetAsync(""); +// var subsequentResponse = await client.GetAsync(""); + +// await AssertCachedResponseAsync(initialResponse, subsequentResponse); +// } +// } +// } + +// [Fact] +// public async Task ServesFreshContent_IfVaryHeader_Mismatches() +// { +// var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Response.Headers.Vary = HeaderNames.From); + +// foreach (var builder in builders) +// { +// using var host = builder.Build(); + +// await host.StartAsync(); + +// using (var server = host.GetTestServer()) +// { +// var client = server.CreateClient(); +// client.DefaultRequestHeaders.From = "user@example.com"; +// var initialResponse = await client.GetAsync(""); +// client.DefaultRequestHeaders.From = "user2@example.com"; +// var subsequentResponse = await client.GetAsync(""); + +// await AssertFreshResponseAsync(initialResponse, subsequentResponse); +// } +// } +// } + +// [Fact] +// public async Task ServesCachedContent_IfVaryQueryKeys_Matches() +// { +// var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Features.Get().VaryByQueryKeys = new[] { "query" }); + +// foreach (var builder in builders) +// { +// using var host = builder.Build(); + +// await host.StartAsync(); + +// using (var server = host.GetTestServer()) +// { +// var client = server.CreateClient(); +// var initialResponse = await client.GetAsync("?query=value"); +// var subsequentResponse = await client.GetAsync("?query=value"); + +// await AssertCachedResponseAsync(initialResponse, subsequentResponse); +// } +// } +// } + +// [Fact] +// public async Task ServesCachedContent_IfVaryQueryKeysExplicit_Matches_QueryKeyCaseInsensitive() +// { +// var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Features.Get().VaryByQueryKeys = new[] { "QueryA", "queryb" }); + +// foreach (var builder in builders) +// { +// using var host = builder.Build(); + +// await host.StartAsync(); + +// using (var server = host.GetTestServer()) +// { +// var client = server.CreateClient(); +// var initialResponse = await client.GetAsync("?querya=valuea&queryb=valueb"); +// var subsequentResponse = await client.GetAsync("?QueryA=valuea&QueryB=valueb"); + +// await AssertCachedResponseAsync(initialResponse, subsequentResponse); +// } +// } +// } + +// [Fact] +// public async Task ServesCachedContent_IfVaryQueryKeyStar_Matches_QueryKeyCaseInsensitive() +// { +// var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Features.Get().VaryByQueryKeys = new[] { "*" }); + +// foreach (var builder in builders) +// { +// using var host = builder.Build(); + +// await host.StartAsync(); + +// using (var server = host.GetTestServer()) +// { +// var client = server.CreateClient(); +// var initialResponse = await client.GetAsync("?querya=valuea&queryb=valueb"); +// var subsequentResponse = await client.GetAsync("?QueryA=valuea&QueryB=valueb"); + +// await AssertCachedResponseAsync(initialResponse, subsequentResponse); +// } +// } +// } + +// [Fact] +// public async Task ServesCachedContent_IfVaryQueryKeyExplicit_Matches_OrderInsensitive() +// { +// var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Features.Get().VaryByQueryKeys = new[] { "QueryB", "QueryA" }); + +// foreach (var builder in builders) +// { +// using var host = builder.Build(); + +// await host.StartAsync(); + +// using (var server = host.GetTestServer()) +// { +// var client = server.CreateClient(); +// var initialResponse = await client.GetAsync("?QueryA=ValueA&QueryB=ValueB"); +// var subsequentResponse = await client.GetAsync("?QueryB=ValueB&QueryA=ValueA"); + +// await AssertCachedResponseAsync(initialResponse, subsequentResponse); +// } +// } +// } + +// [Fact] +// public async Task ServesCachedContent_IfVaryQueryKeyStar_Matches_OrderInsensitive() +// { +// var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Features.Get().VaryByQueryKeys = new[] { "*" }); + +// foreach (var builder in builders) +// { +// using var host = builder.Build(); + +// await host.StartAsync(); + +// using (var server = host.GetTestServer()) +// { +// var client = server.CreateClient(); +// var initialResponse = await client.GetAsync("?QueryA=ValueA&QueryB=ValueB"); +// var subsequentResponse = await client.GetAsync("?QueryB=ValueB&QueryA=ValueA"); + +// await AssertCachedResponseAsync(initialResponse, subsequentResponse); +// } +// } +// } + +// [Fact] +// public async Task ServesFreshContent_IfVaryQueryKey_Mismatches() +// { +// var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Features.Get().VaryByQueryKeys = new[] { "query" }); + +// foreach (var builder in builders) +// { +// using var host = builder.Build(); + +// await host.StartAsync(); + +// using (var server = host.GetTestServer()) +// { +// var client = server.CreateClient(); +// var initialResponse = await client.GetAsync("?query=value"); +// var subsequentResponse = await client.GetAsync("?query=value2"); + +// await AssertFreshResponseAsync(initialResponse, subsequentResponse); +// } +// } +// } + +// [Fact] +// public async Task ServesFreshContent_IfVaryQueryKeyExplicit_Mismatch_QueryKeyCaseSensitive() +// { +// var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Features.Get().VaryByQueryKeys = new[] { "QueryA", "QueryB" }); + +// foreach (var builder in builders) +// { +// using var host = builder.Build(); + +// await host.StartAsync(); + +// using (var server = host.GetTestServer()) +// { +// var client = server.CreateClient(); +// var initialResponse = await client.GetAsync("?querya=valuea&queryb=valueb"); +// var subsequentResponse = await client.GetAsync("?querya=ValueA&queryb=ValueB"); + +// await AssertFreshResponseAsync(initialResponse, subsequentResponse); +// } +// } +// } + +// [Fact] +// public async Task ServesFreshContent_IfVaryQueryKeyStar_Mismatch_QueryKeyValueCaseSensitive() +// { +// var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Features.Get().VaryByQueryKeys = new[] { "*" }); + +// foreach (var builder in builders) +// { +// using var host = builder.Build(); + +// await host.StartAsync(); + +// using (var server = host.GetTestServer()) +// { +// var client = server.CreateClient(); +// var initialResponse = await client.GetAsync("?querya=valuea&queryb=valueb"); +// var subsequentResponse = await client.GetAsync("?querya=ValueA&queryb=ValueB"); + +// await AssertFreshResponseAsync(initialResponse, subsequentResponse); +// } +// } +// } + +// [Fact] +// public async Task ServesFreshContent_IfRequestRequirements_NotMet() +// { +// var builders = TestUtils.CreateBuildersWithResponseCaching(); + +// foreach (var builder in builders) +// { +// using var host = builder.Build(); + +// await host.StartAsync(); + +// using (var server = host.GetTestServer()) +// { +// var client = server.CreateClient(); +// var initialResponse = await client.GetAsync(""); +// client.DefaultRequestHeaders.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue() +// { +// MaxAge = TimeSpan.FromSeconds(0) +// }; +// var subsequentResponse = await client.GetAsync(""); + +// await AssertFreshResponseAsync(initialResponse, subsequentResponse); +// } +// } +// } + +// [Fact] +// public async Task Serves504_IfOnlyIfCachedHeader_IsSpecified() +// { +// var builders = TestUtils.CreateBuildersWithResponseCaching(); + +// foreach (var builder in builders) +// { +// using var host = builder.Build(); + +// await host.StartAsync(); + +// using (var server = host.GetTestServer()) +// { +// var client = server.CreateClient(); +// var initialResponse = await client.GetAsync(""); +// client.DefaultRequestHeaders.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue() +// { +// OnlyIfCached = true +// }; +// var subsequentResponse = await client.GetAsync("/different"); + +// initialResponse.EnsureSuccessStatusCode(); +// Assert.Equal(System.Net.HttpStatusCode.GatewayTimeout, subsequentResponse.StatusCode); +// } +// } +// } + +// [Fact] +// public async Task ServesFreshContent_IfSetCookie_IsSpecified() +// { +// var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Response.Headers.SetCookie = "cookieName=cookieValue"); + +// foreach (var builder in builders) +// { +// using var host = builder.Build(); + +// await host.StartAsync(); + +// using (var server = host.GetTestServer()) +// { +// var client = server.CreateClient(); +// var initialResponse = await client.GetAsync(""); +// var subsequentResponse = await client.GetAsync(""); + +// await AssertFreshResponseAsync(initialResponse, subsequentResponse); +// } +// } +// } + +// [Fact] +// public async Task ServesCachedContent_IfSubsequentRequestContainsNoStore() +// { +// var builders = TestUtils.CreateBuildersWithResponseCaching(); + +// foreach (var builder in builders) +// { +// using var host = builder.Build(); + +// await host.StartAsync(); + +// using (var server = host.GetTestServer()) +// { +// var client = server.CreateClient(); +// var initialResponse = await client.GetAsync(""); +// client.DefaultRequestHeaders.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue() +// { +// NoStore = true +// }; +// var subsequentResponse = await client.GetAsync(""); + +// await AssertCachedResponseAsync(initialResponse, subsequentResponse); +// } +// } +// } + +// [Fact] +// public async Task ServesFreshContent_IfInitialRequestContainsNoStore() +// { +// var builders = TestUtils.CreateBuildersWithResponseCaching(); + +// foreach (var builder in builders) +// { +// using var host = builder.Build(); + +// await host.StartAsync(); + +// using (var server = host.GetTestServer()) +// { +// var client = server.CreateClient(); +// client.DefaultRequestHeaders.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue() +// { +// NoStore = true +// }; +// var initialResponse = await client.GetAsync(""); +// var subsequentResponse = await client.GetAsync(""); + +// await AssertFreshResponseAsync(initialResponse, subsequentResponse); +// } +// } +// } + +// [Fact] +// public async Task ServesFreshContent_IfInitialResponseContainsNoStore() +// { +// var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Response.Headers.CacheControl = CacheControlHeaderValue.NoStoreString); + +// foreach (var builder in builders) +// { +// using var host = builder.Build(); + +// await host.StartAsync(); + +// using (var server = host.GetTestServer()) +// { +// var client = server.CreateClient(); +// var initialResponse = await client.GetAsync(""); +// var subsequentResponse = await client.GetAsync(""); + +// await AssertFreshResponseAsync(initialResponse, subsequentResponse); +// } +// } +// } + +// [Fact] +// public async Task Serves304_IfIfModifiedSince_Satisfied() +// { +// var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => +// { +// context.Response.GetTypedHeaders().ETag = new EntityTagHeaderValue("\"E1\""); +// context.Response.Headers.ContentLocation = "/"; +// context.Response.Headers.Vary = HeaderNames.From; +// }); + +// foreach (var builder in builders) +// { +// using var host = builder.Build(); + +// await host.StartAsync(); + +// using (var server = host.GetTestServer()) +// { +// var client = server.CreateClient(); +// var initialResponse = await client.GetAsync("?Expires=90"); +// client.DefaultRequestHeaders.IfModifiedSince = DateTimeOffset.MaxValue; +// var subsequentResponse = await client.GetAsync(""); + +// initialResponse.EnsureSuccessStatusCode(); +// Assert.Equal(System.Net.HttpStatusCode.NotModified, subsequentResponse.StatusCode); +// Assert304Headers(initialResponse, subsequentResponse); +// } +// } +// } + +// [Fact] +// public async Task ServesCachedContent_IfIfModifiedSince_NotSatisfied() +// { +// var builders = TestUtils.CreateBuildersWithResponseCaching(); + +// foreach (var builder in builders) +// { +// using var host = builder.Build(); + +// await host.StartAsync(); + +// using (var server = host.GetTestServer()) +// { +// var client = server.CreateClient(); +// var initialResponse = await client.GetAsync(""); +// client.DefaultRequestHeaders.IfModifiedSince = DateTimeOffset.MinValue; +// var subsequentResponse = await client.GetAsync(""); + +// await AssertCachedResponseAsync(initialResponse, subsequentResponse); +// } +// } +// } + +// [Fact] +// public async Task Serves304_IfIfNoneMatch_Satisfied() +// { +// var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => +// { +// context.Response.GetTypedHeaders().ETag = new EntityTagHeaderValue("\"E1\""); +// context.Response.Headers.ContentLocation = "/"; +// context.Response.Headers.Vary = HeaderNames.From; +// }); + +// foreach (var builder in builders) +// { +// using var host = builder.Build(); + +// await host.StartAsync(); + +// using (var server = host.GetTestServer()) +// { +// var client = server.CreateClient(); +// var initialResponse = await client.GetAsync("?Expires=90"); +// client.DefaultRequestHeaders.IfNoneMatch.Add(new System.Net.Http.Headers.EntityTagHeaderValue("\"E1\"")); +// var subsequentResponse = await client.GetAsync(""); + +// initialResponse.EnsureSuccessStatusCode(); +// Assert.Equal(System.Net.HttpStatusCode.NotModified, subsequentResponse.StatusCode); +// Assert304Headers(initialResponse, subsequentResponse); +// } +// } +// } + +// [Fact] +// public async Task ServesCachedContent_IfIfNoneMatch_NotSatisfied() +// { +// var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Response.GetTypedHeaders().ETag = new EntityTagHeaderValue("\"E1\"")); + +// foreach (var builder in builders) +// { +// using var host = builder.Build(); + +// await host.StartAsync(); + +// using (var server = host.GetTestServer()) +// { +// var client = server.CreateClient(); +// var initialResponse = await client.GetAsync(""); +// client.DefaultRequestHeaders.IfNoneMatch.Add(new System.Net.Http.Headers.EntityTagHeaderValue("\"E2\"")); +// var subsequentResponse = await client.GetAsync(""); + +// await AssertCachedResponseAsync(initialResponse, subsequentResponse); +// } +// } +// } + +// [Fact] +// public async Task ServesCachedContent_IfBodySize_IsCacheable() +// { +// var builders = TestUtils.CreateBuildersWithResponseCaching(options: new ResponseCachingOptions() +// { +// MaximumBodySize = 100 +// }); + +// foreach (var builder in builders) +// { +// using var host = builder.Build(); + +// await host.StartAsync(); + +// using (var server = host.GetTestServer()) +// { +// var client = server.CreateClient(); +// var initialResponse = await client.GetAsync(""); +// var subsequentResponse = await client.GetAsync(""); + +// await AssertCachedResponseAsync(initialResponse, subsequentResponse); +// } +// } +// } + +// [Fact] +// public async Task ServesFreshContent_IfBodySize_IsNotCacheable() +// { +// var builders = TestUtils.CreateBuildersWithResponseCaching(options: new ResponseCachingOptions() +// { +// MaximumBodySize = 1 +// }); + +// foreach (var builder in builders) +// { +// using var host = builder.Build(); + +// await host.StartAsync(); + +// using (var server = host.GetTestServer()) +// { +// var client = server.CreateClient(); +// var initialResponse = await client.GetAsync(""); +// var subsequentResponse = await client.GetAsync("/different"); + +// await AssertFreshResponseAsync(initialResponse, subsequentResponse); +// } +// } +// } + +// [Fact] +// public async Task ServesFreshContent_CaseSensitivePaths_IsNotCacheable() +// { +// var builders = TestUtils.CreateBuildersWithResponseCaching(options: new ResponseCachingOptions() +// { +// UseCaseSensitivePaths = true +// }); + +// foreach (var builder in builders) +// { +// using var host = builder.Build(); + +// await host.StartAsync(); + +// using (var server = host.GetTestServer()) +// { +// var client = server.CreateClient(); +// var initialResponse = await client.GetAsync("/path"); +// var subsequentResponse = await client.GetAsync("/Path"); + +// await AssertFreshResponseAsync(initialResponse, subsequentResponse); +// } +// } +// } + +// [Fact] +// public async Task ServesCachedContent_WithoutReplacingCachedVaryBy_OnCacheMiss() +// { +// var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Response.Headers.Vary = HeaderNames.From); + +// foreach (var builder in builders) +// { +// using var host = builder.Build(); + +// await host.StartAsync(); + +// using (var server = host.GetTestServer()) +// { +// var client = server.CreateClient(); +// client.DefaultRequestHeaders.From = "user@example.com"; +// var initialResponse = await client.GetAsync(""); +// client.DefaultRequestHeaders.From = "user2@example.com"; +// var otherResponse = await client.GetAsync(""); +// client.DefaultRequestHeaders.From = "user@example.com"; +// var subsequentResponse = await client.GetAsync(""); + +// await AssertCachedResponseAsync(initialResponse, subsequentResponse); +// } +// } +// } + +// [Fact] +// public async Task ServesFreshContent_IfCachedVaryByUpdated_OnCacheMiss() +// { +// var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Response.Headers.Vary = context.Request.Headers.Pragma); + +// foreach (var builder in builders) +// { +// using var host = builder.Build(); + +// await host.StartAsync(); + +// using (var server = host.GetTestServer()) +// { +// var client = server.CreateClient(); +// client.DefaultRequestHeaders.From = "user@example.com"; +// client.DefaultRequestHeaders.Pragma.Clear(); +// client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("From")); +// client.DefaultRequestHeaders.MaxForwards = 1; +// var initialResponse = await client.GetAsync(""); +// client.DefaultRequestHeaders.From = "user2@example.com"; +// client.DefaultRequestHeaders.Pragma.Clear(); +// client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("Max-Forwards")); +// client.DefaultRequestHeaders.MaxForwards = 2; +// var otherResponse = await client.GetAsync(""); +// client.DefaultRequestHeaders.From = "user@example.com"; +// client.DefaultRequestHeaders.Pragma.Clear(); +// client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("From")); +// client.DefaultRequestHeaders.MaxForwards = 1; +// var subsequentResponse = await client.GetAsync(""); + +// await AssertFreshResponseAsync(initialResponse, subsequentResponse); +// } +// } +// } + +// [Fact] +// public async Task ServesCachedContent_IfCachedVaryByNotUpdated_OnCacheMiss() +// { +// var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Response.Headers.Vary = context.Request.Headers.Pragma); + +// foreach (var builder in builders) +// { +// using var host = builder.Build(); + +// await host.StartAsync(); + +// using (var server = host.GetTestServer()) +// { +// var client = server.CreateClient(); +// client.DefaultRequestHeaders.From = "user@example.com"; +// client.DefaultRequestHeaders.Pragma.Clear(); +// client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("From")); +// client.DefaultRequestHeaders.MaxForwards = 1; +// var initialResponse = await client.GetAsync(""); +// client.DefaultRequestHeaders.From = "user2@example.com"; +// client.DefaultRequestHeaders.Pragma.Clear(); +// client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("From")); +// client.DefaultRequestHeaders.MaxForwards = 2; +// var otherResponse = await client.GetAsync(""); +// client.DefaultRequestHeaders.From = "user@example.com"; +// client.DefaultRequestHeaders.Pragma.Clear(); +// client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("From")); +// client.DefaultRequestHeaders.MaxForwards = 1; +// var subsequentResponse = await client.GetAsync(""); + +// await AssertCachedResponseAsync(initialResponse, subsequentResponse); +// } +// } +// } + +// [Fact] +// public async Task ServesCachedContent_IfAvailable_UsingHead_WithContentLength() +// { +// var builders = TestUtils.CreateBuildersWithResponseCaching(); + +// foreach (var builder in builders) +// { +// using var host = builder.Build(); + +// await host.StartAsync(); + +// using (var server = host.GetTestServer()) +// { +// var client = server.CreateClient(); +// var initialResponse = await client.SendAsync(TestUtils.CreateRequest("HEAD", "?contentLength=10")); +// var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest("HEAD", "?contentLength=10")); + +// await AssertCachedResponseAsync(initialResponse, subsequentResponse); +// } +// } +// } + +// private static void Assert304Headers(HttpResponseMessage initialResponse, HttpResponseMessage subsequentResponse) +// { +// // https://tools.ietf.org/html/rfc7232#section-4.1 +// // The server generating a 304 response MUST generate any of the +// // following header fields that would have been sent in a 200 (OK) +// // response to the same request: Cache-Control, Content-Location, Date, +// // ETag, Expires, and Vary. + +// Assert.Equal(initialResponse.Headers.CacheControl, subsequentResponse.Headers.CacheControl); +// Assert.Equal(initialResponse.Content.Headers.ContentLocation, subsequentResponse.Content.Headers.ContentLocation); +// Assert.Equal(initialResponse.Headers.Date, subsequentResponse.Headers.Date); +// Assert.Equal(initialResponse.Headers.ETag, subsequentResponse.Headers.ETag); +// Assert.Equal(initialResponse.Content.Headers.Expires, subsequentResponse.Content.Headers.Expires); +// Assert.Equal(initialResponse.Headers.Vary, subsequentResponse.Headers.Vary); +// } + +// private static async Task AssertCachedResponseAsync(HttpResponseMessage initialResponse, HttpResponseMessage subsequentResponse) +// { +// initialResponse.EnsureSuccessStatusCode(); +// subsequentResponse.EnsureSuccessStatusCode(); + +// foreach (var header in initialResponse.Headers) +// { +// Assert.Equal(initialResponse.Headers.GetValues(header.Key), subsequentResponse.Headers.GetValues(header.Key)); +// } +// Assert.True(subsequentResponse.Headers.Contains(HeaderNames.Age)); +// Assert.Equal(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync()); +// } + +// private static async Task AssertFreshResponseAsync(HttpResponseMessage initialResponse, HttpResponseMessage subsequentResponse) +// { +// initialResponse.EnsureSuccessStatusCode(); +// subsequentResponse.EnsureSuccessStatusCode(); + +// Assert.False(subsequentResponse.Headers.Contains(HeaderNames.Age)); + +// if (initialResponse.RequestMessage.Method == HttpMethod.Head && +// subsequentResponse.RequestMessage.Method == HttpMethod.Head) +// { +// Assert.True(initialResponse.Headers.Contains("X-Value")); +// Assert.NotEqual(initialResponse.Headers.GetValues("X-Value"), subsequentResponse.Headers.GetValues("X-Value")); +// } +// else +// { +// Assert.NotEqual(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync()); +// } +// } +//} diff --git a/src/Middleware/OutputCaching/test/SegmentWriteStreamTests.cs b/src/Middleware/OutputCaching/test/SegmentWriteStreamTests.cs new file mode 100644 index 000000000000..2277bf2b5564 --- /dev/null +++ b/src/Middleware/OutputCaching/test/SegmentWriteStreamTests.cs @@ -0,0 +1,105 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +//namespace Microsoft.AspNetCore.ResponseCaching.Tests; + +//public class SegmentWriteStreamTests +//{ +// private static readonly byte[] WriteData = new byte[] +// { +// 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14 +// }; + +// [Theory] +// [InlineData(0)] +// [InlineData(-1)] +// public void SegmentWriteStream_InvalidSegmentSize_Throws(int segmentSize) +// { +// Assert.Throws(() => new SegmentWriteStream(segmentSize)); +// } + +// [Fact] +// public void ReadAndSeekOperations_Throws() +// { +// var stream = new SegmentWriteStream(1); + +// Assert.Throws(() => stream.Read(new byte[1], 0, 0)); +// Assert.Throws(() => stream.Position = 0); +// Assert.Throws(() => stream.Seek(0, SeekOrigin.Begin)); +// } + +// [Fact] +// public void GetSegments_ExtractionDisablesWriting() +// { +// var stream = new SegmentWriteStream(1); + +// Assert.True(stream.CanWrite); +// Assert.Empty(stream.GetSegments()); +// Assert.False(stream.CanWrite); +// } + +// [Theory] +// [InlineData(4)] +// [InlineData(5)] +// [InlineData(6)] +// public void WriteByte_CanWriteAllBytes(int segmentSize) +// { +// var stream = new SegmentWriteStream(segmentSize); + +// foreach (var datum in WriteData) +// { +// stream.WriteByte(datum); +// } +// var segments = stream.GetSegments(); + +// Assert.Equal(WriteData.Length, stream.Length); +// Assert.Equal((WriteData.Length + segmentSize - 1) / segmentSize, segments.Count); + +// for (var i = 0; i < WriteData.Length; i += segmentSize) +// { +// var expectedSegmentSize = Math.Min(segmentSize, WriteData.Length - i); +// var expectedSegment = new byte[expectedSegmentSize]; +// for (int j = 0; j < expectedSegmentSize; j++) +// { +// expectedSegment[j] = (byte)(i + j); +// } +// var segment = segments[i / segmentSize]; + +// Assert.Equal(expectedSegmentSize, segment.Length); +// Assert.True(expectedSegment.SequenceEqual(segment)); +// } +// } + +// [Theory] +// [InlineData(4)] +// [InlineData(5)] +// [InlineData(6)] +// public void Write_CanWriteAllBytes(int writeSize) +// { +// var segmentSize = 5; +// var stream = new SegmentWriteStream(segmentSize); + +// for (var i = 0; i < WriteData.Length; i += writeSize) +// { +// stream.Write(WriteData, i, Math.Min(writeSize, WriteData.Length - i)); +// } +// var segments = stream.GetSegments(); + +// Assert.Equal(WriteData.Length, stream.Length); +// Assert.Equal((WriteData.Length + segmentSize - 1) / segmentSize, segments.Count); + +// for (var i = 0; i < WriteData.Length; i += segmentSize) +// { +// var expectedSegmentSize = Math.Min(segmentSize, WriteData.Length - i); +// var expectedSegment = new byte[expectedSegmentSize]; +// for (int j = 0; j < expectedSegmentSize; j++) +// { +// expectedSegment[j] = (byte)(i + j); +// } +// var segment = segments[i / segmentSize]; + +// Assert.Equal(expectedSegmentSize, segment.Length); +// Assert.True(expectedSegment.SequenceEqual(segment)); +// } +// } +//} diff --git a/src/Middleware/OutputCaching/test/TestDocument.txt b/src/Middleware/OutputCaching/test/TestDocument.txt new file mode 100644 index 000000000000..fb31ae6de8a7 --- /dev/null +++ b/src/Middleware/OutputCaching/test/TestDocument.txt @@ -0,0 +1 @@ +0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ \ No newline at end of file diff --git a/src/Middleware/OutputCaching/test/TestUtils.cs b/src/Middleware/OutputCaching/test/TestUtils.cs new file mode 100644 index 000000000000..27f4c2c7619e --- /dev/null +++ b/src/Middleware/OutputCaching/test/TestUtils.cs @@ -0,0 +1,403 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +//using System.Globalization; +//using System.Net.Http; +//using System.Text; +//using Microsoft.AspNetCore.Builder; +//using Microsoft.AspNetCore.Hosting; +//using Microsoft.AspNetCore.Http; +//using Microsoft.AspNetCore.Http.Features; +//using Microsoft.AspNetCore.TestHost; +//using Microsoft.Extensions.DependencyInjection; +//using Microsoft.Extensions.Hosting; +//using Microsoft.Extensions.Logging; +//using Microsoft.Extensions.Logging.Abstractions; +//using Microsoft.Extensions.Logging.Testing; +//using Microsoft.Extensions.ObjectPool; +//using Microsoft.Extensions.Options; +//using Microsoft.Extensions.Primitives; +//using Microsoft.Net.Http.Headers; + +//namespace Microsoft.AspNetCore.ResponseCaching.Tests; + +//internal class TestUtils +//{ +// static TestUtils() +// { +// // Force sharding in tests +// StreamUtilities.BodySegmentSize = 10; +// } + +// private static bool TestRequestDelegate(HttpContext context, string guid) +// { +// var headers = context.Response.GetTypedHeaders(); + +// var expires = context.Request.Query["Expires"]; +// if (!string.IsNullOrEmpty(expires)) +// { +// headers.Expires = DateTimeOffset.Now.AddSeconds(int.Parse(expires, CultureInfo.InvariantCulture)); +// } + +// if (headers.CacheControl == null) +// { +// headers.CacheControl = new CacheControlHeaderValue +// { +// Public = true, +// MaxAge = string.IsNullOrEmpty(expires) ? TimeSpan.FromSeconds(10) : (TimeSpan?)null +// }; +// } +// else +// { +// headers.CacheControl.Public = true; +// headers.CacheControl.MaxAge = string.IsNullOrEmpty(expires) ? TimeSpan.FromSeconds(10) : (TimeSpan?)null; +// } +// headers.Date = DateTimeOffset.UtcNow; +// headers.Headers["X-Value"] = guid; + +// var contentLength = context.Request.Query["ContentLength"]; +// if (!string.IsNullOrEmpty(contentLength)) +// { +// headers.ContentLength = long.Parse(contentLength, CultureInfo.InvariantCulture); +// } + +// if (context.Request.Method != "HEAD") +// { +// return true; +// } +// return false; +// } + +// internal static async Task TestRequestDelegateWriteAsync(HttpContext context) +// { +// var uniqueId = Guid.NewGuid().ToString(); +// if (TestRequestDelegate(context, uniqueId)) +// { +// await context.Response.WriteAsync(uniqueId); +// } +// } + +// internal static async Task TestRequestDelegateSendFileAsync(HttpContext context) +// { +// var path = Path.Combine(AppContext.BaseDirectory, "TestDocument.txt"); +// var uniqueId = Guid.NewGuid().ToString(); +// if (TestRequestDelegate(context, uniqueId)) +// { +// await context.Response.SendFileAsync(path, 0, null); +// await context.Response.WriteAsync(uniqueId); +// } +// } + +// internal static Task TestRequestDelegateWrite(HttpContext context) +// { +// var uniqueId = Guid.NewGuid().ToString(); +// if (TestRequestDelegate(context, uniqueId)) +// { +// var feature = context.Features.Get(); +// if (feature != null) +// { +// feature.AllowSynchronousIO = true; +// } +// context.Response.Write(uniqueId); +// } +// return Task.CompletedTask; +// } + +// internal static IResponseCachingKeyProvider CreateTestKeyProvider() +// { +// return CreateTestKeyProvider(new ResponseCachingOptions()); +// } + +// internal static IResponseCachingKeyProvider CreateTestKeyProvider(ResponseCachingOptions options) +// { +// return new ResponseCachingKeyProvider(new DefaultObjectPoolProvider(), Options.Create(options)); +// } + +// internal static IEnumerable CreateBuildersWithResponseCaching( +// Action configureDelegate = null, +// ResponseCachingOptions options = null, +// Action contextAction = null) +// { +// return CreateBuildersWithResponseCaching(configureDelegate, options, new RequestDelegate[] +// { +// context => +// { +// contextAction?.Invoke(context); +// return TestRequestDelegateWrite(context); +// }, +// context => +// { +// contextAction?.Invoke(context); +// return TestRequestDelegateWriteAsync(context); +// }, +// context => +// { +// contextAction?.Invoke(context); +// return TestRequestDelegateSendFileAsync(context); +// }, +// }); +// } + +// private static IEnumerable CreateBuildersWithResponseCaching( +// Action configureDelegate = null, +// ResponseCachingOptions options = null, +// IEnumerable requestDelegates = null) +// { +// if (configureDelegate == null) +// { +// configureDelegate = app => { }; +// } +// if (requestDelegates == null) +// { +// requestDelegates = new RequestDelegate[] +// { +// TestRequestDelegateWriteAsync, +// TestRequestDelegateWrite +// }; +// } + +// foreach (var requestDelegate in requestDelegates) +// { +// // Test with in memory ResponseCache +// yield return new HostBuilder() +// .ConfigureWebHost(webHostBuilder => +// { +// webHostBuilder +// .UseTestServer() +// .ConfigureServices(services => +// { +// services.AddResponseCaching(responseCachingOptions => +// { +// if (options != null) +// { +// responseCachingOptions.MaximumBodySize = options.MaximumBodySize; +// responseCachingOptions.UseCaseSensitivePaths = options.UseCaseSensitivePaths; +// responseCachingOptions.SystemClock = options.SystemClock; +// } +// }); +// }) +// .Configure(app => +// { +// configureDelegate(app); +// app.UseResponseCaching(); +// app.Run(requestDelegate); +// }); +// }); +// } +// } + +// internal static ResponseCachingMiddleware CreateTestMiddleware( +// RequestDelegate next = null, +// IResponseCache cache = null, +// ResponseCachingOptions options = null, +// TestSink testSink = null, +// IResponseCachingKeyProvider keyProvider = null, +// IResponseCachingPolicyProvider policyProvider = null) +// { +// if (next == null) +// { +// next = httpContext => Task.CompletedTask; +// } +// if (cache == null) +// { +// cache = new TestResponseCache(); +// } +// if (options == null) +// { +// options = new ResponseCachingOptions(); +// } +// if (keyProvider == null) +// { +// keyProvider = new ResponseCachingKeyProvider(new DefaultObjectPoolProvider(), Options.Create(options)); +// } +// if (policyProvider == null) +// { +// policyProvider = new TestResponseCachingPolicyProvider(); +// } + +// return new ResponseCachingMiddleware( +// next, +// Options.Create(options), +// testSink == null ? (ILoggerFactory)NullLoggerFactory.Instance : new TestLoggerFactory(testSink, true), +// policyProvider, +// cache, +// keyProvider); +// } + +// internal static ResponseCachingContext CreateTestContext() +// { +// return new ResponseCachingContext(new DefaultHttpContext(), NullLogger.Instance) +// { +// ResponseTime = DateTimeOffset.UtcNow +// }; +// } + +// internal static ResponseCachingContext CreateTestContext(ITestSink testSink) +// { +// return new ResponseCachingContext(new DefaultHttpContext(), new TestLogger("ResponseCachingTests", testSink, true)) +// { +// ResponseTime = DateTimeOffset.UtcNow +// }; +// } + +// internal static void AssertLoggedMessages(IEnumerable messages, params LoggedMessage[] expectedMessages) +// { +// var messageList = messages.ToList(); +// Assert.Equal(messageList.Count, expectedMessages.Length); + +// for (var i = 0; i < messageList.Count; i++) +// { +// Assert.Equal(expectedMessages[i].EventId, messageList[i].EventId); +// Assert.Equal(expectedMessages[i].LogLevel, messageList[i].LogLevel); +// } +// } + +// public static HttpRequestMessage CreateRequest(string method, string requestUri) +// { +// return new HttpRequestMessage(new HttpMethod(method), requestUri); +// } +//} + +//internal static class HttpResponseWritingExtensions +//{ +// internal static void Write(this HttpResponse response, string text) +// { +// if (response == null) +// { +// throw new ArgumentNullException(nameof(response)); +// } + +// if (text == null) +// { +// throw new ArgumentNullException(nameof(text)); +// } + +// byte[] data = Encoding.UTF8.GetBytes(text); +// response.Body.Write(data, 0, data.Length); +// } +//} + +//internal class LoggedMessage +//{ +// internal static LoggedMessage RequestMethodNotCacheable => new LoggedMessage(1, LogLevel.Debug); +// internal static LoggedMessage RequestWithAuthorizationNotCacheable => new LoggedMessage(2, LogLevel.Debug); +// internal static LoggedMessage RequestWithNoCacheNotCacheable => new LoggedMessage(3, LogLevel.Debug); +// internal static LoggedMessage RequestWithPragmaNoCacheNotCacheable => new LoggedMessage(4, LogLevel.Debug); +// internal static LoggedMessage ExpirationMinFreshAdded => new LoggedMessage(5, LogLevel.Debug); +// internal static LoggedMessage ExpirationSharedMaxAgeExceeded => new LoggedMessage(6, LogLevel.Debug); +// internal static LoggedMessage ExpirationMustRevalidate => new LoggedMessage(7, LogLevel.Debug); +// internal static LoggedMessage ExpirationMaxStaleSatisfied => new LoggedMessage(8, LogLevel.Debug); +// internal static LoggedMessage ExpirationMaxAgeExceeded => new LoggedMessage(9, LogLevel.Debug); +// internal static LoggedMessage ExpirationExpiresExceeded => new LoggedMessage(10, LogLevel.Debug); +// internal static LoggedMessage ResponseWithoutPublicNotCacheable => new LoggedMessage(11, LogLevel.Debug); +// internal static LoggedMessage ResponseWithNoStoreNotCacheable => new LoggedMessage(12, LogLevel.Debug); +// internal static LoggedMessage ResponseWithNoCacheNotCacheable => new LoggedMessage(13, LogLevel.Debug); +// internal static LoggedMessage ResponseWithSetCookieNotCacheable => new LoggedMessage(14, LogLevel.Debug); +// internal static LoggedMessage ResponseWithVaryStarNotCacheable => new LoggedMessage(15, LogLevel.Debug); +// internal static LoggedMessage ResponseWithPrivateNotCacheable => new LoggedMessage(16, LogLevel.Debug); +// internal static LoggedMessage ResponseWithUnsuccessfulStatusCodeNotCacheable => new LoggedMessage(17, LogLevel.Debug); +// internal static LoggedMessage NotModifiedIfNoneMatchStar => new LoggedMessage(18, LogLevel.Debug); +// internal static LoggedMessage NotModifiedIfNoneMatchMatched => new LoggedMessage(19, LogLevel.Debug); +// internal static LoggedMessage NotModifiedIfModifiedSinceSatisfied => new LoggedMessage(20, LogLevel.Debug); +// internal static LoggedMessage NotModifiedServed => new LoggedMessage(21, LogLevel.Information); +// internal static LoggedMessage CachedResponseServed => new LoggedMessage(22, LogLevel.Information); +// internal static LoggedMessage GatewayTimeoutServed => new LoggedMessage(23, LogLevel.Information); +// internal static LoggedMessage NoResponseServed => new LoggedMessage(24, LogLevel.Information); +// internal static LoggedMessage VaryByRulesUpdated => new LoggedMessage(25, LogLevel.Debug); +// internal static LoggedMessage ResponseCached => new LoggedMessage(26, LogLevel.Information); +// internal static LoggedMessage ResponseNotCached => new LoggedMessage(27, LogLevel.Information); +// internal static LoggedMessage ResponseContentLengthMismatchNotCached => new LoggedMessage(28, LogLevel.Warning); +// internal static LoggedMessage ExpirationInfiniteMaxStaleSatisfied => new LoggedMessage(29, LogLevel.Debug); + +// private LoggedMessage(int evenId, LogLevel logLevel) +// { +// EventId = evenId; +// LogLevel = logLevel; +// } + +// internal int EventId { get; } +// internal LogLevel LogLevel { get; } +//} + +//internal class TestResponseCachingPolicyProvider : IResponseCachingPolicyProvider +//{ +// public bool AllowCacheLookupValue { get; set; } = false; +// public bool AllowCacheStorageValue { get; set; } = false; +// public bool AttemptResponseCachingValue { get; set; } = false; +// public bool IsCachedEntryFreshValue { get; set; } = true; +// public bool IsResponseCacheableValue { get; set; } = true; + +// public bool AllowCacheLookup(ResponseCachingContext context) => AllowCacheLookupValue; + +// public bool AllowCacheStorage(ResponseCachingContext context) => AllowCacheStorageValue; + +// public bool AttemptResponseCaching(ResponseCachingContext context) => AttemptResponseCachingValue; + +// public bool IsCachedEntryFresh(ResponseCachingContext context) => IsCachedEntryFreshValue; + +// public bool IsResponseCacheable(ResponseCachingContext context) => IsResponseCacheableValue; +//} + +//internal class TestResponseCachingKeyProvider : IResponseCachingKeyProvider +//{ +// private readonly string _baseKey; +// private readonly StringValues _varyKey; + +// public TestResponseCachingKeyProvider(string lookupBaseKey = null, StringValues? lookupVaryKey = null) +// { +// _baseKey = lookupBaseKey; +// if (lookupVaryKey.HasValue) +// { +// _varyKey = lookupVaryKey.Value; +// } +// } + +// public IEnumerable CreateLookupVaryByKeys(ResponseCachingContext context) +// { +// foreach (var varyKey in _varyKey) +// { +// yield return _baseKey + varyKey; +// } +// } + +// public string CreateBaseKey(ResponseCachingContext context) +// { +// return _baseKey; +// } + +// public string CreateStorageVaryByKey(ResponseCachingContext context) +// { +// throw new NotImplementedException(); +// } +//} + +//internal class TestResponseCache : IResponseCache +//{ +// private readonly IDictionary _storage = new Dictionary(); +// public int GetCount { get; private set; } +// public int SetCount { get; private set; } + +// public IResponseCacheEntry Get(string key) +// { +// GetCount++; +// try +// { +// return _storage[key]; +// } +// catch +// { +// return null; +// } +// } + +// public void Set(string key, IResponseCacheEntry entry, TimeSpan validFor) +// { +// SetCount++; +// _storage[key] = entry; +// } +//} + +//internal class TestClock : ISystemClock +//{ +// public DateTimeOffset UtcNow { get; set; } +//} diff --git a/src/Mvc/Mvc.Core/src/Filters/IOutputCacheFilter.cs b/src/Mvc/Mvc.Core/src/Filters/IOutputCacheFilter.cs new file mode 100644 index 000000000000..d852233e4862 --- /dev/null +++ b/src/Mvc/Mvc.Core/src/Filters/IOutputCacheFilter.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Mvc.Filters; + +/// +/// A filter which sets the appropriate headers related to Output caching. +/// +internal interface IOutputCacheFilter : IFilterMetadata +{ +} diff --git a/src/Mvc/Mvc.Core/src/Filters/OutputCacheFilter.cs b/src/Mvc/Mvc.Core/src/Filters/OutputCacheFilter.cs new file mode 100644 index 000000000000..39e95f553511 --- /dev/null +++ b/src/Mvc/Mvc.Core/src/Filters/OutputCacheFilter.cs @@ -0,0 +1,59 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Mvc.Core; +using Microsoft.AspNetCore.OutputCaching; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNetCore.Mvc.Filters; + +/// +/// An which sets the appropriate headers related to output caching. +/// +internal partial class OutputCacheFilter : IActionFilter +{ + private readonly ILogger _logger; + + /// + /// Creates a new instance of + /// + /// The . + public OutputCacheFilter(ILoggerFactory loggerFactory) + { + _logger = loggerFactory.CreateLogger(GetType()); + } + + public void OnActionExecuting(ActionExecutingContext context) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + // If there are more filters which can override the values written by this filter, + // then skip execution of this filter. + var effectivePolicy = context.FindEffectivePolicy(); + if (effectivePolicy != null && effectivePolicy != this) + { + Log.NotMostEffectiveFilter(_logger, GetType(), effectivePolicy.GetType(), typeof(IOutputCacheFilter)); + return; + } + + var outputCachingFeature = context.HttpContext.Features.Get(); + if (outputCachingFeature == null) + { + throw new InvalidOperationException( + Resources.FormatOutputCacheAttribute_Requires_OutputCachingMiddleware(nameof(OutputCacheAttribute))); + } + } + + public void OnActionExecuted(ActionExecutedContext context) + { + } + + private static partial class Log + { + [LoggerMessage(4, LogLevel.Debug, "Execution of filter {OverriddenFilter} is preempted by filter {OverridingFilter} which is the most effective filter implementing policy {FilterPolicy}.", EventName = "NotMostEffectiveFilter")] + public static partial void NotMostEffectiveFilter(ILogger logger, Type overriddenFilter, Type overridingFilter, Type filterPolicy); + } +} diff --git a/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj b/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj index 9d9b2ac58e91..d437875d6aa5 100644 --- a/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj +++ b/src/Mvc/Mvc.Core/src/Microsoft.AspNetCore.Mvc.Core.csproj @@ -1,4 +1,4 @@ - + ASP.NET Core MVC core components. Contains common action result types, attribute routing, application model conventions, API explorer, application parts, filters, formatters, model binding, and more. @@ -48,6 +48,7 @@ Microsoft.AspNetCore.Mvc.RouteAttribute + diff --git a/src/Mvc/Mvc.Core/src/OutputCacheAttribute.cs b/src/Mvc/Mvc.Core/src/OutputCacheAttribute.cs new file mode 100644 index 000000000000..44245e3ff0eb --- /dev/null +++ b/src/Mvc/Mvc.Core/src/OutputCacheAttribute.cs @@ -0,0 +1,88 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.AspNetCore.OutputCaching; + +namespace Microsoft.AspNetCore.Mvc; + +/// +/// Specifies the parameters necessary for setting appropriate headers in output caching. +/// +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] +public class OutputCacheAttribute : Attribute, IOrderedFilter, IPoliciesMetadata +{ + // A nullable-int cannot be used as an Attribute parameter. + // Hence this nullable-int is present to back the Duration property. + // The same goes for nullable-ResponseCacheLocation and nullable-bool. + private int? _duration; + private bool? _noStore; + + private List? _policies; + + /// + /// Gets or sets the duration in seconds for which the response is cached. + /// + public int Duration + { + get => _duration ?? 0; + set => _duration = value; + } + + /// + /// Gets or sets the value which determines whether the data should be stored or not. + /// When set to , the response won't be cached. + /// + public bool NoStore + { + get => _noStore ?? false; + set => _noStore = value; + } + + /// + /// Gets or sets the query keys to vary by. + /// + /// + /// requires the output cache middleware. + /// + public string[]? VaryByQueryKeys { get; set; } + + /// + /// Gets or sets the value of the cache profile name. + /// + public string? Profile { get; set; } + + /// + public int Order { get; set; } + + public List Policies => _policies ??= GetPolicies(); + + private List GetPolicies() + { + var policies = new List(5); + + policies.Add(ConfigureCachingPolicy.Instance); + + if (_noStore != null && _noStore.Value) + { + policies.Add(new NoCachingPolicy()); + } + + if (Profile != null) + { + policies.Add(new ProfilePolicy(Profile)); + } + + if (VaryByQueryKeys != null) + { + policies.Add(new VaryByQueryPolicy(VaryByQueryKeys)); + } + + if (_duration != null) + { + policies.Add(new ExpirationPolicy(TimeSpan.FromSeconds(_duration.Value))); + } + + return policies; + } +} diff --git a/src/Mvc/Mvc.Core/src/Resources.resx b/src/Mvc/Mvc.Core/src/Resources.resx index 206a2f420b5c..3427c5ab3f1f 100644 --- a/src/Mvc/Mvc.Core/src/Resources.resx +++ b/src/Mvc/Mvc.Core/src/Resources.resx @@ -510,7 +510,10 @@ Could not parse '{0}'. Content types with wildcards are not supported. + + '{0}' requires the output cache middleware. + The type '{0}' does not contain a TryParse method and the binder '{1}' cannot be used. - \ No newline at end of file + diff --git a/src/Mvc/Mvc.slnf b/src/Mvc/Mvc.slnf index 1dee0bc428eb..8a04dc070284 100644 --- a/src/Mvc/Mvc.slnf +++ b/src/Mvc/Mvc.slnf @@ -46,6 +46,8 @@ "src\\Middleware\\HttpOverrides\\src\\Microsoft.AspNetCore.HttpOverrides.csproj", "src\\Middleware\\Localization.Routing\\src\\Microsoft.AspNetCore.Localization.Routing.csproj", "src\\Middleware\\Localization\\src\\Microsoft.AspNetCore.Localization.csproj", + "src\\Middleware\\OutputCaching.Abstractions\\src\\Microsoft.AspNetCore.OutputCaching.Abstractions.csproj", + "src\\Middleware\\OutputCaching\\src\\Microsoft.AspNetCore.OutputCaching.csproj", "src\\Middleware\\ResponseCaching.Abstractions\\src\\Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj", "src\\Middleware\\ResponseCaching\\src\\Microsoft.AspNetCore.ResponseCaching.csproj", "src\\Middleware\\Session\\src\\Microsoft.AspNetCore.Session.csproj", diff --git a/src/Mvc/samples/MvcSandbox/Controllers/HomeController.cs b/src/Mvc/samples/MvcSandbox/Controllers/HomeController.cs index f5d5c9df5662..96a6a3a64ded 100644 --- a/src/Mvc/samples/MvcSandbox/Controllers/HomeController.cs +++ b/src/Mvc/samples/MvcSandbox/Controllers/HomeController.cs @@ -10,6 +10,7 @@ public class HomeController : Controller [ModelBinder] public string Id { get; set; } + [OutputCache(Duration = 10, VaryByQueryKeys = new [] { "culture" })] public IActionResult Index() { return View(); diff --git a/src/Mvc/samples/MvcSandbox/MvcSandbox.csproj b/src/Mvc/samples/MvcSandbox/MvcSandbox.csproj index b6774817765a..aecdc3456b06 100644 --- a/src/Mvc/samples/MvcSandbox/MvcSandbox.csproj +++ b/src/Mvc/samples/MvcSandbox/MvcSandbox.csproj @@ -1,4 +1,4 @@ - + $(DefaultNetCoreTargetFramework) true @@ -11,6 +11,7 @@ + diff --git a/src/Mvc/samples/MvcSandbox/Pages/PagesHome.cshtml b/src/Mvc/samples/MvcSandbox/Pages/PagesHome.cshtml index d4d5241a6072..7d28b1821905 100644 --- a/src/Mvc/samples/MvcSandbox/Pages/PagesHome.cshtml +++ b/src/Mvc/samples/MvcSandbox/Pages/PagesHome.cshtml @@ -14,4 +14,5 @@

Pages Home

This sandbox should give you a quick view of a basic MVC application.

-
\ No newline at end of file + @DateTime.UtcNow.ToString("o") + diff --git a/src/Mvc/samples/MvcSandbox/Views/Home/Index.cshtml b/src/Mvc/samples/MvcSandbox/Views/Home/Index.cshtml index b1c5cc559876..8572a64234e1 100644 --- a/src/Mvc/samples/MvcSandbox/Views/Home/Index.cshtml +++ b/src/Mvc/samples/MvcSandbox/Views/Home/Index.cshtml @@ -5,4 +5,5 @@

Sandbox

This sandbox should give you a quick view of a basic MVC application.

+ @DateTime.UtcNow.ToString("o")
diff --git a/src/Shared/TaskToApm.cs b/src/Shared/TaskToApm.cs index 0647dc4a00ed..cec6fa53df53 100644 --- a/src/Shared/TaskToApm.cs +++ b/src/Shared/TaskToApm.cs @@ -42,7 +42,7 @@ public static void End(IAsyncResult asyncResult) return; } - throw new ArgumentNullException(nameof(asyncResult)); + ArgumentNullException.ThrowIfNull(asyncResult, nameof(asyncResult)); } /// Processes an IAsyncResult returned by Begin. From 2b2685e6cfd34fd6dca67be83926026752e9878b Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Tue, 5 Apr 2022 14:32:35 -0700 Subject: [PATCH 02/48] Fix MVC attribute --- src/Mvc/Mvc.Core/src/OutputCacheAttribute.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Mvc/Mvc.Core/src/OutputCacheAttribute.cs b/src/Mvc/Mvc.Core/src/OutputCacheAttribute.cs index 44245e3ff0eb..7aeb739bbebb 100644 --- a/src/Mvc/Mvc.Core/src/OutputCacheAttribute.cs +++ b/src/Mvc/Mvc.Core/src/OutputCacheAttribute.cs @@ -61,11 +61,11 @@ private List GetPolicies() { var policies = new List(5); - policies.Add(ConfigureCachingPolicy.Instance); + policies.Add(EnableCachingPolicy.Instance); if (_noStore != null && _noStore.Value) { - policies.Add(new NoCachingPolicy()); + policies.Add(new NoStorePolicy()); } if (Profile != null) From 6380e9c1e4a823279d5492daf9cf81507a658ff4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Ros?= Date: Wed, 6 Apr 2022 07:39:36 -0700 Subject: [PATCH 03/48] Update src/Middleware/OutputCaching/src/Policies/EnableCachingPolicy.cs Co-authored-by: Kahbazi --- .../OutputCaching/src/Policies/EnableCachingPolicy.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Middleware/OutputCaching/src/Policies/EnableCachingPolicy.cs b/src/Middleware/OutputCaching/src/Policies/EnableCachingPolicy.cs index 6ddf21d11550..0c6b8bac0abc 100644 --- a/src/Middleware/OutputCaching/src/Policies/EnableCachingPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/EnableCachingPolicy.cs @@ -6,7 +6,7 @@ namespace Microsoft.AspNetCore.OutputCaching; /// /// A policy that enables caching /// -public class EnableCachingPolicy : IOutputCachingPolicy +public sealed class EnableCachingPolicy : IOutputCachingPolicy { public static EnableCachingPolicy Instance = new EnableCachingPolicy(); From 1790d1722a4ae8bb01f9efbe020e5f80c7d60c12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Ros?= Date: Wed, 6 Apr 2022 07:40:04 -0700 Subject: [PATCH 04/48] Update src/Middleware/OutputCaching/src/DispatcherExtensions.cs Co-authored-by: Kahbazi --- src/Middleware/OutputCaching/src/DispatcherExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Middleware/OutputCaching/src/DispatcherExtensions.cs b/src/Middleware/OutputCaching/src/DispatcherExtensions.cs index ba2bb433d1da..ad72d388dfcd 100644 --- a/src/Middleware/OutputCaching/src/DispatcherExtensions.cs +++ b/src/Middleware/OutputCaching/src/DispatcherExtensions.cs @@ -5,7 +5,7 @@ namespace Microsoft.AspNetCore.OutputCaching; -internal class WorkDispatcher where TKey : notnull +internal sealed class WorkDispatcher where TKey : notnull { private readonly ConcurrentDictionary> _workers = new(); From 7629b384ed399640c2499d933b527b968b50e02d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Ros?= Date: Wed, 6 Apr 2022 07:40:38 -0700 Subject: [PATCH 05/48] Update src/Middleware/OutputCaching/src/Policies/PredicatePolicy.cs Co-authored-by: Kahbazi --- src/Middleware/OutputCaching/src/Policies/PredicatePolicy.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Middleware/OutputCaching/src/Policies/PredicatePolicy.cs b/src/Middleware/OutputCaching/src/Policies/PredicatePolicy.cs index 6594ab1829a0..3ec734d28d6c 100644 --- a/src/Middleware/OutputCaching/src/Policies/PredicatePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/PredicatePolicy.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. namespace Microsoft.AspNetCore.OutputCaching.Policies; + public class PredicatePolicy : IOutputCachingPolicy { // TODO: Accept a non async predicate too? From 6732dae6c1c14d33a547e8e606b45fa4b3d2b083 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Ros?= Date: Wed, 6 Apr 2022 07:40:52 -0700 Subject: [PATCH 06/48] Update src/Middleware/OutputCaching/src/Policies/ExpirationPolicy.cs Co-authored-by: Kahbazi --- src/Middleware/OutputCaching/src/Policies/ExpirationPolicy.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Middleware/OutputCaching/src/Policies/ExpirationPolicy.cs b/src/Middleware/OutputCaching/src/Policies/ExpirationPolicy.cs index 425a2a113abd..324fe4eb85ca 100644 --- a/src/Middleware/OutputCaching/src/Policies/ExpirationPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/ExpirationPolicy.cs @@ -6,7 +6,7 @@ namespace Microsoft.AspNetCore.OutputCaching; /// /// A policy that defines a custom expiration timespan. /// -public class ExpirationPolicy : IOutputCachingPolicy +public sealed class ExpirationPolicy : IOutputCachingPolicy { private readonly TimeSpan _expiration; From 92e9da46805e53f0541ce673f34546e72fe788cc Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Wed, 6 Apr 2022 08:13:39 -0700 Subject: [PATCH 07/48] Fix mention of response cache --- src/Middleware/OutputCaching.Abstractions/src/IOutputCache.cs | 2 +- src/Middleware/OutputCaching/src/OutputCachingOptions.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Middleware/OutputCaching.Abstractions/src/IOutputCache.cs b/src/Middleware/OutputCaching.Abstractions/src/IOutputCache.cs index 8556cacf6e63..d4fe368081f8 100644 --- a/src/Middleware/OutputCaching.Abstractions/src/IOutputCache.cs +++ b/src/Middleware/OutputCaching.Abstractions/src/IOutputCache.cs @@ -6,7 +6,7 @@ namespace Microsoft.AspNetCore.OutputCaching; public interface IOutputCacheStore { /// - /// Evicts cache entries by tag. + /// Evicts cached responses by tag. /// /// The tag to evict. ValueTask EvictByTagAsync(string tag); diff --git a/src/Middleware/OutputCaching/src/OutputCachingOptions.cs b/src/Middleware/OutputCaching/src/OutputCachingOptions.cs index e26688d54ee4..d72192e4745f 100644 --- a/src/Middleware/OutputCaching/src/OutputCachingOptions.cs +++ b/src/Middleware/OutputCaching/src/OutputCachingOptions.cs @@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.OutputCaching; public class OutputCachingOptions { /// - /// The size limit for the response cache middleware in bytes. The default is set to 100 MB. + /// The size limit for the output cache middleware in bytes. The default is set to 100 MB. /// When this limit is exceeded, no new responses will be cached until older entries are /// evicted. /// From 85c5b0e40f30a4af21d4cb27ec4e4e3bdf3ae152 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Ros?= Date: Wed, 6 Apr 2022 08:15:10 -0700 Subject: [PATCH 08/48] Update src/Middleware/OutputCaching/src/OutputCachingPolicyProvider.cs Co-authored-by: Kahbazi --- .../OutputCaching/src/OutputCachingPolicyProvider.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Middleware/OutputCaching/src/OutputCachingPolicyProvider.cs b/src/Middleware/OutputCaching/src/OutputCachingPolicyProvider.cs index 108486296549..08a65c06c27c 100644 --- a/src/Middleware/OutputCaching/src/OutputCachingPolicyProvider.cs +++ b/src/Middleware/OutputCaching/src/OutputCachingPolicyProvider.cs @@ -8,11 +8,11 @@ namespace Microsoft.AspNetCore.OutputCaching; internal sealed class OutputCachingPolicyProvider : IOutputCachingPolicyProvider { - private readonly OutputCachingOptions options; + private readonly OutputCachingOptions _options; public OutputCachingPolicyProvider(IOptions options) { - this.options = options.Value; + _options = options.Value; } public async Task OnRequestAsync(IOutputCachingContext context) From d79df7ed46025031ed86935eed2b5f4a0d696084 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Wed, 6 Apr 2022 08:27:52 -0700 Subject: [PATCH 09/48] Use IReadOnlyList in IPoliciesMetadata --- .../src/IPoliciesMetadata.cs | 2 +- .../src/OutputCacheEntry.cs | 1 + .../OutputCaching/src/OutputCacheAttribute.cs | 3 ++- .../OutputCaching/src/Policies/PoliciesMetadata.cs | 6 +++++- .../OutputCaching/src/Policies/PolicyExtensions.cs | 13 ++++++++----- 5 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/Middleware/OutputCaching.Abstractions/src/IPoliciesMetadata.cs b/src/Middleware/OutputCaching.Abstractions/src/IPoliciesMetadata.cs index 5ca892772aae..1df26f053f3e 100644 --- a/src/Middleware/OutputCaching.Abstractions/src/IPoliciesMetadata.cs +++ b/src/Middleware/OutputCaching.Abstractions/src/IPoliciesMetadata.cs @@ -5,5 +5,5 @@ namespace Microsoft.AspNetCore.OutputCaching; public interface IPoliciesMetadata { - List Policies { get; } + IReadOnlyList Policies { get; } } diff --git a/src/Middleware/OutputCaching.Abstractions/src/OutputCacheEntry.cs b/src/Middleware/OutputCaching.Abstractions/src/OutputCacheEntry.cs index 7cc3f64d559c..fcc832f3e196 100644 --- a/src/Middleware/OutputCaching.Abstractions/src/OutputCacheEntry.cs +++ b/src/Middleware/OutputCaching.Abstractions/src/OutputCacheEntry.cs @@ -14,5 +14,6 @@ public class OutputCacheEntry public IHeaderDictionary Headers { get; set; } = default!; public CachedResponseBody Body { get; set; } = default!; + public string[]? Tags { get; set; } } diff --git a/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs b/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs index 5b71f7d1b3d5..be83e05c58a6 100644 --- a/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs @@ -49,7 +49,8 @@ public bool NoStore /// public string? Profile { get; set; } - public List Policies => _policies ??= GetPolicies(); + /// + public IReadOnlyList Policies => _policies ??= GetPolicies(); private List GetPolicies() { diff --git a/src/Middleware/OutputCaching/src/Policies/PoliciesMetadata.cs b/src/Middleware/OutputCaching/src/Policies/PoliciesMetadata.cs index f38d84795da8..047dacc8a25c 100644 --- a/src/Middleware/OutputCaching/src/Policies/PoliciesMetadata.cs +++ b/src/Middleware/OutputCaching/src/Policies/PoliciesMetadata.cs @@ -5,5 +5,9 @@ namespace Microsoft.AspNetCore.OutputCaching.Policies; internal sealed class PoliciesMetadata : IPoliciesMetadata { - public List Policies { get; } = new(); + private readonly List _policies = new(); + + public IReadOnlyList Policies => _policies; + + public void Add(IOutputCachingPolicy policy) => _policies.Add(policy); } diff --git a/src/Middleware/OutputCaching/src/Policies/PolicyExtensions.cs b/src/Middleware/OutputCaching/src/Policies/PolicyExtensions.cs index 8bfc3cbe8994..e5b6618a967a 100644 --- a/src/Middleware/OutputCaching/src/Policies/PolicyExtensions.cs +++ b/src/Middleware/OutputCaching/src/Policies/PolicyExtensions.cs @@ -13,7 +13,7 @@ public static TBuilder OutputCache(this TBuilder builder) where TBuild var policiesMetadata = new PoliciesMetadata(); // Enable caching if this method is invoked on an endpoint, extra policies can disable it - policiesMetadata.Policies.Add(EnableCachingPolicy.Instance); + policiesMetadata.Add(EnableCachingPolicy.Instance); builder.Add(endpointBuilder => { @@ -30,9 +30,12 @@ public static TBuilder OutputCache(this TBuilder builder, params IOutp var policiesMetadata = new PoliciesMetadata(); // Enable caching if this method is invoked on an endpoint, extra policies can disable it - policiesMetadata.Policies.Add(EnableCachingPolicy.Instance); + policiesMetadata.Add(EnableCachingPolicy.Instance); - policiesMetadata.Policies.AddRange(items); + foreach (var item in items) + { + policiesMetadata.Add(item); + } builder.Add(endpointBuilder => { @@ -51,9 +54,9 @@ public static TBuilder OutputCache(this TBuilder builder, Action { From 7a4f0beaff89272967ef3438bcea30e608f0febd Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Wed, 6 Apr 2022 10:09:43 -0700 Subject: [PATCH 10/48] Add XML documentation --- .../src/CachedResponseBody.cs | 20 +++ .../src/CachedVaryByRules.cs | 16 ++- .../src/IOutputCache.cs | 3 + .../src/IOutputCachingContext.cs | 47 +++++++ .../src/IOutputCachingFeature.cs | 6 + .../src/IOutputCachingPolicy.cs | 16 +++ .../src/IOutputCachingPolicyProvider.cs | 6 +- .../src/IPoliciesMetadata.cs | 6 + ...pNetCore.OutputCaching.Abstractions.csproj | 1 - .../src/OutputCacheEntry.cs | 18 +++ .../src/PublicAPI.Unshipped.txt | 71 ++++++++++ .../src/Memory/MemoryOutputCache.cs | 24 ++-- .../Microsoft.AspNetCore.OutputCaching.csproj | 1 - .../OutputCaching/src/OutputCachingContext.cs | 2 +- .../OutputCaching/src/OutputCachingFeature.cs | 6 + .../src/OutputCachingMiddleware.cs | 1 + .../OutputCaching/src/OutputCachingOptions.cs | 3 + .../src/OutputCachingPolicyProvider.cs | 6 +- .../src/Policies/CompositePolicy.cs | 12 +- .../src/Policies/DefaultOutputCachePolicy.cs | 7 +- .../src/Policies/EnableCachingPolicy.cs | 6 + .../src/Policies/ExpirationPolicy.cs | 9 +- .../src/Policies/LockingPolicy.cs | 9 +- .../src/Policies/NoStorePolicy.cs | 5 +- .../src/Policies/OutputCachePolicyBuilder.cs | 65 +++++++++ .../src/Policies/PolicyExtensions.cs | 15 ++- .../src/Policies/PredicatePolicy.cs | 13 +- .../src/Policies/ProfilePolicy.cs | 13 +- .../src/Policies/ResponseCachingPolicy.cs | 5 +- .../OutputCaching/src/Policies/TagsPolicy.cs | 9 +- .../src/Policies/VaryByQueryPolicy.cs | 5 +- .../src/Policies/VaryByValuePolicy.cs | 9 +- .../OutputCaching/src/PublicAPI.Unshipped.txt | 125 ++++++++++++++++++ 33 files changed, 523 insertions(+), 37 deletions(-) diff --git a/src/Middleware/OutputCaching.Abstractions/src/CachedResponseBody.cs b/src/Middleware/OutputCaching.Abstractions/src/CachedResponseBody.cs index 727606e3e79e..b354fbe850eb 100644 --- a/src/Middleware/OutputCaching.Abstractions/src/CachedResponseBody.cs +++ b/src/Middleware/OutputCaching.Abstractions/src/CachedResponseBody.cs @@ -5,18 +5,38 @@ namespace Microsoft.AspNetCore.OutputCaching; +/// +/// Represents a cached response body. +/// public class CachedResponseBody { + /// + /// Creates a new instance. + /// + /// The segments. + /// The length. public CachedResponseBody(List segments, long length) { Segments = segments; Length = length; } + /// + /// Gets the segments of the body. + /// public List Segments { get; } + /// + /// Gets the length of the body. + /// public long Length { get; } + /// + /// Copies the body to a . + /// + /// The destination + /// The cancellation token. + /// public async Task CopyToAsync(PipeWriter destination, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(destination, nameof(destination)); diff --git a/src/Middleware/OutputCaching.Abstractions/src/CachedVaryByRules.cs b/src/Middleware/OutputCaching.Abstractions/src/CachedVaryByRules.cs index de63617b4111..6279d3963849 100644 --- a/src/Middleware/OutputCaching.Abstractions/src/CachedVaryByRules.cs +++ b/src/Middleware/OutputCaching.Abstractions/src/CachedVaryByRules.cs @@ -5,14 +5,28 @@ namespace Microsoft.AspNetCore.OutputCaching; +/// +/// Represents vary-by rules. +/// public class CachedVaryByRules { + /// + /// Returns a dictionary of custom values to vary by. + /// public Dictionary VaryByCustom { get; } = new(StringComparer.OrdinalIgnoreCase); + /// + /// Gets or sets the list of headers to vary by. + /// public StringValues Headers { get; set; } + /// + /// Gets or sets the list of query string keys to vary by. + /// public StringValues QueryKeys { get; set; } - // Normalized version of VaryByCustom + /// + /// Gets or sets a prefix to vary by. + /// public StringValues VaryByPrefix { get; set; } } diff --git a/src/Middleware/OutputCaching.Abstractions/src/IOutputCache.cs b/src/Middleware/OutputCaching.Abstractions/src/IOutputCache.cs index d4fe368081f8..1ed593643e1e 100644 --- a/src/Middleware/OutputCaching.Abstractions/src/IOutputCache.cs +++ b/src/Middleware/OutputCaching.Abstractions/src/IOutputCache.cs @@ -3,6 +3,9 @@ namespace Microsoft.AspNetCore.OutputCaching; +/// +/// Represents a store for cached responses. +/// public interface IOutputCacheStore { /// diff --git a/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingContext.cs b/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingContext.cs index 9ef3c9f3ad32..34a28fd91c73 100644 --- a/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingContext.cs +++ b/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingContext.cs @@ -6,22 +6,69 @@ namespace Microsoft.AspNetCore.OutputCaching; +/// +/// Represent the current caching context for the request. +/// public interface IOutputCachingContext { + /// + /// Gets the cached entry age. + /// TimeSpan? CachedEntryAge { get; } + + /// + /// Gets the . + /// HttpContext HttpContext { get; } + + /// + /// Gets the response time. + /// DateTimeOffset? ResponseTime { get; } + + /// + /// Gets the response date. + /// DateTimeOffset? ResponseDate { get; } + + /// + /// Gets the response expiration. + /// DateTimeOffset? ResponseExpires { get; } + + /// + /// Gets the response shared max age. + /// TimeSpan? ResponseSharedMaxAge { get; } + + /// + /// Gets the response max age. + /// TimeSpan? ResponseMaxAge { get; } + /// /// The custom expiration timespan for the response /// public TimeSpan? ResponseExpirationTimeSpan { get; set; } + + /// + /// Gets the cached response headers. + /// IHeaderDictionary CachedResponseHeaders { get; } + + /// + /// Gets the instance. + /// CachedVaryByRules CachedVaryByRules { get; } + + /// + /// Gets the tags of the cached response. + /// HashSet Tags { get; } + + /// + /// Gets the logger. + /// ILogger Logger { get; } /// diff --git a/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingFeature.cs b/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingFeature.cs index da551035d2da..8a6fd2957579 100644 --- a/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingFeature.cs +++ b/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingFeature.cs @@ -8,7 +8,13 @@ namespace Microsoft.AspNetCore.OutputCaching; /// public interface IOutputCachingFeature { + /// + /// Gets the caching context. + /// IOutputCachingContext Context { get; } + /// + /// Gets the policies. + /// List Policies { get; } } diff --git a/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingPolicy.cs b/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingPolicy.cs index 4805a680479d..87b1d15b3d06 100644 --- a/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingPolicy.cs +++ b/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingPolicy.cs @@ -8,7 +8,23 @@ namespace Microsoft.AspNetCore.OutputCaching; /// public interface IOutputCachingPolicy { + /// + /// Updates the before the cache middleware is invoked. + /// At that point the cache middleware can still be enabled or disabled for the request. + /// + /// The current request's caching context. Task OnRequestAsync(IOutputCachingContext context); + + /// + /// Updates the before the cached response is used. + /// At that point the freshness of the cached response can be updated. + /// + /// The current request's caching context. Task OnServeFromCacheAsync(IOutputCachingContext context); + + /// + /// Updates the before the response is served and can be cached. + /// At that point cacheability of the response can be updated. + /// Task OnServeResponseAsync(IOutputCachingContext context); } diff --git a/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingPolicyProvider.cs b/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingPolicyProvider.cs index 23a8f6de2a39..7f58a79d3887 100644 --- a/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingPolicyProvider.cs +++ b/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingPolicyProvider.cs @@ -11,21 +11,21 @@ public interface IOutputCachingPolicyProvider /// /// Determine whether the response caching logic should be attempted for the incoming HTTP request. /// - /// The . + /// The . /// true if response caching logic should be attempted; otherwise false. Task OnRequestAsync(IOutputCachingContext context); /// /// Determine whether the response retrieved from the response cache is fresh and can be served. /// - /// The . + /// The . /// true if cache lookup for this cache entry is allowed; otherwise false. Task OnServeFromCacheAsync(IOutputCachingContext context); /// /// Determine whether the response can be cached for future requests. /// - /// The . + /// The . /// true if cache lookup for this request is allowed; otherwise false. Task OnServeResponseAsync(IOutputCachingContext context); } diff --git a/src/Middleware/OutputCaching.Abstractions/src/IPoliciesMetadata.cs b/src/Middleware/OutputCaching.Abstractions/src/IPoliciesMetadata.cs index 1df26f053f3e..f20c9bba4db6 100644 --- a/src/Middleware/OutputCaching.Abstractions/src/IPoliciesMetadata.cs +++ b/src/Middleware/OutputCaching.Abstractions/src/IPoliciesMetadata.cs @@ -3,7 +3,13 @@ namespace Microsoft.AspNetCore.OutputCaching; +/// +/// Represents policies metadata for an endpoint. +/// public interface IPoliciesMetadata { + /// + /// Gets the policies. + /// IReadOnlyList Policies { get; } } diff --git a/src/Middleware/OutputCaching.Abstractions/src/Microsoft.AspNetCore.OutputCaching.Abstractions.csproj b/src/Middleware/OutputCaching.Abstractions/src/Microsoft.AspNetCore.OutputCaching.Abstractions.csproj index ec50117005e0..d1b27995a24e 100644 --- a/src/Middleware/OutputCaching.Abstractions/src/Microsoft.AspNetCore.OutputCaching.Abstractions.csproj +++ b/src/Middleware/OutputCaching.Abstractions/src/Microsoft.AspNetCore.OutputCaching.Abstractions.csproj @@ -8,7 +8,6 @@ aspnetcore;cache;caching false enable - false
diff --git a/src/Middleware/OutputCaching.Abstractions/src/OutputCacheEntry.cs b/src/Middleware/OutputCaching.Abstractions/src/OutputCacheEntry.cs index fcc832f3e196..3d7053487d40 100644 --- a/src/Middleware/OutputCaching.Abstractions/src/OutputCacheEntry.cs +++ b/src/Middleware/OutputCaching.Abstractions/src/OutputCacheEntry.cs @@ -5,15 +5,33 @@ namespace Microsoft.AspNetCore.OutputCaching; +/// +/// Represents a cached response entry. +/// public class OutputCacheEntry { + /// + /// Gets the created date and time of the cache entry. + /// public DateTimeOffset Created { get; set; } + /// + /// Gets the status code of the cache entry. + /// public int StatusCode { get; set; } + /// + /// Gets the headers of the cache entry. + /// public IHeaderDictionary Headers { get; set; } = default!; + /// + /// Gets the body of the cache entry. + /// public CachedResponseBody Body { get; set; } = default!; + /// + /// Gets the tags of the cache entry. + /// public string[]? Tags { get; set; } } diff --git a/src/Middleware/OutputCaching.Abstractions/src/PublicAPI.Unshipped.txt b/src/Middleware/OutputCaching.Abstractions/src/PublicAPI.Unshipped.txt index e69de29bb2d1..af8e0ecb1e09 100644 --- a/src/Middleware/OutputCaching.Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/OutputCaching.Abstractions/src/PublicAPI.Unshipped.txt @@ -0,0 +1,71 @@ +Microsoft.AspNetCore.OutputCaching.CachedResponseBody +Microsoft.AspNetCore.OutputCaching.CachedResponseBody.CachedResponseBody(System.Collections.Generic.List! segments, long length) -> void +Microsoft.AspNetCore.OutputCaching.CachedResponseBody.CopyToAsync(System.IO.Pipelines.PipeWriter! destination, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.CachedResponseBody.Length.get -> long +Microsoft.AspNetCore.OutputCaching.CachedResponseBody.Segments.get -> System.Collections.Generic.List! +Microsoft.AspNetCore.OutputCaching.CachedVaryByRules +Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.CachedVaryByRules() -> void +Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.Headers.get -> Microsoft.Extensions.Primitives.StringValues +Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.Headers.set -> void +Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.QueryKeys.get -> Microsoft.Extensions.Primitives.StringValues +Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.QueryKeys.set -> void +Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.VaryByCustom.get -> System.Collections.Generic.Dictionary! +Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.VaryByPrefix.get -> Microsoft.Extensions.Primitives.StringValues +Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.VaryByPrefix.set -> void +Microsoft.AspNetCore.OutputCaching.IOutputCacheStore +Microsoft.AspNetCore.OutputCaching.IOutputCacheStore.EvictByTagAsync(string! tag) -> System.Threading.Tasks.ValueTask +Microsoft.AspNetCore.OutputCaching.IOutputCacheStore.GetAsync(string! key) -> System.Threading.Tasks.ValueTask +Microsoft.AspNetCore.OutputCaching.IOutputCacheStore.SetAsync(string! key, Microsoft.AspNetCore.OutputCaching.OutputCacheEntry! entry, System.TimeSpan validFor) -> System.Threading.Tasks.ValueTask +Microsoft.AspNetCore.OutputCaching.IOutputCachingContext +Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.AllowCacheLookup.get -> bool +Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.AllowCacheLookup.set -> void +Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.AllowCacheStorage.get -> bool +Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.AllowCacheStorage.set -> void +Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.AllowLocking.get -> bool +Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.AllowLocking.set -> void +Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.AttemptResponseCaching.get -> bool +Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.AttemptResponseCaching.set -> void +Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.CachedEntryAge.get -> System.TimeSpan? +Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.CachedResponseHeaders.get -> Microsoft.AspNetCore.Http.IHeaderDictionary! +Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.CachedVaryByRules.get -> Microsoft.AspNetCore.OutputCaching.CachedVaryByRules! +Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.EnableOutputCaching.get -> bool +Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.EnableOutputCaching.set -> void +Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.HttpContext.get -> Microsoft.AspNetCore.Http.HttpContext! +Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.IsCacheEntryFresh.get -> bool +Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.IsCacheEntryFresh.set -> void +Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.IsResponseCacheable.get -> bool +Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.IsResponseCacheable.set -> void +Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.Logger.get -> Microsoft.Extensions.Logging.ILogger! +Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.ResponseDate.get -> System.DateTimeOffset? +Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.ResponseExpirationTimeSpan.get -> System.TimeSpan? +Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.ResponseExpirationTimeSpan.set -> void +Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.ResponseExpires.get -> System.DateTimeOffset? +Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.ResponseMaxAge.get -> System.TimeSpan? +Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.ResponseSharedMaxAge.get -> System.TimeSpan? +Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.ResponseTime.get -> System.DateTimeOffset? +Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.Tags.get -> System.Collections.Generic.HashSet! +Microsoft.AspNetCore.OutputCaching.IOutputCachingFeature +Microsoft.AspNetCore.OutputCaching.IOutputCachingFeature.Context.get -> Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! +Microsoft.AspNetCore.OutputCaching.IOutputCachingFeature.Policies.get -> System.Collections.Generic.List! +Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy +Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy.OnRequestAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy.OnServeFromCacheAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy.OnServeResponseAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicyProvider +Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicyProvider.OnRequestAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicyProvider.OnServeFromCacheAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicyProvider.OnServeResponseAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.IPoliciesMetadata +Microsoft.AspNetCore.OutputCaching.IPoliciesMetadata.Policies.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.AspNetCore.OutputCaching.OutputCacheEntry +Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.Body.get -> Microsoft.AspNetCore.OutputCaching.CachedResponseBody! +Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.Body.set -> void +Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.Created.get -> System.DateTimeOffset +Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.Created.set -> void +Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.Headers.get -> Microsoft.AspNetCore.Http.IHeaderDictionary! +Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.Headers.set -> void +Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.OutputCacheEntry() -> void +Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.StatusCode.get -> int +Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.StatusCode.set -> void +Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.Tags.get -> string![]? +Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.Tags.set -> void \ No newline at end of file diff --git a/src/Middleware/OutputCaching/src/Memory/MemoryOutputCache.cs b/src/Middleware/OutputCaching/src/Memory/MemoryOutputCache.cs index 5712c3a388fd..018f2310b2e6 100644 --- a/src/Middleware/OutputCaching/src/Memory/MemoryOutputCache.cs +++ b/src/Middleware/OutputCaching/src/Memory/MemoryOutputCache.cs @@ -44,11 +44,10 @@ public ValueTask EvictByTagAsync(string tag) StatusCode = memoryCachedResponse.StatusCode, Headers = memoryCachedResponse.Headers, Body = memoryCachedResponse.Body, + Tags = memoryCachedResponse.Tags.ToArray() }; - outputCacheEntry.Tags = memoryCachedResponse.Tags.ToArray(); - - return ValueTask.FromResult(outputCacheEntry); + return ValueTask.FromResult(outputCacheEntry); } return ValueTask.FromResult(default(OutputCacheEntry)); @@ -56,16 +55,21 @@ public ValueTask EvictByTagAsync(string tag) public ValueTask SetAsync(string key, OutputCacheEntry cachedResponse, TimeSpan validFor) { - foreach (var tag in cachedResponse.Tags) + if (cachedResponse.Tags != null) { - var keys = _taggedEntries.GetOrAdd(tag, _ => new HashSet()); + foreach (var tag in cachedResponse.Tags) + { + var keys = _taggedEntries.GetOrAdd(tag, _ => new HashSet()); - // Copy the list of tags to prevent locking + // Copy the list of tags to prevent locking - var local = new HashSet(keys); - local.Add(key); + var local = new HashSet(keys) + { + key + }; - _taggedEntries[tag] = local; + _taggedEntries[tag] = local; + } } _cache.Set( @@ -76,7 +80,7 @@ public ValueTask SetAsync(string key, OutputCacheEntry cachedResponse, TimeSpan StatusCode = cachedResponse.StatusCode, Headers = cachedResponse.Headers, Body = cachedResponse.Body, - Tags = cachedResponse.Tags + Tags = cachedResponse.Tags ?? Array.Empty() }, new MemoryCacheEntryOptions { diff --git a/src/Middleware/OutputCaching/src/Microsoft.AspNetCore.OutputCaching.csproj b/src/Middleware/OutputCaching/src/Microsoft.AspNetCore.OutputCaching.csproj index 9b8baac0274b..4d81e6a62ceb 100644 --- a/src/Middleware/OutputCaching/src/Microsoft.AspNetCore.OutputCaching.csproj +++ b/src/Middleware/OutputCaching/src/Microsoft.AspNetCore.OutputCaching.csproj @@ -9,7 +9,6 @@ aspnetcore;cache;caching false enable - false
diff --git a/src/Middleware/OutputCaching/src/OutputCachingContext.cs b/src/Middleware/OutputCaching/src/OutputCachingContext.cs index eb5a967e4e98..4c5b96eac413 100644 --- a/src/Middleware/OutputCaching/src/OutputCachingContext.cs +++ b/src/Middleware/OutputCaching/src/OutputCachingContext.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.OutputCaching; -public class OutputCachingContext : IOutputCachingContext +internal class OutputCachingContext : IOutputCachingContext { private DateTimeOffset? _responseDate; private bool _parsedResponseDate; diff --git a/src/Middleware/OutputCaching/src/OutputCachingFeature.cs b/src/Middleware/OutputCaching/src/OutputCachingFeature.cs index 74e80cabaad1..f979a23b64b4 100644 --- a/src/Middleware/OutputCaching/src/OutputCachingFeature.cs +++ b/src/Middleware/OutputCaching/src/OutputCachingFeature.cs @@ -6,12 +6,18 @@ namespace Microsoft.AspNetCore.OutputCaching; /// Default implementation for public class OutputCachingFeature : IOutputCachingFeature { + /// + /// Creates a new instance. + /// + /// public OutputCachingFeature(IOutputCachingContext context) { Context = context; } + /// public IOutputCachingContext Context { get; } + /// public List Policies { get; } = new(); } diff --git a/src/Middleware/OutputCaching/src/OutputCachingMiddleware.cs b/src/Middleware/OutputCaching/src/OutputCachingMiddleware.cs index 1451965b1248..32bd8db65242 100644 --- a/src/Middleware/OutputCaching/src/OutputCachingMiddleware.cs +++ b/src/Middleware/OutputCaching/src/OutputCachingMiddleware.cs @@ -35,6 +35,7 @@ public class OutputCachingMiddleware /// The representing the next middleware in the pipeline. /// The options for this middleware. /// The used for logging. + /// The store. /// The used for creating instances. public OutputCachingMiddleware( RequestDelegate next, diff --git a/src/Middleware/OutputCaching/src/OutputCachingOptions.cs b/src/Middleware/OutputCaching/src/OutputCachingOptions.cs index d72192e4745f..8cd5ea8ac378 100644 --- a/src/Middleware/OutputCaching/src/OutputCachingOptions.cs +++ b/src/Middleware/OutputCaching/src/OutputCachingOptions.cs @@ -33,6 +33,9 @@ public class OutputCachingOptions /// public bool UseCaseSensitivePaths { get; set; } + /// + /// Gets the policies applied to all requests. + /// public List Policies { get; } = new() { new DefaultOutputCachePolicy() }; /// diff --git a/src/Middleware/OutputCaching/src/OutputCachingPolicyProvider.cs b/src/Middleware/OutputCaching/src/OutputCachingPolicyProvider.cs index 08a65c06c27c..30af8a577a59 100644 --- a/src/Middleware/OutputCaching/src/OutputCachingPolicyProvider.cs +++ b/src/Middleware/OutputCaching/src/OutputCachingPolicyProvider.cs @@ -17,7 +17,7 @@ public OutputCachingPolicyProvider(IOptions options) public async Task OnRequestAsync(IOutputCachingContext context) { - foreach (var policy in options.Policies) + foreach (var policy in _options.Policies) { await policy.OnRequestAsync(context); } @@ -42,7 +42,7 @@ public async Task OnRequestAsync(IOutputCachingContext context) public async Task OnServeFromCacheAsync(IOutputCachingContext context) { - foreach (var policy in options.Policies) + foreach (var policy in _options.Policies) { await policy.OnServeFromCacheAsync(context); } @@ -72,7 +72,7 @@ public async Task OnServeFromCacheAsync(IOutputCachingContext context) public async Task OnServeResponseAsync(IOutputCachingContext context) { - foreach (var policy in options.Policies) + foreach (var policy in _options.Policies) { await policy.OnServeResponseAsync(context); } diff --git a/src/Middleware/OutputCaching/src/Policies/CompositePolicy.cs b/src/Middleware/OutputCaching/src/Policies/CompositePolicy.cs index 9206a8837f33..375f284bc352 100644 --- a/src/Middleware/OutputCaching/src/Policies/CompositePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/CompositePolicy.cs @@ -3,15 +3,23 @@ namespace Microsoft.AspNetCore.OutputCaching.Policies; -public class CompositePolicy : IOutputCachingPolicy +/// +/// A composite policy. +/// +public sealed class CompositePolicy : IOutputCachingPolicy { private readonly IOutputCachingPolicy[] _policies; + /// + /// Creates a new instance of + /// + /// The policies to include. public CompositePolicy(params IOutputCachingPolicy[] policies!!) { _policies = policies; } + /// public async Task OnRequestAsync(IOutputCachingContext context) { foreach (var policy in _policies) @@ -20,6 +28,7 @@ public async Task OnRequestAsync(IOutputCachingContext context) } } + /// public async Task OnServeFromCacheAsync(IOutputCachingContext context) { foreach (var policy in _policies) @@ -28,6 +37,7 @@ public async Task OnServeFromCacheAsync(IOutputCachingContext context) } } + /// public async Task OnServeResponseAsync(IOutputCachingContext context) { foreach (var policy in _policies) diff --git a/src/Middleware/OutputCaching/src/Policies/DefaultOutputCachePolicy.cs b/src/Middleware/OutputCaching/src/Policies/DefaultOutputCachePolicy.cs index a973aacb08f5..f0ee8568cd00 100644 --- a/src/Middleware/OutputCaching/src/Policies/DefaultOutputCachePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/DefaultOutputCachePolicy.cs @@ -7,10 +7,11 @@ namespace Microsoft.AspNetCore.OutputCaching; /// -/// Default policy. +/// The default policy. /// -public class DefaultOutputCachePolicy : IOutputCachingPolicy +public sealed class DefaultOutputCachePolicy : IOutputCachingPolicy { + /// public Task OnRequestAsync(IOutputCachingContext context) { context.AttemptResponseCaching = AttemptOutputCaching(context); @@ -25,12 +26,14 @@ public Task OnRequestAsync(IOutputCachingContext context) return Task.CompletedTask; } + /// public Task OnServeFromCacheAsync(IOutputCachingContext context) { context.IsCacheEntryFresh = true; return Task.CompletedTask; } + /// public Task OnServeResponseAsync(IOutputCachingContext context) { context.IsResponseCacheable = true; diff --git a/src/Middleware/OutputCaching/src/Policies/EnableCachingPolicy.cs b/src/Middleware/OutputCaching/src/Policies/EnableCachingPolicy.cs index 0c6b8bac0abc..10c533edf32b 100644 --- a/src/Middleware/OutputCaching/src/Policies/EnableCachingPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/EnableCachingPolicy.cs @@ -8,8 +8,12 @@ namespace Microsoft.AspNetCore.OutputCaching; /// public sealed class EnableCachingPolicy : IOutputCachingPolicy { + /// + /// Default instance of . + /// public static EnableCachingPolicy Instance = new EnableCachingPolicy(); + /// public Task OnRequestAsync(IOutputCachingContext context) { context.EnableOutputCaching = true; @@ -17,11 +21,13 @@ public Task OnRequestAsync(IOutputCachingContext context) return Task.CompletedTask; } + /// public Task OnServeResponseAsync(IOutputCachingContext context) { return Task.CompletedTask; } + /// public Task OnServeFromCacheAsync(IOutputCachingContext context) { return Task.CompletedTask; diff --git a/src/Middleware/OutputCaching/src/Policies/ExpirationPolicy.cs b/src/Middleware/OutputCaching/src/Policies/ExpirationPolicy.cs index 324fe4eb85ca..ca4c82505823 100644 --- a/src/Middleware/OutputCaching/src/Policies/ExpirationPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/ExpirationPolicy.cs @@ -4,17 +4,22 @@ namespace Microsoft.AspNetCore.OutputCaching; /// -/// A policy that defines a custom expiration timespan. +/// A policy that defines a custom expiration duration. /// public sealed class ExpirationPolicy : IOutputCachingPolicy { private readonly TimeSpan _expiration; + /// + /// Creates a new instance. + /// + /// The expiration duration. public ExpirationPolicy(TimeSpan expiration) { _expiration = expiration; } + /// public Task OnRequestAsync(IOutputCachingContext context) { context.ResponseExpirationTimeSpan = _expiration; @@ -22,11 +27,13 @@ public Task OnRequestAsync(IOutputCachingContext context) return Task.CompletedTask; } + /// public Task OnServeFromCacheAsync(IOutputCachingContext context) { return Task.CompletedTask; } + /// public Task OnServeResponseAsync(IOutputCachingContext context) { return Task.CompletedTask; diff --git a/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs b/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs index 275599167cf0..ef19cef99718 100644 --- a/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs @@ -4,17 +4,22 @@ namespace Microsoft.AspNetCore.OutputCaching; /// -/// A policy that defines a custom expiration timespan. +/// A policy that changes the locking behavior. /// public class LockingPolicy : IOutputCachingPolicy { private readonly bool _lockResponse; + /// + /// Creates a new instance of . + /// + /// Whether to lock responses or not. public LockingPolicy(bool lockResponse) { _lockResponse = lockResponse; } + /// public Task OnRequestAsync(IOutputCachingContext context) { context.AllowLocking = _lockResponse; @@ -22,11 +27,13 @@ public Task OnRequestAsync(IOutputCachingContext context) return Task.CompletedTask; } + /// public Task OnServeFromCacheAsync(IOutputCachingContext context) { return Task.CompletedTask; } + /// public Task OnServeResponseAsync(IOutputCachingContext context) { return Task.CompletedTask; diff --git a/src/Middleware/OutputCaching/src/Policies/NoStorePolicy.cs b/src/Middleware/OutputCaching/src/Policies/NoStorePolicy.cs index 1ee6ba1ef0fd..00b4d7cc1d4c 100644 --- a/src/Middleware/OutputCaching/src/Policies/NoStorePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/NoStorePolicy.cs @@ -4,10 +4,11 @@ namespace Microsoft.AspNetCore.OutputCaching; /// -/// A policy that prevents caching +/// A policy that prevents the response from being cached. /// public class NoStorePolicy : IOutputCachingPolicy { + /// public Task OnServeResponseAsync(IOutputCachingContext context) { context.IsResponseCacheable = false; @@ -15,11 +16,13 @@ public Task OnServeResponseAsync(IOutputCachingContext context) return Task.CompletedTask; } + /// public Task OnServeFromCacheAsync(IOutputCachingContext context) { return Task.CompletedTask; } + /// public Task OnRequestAsync(IOutputCachingContext context) { return Task.CompletedTask; diff --git a/src/Middleware/OutputCaching/src/Policies/OutputCachePolicyBuilder.cs b/src/Middleware/OutputCaching/src/Policies/OutputCachePolicyBuilder.cs index 55e595f99b91..7d30ae1903f3 100644 --- a/src/Middleware/OutputCaching/src/Policies/OutputCachePolicyBuilder.cs +++ b/src/Middleware/OutputCaching/src/Policies/OutputCachePolicyBuilder.cs @@ -7,17 +7,28 @@ namespace Microsoft.AspNetCore.OutputCaching; +/// +/// Provides helper methods to create custom policies. +/// public class OutputCachePolicyBuilder { private List Policies { get; } = new(); private List>> Requirements { get; } = new(); + /// + /// Adds a requirement to the current policy. + /// + /// The predicate applied to the policy. public OutputCachePolicyBuilder When(Func> predicate) { Requirements.Add(predicate); return this; } + /// + /// Adds a requirement to the current policy based on the request path. + /// + /// The base path to limit the policy to. public OutputCachePolicyBuilder Path(PathString pathBase) { ArgumentNullException.ThrowIfNull(pathBase, nameof(pathBase)); @@ -30,6 +41,10 @@ public OutputCachePolicyBuilder Path(PathString pathBase) return this; } + /// + /// Adds a requirement to the current policy based on the request path. + /// + /// The base paths to limit the policy to. public OutputCachePolicyBuilder Path(params PathString[] pathBases) { ArgumentNullException.ThrowIfNull(pathBases, nameof(pathBases)); @@ -42,6 +57,10 @@ public OutputCachePolicyBuilder Path(params PathString[] pathBases) return this; } + /// + /// Adds a requirement to the current policy based on the request method. + /// + /// The method to limit the policy to. public OutputCachePolicyBuilder Method(string method) { ArgumentNullException.ThrowIfNull(method, nameof(method)); @@ -55,6 +74,10 @@ public OutputCachePolicyBuilder Method(string method) return this; } + /// + /// Adds a requirement to the current policy based on the request method. + /// + /// The methods to limit the policy to. public OutputCachePolicyBuilder Method(params string[] methods) { ArgumentNullException.ThrowIfNull(methods, nameof(methods)); @@ -68,6 +91,10 @@ public OutputCachePolicyBuilder Method(params string[] methods) return this; } + /// + /// Adds a policy to vary the cached responses by query strings. + /// + /// The query keys to vary the cached responses by. public OutputCachePolicyBuilder VaryByQuery(params string[] queryKeys) { ArgumentNullException.ThrowIfNull(queryKeys, nameof(queryKeys)); @@ -76,6 +103,10 @@ public OutputCachePolicyBuilder VaryByQuery(params string[] queryKeys) return this; } + /// + /// Adds a policy to vary the cached responses by custom values. + /// + /// The value to vary the cached responses by. public OutputCachePolicyBuilder VaryByValue(Func> varyBy) { ArgumentNullException.ThrowIfNull(varyBy, nameof(varyBy)); @@ -84,6 +115,10 @@ public OutputCachePolicyBuilder VaryByValue(Func> varyBy) return this; } + /// + /// Adds a policy to vary the cached responses by custom key/value. + /// + /// The key/value to vary the cached responses by. public OutputCachePolicyBuilder VaryByValue(Func> varyBy) { ArgumentNullException.ThrowIfNull(varyBy, nameof(varyBy)); @@ -92,6 +127,10 @@ public OutputCachePolicyBuilder VaryByValue(Func> varyBy) return this; } + /// + /// Adds a policy to vary the cached responses by custom values. + /// + /// The value to vary the cached responses by. public OutputCachePolicyBuilder VaryByValue(Func varyBy) { ArgumentNullException.ThrowIfNull(varyBy, nameof(varyBy)); @@ -100,6 +139,10 @@ public OutputCachePolicyBuilder VaryByValue(Func varyBy) return this; } + /// + /// Adds a policy to vary the cached responses by custom key/value. + /// + /// The key/value to vary the cached responses by. public OutputCachePolicyBuilder VaryByValue(Func<(string, string)> varyBy) { ArgumentNullException.ThrowIfNull(varyBy, nameof(varyBy)); @@ -108,6 +151,10 @@ public OutputCachePolicyBuilder VaryByValue(Func<(string, string)> varyBy) return this; } + /// + /// Adds a named policy. + /// + /// The name of the policy to add. public OutputCachePolicyBuilder Profile(string profileName) { ArgumentNullException.ThrowIfNull(profileName, nameof(profileName)); @@ -117,6 +164,10 @@ public OutputCachePolicyBuilder Profile(string profileName) return this; } + /// + /// Adds a policy to tag the cached response. + /// + /// The tags to add to the cached reponse. public OutputCachePolicyBuilder Tag(params string[] tags) { ArgumentNullException.ThrowIfNull(tags, nameof(tags)); @@ -125,18 +176,29 @@ public OutputCachePolicyBuilder Tag(params string[] tags) return this; } + /// + /// Adds a policy to change the cached response expiration. + /// + /// The expiration of the cached reponse. public OutputCachePolicyBuilder Expires(TimeSpan expiration) { Policies.Add(new ExpirationPolicy(expiration)); return this; } + /// + /// Adds a policy to change the request locking strategy. + /// + /// Whether the request should be locked. public OutputCachePolicyBuilder Lock(bool lockResponse = true) { Policies.Add(new LockingPolicy(lockResponse)); return this; } + /// + /// Clears the current policies. + /// public OutputCachePolicyBuilder Clear() { Requirements.Clear(); @@ -144,6 +206,9 @@ public OutputCachePolicyBuilder Clear() return this; } + /// + /// Adds a policy to prevent the response from being cached. + /// public OutputCachePolicyBuilder NoStore() { Policies.Add(new NoStorePolicy()); diff --git a/src/Middleware/OutputCaching/src/Policies/PolicyExtensions.cs b/src/Middleware/OutputCaching/src/Policies/PolicyExtensions.cs index e5b6618a967a..7fca54d2480c 100644 --- a/src/Middleware/OutputCaching/src/Policies/PolicyExtensions.cs +++ b/src/Middleware/OutputCaching/src/Policies/PolicyExtensions.cs @@ -4,8 +4,15 @@ using Microsoft.AspNetCore.Builder; namespace Microsoft.AspNetCore.OutputCaching.Policies; + +/// +/// A set of endpoint extension methods. +/// public static class PolicyExtensions { + /// + /// Marks an endpoint to be cached. + /// public static TBuilder OutputCache(this TBuilder builder) where TBuilder : IEndpointConventionBuilder { ArgumentNullException.ThrowIfNull(builder, nameof(builder)); @@ -22,6 +29,9 @@ public static TBuilder OutputCache(this TBuilder builder) where TBuild return builder; } + /// + /// Marks an endpoint to be cached with the specified policies. + /// public static TBuilder OutputCache(this TBuilder builder, params IOutputCachingPolicy[] items) where TBuilder : IEndpointConventionBuilder { ArgumentNullException.ThrowIfNull(builder, nameof(builder)); @@ -42,8 +52,11 @@ public static TBuilder OutputCache(this TBuilder builder, params IOutp endpointBuilder.Metadata.Add(policiesMetadata); }); return builder; - } + } + /// + /// Marks an endpoint to be cached with the specified policies. + /// public static TBuilder OutputCache(this TBuilder builder, Action policy) where TBuilder : IEndpointConventionBuilder { ArgumentNullException.ThrowIfNull(builder, nameof(builder)); diff --git a/src/Middleware/OutputCaching/src/Policies/PredicatePolicy.cs b/src/Middleware/OutputCaching/src/Policies/PredicatePolicy.cs index 3ec734d28d6c..8d65f4ea0335 100644 --- a/src/Middleware/OutputCaching/src/Policies/PredicatePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/PredicatePolicy.cs @@ -3,19 +3,28 @@ namespace Microsoft.AspNetCore.OutputCaching.Policies; -public class PredicatePolicy : IOutputCachingPolicy +/// +/// A policy that adds a requirement to another policy. +/// +public sealed class PredicatePolicy : IOutputCachingPolicy { // TODO: Accept a non async predicate too? private readonly Func> _predicate; private readonly IOutputCachingPolicy _policy; + /// + /// Creates a new instance. + /// + /// The predicate. + /// The policy. public PredicatePolicy(Func> predicate, IOutputCachingPolicy policy) { _predicate = predicate; _policy = policy; } + /// public Task OnRequestAsync(IOutputCachingContext context) { if (_predicate == null) @@ -46,11 +55,13 @@ async static Task Awaited(Task task, IOutputCachingPolicy policy, IOutputC } } + /// public Task OnServeFromCacheAsync(IOutputCachingContext context) { return Task.CompletedTask; } + /// public Task OnServeResponseAsync(IOutputCachingContext context) { return Task.CompletedTask; diff --git a/src/Middleware/OutputCaching/src/Policies/ProfilePolicy.cs b/src/Middleware/OutputCaching/src/Policies/ProfilePolicy.cs index 9513dd176f49..ca8fb2d6cda0 100644 --- a/src/Middleware/OutputCaching/src/Policies/ProfilePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/ProfilePolicy.cs @@ -7,17 +7,22 @@ namespace Microsoft.AspNetCore.OutputCaching; /// -/// A policy that prevents caching +/// A policy represented by a named profile. /// -public class ProfilePolicy : IOutputCachingPolicy +public sealed class ProfilePolicy : IOutputCachingPolicy { private readonly string _profileName; + /// + /// Create a new instance. + /// + /// The name of the profile. public ProfilePolicy(string profileName) { _profileName = profileName; } + /// public Task OnServeResponseAsync(IOutputCachingContext context) { var policy = GetProfilePolicy(context); @@ -30,6 +35,7 @@ public Task OnServeResponseAsync(IOutputCachingContext context) return policy.OnServeResponseAsync(context); } + /// public Task OnServeFromCacheAsync(IOutputCachingContext context) { var policy = GetProfilePolicy(context); @@ -42,6 +48,7 @@ public Task OnServeFromCacheAsync(IOutputCachingContext context) return policy.OnServeFromCacheAsync(context); } + /// public Task OnRequestAsync(IOutputCachingContext context) { var policy = GetProfilePolicy(context); @@ -54,7 +61,7 @@ public Task OnRequestAsync(IOutputCachingContext context) return policy.OnRequestAsync(context); ; } - internal IOutputCachingPolicy GetProfilePolicy(IOutputCachingContext context) + internal IOutputCachingPolicy? GetProfilePolicy(IOutputCachingContext context) { var options = context.HttpContext.RequestServices.GetRequiredService>(); diff --git a/src/Middleware/OutputCaching/src/Policies/ResponseCachingPolicy.cs b/src/Middleware/OutputCaching/src/Policies/ResponseCachingPolicy.cs index bf03ca9c8f6e..80c3ac2c84d5 100644 --- a/src/Middleware/OutputCaching/src/Policies/ResponseCachingPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/ResponseCachingPolicy.cs @@ -10,8 +10,9 @@ namespace Microsoft.AspNetCore.OutputCaching; /// /// Provides the policy implemented by Response caching /// -public class ResponseCachingPolicy : IOutputCachingPolicy +public sealed class ResponseCachingPolicy : IOutputCachingPolicy { + /// public Task OnRequestAsync(IOutputCachingContext context) { context.AttemptResponseCaching = AttemptOutputCaching(context); @@ -25,6 +26,7 @@ public Task OnRequestAsync(IOutputCachingContext context) return Task.CompletedTask; } + /// public Task OnServeResponseAsync(IOutputCachingContext context) { context.IsResponseCacheable = IsResponseCacheable(context); @@ -32,6 +34,7 @@ public Task OnServeResponseAsync(IOutputCachingContext context) return Task.CompletedTask; } + /// public Task OnServeFromCacheAsync(IOutputCachingContext context) { context.IsCacheEntryFresh = IsCachedEntryFresh(context); diff --git a/src/Middleware/OutputCaching/src/Policies/TagsPolicy.cs b/src/Middleware/OutputCaching/src/Policies/TagsPolicy.cs index 0a41cd05da5e..6906731cd11c 100644 --- a/src/Middleware/OutputCaching/src/Policies/TagsPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/TagsPolicy.cs @@ -6,15 +6,20 @@ namespace Microsoft.AspNetCore.OutputCaching; /// /// A policy that defines custom tags on the cache entry. /// -public class TagsPolicy : IOutputCachingPolicy +public sealed class TagsPolicy : IOutputCachingPolicy { private readonly string[] _tags; + /// + /// Creates a new instance. + /// + /// The tags. public TagsPolicy(params string[] tags) { _tags = tags; } + /// public Task OnRequestAsync(IOutputCachingContext context) { foreach (var tag in _tags) @@ -25,11 +30,13 @@ public Task OnRequestAsync(IOutputCachingContext context) return Task.CompletedTask; } + /// public Task OnServeFromCacheAsync(IOutputCachingContext context) { return Task.CompletedTask; } + /// public Task OnServeResponseAsync(IOutputCachingContext context) { return Task.CompletedTask; diff --git a/src/Middleware/OutputCaching/src/Policies/VaryByQueryPolicy.cs b/src/Middleware/OutputCaching/src/Policies/VaryByQueryPolicy.cs index fa54ae4bd540..25a45310b8e8 100644 --- a/src/Middleware/OutputCaching/src/Policies/VaryByQueryPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/VaryByQueryPolicy.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.OutputCaching; /// When applied, the cached content will be different for every value of the provided query string keys. /// It also disables the default behavior which is to vary on all query string keys. /// -public class VaryByQueryPolicy : IOutputCachingPolicy +public sealed class VaryByQueryPolicy : IOutputCachingPolicy { private StringValues _queryKeys { get; set; } @@ -36,6 +36,7 @@ public VaryByQueryPolicy(params string[] queryKeys) _queryKeys = queryKeys; } + /// public Task OnRequestAsync(IOutputCachingContext context) { // No vary by query? @@ -57,11 +58,13 @@ public Task OnRequestAsync(IOutputCachingContext context) return Task.CompletedTask; } + /// public Task OnServeFromCacheAsync(IOutputCachingContext context) { return Task.CompletedTask; } + /// public Task OnServeResponseAsync(IOutputCachingContext context) { return Task.CompletedTask; diff --git a/src/Middleware/OutputCaching/src/Policies/VaryByValuePolicy.cs b/src/Middleware/OutputCaching/src/Policies/VaryByValuePolicy.cs index 5d0591869c3a..12d59bf878e7 100644 --- a/src/Middleware/OutputCaching/src/Policies/VaryByValuePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/VaryByValuePolicy.cs @@ -6,10 +6,10 @@ namespace Microsoft.AspNetCore.OutputCaching; /// /// When applied, the cached content will be different for every provided value. /// -public class VaryByValuePolicy : IOutputCachingPolicy +public sealed class VaryByValuePolicy : IOutputCachingPolicy { - private readonly Action _varyBy; - private readonly Func _varyByAsync; + private readonly Action? _varyBy; + private readonly Func? _varyByAsync; /// /// Creates a policy that doesn't vary the cached content based on values. @@ -58,6 +58,7 @@ public VaryByValuePolicy(Func> varyBy) }; } + /// public Task OnRequestAsync(IOutputCachingContext context) { _varyBy?.Invoke(context.CachedVaryByRules); @@ -65,11 +66,13 @@ public Task OnRequestAsync(IOutputCachingContext context) return _varyByAsync?.Invoke(context.CachedVaryByRules) ?? Task.CompletedTask; } + /// public Task OnServeFromCacheAsync(IOutputCachingContext context) { return Task.CompletedTask; } + /// public Task OnServeResponseAsync(IOutputCachingContext context) { return Task.CompletedTask; diff --git a/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt b/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..a1531e3fdf6a 100644 --- a/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt @@ -1 +1,126 @@ #nullable enable +Microsoft.AspNetCore.Builder.OutputCachingExtensions +Microsoft.AspNetCore.OutputCaching.DefaultOutputCachePolicy +Microsoft.AspNetCore.OutputCaching.DefaultOutputCachePolicy.DefaultOutputCachePolicy() -> void +Microsoft.AspNetCore.OutputCaching.DefaultOutputCachePolicy.OnRequestAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.DefaultOutputCachePolicy.OnServeFromCacheAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.DefaultOutputCachePolicy.OnServeResponseAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.EnableCachingPolicy +Microsoft.AspNetCore.OutputCaching.EnableCachingPolicy.EnableCachingPolicy() -> void +Microsoft.AspNetCore.OutputCaching.EnableCachingPolicy.OnRequestAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.EnableCachingPolicy.OnServeFromCacheAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.EnableCachingPolicy.OnServeResponseAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.ExpirationPolicy +Microsoft.AspNetCore.OutputCaching.ExpirationPolicy.ExpirationPolicy(System.TimeSpan expiration) -> void +Microsoft.AspNetCore.OutputCaching.ExpirationPolicy.OnRequestAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.ExpirationPolicy.OnServeFromCacheAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.ExpirationPolicy.OnServeResponseAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.LockingPolicy +Microsoft.AspNetCore.OutputCaching.LockingPolicy.LockingPolicy(bool lockResponse) -> void +Microsoft.AspNetCore.OutputCaching.LockingPolicy.OnRequestAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.LockingPolicy.OnServeFromCacheAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.LockingPolicy.OnServeResponseAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.NoStorePolicy +Microsoft.AspNetCore.OutputCaching.NoStorePolicy.NoStorePolicy() -> void +Microsoft.AspNetCore.OutputCaching.NoStorePolicy.OnRequestAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.NoStorePolicy.OnServeFromCacheAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.NoStorePolicy.OnServeResponseAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute +Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.Duration.get -> int +Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.Duration.set -> void +Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.NoStore.get -> bool +Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.NoStore.set -> void +Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.OutputCacheAttribute() -> void +Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.Policies.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.Profile.get -> string? +Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.Profile.set -> void +Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.VaryByQueryKeys.get -> string![]? +Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.VaryByQueryKeys.set -> void +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Build() -> Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Clear() -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Expires(System.TimeSpan expiration) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Lock(bool lockResponse = true) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Method(params string![]! methods) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Method(string! method) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.NoStore() -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.OutputCachePolicyBuilder() -> void +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Path(Microsoft.AspNetCore.Http.PathString pathBase) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Path(params Microsoft.AspNetCore.Http.PathString[]! pathBases) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Profile(string! profileName) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Tag(params string![]! tags) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByQuery(params string![]! queryKeys) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.Func<(string!, string!)>! varyBy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.Func!>! varyBy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.Func!>! varyBy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.Func! varyBy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.When(System.Func!>! predicate) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachingFeature +Microsoft.AspNetCore.OutputCaching.OutputCachingFeature.Context.get -> Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! +Microsoft.AspNetCore.OutputCaching.OutputCachingFeature.OutputCachingFeature(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> void +Microsoft.AspNetCore.OutputCaching.OutputCachingFeature.Policies.get -> System.Collections.Generic.List! +Microsoft.AspNetCore.OutputCaching.OutputCachingMiddleware +Microsoft.AspNetCore.OutputCaching.OutputCachingMiddleware.Invoke(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.OutputCachingMiddleware.OutputCachingMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Options.IOptions! options, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, Microsoft.AspNetCore.OutputCaching.IOutputCacheStore! outputCache, Microsoft.Extensions.ObjectPool.ObjectPoolProvider! poolProvider) -> void +Microsoft.AspNetCore.OutputCaching.OutputCachingOptions +Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.DefaultExpirationTimeSpan.get -> System.TimeSpan +Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.DefaultExpirationTimeSpan.set -> void +Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.MaximumBodySize.get -> long +Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.MaximumBodySize.set -> void +Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.OutputCachingOptions() -> void +Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.Policies.get -> System.Collections.Generic.List! +Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.Profiles.get -> System.Collections.Generic.IDictionary! +Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.SizeLimit.get -> long +Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.SizeLimit.set -> void +Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.UseCaseSensitivePaths.get -> bool +Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.UseCaseSensitivePaths.set -> void +Microsoft.AspNetCore.OutputCaching.Policies.CompositePolicy +Microsoft.AspNetCore.OutputCaching.Policies.CompositePolicy.CompositePolicy(params Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy![]! policies) -> void +Microsoft.AspNetCore.OutputCaching.Policies.CompositePolicy.OnRequestAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.Policies.CompositePolicy.OnServeFromCacheAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.Policies.CompositePolicy.OnServeResponseAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.Policies.PolicyExtensions +Microsoft.AspNetCore.OutputCaching.Policies.PredicatePolicy +Microsoft.AspNetCore.OutputCaching.Policies.PredicatePolicy.OnRequestAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.Policies.PredicatePolicy.OnServeFromCacheAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.Policies.PredicatePolicy.OnServeResponseAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.Policies.PredicatePolicy.PredicatePolicy(System.Func!>! predicate, Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy! policy) -> void +Microsoft.AspNetCore.OutputCaching.ProfilePolicy +Microsoft.AspNetCore.OutputCaching.ProfilePolicy.OnRequestAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.ProfilePolicy.OnServeFromCacheAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.ProfilePolicy.OnServeResponseAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.ProfilePolicy.ProfilePolicy(string! profileName) -> void +Microsoft.AspNetCore.OutputCaching.ResponseCachingPolicy +Microsoft.AspNetCore.OutputCaching.ResponseCachingPolicy.OnRequestAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.ResponseCachingPolicy.OnServeFromCacheAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.ResponseCachingPolicy.OnServeResponseAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.ResponseCachingPolicy.ResponseCachingPolicy() -> void +Microsoft.AspNetCore.OutputCaching.TagsPolicy +Microsoft.AspNetCore.OutputCaching.TagsPolicy.OnRequestAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.TagsPolicy.OnServeFromCacheAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.TagsPolicy.OnServeResponseAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.TagsPolicy.TagsPolicy(params string![]! tags) -> void +Microsoft.AspNetCore.OutputCaching.VaryByQueryPolicy +Microsoft.AspNetCore.OutputCaching.VaryByQueryPolicy.OnRequestAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.VaryByQueryPolicy.OnServeFromCacheAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.VaryByQueryPolicy.OnServeResponseAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.VaryByQueryPolicy.VaryByQueryPolicy() -> void +Microsoft.AspNetCore.OutputCaching.VaryByQueryPolicy.VaryByQueryPolicy(params string![]! queryKeys) -> void +Microsoft.AspNetCore.OutputCaching.VaryByQueryPolicy.VaryByQueryPolicy(string! queryKey) -> void +Microsoft.AspNetCore.OutputCaching.VaryByValuePolicy +Microsoft.AspNetCore.OutputCaching.VaryByValuePolicy.OnRequestAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.VaryByValuePolicy.OnServeFromCacheAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.VaryByValuePolicy.OnServeResponseAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.VaryByValuePolicy.VaryByValuePolicy() -> void +Microsoft.AspNetCore.OutputCaching.VaryByValuePolicy.VaryByValuePolicy(System.Func<(string!, string!)>! varyBy) -> void +Microsoft.AspNetCore.OutputCaching.VaryByValuePolicy.VaryByValuePolicy(System.Func!>! varyBy) -> void +Microsoft.AspNetCore.OutputCaching.VaryByValuePolicy.VaryByValuePolicy(System.Func!>! varyBy) -> void +Microsoft.AspNetCore.OutputCaching.VaryByValuePolicy.VaryByValuePolicy(System.Func! varyBy) -> void +Microsoft.Extensions.DependencyInjection.OutputCachingServicesExtensions +static Microsoft.AspNetCore.Builder.OutputCachingExtensions.UseOutputCaching(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! +static Microsoft.AspNetCore.OutputCaching.EnableCachingPolicy.Instance -> Microsoft.AspNetCore.OutputCaching.EnableCachingPolicy! +static Microsoft.AspNetCore.OutputCaching.Policies.PolicyExtensions.OutputCache(this TBuilder builder) -> TBuilder +static Microsoft.AspNetCore.OutputCaching.Policies.PolicyExtensions.OutputCache(this TBuilder builder, System.Action! policy) -> TBuilder +static Microsoft.AspNetCore.OutputCaching.Policies.PolicyExtensions.OutputCache(this TBuilder builder, params Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy![]! items) -> TBuilder +static Microsoft.Extensions.DependencyInjection.OutputCachingServicesExtensions.AddOutputCaching(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.DependencyInjection.OutputCachingServicesExtensions.AddOutputCaching(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! From d80d247e306cba82f589c85c04a5dfe257974171 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Wed, 6 Apr 2022 10:10:37 -0700 Subject: [PATCH 11/48] Missing changes --- .../samples/OutputCachingSample/Startup.cs | 3 ++- src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt | 13 +++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs b/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs index 0b8409be09c2..d81985d25f2c 100644 --- a/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs +++ b/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Globalization; using Microsoft.AspNetCore.OutputCaching; using Microsoft.AspNetCore.OutputCaching.Policies; using Microsoft.Net.Http.Headers; @@ -41,7 +42,7 @@ // Cached entries will vary by culture, but any other additional query is ignored and returns the same cached content app.MapGet("/query", Gravatar.WriteGravatar).OutputCache(p => p.VaryByQuery("culture")); -app.MapGet("/vary", Gravatar.WriteGravatar).OutputCache(c => c.VaryByValue(() => ("time", (DateTime.Now.Second % 2).ToString()))); +app.MapGet("/vary", Gravatar.WriteGravatar).OutputCache(c => c.VaryByValue(() => ("time", (DateTime.Now.Second % 2).ToString(CultureInfo.InvariantCulture)))); long requests = 0; diff --git a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt index 80b80b352063..7b5bb219c05b 100644 --- a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt +++ b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt @@ -13,6 +13,19 @@ Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.SystemTextJsonValidationMetadataP Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.SystemTextJsonValidationMetadataProvider.SystemTextJsonValidationMetadataProvider(System.Text.Json.JsonNamingPolicy! namingPolicy) -> void Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.ValidationMetadata.ValidationModelName.get -> string? Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.ValidationMetadata.ValidationModelName.set -> void +Microsoft.AspNetCore.Mvc.OutputCacheAttribute +Microsoft.AspNetCore.Mvc.OutputCacheAttribute.Duration.get -> int +Microsoft.AspNetCore.Mvc.OutputCacheAttribute.Duration.set -> void +Microsoft.AspNetCore.Mvc.OutputCacheAttribute.NoStore.get -> bool +Microsoft.AspNetCore.Mvc.OutputCacheAttribute.NoStore.set -> void +Microsoft.AspNetCore.Mvc.OutputCacheAttribute.Order.get -> int +Microsoft.AspNetCore.Mvc.OutputCacheAttribute.Order.set -> void +Microsoft.AspNetCore.Mvc.OutputCacheAttribute.OutputCacheAttribute() -> void +Microsoft.AspNetCore.Mvc.OutputCacheAttribute.Policies.get -> System.Collections.Generic.List! +Microsoft.AspNetCore.Mvc.OutputCacheAttribute.Profile.get -> string? +Microsoft.AspNetCore.Mvc.OutputCacheAttribute.Profile.set -> void +Microsoft.AspNetCore.Mvc.OutputCacheAttribute.VaryByQueryKeys.get -> string![]? +Microsoft.AspNetCore.Mvc.OutputCacheAttribute.VaryByQueryKeys.set -> void virtual Microsoft.AspNetCore.Mvc.Infrastructure.ConfigureCompatibilityOptions.PostConfigure(string? name, TOptions! options) -> void *REMOVED*~Microsoft.AspNetCore.Mvc.Formatters.FormatFilter.FormatFilter(Microsoft.Extensions.Options.IOptions! options, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> void Microsoft.AspNetCore.Mvc.Formatters.FormatFilter.FormatFilter(Microsoft.Extensions.Options.IOptions! options, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> void From c3785dda83c2e35bcf10526fad5d07157f5fa7c6 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Wed, 6 Apr 2022 10:12:17 -0700 Subject: [PATCH 12/48] Remove unused file --- src/Middleware/OutputCaching/src/FastGuid.cs | 73 -------------------- 1 file changed, 73 deletions(-) delete mode 100644 src/Middleware/OutputCaching/src/FastGuid.cs diff --git a/src/Middleware/OutputCaching/src/FastGuid.cs b/src/Middleware/OutputCaching/src/FastGuid.cs deleted file mode 100644 index 9589996d3cf9..000000000000 --- a/src/Middleware/OutputCaching/src/FastGuid.cs +++ /dev/null @@ -1,73 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.AspNetCore.OutputCaching; - -internal class FastGuid -{ - // Base32 encoding - in ascii sort order for easy text based sorting - private static readonly char[] s_encode32Chars = "0123456789ABCDEFGHIJKLMNOPQRSTUV".ToCharArray(); - // Global ID - private static long NextId = InitializeNextId(); - - // Instance components - private string? _idString; - - internal long IdValue { get; } - - internal string IdString - { - get - { - if (_idString == null) - { - _idString = GenerateGuidString(this); - } - return _idString; - } - } - - // Static constructor to initialize global components - private static long InitializeNextId() - { - var guidBytes = Guid.NewGuid().ToByteArray(); - - // Use the first 4 bytes from the Guid to initialize global ID - return - guidBytes[0] << 32 | - guidBytes[1] << 40 | - guidBytes[2] << 48 | - guidBytes[3] << 56; - } - - internal FastGuid(long id) - { - IdValue = id; - } - - internal static FastGuid NewGuid() - { - return new FastGuid(Interlocked.Increment(ref NextId)); - } - - private static string GenerateGuidString(FastGuid guid) - { - return string.Create(13, guid.IdValue, (buffer, value) => - { - char[] encode32Chars = s_encode32Chars; - buffer[12] = encode32Chars[value & 31]; - buffer[11] = encode32Chars[(value >> 5) & 31]; - buffer[10] = encode32Chars[(value >> 10) & 31]; - buffer[9] = encode32Chars[(value >> 15) & 31]; - buffer[8] = encode32Chars[(value >> 20) & 31]; - buffer[7] = encode32Chars[(value >> 25) & 31]; - buffer[6] = encode32Chars[(value >> 30) & 31]; - buffer[5] = encode32Chars[(value >> 35) & 31]; - buffer[4] = encode32Chars[(value >> 40) & 31]; - buffer[3] = encode32Chars[(value >> 45) & 31]; - buffer[2] = encode32Chars[(value >> 50) & 31]; - buffer[1] = encode32Chars[(value >> 55) & 31]; - buffer[0] = encode32Chars[(value >> 60) & 31]; - }); - } -} From d1e35965014671afcaeff7aaf6f7812de03aec09 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Wed, 6 Apr 2022 10:19:36 -0700 Subject: [PATCH 13/48] PR feedback --- .../src/Memory/MemoryCachedResponse.cs | 1 + .../src/Memory/MemoryOutputCache.cs | 2 +- .../src/OutputCachingPolicyProvider.cs | 18 +++++++++--------- .../src/Policies/LockingPolicy.cs | 16 +++++++++++----- .../OutputCaching/src/PublicAPI.Unshipped.txt | 3 ++- 5 files changed, 24 insertions(+), 16 deletions(-) diff --git a/src/Middleware/OutputCaching/src/Memory/MemoryCachedResponse.cs b/src/Middleware/OutputCaching/src/Memory/MemoryCachedResponse.cs index c34d0e9a0f88..a50e4c1fdfb2 100644 --- a/src/Middleware/OutputCaching/src/Memory/MemoryCachedResponse.cs +++ b/src/Middleware/OutputCaching/src/Memory/MemoryCachedResponse.cs @@ -14,5 +14,6 @@ internal class MemoryCachedResponse public IHeaderDictionary Headers { get; set; } = new HeaderDictionary(); public CachedResponseBody Body { get; set; } = default!; + public string[] Tags { get; set; } = default!; } diff --git a/src/Middleware/OutputCaching/src/Memory/MemoryOutputCache.cs b/src/Middleware/OutputCaching/src/Memory/MemoryOutputCache.cs index 018f2310b2e6..27acd42c143b 100644 --- a/src/Middleware/OutputCaching/src/Memory/MemoryOutputCache.cs +++ b/src/Middleware/OutputCaching/src/Memory/MemoryOutputCache.cs @@ -44,7 +44,7 @@ public ValueTask EvictByTagAsync(string tag) StatusCode = memoryCachedResponse.StatusCode, Headers = memoryCachedResponse.Headers, Body = memoryCachedResponse.Body, - Tags = memoryCachedResponse.Tags.ToArray() + Tags = memoryCachedResponse.Tags }; return ValueTask.FromResult(outputCacheEntry); diff --git a/src/Middleware/OutputCaching/src/OutputCachingPolicyProvider.cs b/src/Middleware/OutputCaching/src/OutputCachingPolicyProvider.cs index 30af8a577a59..73211a2ff5ae 100644 --- a/src/Middleware/OutputCaching/src/OutputCachingPolicyProvider.cs +++ b/src/Middleware/OutputCaching/src/OutputCachingPolicyProvider.cs @@ -22,9 +22,9 @@ public async Task OnRequestAsync(IOutputCachingContext context) await policy.OnRequestAsync(context); } - var policiesMedata = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata(); + var policiesMetadata = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata(); - if (policiesMedata != null) + if (policiesMetadata != null) { // TODO: Log only? @@ -33,7 +33,7 @@ public async Task OnRequestAsync(IOutputCachingContext context) throw new InvalidOperationException("Can't define output caching policies after headers have been sent to client."); } - foreach (var policy in policiesMedata.Policies) + foreach (var policy in policiesMetadata.Policies) { await policy.OnRequestAsync(context); } @@ -59,11 +59,11 @@ public async Task OnServeFromCacheAsync(IOutputCachingContext context) } } - var policiesMedata = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata(); + var policiesMetadata = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata(); - if (policiesMedata != null) + if (policiesMetadata != null) { - foreach (var policy in policiesMedata.Policies) + foreach (var policy in policiesMetadata.Policies) { await policy.OnServeFromCacheAsync(context); } @@ -89,11 +89,11 @@ public async Task OnServeResponseAsync(IOutputCachingContext context) } } - var policiesMedata = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata(); + var policiesMetadata = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata(); - if (policiesMedata != null) + if (policiesMetadata != null) { - foreach (var policy in policiesMedata.Policies) + foreach (var policy in policiesMetadata.Policies) { await policy.OnServeResponseAsync(context); } diff --git a/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs b/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs index ef19cef99718..78a37e5e0fb2 100644 --- a/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs @@ -10,15 +10,21 @@ public class LockingPolicy : IOutputCachingPolicy { private readonly bool _lockResponse; - /// - /// Creates a new instance of . - /// - /// Whether to lock responses or not. - public LockingPolicy(bool lockResponse) + private LockingPolicy(bool lockResponse) { _lockResponse = lockResponse; } + /// + /// A policy that enables locking. + /// + public static LockingPolicy Enabled = new(true); + + /// + /// A policy that disabled locking/ + /// + public static LockingPolicy Disabled = new(false); + /// public Task OnRequestAsync(IOutputCachingContext context) { diff --git a/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt b/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt index a1531e3fdf6a..af250c4a157f 100644 --- a/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt @@ -16,7 +16,6 @@ Microsoft.AspNetCore.OutputCaching.ExpirationPolicy.OnRequestAsync(Microsoft.Asp Microsoft.AspNetCore.OutputCaching.ExpirationPolicy.OnServeFromCacheAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.OutputCaching.ExpirationPolicy.OnServeResponseAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.OutputCaching.LockingPolicy -Microsoft.AspNetCore.OutputCaching.LockingPolicy.LockingPolicy(bool lockResponse) -> void Microsoft.AspNetCore.OutputCaching.LockingPolicy.OnRequestAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.OutputCaching.LockingPolicy.OnServeFromCacheAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.OutputCaching.LockingPolicy.OnServeResponseAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! @@ -119,6 +118,8 @@ Microsoft.AspNetCore.OutputCaching.VaryByValuePolicy.VaryByValuePolicy(System.Fu Microsoft.Extensions.DependencyInjection.OutputCachingServicesExtensions static Microsoft.AspNetCore.Builder.OutputCachingExtensions.UseOutputCaching(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! static Microsoft.AspNetCore.OutputCaching.EnableCachingPolicy.Instance -> Microsoft.AspNetCore.OutputCaching.EnableCachingPolicy! +static Microsoft.AspNetCore.OutputCaching.LockingPolicy.Disabled -> Microsoft.AspNetCore.OutputCaching.LockingPolicy! +static Microsoft.AspNetCore.OutputCaching.LockingPolicy.Enabled -> Microsoft.AspNetCore.OutputCaching.LockingPolicy! static Microsoft.AspNetCore.OutputCaching.Policies.PolicyExtensions.OutputCache(this TBuilder builder) -> TBuilder static Microsoft.AspNetCore.OutputCaching.Policies.PolicyExtensions.OutputCache(this TBuilder builder, System.Action! policy) -> TBuilder static Microsoft.AspNetCore.OutputCaching.Policies.PolicyExtensions.OutputCache(this TBuilder builder, params Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy![]! items) -> TBuilder From d77fa70e66834b9c7bdc2f356ee9120504dc64e5 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Wed, 6 Apr 2022 10:26:54 -0700 Subject: [PATCH 14/48] Fix build --- src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs | 4 ++-- .../OutputCaching/src/Policies/OutputCachePolicyBuilder.cs | 2 +- src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs b/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs index 78a37e5e0fb2..257154491650 100644 --- a/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs @@ -18,12 +18,12 @@ private LockingPolicy(bool lockResponse) /// /// A policy that enables locking. /// - public static LockingPolicy Enabled = new(true); + public static readonly LockingPolicy Enabled = new(true); /// /// A policy that disabled locking/ /// - public static LockingPolicy Disabled = new(false); + public static readonly LockingPolicy Disabled = new(false); /// public Task OnRequestAsync(IOutputCachingContext context) diff --git a/src/Middleware/OutputCaching/src/Policies/OutputCachePolicyBuilder.cs b/src/Middleware/OutputCaching/src/Policies/OutputCachePolicyBuilder.cs index 7d30ae1903f3..35617e1b73c3 100644 --- a/src/Middleware/OutputCaching/src/Policies/OutputCachePolicyBuilder.cs +++ b/src/Middleware/OutputCaching/src/Policies/OutputCachePolicyBuilder.cs @@ -192,7 +192,7 @@ public OutputCachePolicyBuilder Expires(TimeSpan expiration) /// Whether the request should be locked. public OutputCachePolicyBuilder Lock(bool lockResponse = true) { - Policies.Add(new LockingPolicy(lockResponse)); + Policies.Add(lockResponse ? LockingPolicy.Enabled : LockingPolicy.Disabled); return this; } diff --git a/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt b/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt index af250c4a157f..0bbb2de3b35a 100644 --- a/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt @@ -118,10 +118,10 @@ Microsoft.AspNetCore.OutputCaching.VaryByValuePolicy.VaryByValuePolicy(System.Fu Microsoft.Extensions.DependencyInjection.OutputCachingServicesExtensions static Microsoft.AspNetCore.Builder.OutputCachingExtensions.UseOutputCaching(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! static Microsoft.AspNetCore.OutputCaching.EnableCachingPolicy.Instance -> Microsoft.AspNetCore.OutputCaching.EnableCachingPolicy! -static Microsoft.AspNetCore.OutputCaching.LockingPolicy.Disabled -> Microsoft.AspNetCore.OutputCaching.LockingPolicy! -static Microsoft.AspNetCore.OutputCaching.LockingPolicy.Enabled -> Microsoft.AspNetCore.OutputCaching.LockingPolicy! static Microsoft.AspNetCore.OutputCaching.Policies.PolicyExtensions.OutputCache(this TBuilder builder) -> TBuilder static Microsoft.AspNetCore.OutputCaching.Policies.PolicyExtensions.OutputCache(this TBuilder builder, System.Action! policy) -> TBuilder static Microsoft.AspNetCore.OutputCaching.Policies.PolicyExtensions.OutputCache(this TBuilder builder, params Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy![]! items) -> TBuilder static Microsoft.Extensions.DependencyInjection.OutputCachingServicesExtensions.AddOutputCaching(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.Extensions.DependencyInjection.OutputCachingServicesExtensions.AddOutputCaching(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static readonly Microsoft.AspNetCore.OutputCaching.LockingPolicy.Disabled -> Microsoft.AspNetCore.OutputCaching.LockingPolicy! +static readonly Microsoft.AspNetCore.OutputCaching.LockingPolicy.Enabled -> Microsoft.AspNetCore.OutputCaching.LockingPolicy! From c00ac9c92dad85008d9794d6543ed909e6bbc57d Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Wed, 6 Apr 2022 10:38:04 -0700 Subject: [PATCH 15/48] Add Path sample --- .../samples/OutputCachingSample/Startup.cs | 13 +++++++++---- .../OutputCaching/src/Memory/MemoryOutputCache.cs | 1 - 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs b/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs index d81985d25f2c..5b2f0862cde9 100644 --- a/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs +++ b/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs @@ -14,17 +14,18 @@ options.Policies.Add(new EnableCachingPolicy()); options.Profiles["NoCache"] = new OutputCachePolicyBuilder().NoStore().Build(); + + // Tag any request to "/blog** + options.Policies.Add(new OutputCachePolicyBuilder().Path("/blog").Tag("blog").Build()); }); var app = builder.Build(); app.UseOutputCaching(); -app.MapGet("/", Gravatar.WriteGravatar).OutputCache(x => x.Tag("home")); - -app.MapGet("/a", Gravatar.WriteGravatar).OutputCache(); +app.MapGet("/", Gravatar.WriteGravatar); -app.MapGet("/b", Gravatar.WriteGravatar); +app.MapGet("/cached", Gravatar.WriteGravatar).OutputCache(); app.MapGet("/nocache", Gravatar.WriteGravatar).OutputCache(x => x.NoStore()); @@ -32,6 +33,10 @@ app.MapGet("/attribute", [OutputCache(Profile = "NoCache")] (c) => Gravatar.WriteGravatar(c)); +// This is tagged with 'blog' +app.MapGet("/blog", Gravatar.WriteGravatar).OutputCache(); +app.MapGet("/blog/post/{id}", Gravatar.WriteGravatar).OutputCache(); + app.MapPost("/purge/{tag}", async (IOutputCacheStore cache, string tag) => { // POST such that the endpoint is not cached itself diff --git a/src/Middleware/OutputCaching/src/Memory/MemoryOutputCache.cs b/src/Middleware/OutputCaching/src/Memory/MemoryOutputCache.cs index 27acd42c143b..7f60ca5124c6 100644 --- a/src/Middleware/OutputCaching/src/Memory/MemoryOutputCache.cs +++ b/src/Middleware/OutputCaching/src/Memory/MemoryOutputCache.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Concurrent; -using System.Linq; using Microsoft.Extensions.Caching.Memory; namespace Microsoft.AspNetCore.OutputCaching.Memory; From 969f4d57ca538fcf854d7fd26e01e9ca0345de29 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Wed, 6 Apr 2022 10:50:21 -0700 Subject: [PATCH 16/48] Fix build --- src/Mvc/Mvc.Core/src/OutputCacheAttribute.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Mvc/Mvc.Core/src/OutputCacheAttribute.cs b/src/Mvc/Mvc.Core/src/OutputCacheAttribute.cs index 7aeb739bbebb..db515d6e6f61 100644 --- a/src/Mvc/Mvc.Core/src/OutputCacheAttribute.cs +++ b/src/Mvc/Mvc.Core/src/OutputCacheAttribute.cs @@ -55,7 +55,8 @@ public bool NoStore /// public int Order { get; set; } - public List Policies => _policies ??= GetPolicies(); + /// + public IReadOnlyList Policies => _policies ??= GetPolicies(); private List GetPolicies() { From 96d64d475aa48146dbe88fa5efb473fd0a358994 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Wed, 6 Apr 2022 11:03:03 -0700 Subject: [PATCH 17/48] Update public api --- src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt index 7b5bb219c05b..dbf2f58a27f6 100644 --- a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt +++ b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt @@ -21,7 +21,7 @@ Microsoft.AspNetCore.Mvc.OutputCacheAttribute.NoStore.set -> void Microsoft.AspNetCore.Mvc.OutputCacheAttribute.Order.get -> int Microsoft.AspNetCore.Mvc.OutputCacheAttribute.Order.set -> void Microsoft.AspNetCore.Mvc.OutputCacheAttribute.OutputCacheAttribute() -> void -Microsoft.AspNetCore.Mvc.OutputCacheAttribute.Policies.get -> System.Collections.Generic.List! +Microsoft.AspNetCore.Mvc.OutputCacheAttribute.Policies.get -> System.Collections.Generic.IReadOnlyList! Microsoft.AspNetCore.Mvc.OutputCacheAttribute.Profile.get -> string? Microsoft.AspNetCore.Mvc.OutputCacheAttribute.Profile.set -> void Microsoft.AspNetCore.Mvc.OutputCacheAttribute.VaryByQueryKeys.get -> string![]? From 59fb7bc7bbf4261c40c2a08bc9dd53d63ba84732 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Thu, 14 Apr 2022 15:28:32 -0700 Subject: [PATCH 18/48] Fix typos [skip ci] --- .../src/IOutputCachingContext.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingContext.cs b/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingContext.cs index 34a28fd91c73..253e1aad78ee 100644 --- a/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingContext.cs +++ b/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingContext.cs @@ -72,37 +72,37 @@ public interface IOutputCachingContext ILogger Logger { get; } /// - /// Determine whether the output caching logic should is configured for the incoming HTTP request. + /// Determines whether the output caching logic should be configured for the incoming HTTP request. /// bool EnableOutputCaching { get; set; } /// - /// Determine whether the output caching logic should be attempted for the incoming HTTP request. + /// Determines whether the output caching logic should be attempted for the incoming HTTP request. /// bool AttemptResponseCaching { get; set; } /// - /// Determine whether a cache lookup is allowed for the incoming HTTP request. + /// Determines whether a cache lookup is allowed for the incoming HTTP request. /// bool AllowCacheLookup { get; set; } /// - /// Determine whether storage of the response is allowed for the incoming HTTP request. + /// Determines whether storage of the response is allowed for the incoming HTTP request. /// bool AllowCacheStorage { get; set; } /// - /// Determine whether request should be locked. + /// Determines whether the request should be locked. /// bool AllowLocking { get; set; } /// - /// Determine whether the response received by the middleware can be cached for future requests. + /// Determines whether the response received by the middleware can be cached for future requests. /// bool IsResponseCacheable { get; set; } /// - /// Determine whether the response retrieved from the response cache is fresh and can be served. + /// Determines whether the response retrieved from the response cache is fresh and can be served. /// bool IsCacheEntryFresh { get; set; } } From 7d4f1a2c54a0183b7139844bcd1f8c25f13ba008 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Wed, 20 Apr 2022 13:24:46 -0700 Subject: [PATCH 19/48] Update sample [skip ci] --- .../samples/OutputCachingSample/Startup.cs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs b/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs index 5b2f0862cde9..bcaace5d701a 100644 --- a/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs +++ b/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs @@ -1,22 +1,20 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Globalization; using Microsoft.AspNetCore.OutputCaching; using Microsoft.AspNetCore.OutputCaching.Policies; -using Microsoft.Net.Http.Headers; var builder = WebApplication.CreateBuilder(args); builder.Services.AddOutputCaching(options => { - // options.Policies.Clear(); options.Policies.Add(new EnableCachingPolicy()); options.Profiles["NoCache"] = new OutputCachePolicyBuilder().NoStore().Build(); // Tag any request to "/blog** options.Policies.Add(new OutputCachePolicyBuilder().Path("/blog").Tag("blog").Build()); + }); var app = builder.Build(); @@ -33,9 +31,8 @@ app.MapGet("/attribute", [OutputCache(Profile = "NoCache")] (c) => Gravatar.WriteGravatar(c)); -// This is tagged with 'blog' -app.MapGet("/blog", Gravatar.WriteGravatar).OutputCache(); -app.MapGet("/blog/post/{id}", Gravatar.WriteGravatar).OutputCache(); +app.MapGet("/blog", Gravatar.WriteGravatar); +app.MapGet("/blog/post/{id}", Gravatar.WriteGravatar); app.MapPost("/purge/{tag}", async (IOutputCacheStore cache, string tag) => { @@ -62,8 +59,8 @@ app.MapGet("/headers", async context => { // From a browser this endpoint won't be cached because of max-age: 0 - context.Response.Headers.CacheControl = CacheControlHeaderValue.PublicString; - await context.Response.WriteAsync("Headers " + DateTime.UtcNow.ToString("o")); + context.Response.Headers.CacheControl = "public"; + await Gravatar.WriteGravatar(context); }).OutputCache(new ResponseCachingPolicy()); // Etag @@ -75,7 +72,7 @@ var etag = $"\"{Guid.NewGuid().ToString("n")}\""; context.Response.Headers.ETag = etag; - await context.Response.WriteAsync("Hello"); + await Gravatar.WriteGravatar(context); }).OutputCache(); // When the request header If-Modified-Since is provided, return 304 if the cached entry is older From 9d0aef121db4edd76e7c079bd7363e5116b94fbf Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Wed, 20 Apr 2022 13:35:20 -0700 Subject: [PATCH 20/48] Fix build --- .../OutputCaching/samples/OutputCachingSample/Startup.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs b/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs index bcaace5d701a..07b096e6203c 100644 --- a/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs +++ b/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Globalization; using Microsoft.AspNetCore.OutputCaching; using Microsoft.AspNetCore.OutputCaching.Policies; From 69739e6a5a3f1017653cdada4611126796b5b1fc Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Tue, 17 May 2022 10:42:43 -0700 Subject: [PATCH 21/48] API cleaning and test project --- AspNetCore.sln | 19 + .../src/CachedVaryByRules.cs | 5 +- .../src/IOutputCache.cs | 4 +- ...tputCacheEntry.cs => IOutputCacheEntry.cs} | 6 +- .../src/IOutputCachingContext.cs | 2 +- .../src/IOutputCachingPolicyProvider.cs | 9 +- .../src/PublicAPI.Unshipped.txt | 35 +- .../OutputCaching/OutputCaching.slnf | 3 +- .../OutputCaching/src/CacheEntryHelpers.cs | 2 +- .../Interfaces/IOutputCachingKeyProvider.cs | 6 +- .../src/Interfaces/ISystemClock.cs | 2 +- ...tputCache.cs => MemoryOutputCacheStore.cs} | 8 +- .../OutputCaching/src/OutputCacheEntry.cs | 25 + .../OutputCaching/src/OutputCachingContext.cs | 5 +- .../src/OutputCachingKeyProvider.cs | 2 +- .../src/OutputCachingMiddleware.cs | 65 +- .../src/Policies/DefaultOutputCachePolicy.cs | 2 +- .../src/Policies/ResponseCachingPolicy.cs | 2 +- .../src/Policies/VaryByValuePolicy.cs | 4 +- .../src/Streams/SegmentWriteStream.cs | 8 +- .../OutputCaching/src/SystemClock.cs | 2 +- .../test/CachedResponseBodyTests.cs | 232 ++--- ...oft.AspNetCore.OutputCaching.Tests.csproj} | 2 +- .../test/OutputCachingKeyProviderTests.cs | 214 ++++ .../test/OutputCachingMiddlewareTests.cs | 918 +++++++++++++++++ .../test/ResponseCachingFeatureTests.cs | 54 - .../test/ResponseCachingKeyProviderTests.cs | 214 ---- .../test/ResponseCachingMiddlewareTests.cs | 966 ------------------ .../test/SegmentWriteStreamTests.cs | 204 ++-- .../OutputCaching/test/TestUtils.cs | 812 +++++++-------- 30 files changed, 1898 insertions(+), 1934 deletions(-) rename src/Middleware/OutputCaching.Abstractions/src/{OutputCacheEntry.cs => IOutputCacheEntry.cs} (84%) rename src/Middleware/OutputCaching/src/Memory/{MemoryOutputCache.cs => MemoryOutputCacheStore.cs} (89%) create mode 100644 src/Middleware/OutputCaching/src/OutputCacheEntry.cs rename src/Middleware/OutputCaching/test/{Microsoft.AspNetCore.ResponseCaching.Tests.csproj => Microsoft.AspNetCore.OutputCaching.Tests.csproj} (87%) create mode 100644 src/Middleware/OutputCaching/test/OutputCachingKeyProviderTests.cs create mode 100644 src/Middleware/OutputCaching/test/OutputCachingMiddlewareTests.cs delete mode 100644 src/Middleware/OutputCaching/test/ResponseCachingFeatureTests.cs delete mode 100644 src/Middleware/OutputCaching/test/ResponseCachingKeyProviderTests.cs delete mode 100644 src/Middleware/OutputCaching/test/ResponseCachingMiddlewareTests.cs diff --git a/AspNetCore.sln b/AspNetCore.sln index 8019cd3e265c..8453a698e181 100644 --- a/AspNetCore.sln +++ b/AspNetCore.sln @@ -1706,6 +1706,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "BuildAfterTargetingPack", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BuildAfterTargetingPack", "src\BuildAfterTargetingPack\BuildAfterTargetingPack.csproj", "{8FED7E65-A7DD-4F13-8980-BF03E77B6C85}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.OutputCaching.Tests", "src\Middleware\OutputCaching\test\Microsoft.AspNetCore.OutputCaching.Tests.csproj", "{046F43BC-BEE4-48B7-8C09-ED0A1054A2D7}" +EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ResultsOfTGenerator", "src\Http\Http.Results\tools\ResultsOfTGenerator\ResultsOfTGenerator.csproj", "{9716D0D0-2251-44DD-8596-67D253EEF41C}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "OpenApi", "OpenApi", "{2299CCD8-8F9C-4F2B-A633-9BF4DA81022B}" @@ -10211,6 +10213,22 @@ Global {8FED7E65-A7DD-4F13-8980-BF03E77B6C85}.Release|x64.Build.0 = Release|Any CPU {8FED7E65-A7DD-4F13-8980-BF03E77B6C85}.Release|x86.ActiveCfg = Release|Any CPU {8FED7E65-A7DD-4F13-8980-BF03E77B6C85}.Release|x86.Build.0 = Release|Any CPU + {046F43BC-BEE4-48B7-8C09-ED0A1054A2D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {046F43BC-BEE4-48B7-8C09-ED0A1054A2D7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {046F43BC-BEE4-48B7-8C09-ED0A1054A2D7}.Debug|arm64.ActiveCfg = Debug|Any CPU + {046F43BC-BEE4-48B7-8C09-ED0A1054A2D7}.Debug|arm64.Build.0 = Debug|Any CPU + {046F43BC-BEE4-48B7-8C09-ED0A1054A2D7}.Debug|x64.ActiveCfg = Debug|Any CPU + {046F43BC-BEE4-48B7-8C09-ED0A1054A2D7}.Debug|x64.Build.0 = Debug|Any CPU + {046F43BC-BEE4-48B7-8C09-ED0A1054A2D7}.Debug|x86.ActiveCfg = Debug|Any CPU + {046F43BC-BEE4-48B7-8C09-ED0A1054A2D7}.Debug|x86.Build.0 = Debug|Any CPU + {046F43BC-BEE4-48B7-8C09-ED0A1054A2D7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {046F43BC-BEE4-48B7-8C09-ED0A1054A2D7}.Release|Any CPU.Build.0 = Release|Any CPU + {046F43BC-BEE4-48B7-8C09-ED0A1054A2D7}.Release|arm64.ActiveCfg = Release|Any CPU + {046F43BC-BEE4-48B7-8C09-ED0A1054A2D7}.Release|arm64.Build.0 = Release|Any CPU + {046F43BC-BEE4-48B7-8C09-ED0A1054A2D7}.Release|x64.ActiveCfg = Release|Any CPU + {046F43BC-BEE4-48B7-8C09-ED0A1054A2D7}.Release|x64.Build.0 = Release|Any CPU + {046F43BC-BEE4-48B7-8C09-ED0A1054A2D7}.Release|x86.ActiveCfg = Release|Any CPU + {046F43BC-BEE4-48B7-8C09-ED0A1054A2D7}.Release|x86.Build.0 = Release|Any CPU {9716D0D0-2251-44DD-8596-67D253EEF41C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {9716D0D0-2251-44DD-8596-67D253EEF41C}.Debug|Any CPU.Build.0 = Debug|Any CPU {9716D0D0-2251-44DD-8596-67D253EEF41C}.Debug|arm64.ActiveCfg = Debug|Any CPU @@ -11168,6 +11186,7 @@ Global {B7DAA48B-8E5E-4A5D-9FEB-E6D49AE76A04} = {41BB7BA4-AC08-4E9A-83EA-6D587A5B951C} {489020F2-80D9-4468-A5D3-07E785837A5D} = {017429CC-C5FB-48B4-9C46-034E29EE2F06} {8FED7E65-A7DD-4F13-8980-BF03E77B6C85} = {489020F2-80D9-4468-A5D3-07E785837A5D} + {046F43BC-BEE4-48B7-8C09-ED0A1054A2D7} = {AA5ABFBC-177C-421E-B743-005E0FD1248B} {9716D0D0-2251-44DD-8596-67D253EEF41C} = {323C3EB6-1D15-4B3D-918D-699D7F64DED9} {2299CCD8-8F9C-4F2B-A633-9BF4DA81022B} = {017429CC-C5FB-48B4-9C46-034E29EE2F06} {3AEFB466-6310-4F3F-923F-9154224E3629} = {2299CCD8-8F9C-4F2B-A633-9BF4DA81022B} diff --git a/src/Middleware/OutputCaching.Abstractions/src/CachedVaryByRules.cs b/src/Middleware/OutputCaching.Abstractions/src/CachedVaryByRules.cs index 6279d3963849..df0083dc047d 100644 --- a/src/Middleware/OutputCaching.Abstractions/src/CachedVaryByRules.cs +++ b/src/Middleware/OutputCaching.Abstractions/src/CachedVaryByRules.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Collections.Immutable; using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.OutputCaching; @@ -11,9 +12,9 @@ namespace Microsoft.AspNetCore.OutputCaching; public class CachedVaryByRules { /// - /// Returns a dictionary of custom values to vary by. + /// Gets the custom values to vary by. /// - public Dictionary VaryByCustom { get; } = new(StringComparer.OrdinalIgnoreCase); + public IDictionary VaryByCustom { get; } = ImmutableDictionary.Empty; /// /// Gets or sets the list of headers to vary by. diff --git a/src/Middleware/OutputCaching.Abstractions/src/IOutputCache.cs b/src/Middleware/OutputCaching.Abstractions/src/IOutputCache.cs index 1ed593643e1e..57ea7f192451 100644 --- a/src/Middleware/OutputCaching.Abstractions/src/IOutputCache.cs +++ b/src/Middleware/OutputCaching.Abstractions/src/IOutputCache.cs @@ -20,7 +20,7 @@ public interface IOutputCacheStore /// /// The cache key to look up. /// The response cache entry if it exists; otherwise null. - ValueTask GetAsync(string key); + ValueTask GetAsync(string key); /// /// Stores the given response in the response cache. @@ -28,5 +28,5 @@ public interface IOutputCacheStore /// The cache key to store the response under. /// The response cache entry to store. /// The amount of time the entry will be kept in the cache before expiring, relative to now. - ValueTask SetAsync(string key, OutputCacheEntry entry, TimeSpan validFor); + ValueTask SetAsync(string key, IOutputCacheEntry entry, TimeSpan validFor); } diff --git a/src/Middleware/OutputCaching.Abstractions/src/OutputCacheEntry.cs b/src/Middleware/OutputCaching.Abstractions/src/IOutputCacheEntry.cs similarity index 84% rename from src/Middleware/OutputCaching.Abstractions/src/OutputCacheEntry.cs rename to src/Middleware/OutputCaching.Abstractions/src/IOutputCacheEntry.cs index 3d7053487d40..7a70495e1d48 100644 --- a/src/Middleware/OutputCaching.Abstractions/src/OutputCacheEntry.cs +++ b/src/Middleware/OutputCaching.Abstractions/src/IOutputCacheEntry.cs @@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.OutputCaching; /// /// Represents a cached response entry. /// -public class OutputCacheEntry +public interface IOutputCacheEntry { /// /// Gets the created date and time of the cache entry. @@ -23,12 +23,12 @@ public class OutputCacheEntry /// /// Gets the headers of the cache entry. /// - public IHeaderDictionary Headers { get; set; } = default!; + public IHeaderDictionary Headers { get; set; } /// /// Gets the body of the cache entry. /// - public CachedResponseBody Body { get; set; } = default!; + public CachedResponseBody Body { get; set; } /// /// Gets the tags of the cache entry. diff --git a/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingContext.cs b/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingContext.cs index 253e1aad78ee..0f5f969a8338 100644 --- a/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingContext.cs +++ b/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingContext.cs @@ -79,7 +79,7 @@ public interface IOutputCachingContext /// /// Determines whether the output caching logic should be attempted for the incoming HTTP request. /// - bool AttemptResponseCaching { get; set; } + bool AttemptOutputCaching { get; set; } /// /// Determines whether a cache lookup is allowed for the incoming HTTP request. diff --git a/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingPolicyProvider.cs b/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingPolicyProvider.cs index 7f58a79d3887..23cde9db7023 100644 --- a/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingPolicyProvider.cs +++ b/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingPolicyProvider.cs @@ -9,23 +9,20 @@ namespace Microsoft.AspNetCore.OutputCaching; public interface IOutputCachingPolicyProvider { /// - /// Determine whether the response caching logic should be attempted for the incoming HTTP request. + /// Determines whether the response caching logic should be attempted for the incoming HTTP request. /// /// The . - /// true if response caching logic should be attempted; otherwise false. Task OnRequestAsync(IOutputCachingContext context); /// - /// Determine whether the response retrieved from the response cache is fresh and can be served. + /// Determines whether the response retrieved from the response cache is fresh and can be served. /// /// The . - /// true if cache lookup for this cache entry is allowed; otherwise false. Task OnServeFromCacheAsync(IOutputCachingContext context); /// - /// Determine whether the response can be cached for future requests. + /// Determines whether the response can be cached for future requests. /// /// The . - /// true if cache lookup for this request is allowed; otherwise false. Task OnServeResponseAsync(IOutputCachingContext context); } diff --git a/src/Middleware/OutputCaching.Abstractions/src/PublicAPI.Unshipped.txt b/src/Middleware/OutputCaching.Abstractions/src/PublicAPI.Unshipped.txt index af8e0ecb1e09..f46498d57cdc 100644 --- a/src/Middleware/OutputCaching.Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/OutputCaching.Abstractions/src/PublicAPI.Unshipped.txt @@ -9,13 +9,24 @@ Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.Headers.get -> Microsoft.Ex Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.Headers.set -> void Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.QueryKeys.get -> Microsoft.Extensions.Primitives.StringValues Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.QueryKeys.set -> void -Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.VaryByCustom.get -> System.Collections.Generic.Dictionary! +Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.VaryByCustom.get -> System.Collections.Generic.IDictionary! Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.VaryByPrefix.get -> Microsoft.Extensions.Primitives.StringValues Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.VaryByPrefix.set -> void +Microsoft.AspNetCore.OutputCaching.IOutputCacheEntry +Microsoft.AspNetCore.OutputCaching.IOutputCacheEntry.Body.get -> Microsoft.AspNetCore.OutputCaching.CachedResponseBody! +Microsoft.AspNetCore.OutputCaching.IOutputCacheEntry.Body.set -> void +Microsoft.AspNetCore.OutputCaching.IOutputCacheEntry.Created.get -> System.DateTimeOffset +Microsoft.AspNetCore.OutputCaching.IOutputCacheEntry.Created.set -> void +Microsoft.AspNetCore.OutputCaching.IOutputCacheEntry.Headers.get -> Microsoft.AspNetCore.Http.IHeaderDictionary! +Microsoft.AspNetCore.OutputCaching.IOutputCacheEntry.Headers.set -> void +Microsoft.AspNetCore.OutputCaching.IOutputCacheEntry.StatusCode.get -> int +Microsoft.AspNetCore.OutputCaching.IOutputCacheEntry.StatusCode.set -> void +Microsoft.AspNetCore.OutputCaching.IOutputCacheEntry.Tags.get -> string![]? +Microsoft.AspNetCore.OutputCaching.IOutputCacheEntry.Tags.set -> void Microsoft.AspNetCore.OutputCaching.IOutputCacheStore Microsoft.AspNetCore.OutputCaching.IOutputCacheStore.EvictByTagAsync(string! tag) -> System.Threading.Tasks.ValueTask -Microsoft.AspNetCore.OutputCaching.IOutputCacheStore.GetAsync(string! key) -> System.Threading.Tasks.ValueTask -Microsoft.AspNetCore.OutputCaching.IOutputCacheStore.SetAsync(string! key, Microsoft.AspNetCore.OutputCaching.OutputCacheEntry! entry, System.TimeSpan validFor) -> System.Threading.Tasks.ValueTask +Microsoft.AspNetCore.OutputCaching.IOutputCacheStore.GetAsync(string! key) -> System.Threading.Tasks.ValueTask +Microsoft.AspNetCore.OutputCaching.IOutputCacheStore.SetAsync(string! key, Microsoft.AspNetCore.OutputCaching.IOutputCacheEntry! entry, System.TimeSpan validFor) -> System.Threading.Tasks.ValueTask Microsoft.AspNetCore.OutputCaching.IOutputCachingContext Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.AllowCacheLookup.get -> bool Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.AllowCacheLookup.set -> void @@ -23,8 +34,8 @@ Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.AllowCacheStorage.get - Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.AllowCacheStorage.set -> void Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.AllowLocking.get -> bool Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.AllowLocking.set -> void -Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.AttemptResponseCaching.get -> bool -Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.AttemptResponseCaching.set -> void +Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.AttemptOutputCaching.get -> bool +Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.AttemptOutputCaching.set -> void Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.CachedEntryAge.get -> System.TimeSpan? Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.CachedResponseHeaders.get -> Microsoft.AspNetCore.Http.IHeaderDictionary! Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.CachedVaryByRules.get -> Microsoft.AspNetCore.OutputCaching.CachedVaryByRules! @@ -56,16 +67,4 @@ Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicyProvider.OnRequestAsync(M Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicyProvider.OnServeFromCacheAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicyProvider.OnServeResponseAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.OutputCaching.IPoliciesMetadata -Microsoft.AspNetCore.OutputCaching.IPoliciesMetadata.Policies.get -> System.Collections.Generic.IReadOnlyList! -Microsoft.AspNetCore.OutputCaching.OutputCacheEntry -Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.Body.get -> Microsoft.AspNetCore.OutputCaching.CachedResponseBody! -Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.Body.set -> void -Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.Created.get -> System.DateTimeOffset -Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.Created.set -> void -Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.Headers.get -> Microsoft.AspNetCore.Http.IHeaderDictionary! -Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.Headers.set -> void -Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.OutputCacheEntry() -> void -Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.StatusCode.get -> int -Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.StatusCode.set -> void -Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.Tags.get -> string![]? -Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.Tags.set -> void \ No newline at end of file +Microsoft.AspNetCore.OutputCaching.IPoliciesMetadata.Policies.get -> System.Collections.Generic.IReadOnlyList! \ No newline at end of file diff --git a/src/Middleware/OutputCaching/OutputCaching.slnf b/src/Middleware/OutputCaching/OutputCaching.slnf index 324e210c9722..983dc206f0bd 100644 --- a/src/Middleware/OutputCaching/OutputCaching.slnf +++ b/src/Middleware/OutputCaching/OutputCaching.slnf @@ -4,7 +4,8 @@ "projects": [ "src\\Middleware\\OutputCaching.Abstractions\\src\\Microsoft.AspNetCore.OutputCaching.Abstractions.csproj", "src\\Middleware\\OutputCaching\\samples\\OutputCachingSample\\OutputCachingSample.csproj", - "src\\Middleware\\OutputCaching\\src\\Microsoft.AspNetCore.OutputCaching.csproj" + "src\\Middleware\\OutputCaching\\src\\Microsoft.AspNetCore.OutputCaching.csproj", + "src\\Middleware\\OutputCaching\\test\\Microsoft.AspNetCore.OutputCaching.Tests.csproj" ] } } \ No newline at end of file diff --git a/src/Middleware/OutputCaching/src/CacheEntryHelpers.cs b/src/Middleware/OutputCaching/src/CacheEntryHelpers.cs index e7fdcf1bcbf3..bb6fcda43a18 100644 --- a/src/Middleware/OutputCaching/src/CacheEntryHelpers.cs +++ b/src/Middleware/OutputCaching/src/CacheEntryHelpers.cs @@ -7,7 +7,7 @@ namespace Microsoft.AspNetCore.OutputCaching; internal static class CacheEntryHelpers { - internal static long EstimateCachedResponseSize(OutputCacheEntry cachedResponse) + internal static long EstimateCachedResponseSize(IOutputCacheEntry cachedResponse) { if (cachedResponse == null) { diff --git a/src/Middleware/OutputCaching/src/Interfaces/IOutputCachingKeyProvider.cs b/src/Middleware/OutputCaching/src/Interfaces/IOutputCachingKeyProvider.cs index d5cf14de996a..9d16e44b794e 100644 --- a/src/Middleware/OutputCaching/src/Interfaces/IOutputCachingKeyProvider.cs +++ b/src/Middleware/OutputCaching/src/Interfaces/IOutputCachingKeyProvider.cs @@ -6,9 +6,9 @@ namespace Microsoft.AspNetCore.OutputCaching; internal interface IOutputCachingKeyProvider { /// - /// Create a vary key for storing cached responses. + /// Create a key for storing cached responses. /// /// The . - /// The created vary key. - string CreateStorageVaryByKey(OutputCachingContext context); + /// The created key. + string CreateStorageKey(OutputCachingContext context); } diff --git a/src/Middleware/OutputCaching/src/Interfaces/ISystemClock.cs b/src/Middleware/OutputCaching/src/Interfaces/ISystemClock.cs index 330e860028cf..8e2ead45b9d7 100644 --- a/src/Middleware/OutputCaching/src/Interfaces/ISystemClock.cs +++ b/src/Middleware/OutputCaching/src/Interfaces/ISystemClock.cs @@ -11,5 +11,5 @@ internal interface ISystemClock /// /// Retrieves the current system time in UTC. /// - DateTime UtcNow { get; } + DateTimeOffset UtcNow { get; } } diff --git a/src/Middleware/OutputCaching/src/Memory/MemoryOutputCache.cs b/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs similarity index 89% rename from src/Middleware/OutputCaching/src/Memory/MemoryOutputCache.cs rename to src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs index 7f60ca5124c6..19e0e6bef1f9 100644 --- a/src/Middleware/OutputCaching/src/Memory/MemoryOutputCache.cs +++ b/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs @@ -31,7 +31,7 @@ public ValueTask EvictByTagAsync(string tag) return ValueTask.CompletedTask; } - public ValueTask GetAsync(string key) + public ValueTask GetAsync(string key) { var entry = _cache.Get(key); @@ -46,13 +46,13 @@ public ValueTask EvictByTagAsync(string tag) Tags = memoryCachedResponse.Tags }; - return ValueTask.FromResult(outputCacheEntry); + return ValueTask.FromResult(outputCacheEntry); } - return ValueTask.FromResult(default(OutputCacheEntry)); + return ValueTask.FromResult(default(IOutputCacheEntry)); } - public ValueTask SetAsync(string key, OutputCacheEntry cachedResponse, TimeSpan validFor) + public ValueTask SetAsync(string key, IOutputCacheEntry cachedResponse, TimeSpan validFor) { if (cachedResponse.Tags != null) { diff --git a/src/Middleware/OutputCaching/src/OutputCacheEntry.cs b/src/Middleware/OutputCaching/src/OutputCacheEntry.cs new file mode 100644 index 000000000000..18e6e6fcb069 --- /dev/null +++ b/src/Middleware/OutputCaching/src/OutputCacheEntry.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.OutputCaching; + +/// +internal class OutputCacheEntry : IOutputCacheEntry +{ + /// + public DateTimeOffset Created { get; set; } + + /// + public int StatusCode { get; set; } + + /// + public IHeaderDictionary Headers { get; set; } = default!; + + /// + public CachedResponseBody Body { get; set; } = default!; + + /// + public string[]? Tags { get; set; } +} diff --git a/src/Middleware/OutputCaching/src/OutputCachingContext.cs b/src/Middleware/OutputCaching/src/OutputCachingContext.cs index 4c5b96eac413..0be6004c6b4a 100644 --- a/src/Middleware/OutputCaching/src/OutputCachingContext.cs +++ b/src/Middleware/OutputCaching/src/OutputCachingContext.cs @@ -34,7 +34,7 @@ internal OutputCachingContext(HttpContext httpContext, ILogger logger) /// /// Determine whether the response caching logic should be attempted for the incoming HTTP request. /// - public bool AttemptResponseCaching { get; set; } + public bool AttemptOutputCaching { get; set; } /// /// Determine whether a cache lookup is allowed for the incoming HTTP request. @@ -68,6 +68,7 @@ internal OutputCachingContext(HttpContext httpContext, ILogger logger) public TimeSpan? CachedEntryAge { get; internal set; } public CachedVaryByRules CachedVaryByRules { get; set; } = new(); + public HashSet Tags { get; } = new(); public ILogger Logger { get; } @@ -76,7 +77,7 @@ internal OutputCachingContext(HttpContext httpContext, ILogger logger) internal TimeSpan CachedResponseValidFor { get; set; } - internal OutputCacheEntry CachedResponse { get; set; } + internal IOutputCacheEntry CachedResponse { get; set; } internal bool ResponseStarted { get; set; } diff --git a/src/Middleware/OutputCaching/src/OutputCachingKeyProvider.cs b/src/Middleware/OutputCaching/src/OutputCachingKeyProvider.cs index c0eb13593079..9e7fa502150c 100644 --- a/src/Middleware/OutputCaching/src/OutputCachingKeyProvider.cs +++ b/src/Middleware/OutputCaching/src/OutputCachingKeyProvider.cs @@ -29,7 +29,7 @@ internal OutputCachingKeyProvider(ObjectPoolProvider poolProvider, IOptionsSCHEMEHOST:PORT/PATHBASE/PATHHHeaderName=HeaderValueQQueryName=QueryValue1QueryValue2 - public string CreateStorageVaryByKey(OutputCachingContext context) + public string CreateStorageKey(OutputCachingContext context) { ArgumentNullException.ThrowIfNull(_builderPool, nameof(context)); diff --git a/src/Middleware/OutputCaching/src/OutputCachingMiddleware.cs b/src/Middleware/OutputCaching/src/OutputCachingMiddleware.cs index 32bd8db65242..bc7f7e6afaa5 100644 --- a/src/Middleware/OutputCaching/src/OutputCachingMiddleware.cs +++ b/src/Middleware/OutputCaching/src/OutputCachingMiddleware.cs @@ -26,8 +26,8 @@ public class OutputCachingMiddleware private readonly IOutputCachingPolicyProvider _policyProvider; private readonly IOutputCacheStore _cache; private readonly IOutputCachingKeyProvider _keyProvider; - private readonly WorkDispatcher _outputCacheEntryDispatcher; - private readonly WorkDispatcher _requestDispatcher; + private readonly WorkDispatcher _outputCacheEntryDispatcher; + private readonly WorkDispatcher _requestDispatcher; /// /// Creates a new . @@ -96,20 +96,12 @@ public async Task Invoke(HttpContext httpContext) await _policyProvider.OnRequestAsync(context); // Should we attempt any caching logic? - if (context.EnableOutputCaching && context.AttemptResponseCaching) + if (context.EnableOutputCaching && context.AttemptOutputCaching) { // Can this request be served from cache? if (context.AllowCacheLookup) { - CreateCacheKey(context); - - // Locking cache lookups by default - // TODO: should it be part of the cache implementations or can we assume all caches would benefit from it? - // It makes sense for caches that use IO (disk, network) or need to deserialize the state but could also be a global option - - var cacheEntry = await _outputCacheEntryDispatcher.ScheduleAsync(context.CacheKey, _cache, static async (key, cache) => await cache.GetAsync(key)); - - if (await TryServeFromCacheAsync(context, cacheEntry)) + if (await TryServeFromCacheAsync(context)) { return; } @@ -131,7 +123,7 @@ public async Task Invoke(HttpContext httpContext) // If the result was processed by another request, serve it from cache if (!executed) { - if (await TryServeFromCacheAsync(context, cacheEntry)) + if (await TryServeFromCacheAsync(context)) { return; } @@ -142,7 +134,7 @@ public async Task Invoke(HttpContext httpContext) await ExecuteResponseAsync(); } - async Task ExecuteResponseAsync() + async Task ExecuteResponseAsync() { // Hook up to listen to the response stream ShimResponseStream(context); @@ -183,10 +175,15 @@ public async Task Invoke(HttpContext httpContext) } } - internal async Task TryServeCachedResponseAsync(OutputCachingContext context, OutputCacheEntry cachedResponse) + internal async Task TryServeCachedResponseAsync(OutputCachingContext context, IOutputCacheEntry? cacheEntry) { - context.CachedResponse = cachedResponse; - context.CachedResponseHeaders = cachedResponse.Headers; + if (cacheEntry == null) + { + return false; + } + + context.CachedResponse = cacheEntry; + context.CachedResponseHeaders = context.CachedResponse.Headers; context.ResponseTime = _options.SystemClock.UtcNow; var cachedEntryAge = context.ResponseTime.Value - context.CachedResponse.Created; context.CachedEntryAge = cachedEntryAge > TimeSpan.Zero ? cachedEntryAge : TimeSpan.Zero; @@ -248,14 +245,19 @@ internal async Task TryServeCachedResponseAsync(OutputCachingContext conte return false; } - internal async Task TryServeFromCacheAsync(OutputCachingContext context, OutputCacheEntry? cacheEntry) + internal async Task TryServeFromCacheAsync(OutputCachingContext context) { - if (cacheEntry != null) + CreateCacheKey(context); + + // Locking cache lookups by default + // TODO: should it be part of the cache implementations or can we assume all caches would benefit from it? + // It makes sense for caches that use IO (disk, network) or need to deserialize the state but could also be a global option + + var cacheEntry = await _outputCacheEntryDispatcher.ScheduleAsync(context.CacheKey, _cache, static async (key, cache) => await cache.GetAsync(key)); + + if (await TryServeCachedResponseAsync(context, cacheEntry)) { - if (await TryServeCachedResponseAsync(context, cacheEntry)) - { - return true; - } + return true; } if (HeaderUtilities.ContainsCacheDirective(context.HttpContext.Request.Headers.CacheControl, CacheControlHeaderValue.OnlyIfCachedString)) @@ -300,14 +302,14 @@ private void CreateCacheKey(OutputCachingContext context) } } - context.CacheKey = _keyProvider.CreateStorageVaryByKey(context); + context.CacheKey = _keyProvider.CreateStorageKey(context); } /// /// Finalize cache headers. /// /// - private void FinalizeCacheHeaders(OutputCachingContext context) + internal void FinalizeCacheHeaders(OutputCachingContext context) { if (context.IsResponseCacheable) { @@ -505,7 +507,11 @@ internal static bool ContentIsNotModified(OutputCachingContext context) // Normalize order and casing internal static StringValues GetOrderCasingNormalizedStringValues(StringValues stringValues) { - if (stringValues.Count == 1) + if (stringValues.Count == 0) + { + return StringValues.Empty; + } + else if (stringValues.Count == 1) { return new StringValues(stringValues.ToString().ToUpperInvariant()); } @@ -526,10 +532,15 @@ internal static StringValues GetOrderCasingNormalizedStringValues(StringValues s } } - internal static StringValues GetOrderCasingNormalizedDictionary(Dictionary dictionary) + internal static StringValues GetOrderCasingNormalizedDictionary(IDictionary dictionary) { const char KeySubDelimiter = '\x1f'; + if (dictionary.Count == 0) + { + return StringValues.Empty; + } + var newArray = new string[dictionary.Count]; var i = 0; diff --git a/src/Middleware/OutputCaching/src/Policies/DefaultOutputCachePolicy.cs b/src/Middleware/OutputCaching/src/Policies/DefaultOutputCachePolicy.cs index f0ee8568cd00..e67de21ee518 100644 --- a/src/Middleware/OutputCaching/src/Policies/DefaultOutputCachePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/DefaultOutputCachePolicy.cs @@ -14,7 +14,7 @@ public sealed class DefaultOutputCachePolicy : IOutputCachingPolicy /// public Task OnRequestAsync(IOutputCachingContext context) { - context.AttemptResponseCaching = AttemptOutputCaching(context); + context.AttemptOutputCaching = AttemptOutputCaching(context); context.AllowCacheLookup = true; context.AllowCacheStorage = true; context.AllowLocking = true; diff --git a/src/Middleware/OutputCaching/src/Policies/ResponseCachingPolicy.cs b/src/Middleware/OutputCaching/src/Policies/ResponseCachingPolicy.cs index 80c3ac2c84d5..b77b988e9222 100644 --- a/src/Middleware/OutputCaching/src/Policies/ResponseCachingPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/ResponseCachingPolicy.cs @@ -15,7 +15,7 @@ public sealed class ResponseCachingPolicy : IOutputCachingPolicy /// public Task OnRequestAsync(IOutputCachingContext context) { - context.AttemptResponseCaching = AttemptOutputCaching(context); + context.AttemptOutputCaching = AttemptOutputCaching(context); context.AllowCacheLookup = AllowCacheLookup(context); context.AllowCacheStorage = AllowCacheStorage(context); context.AllowLocking = true; diff --git a/src/Middleware/OutputCaching/src/Policies/VaryByValuePolicy.cs b/src/Middleware/OutputCaching/src/Policies/VaryByValuePolicy.cs index 12d59bf878e7..f143967dacd1 100644 --- a/src/Middleware/OutputCaching/src/Policies/VaryByValuePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/VaryByValuePolicy.cs @@ -23,7 +23,7 @@ public VaryByValuePolicy() /// public VaryByValuePolicy(Func varyBy) { - _varyBy = (c) => c.VaryByPrefix = c.VaryByPrefix + varyBy(); + _varyBy = (c) => c.VaryByPrefix += varyBy(); } /// @@ -31,7 +31,7 @@ public VaryByValuePolicy(Func varyBy) /// public VaryByValuePolicy(Func> varyBy) { - _varyByAsync = async (c) => c.VaryByPrefix = c.VaryByPrefix + await varyBy(); + _varyByAsync = async (c) => c.VaryByPrefix += await varyBy(); } /// diff --git a/src/Middleware/OutputCaching/src/Streams/SegmentWriteStream.cs b/src/Middleware/OutputCaching/src/Streams/SegmentWriteStream.cs index 4d8a845418e8..caf5a518e5ca 100644 --- a/src/Middleware/OutputCaching/src/Streams/SegmentWriteStream.cs +++ b/src/Middleware/OutputCaching/src/Streams/SegmentWriteStream.cs @@ -5,8 +5,8 @@ namespace Microsoft.AspNetCore.OutputCaching; internal sealed class SegmentWriteStream : Stream { - private readonly List _segments = new List(); - private readonly MemoryStream _bufferStream = new MemoryStream(); + private readonly List _segments = new(); + private readonly MemoryStream _bufferStream = new(); private readonly int _segmentSize; private long _length; private bool _closed; @@ -156,8 +156,8 @@ public override void Write(ReadOnlySpan buffer) var bytesWritten = Math.Min(buffer.Length, _segmentSize - (int)_bufferStream.Length); - _bufferStream.Write(buffer.Slice(0, bytesWritten)); - buffer = buffer.Slice(bytesWritten); + _bufferStream.Write(buffer[..bytesWritten]); + buffer = buffer[bytesWritten..]; _length += bytesWritten; } } diff --git a/src/Middleware/OutputCaching/src/SystemClock.cs b/src/Middleware/OutputCaching/src/SystemClock.cs index 00d9f03e58b3..6cb33a828b8d 100644 --- a/src/Middleware/OutputCaching/src/SystemClock.cs +++ b/src/Middleware/OutputCaching/src/SystemClock.cs @@ -11,5 +11,5 @@ internal sealed class SystemClock : ISystemClock /// /// Retrieves the current system time in UTC. /// - public DateTime UtcNow => DateTime.UtcNow; + public DateTimeOffset UtcNow => DateTimeOffset.UtcNow; } diff --git a/src/Middleware/OutputCaching/test/CachedResponseBodyTests.cs b/src/Middleware/OutputCaching/test/CachedResponseBodyTests.cs index 173380c71c55..6867fc2fb7e9 100644 --- a/src/Middleware/OutputCaching/test/CachedResponseBodyTests.cs +++ b/src/Middleware/OutputCaching/test/CachedResponseBodyTests.cs @@ -1,122 +1,122 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -//using System.Buffers; -//using System.Diagnostics; -//using System.IO.Pipelines; +using System.Buffers; +using System.Diagnostics; +using System.IO.Pipelines; -//namespace Microsoft.AspNetCore.ResponseCaching.Tests; +namespace Microsoft.AspNetCore.OutputCaching.Tests; -//public class CachedResponseBodyTests -//{ -// private readonly int _timeout = Debugger.IsAttached ? -1 : 5000; +public class CachedResponseBodyTests +{ + private readonly int _timeout = Debugger.IsAttached ? -1 : 5000; -// [Fact] -// public void GetSegments() -// { -// var segments = new List(); -// var body = new CachedResponseBody(segments, 0); - -// Assert.Same(segments, body.Segments); -// } - -// [Fact] -// public void GetLength() -// { -// var segments = new List(); -// var body = new CachedResponseBody(segments, 42); - -// Assert.Equal(42, body.Length); -// } - -// [Fact] -// public async Task Copy_DoNothingWhenNoSegments() -// { -// var segments = new List(); -// var receivedSegments = new List(); -// var body = new CachedResponseBody(segments, 0); - -// var pipe = new Pipe(); -// using var cts = new CancellationTokenSource(_timeout); - -// var receiverTask = ReceiveDataAsync(pipe.Reader, receivedSegments, cts.Token); -// var copyTask = body.CopyToAsync(pipe.Writer, cts.Token).ContinueWith(_ => pipe.Writer.CompleteAsync()); - -// await Task.WhenAll(receiverTask, copyTask); - -// Assert.Empty(receivedSegments); -// } - -// [Fact] -// public async Task Copy_SingleSegment() -// { -// var segments = new List -// { -// new byte[] { 1 } -// }; -// var receivedSegments = new List(); -// var body = new CachedResponseBody(segments, 0); - -// var pipe = new Pipe(); - -// using var cts = new CancellationTokenSource(_timeout); - -// var receiverTask = ReceiveDataAsync(pipe.Reader, receivedSegments, cts.Token); -// var copyTask = CopyDataAsync(body, pipe.Writer, cts.Token); - -// await Task.WhenAll(receiverTask, copyTask); - -// Assert.Equal(segments, receivedSegments); -// } - -// [Fact] -// public async Task Copy_MultipleSegments() -// { -// var segments = new List -// { -// new byte[] { 1 }, -// new byte[] { 2, 3 } -// }; -// var receivedSegments = new List(); -// var body = new CachedResponseBody(segments, 0); - -// var pipe = new Pipe(); - -// using var cts = new CancellationTokenSource(_timeout); - -// var receiverTask = ReceiveDataAsync(pipe.Reader, receivedSegments, cts.Token); -// var copyTask = CopyDataAsync(body, pipe.Writer, cts.Token); - -// await Task.WhenAll(receiverTask, copyTask); - -// Assert.Equal(new byte[] { 1, 2, 3 }, receivedSegments.SelectMany(x => x).ToArray()); -// } - -// static async Task CopyDataAsync(CachedResponseBody body, PipeWriter writer, CancellationToken cancellationToken) -// { -// await body.CopyToAsync(writer, cancellationToken); -// await writer.CompleteAsync(); -// } - -// static async Task ReceiveDataAsync(PipeReader reader, List receivedSegments, CancellationToken cancellationToken) -// { -// while (true) -// { -// var result = await reader.ReadAsync(cancellationToken); -// var buffer = result.Buffer; - -// foreach (var memory in buffer) -// { -// receivedSegments.Add(memory.ToArray()); -// } - -// reader.AdvanceTo(buffer.End, buffer.End); - -// if (result.IsCompleted) -// { -// break; -// } -// } -// await reader.CompleteAsync(); -// } -//} + [Fact] + public void GetSegments() + { + var segments = new List(); + var body = new CachedResponseBody(segments, 0); + + Assert.Same(segments, body.Segments); + } + + [Fact] + public void GetLength() + { + var segments = new List(); + var body = new CachedResponseBody(segments, 42); + + Assert.Equal(42, body.Length); + } + + [Fact] + public async Task Copy_DoNothingWhenNoSegments() + { + var segments = new List(); + var receivedSegments = new List(); + var body = new CachedResponseBody(segments, 0); + + var pipe = new Pipe(); + using var cts = new CancellationTokenSource(_timeout); + + var receiverTask = ReceiveDataAsync(pipe.Reader, receivedSegments, cts.Token); + var copyTask = body.CopyToAsync(pipe.Writer, cts.Token).ContinueWith(_ => pipe.Writer.CompleteAsync()); + + await Task.WhenAll(receiverTask, copyTask); + + Assert.Empty(receivedSegments); + } + + [Fact] + public async Task Copy_SingleSegment() + { + var segments = new List + { + new byte[] { 1 } + }; + var receivedSegments = new List(); + var body = new CachedResponseBody(segments, 0); + + var pipe = new Pipe(); + + using var cts = new CancellationTokenSource(_timeout); + + var receiverTask = ReceiveDataAsync(pipe.Reader, receivedSegments, cts.Token); + var copyTask = CopyDataAsync(body, pipe.Writer, cts.Token); + + await Task.WhenAll(receiverTask, copyTask); + + Assert.Equal(segments, receivedSegments); + } + + [Fact] + public async Task Copy_MultipleSegments() + { + var segments = new List + { + new byte[] { 1 }, + new byte[] { 2, 3 } + }; + var receivedSegments = new List(); + var body = new CachedResponseBody(segments, 0); + + var pipe = new Pipe(); + + using var cts = new CancellationTokenSource(_timeout); + + var receiverTask = ReceiveDataAsync(pipe.Reader, receivedSegments, cts.Token); + var copyTask = CopyDataAsync(body, pipe.Writer, cts.Token); + + await Task.WhenAll(receiverTask, copyTask); + + Assert.Equal(new byte[] { 1, 2, 3 }, receivedSegments.SelectMany(x => x).ToArray()); + } + + static async Task CopyDataAsync(CachedResponseBody body, PipeWriter writer, CancellationToken cancellationToken) + { + await body.CopyToAsync(writer, cancellationToken); + await writer.CompleteAsync(); + } + + static async Task ReceiveDataAsync(PipeReader reader, List receivedSegments, CancellationToken cancellationToken) + { + while (true) + { + var result = await reader.ReadAsync(cancellationToken); + var buffer = result.Buffer; + + foreach (var memory in buffer) + { + receivedSegments.Add(memory.ToArray()); + } + + reader.AdvanceTo(buffer.End, buffer.End); + + if (result.IsCompleted) + { + break; + } + } + await reader.CompleteAsync(); + } +} diff --git a/src/Middleware/OutputCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests.csproj b/src/Middleware/OutputCaching/test/Microsoft.AspNetCore.OutputCaching.Tests.csproj similarity index 87% rename from src/Middleware/OutputCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests.csproj rename to src/Middleware/OutputCaching/test/Microsoft.AspNetCore.OutputCaching.Tests.csproj index dd7bcb50d1df..f3a12d8a3c3d 100644 --- a/src/Middleware/OutputCaching/test/Microsoft.AspNetCore.ResponseCaching.Tests.csproj +++ b/src/Middleware/OutputCaching/test/Microsoft.AspNetCore.OutputCaching.Tests.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/Middleware/OutputCaching/test/OutputCachingKeyProviderTests.cs b/src/Middleware/OutputCaching/test/OutputCachingKeyProviderTests.cs new file mode 100644 index 000000000000..34ffb714588b --- /dev/null +++ b/src/Middleware/OutputCaching/test/OutputCachingKeyProviderTests.cs @@ -0,0 +1,214 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.OutputCaching.Tests; + +public class OutputCachingKeyProviderTests +{ + private const char KeyDelimiter = '\x1e'; + private const char KeySubDelimiter = '\x1f'; + + [Fact] + public void OutputCachingKeyProvider_CreateStorageKey_IncludesOnlyNormalizedMethodSchemeHostPortAndPath() + { + var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); + var context = TestUtils.CreateTestContext(); + context.HttpContext.Request.Method = "head"; + context.HttpContext.Request.Path = "/path/subpath"; + context.HttpContext.Request.Scheme = "https"; + context.HttpContext.Request.Host = new HostString("example.com", 80); + context.HttpContext.Request.PathBase = "/pathBase"; + context.HttpContext.Request.QueryString = new QueryString("?query.Key=a&query.Value=b"); + + Assert.Equal($"HEAD{KeyDelimiter}HTTPS{KeyDelimiter}EXAMPLE.COM:80/PATHBASE/PATH/SUBPATH", cacheKeyProvider.CreateStorageKey(context)); + } + + [Fact] + public void OutputCachingKeyProvider_CreateStorageKey_CaseInsensitivePath_NormalizesPath() + { + var cacheKeyProvider = TestUtils.CreateTestKeyProvider(new OutputCachingOptions() + { + UseCaseSensitivePaths = false + }); + var context = TestUtils.CreateTestContext(); + context.HttpContext.Request.Method = HttpMethods.Get; + context.HttpContext.Request.Path = "/Path"; + + Assert.Equal($"{HttpMethods.Get}{KeyDelimiter}{KeyDelimiter}/PATH", cacheKeyProvider.CreateStorageKey(context)); + } + + [Fact] + public void OutputCachingKeyProvider_CreateStorageKey_CaseSensitivePath_PreservesPathCase() + { + var cacheKeyProvider = TestUtils.CreateTestKeyProvider(new OutputCachingOptions() + { + UseCaseSensitivePaths = true + }); + var context = TestUtils.CreateTestContext(); + context.HttpContext.Request.Method = HttpMethods.Get; + context.HttpContext.Request.Path = "/Path"; + + Assert.Equal($"{HttpMethods.Get}{KeyDelimiter}{KeyDelimiter}/Path", cacheKeyProvider.CreateStorageKey(context)); + } + + [Fact] + public void OutputCachingKeyProvider_CreateStorageKey_VaryByRulesIsotNull() + { + var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); + var context = TestUtils.CreateTestContext(); + + Assert.NotNull(context.CachedVaryByRules); + } + + [Fact] + public void OutputCachingKeyProvider_CreateStorageKey_ReturnsCachedVaryByGuid_IfVaryByRulesIsEmpty() + { + var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); + var context = TestUtils.CreateTestContext(); + context.CachedVaryByRules = new CachedVaryByRules() + { + VaryByPrefix = Guid.NewGuid().ToString("n"), + }; + + Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}C{KeyDelimiter}{context.CachedVaryByRules.VaryByPrefix}", cacheKeyProvider.CreateStorageKey(context)); + } + + [Fact] + public void OutputCachingKeyProvider_CreateStorageVaryKey_IncludesListedHeadersOnly() + { + var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); + var context = TestUtils.CreateTestContext(); + context.HttpContext.Request.Headers["HeaderA"] = "ValueA"; + context.HttpContext.Request.Headers["HeaderB"] = "ValueB"; + context.CachedVaryByRules = new CachedVaryByRules() + { + Headers = new string[] { "HeaderA", "HeaderC" } + }; + + Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=", + cacheKeyProvider.CreateStorageKey(context)); + } + + [Fact] + public void OutputCachingKeyProvider_CreateStorageVaryKey_HeaderValuesAreSorted() + { + var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); + var context = TestUtils.CreateTestContext(); + context.HttpContext.Request.Headers["HeaderA"] = "ValueB"; + context.HttpContext.Request.Headers.Append("HeaderA", "ValueA"); + context.CachedVaryByRules = new CachedVaryByRules() + { + Headers = new string[] { "HeaderA", "HeaderC" } + }; + + Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueAValueB{KeyDelimiter}HeaderC=", + cacheKeyProvider.CreateStorageKey(context)); + } + + [Fact] + public void OutputCachingKeyProvider_CreateStorageVaryKey_IncludesListedQueryKeysOnly() + { + var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); + var context = TestUtils.CreateTestContext(); + context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueA&QueryB=ValueB"); + context.CachedVaryByRules = new CachedVaryByRules() + { + VaryByPrefix = Guid.NewGuid().ToString("n"), + QueryKeys = new string[] { "QueryA", "QueryC" } + }; + + Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}C{KeyDelimiter}{context.CachedVaryByRules.VaryByPrefix}{KeyDelimiter}Q{KeyDelimiter}QueryA=ValueA{KeyDelimiter}QueryC=", + cacheKeyProvider.CreateStorageKey(context)); + } + + [Fact] + public void OutputCachingKeyProvider_CreateStorageVaryKey_IncludesQueryKeys_QueryKeyCaseInsensitive_UseQueryKeyCasing() + { + var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); + var context = TestUtils.CreateTestContext(); + context.HttpContext.Request.QueryString = new QueryString("?queryA=ValueA&queryB=ValueB"); + context.CachedVaryByRules = new CachedVaryByRules() + { + VaryByPrefix = Guid.NewGuid().ToString("n"), + QueryKeys = new string[] { "QueryA", "QueryC" } + }; + + Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}C{KeyDelimiter}{context.CachedVaryByRules.VaryByPrefix}{KeyDelimiter}Q{KeyDelimiter}QueryA=ValueA{KeyDelimiter}QueryC=", + cacheKeyProvider.CreateStorageKey(context)); + } + + [Fact] + public void OutputCachingKeyProvider_CreateStorageVaryKey_IncludesAllQueryKeysGivenAsterisk() + { + var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); + var context = TestUtils.CreateTestContext(); + context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueA&QueryB=ValueB"); + context.CachedVaryByRules = new CachedVaryByRules() + { + VaryByPrefix = Guid.NewGuid().ToString("n"), + QueryKeys = new string[] { "*" } + }; + + // To support case insensitivity, all query keys are converted to upper case. + // Explicit query keys uses the casing specified in the setting. + Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}C{KeyDelimiter}{context.CachedVaryByRules.VaryByPrefix}{KeyDelimiter}Q{KeyDelimiter}QUERYA=ValueA{KeyDelimiter}QUERYB=ValueB", + cacheKeyProvider.CreateStorageKey(context)); + } + + [Fact] + public void OutputCachingKeyProvider_CreateStorageVaryKey_QueryKeysValuesNotConsolidated() + { + var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); + var context = TestUtils.CreateTestContext(); + context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueA&QueryA=ValueB"); + context.CachedVaryByRules = new CachedVaryByRules() + { + VaryByPrefix = Guid.NewGuid().ToString("n"), + QueryKeys = new string[] { "*" } + }; + + // To support case insensitivity, all query keys are converted to upper case. + // Explicit query keys uses the casing specified in the setting. + Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}C{KeyDelimiter}{context.CachedVaryByRules.VaryByPrefix}{KeyDelimiter}Q{KeyDelimiter}QUERYA=ValueA{KeySubDelimiter}ValueB", + cacheKeyProvider.CreateStorageKey(context)); + } + + [Fact] + public void OutputCachingKeyProvider_CreateStorageVaryKey_QueryKeysValuesAreSorted() + { + var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); + var context = TestUtils.CreateTestContext(); + context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueB&QueryA=ValueA"); + context.CachedVaryByRules = new CachedVaryByRules() + { + VaryByPrefix = Guid.NewGuid().ToString("n"), + QueryKeys = new string[] { "*" } + }; + + // To support case insensitivity, all query keys are converted to upper case. + // Explicit query keys uses the casing specified in the setting. + Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}C{KeyDelimiter}{context.CachedVaryByRules.VaryByPrefix}{KeyDelimiter}Q{KeyDelimiter}QUERYA=ValueA{KeySubDelimiter}ValueB", + cacheKeyProvider.CreateStorageKey(context)); + } + + [Fact] + public void OutputCachingKeyProvider_CreateStorageVaryKey_IncludesListedHeadersAndQueryKeys() + { + var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); + var context = TestUtils.CreateTestContext(); + context.HttpContext.Request.Headers["HeaderA"] = "ValueA"; + context.HttpContext.Request.Headers["HeaderB"] = "ValueB"; + context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueA&QueryB=ValueB"); + context.CachedVaryByRules = new CachedVaryByRules() + { + VaryByPrefix = Guid.NewGuid().ToString("n"), + Headers = new string[] { "HeaderA", "HeaderC" }, + QueryKeys = new string[] { "QueryA", "QueryC" } + }; + + Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}C{KeyDelimiter}{context.CachedVaryByRules.VaryByPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC={KeyDelimiter}Q{KeyDelimiter}QueryA=ValueA{KeyDelimiter}QueryC=", + cacheKeyProvider.CreateStorageKey(context)); + } +} diff --git a/src/Middleware/OutputCaching/test/OutputCachingMiddlewareTests.cs b/src/Middleware/OutputCaching/test/OutputCachingMiddlewareTests.cs new file mode 100644 index 000000000000..7f3ea3e042a3 --- /dev/null +++ b/src/Middleware/OutputCaching/test/OutputCachingMiddlewareTests.cs @@ -0,0 +1,918 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.OutputCaching.Memory; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Extensions.Logging.Testing; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.OutputCaching.Tests; + +public class OutputCachingMiddlewareTests +{ + [Fact] + public async Task TryServeFromCacheAsync_OnlyIfCached_Serves504() + { + var cache = new TestOutputCache(); + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider("BaseKey")); + var context = TestUtils.CreateTestContext(); + context.HttpContext.Request.Headers.CacheControl = new CacheControlHeaderValue() + { + OnlyIfCached = true + }.ToString(); + + Assert.True(await middleware.TryServeFromCacheAsync(context)); + Assert.Equal(StatusCodes.Status504GatewayTimeout, context.HttpContext.Response.StatusCode); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.GatewayTimeoutServed); + } + + [Fact] + public async Task TryServeFromCacheAsync_CachedResponseNotFound_Fails() + { + var cache = new TestOutputCache(); + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider("BaseKey")); + var context = TestUtils.CreateTestContext(); + + Assert.False(await middleware.TryServeFromCacheAsync(context)); + Assert.Equal(1, cache.GetCount); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.NoResponseServed); + } + + [Fact] + public async Task TryServeFromCacheAsync_CachedResponseFound_Succeeds() + { + var cache = new TestOutputCache(); + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider("BaseKey")); + var context = TestUtils.CreateTestContext(); + + await cache.SetAsync( + "BaseKey", + new OutputCacheEntry() + { + Headers = new HeaderDictionary(), + Body = new CachedResponseBody(new List(0), 0) + }, + TimeSpan.Zero); + + Assert.True(await middleware.TryServeFromCacheAsync(context)); + Assert.Equal(1, cache.GetCount); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.CachedResponseServed); + } + + [Fact] + public async Task TryServeFromCacheAsync_CachedResponseFound_OverwritesExistingHeaders() + { + var cache = new TestOutputCache(); + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider("BaseKey")); + var context = TestUtils.CreateTestContext(); + + context.HttpContext.Response.Headers["MyHeader"] = "OldValue"; + await cache.SetAsync( + "BaseKey", + new OutputCacheEntry() + { + Headers = new HeaderDictionary() + { + { "MyHeader", "NewValue" } + }, + Body = new CachedResponseBody(new List(0), 0) + }, + TimeSpan.Zero); + + Assert.True(await middleware.TryServeFromCacheAsync(context)); + Assert.Equal("NewValue", context.HttpContext.Response.Headers["MyHeader"]); + Assert.Equal(1, cache.GetCount); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.CachedResponseServed); + } + + [Fact] + public async Task TryServeFromCacheAsync_CachedResponseFound_Serves304IfPossible() + { + var cache = new TestOutputCache(); + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider("BaseKey")); + var context = TestUtils.CreateTestContext(); + context.HttpContext.Request.Headers.IfNoneMatch = "*"; + + await cache.SetAsync( + "BaseKey", + new OutputCacheEntry() + { + Body = new CachedResponseBody(new List(0), 0) + }, + TimeSpan.Zero); + + Assert.True(await middleware.TryServeFromCacheAsync(context)); + Assert.Equal(1, cache.GetCount); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.NotModifiedServed); + } + + [Fact] + public void ContentIsNotModified_NotConditionalRequest_False() + { + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); + context.CachedResponseHeaders = new HeaderDictionary(); + + Assert.False(OutputCachingMiddleware.ContentIsNotModified(context)); + Assert.Empty(sink.Writes); + } + + [Fact] + public void ContentIsNotModified_IfModifiedSince_FallsbackToDateHeader() + { + var utcNow = DateTimeOffset.UtcNow; + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); + context.CachedResponseHeaders = new HeaderDictionary(); + + context.HttpContext.Request.Headers.IfModifiedSince = HeaderUtilities.FormatDate(utcNow); + + // Verify modifications in the past succeeds + context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10)); + Assert.True(OutputCachingMiddleware.ContentIsNotModified(context)); + Assert.Single(sink.Writes); + + // Verify modifications at present succeeds + context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow); + Assert.True(OutputCachingMiddleware.ContentIsNotModified(context)); + Assert.Equal(2, sink.Writes.Count); + + // Verify modifications in the future fails + context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); + Assert.False(OutputCachingMiddleware.ContentIsNotModified(context)); + + // Verify logging + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.NotModifiedIfModifiedSinceSatisfied, + LoggedMessage.NotModifiedIfModifiedSinceSatisfied); + } + + [Fact] + public void ContentIsNotModified_IfModifiedSince_LastModifiedOverridesDateHeader() + { + var utcNow = DateTimeOffset.UtcNow; + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); + context.CachedResponseHeaders = new HeaderDictionary(); + + context.HttpContext.Request.Headers.IfModifiedSince = HeaderUtilities.FormatDate(utcNow); + + // Verify modifications in the past succeeds + context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); + context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10)); + Assert.True(OutputCachingMiddleware.ContentIsNotModified(context)); + Assert.Single(sink.Writes); + + // Verify modifications at present + context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); + context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow); + Assert.True(OutputCachingMiddleware.ContentIsNotModified(context)); + Assert.Equal(2, sink.Writes.Count); + + // Verify modifications in the future fails + context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10)); + context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); + Assert.False(OutputCachingMiddleware.ContentIsNotModified(context)); + + // Verify logging + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.NotModifiedIfModifiedSinceSatisfied, + LoggedMessage.NotModifiedIfModifiedSinceSatisfied); + } + + [Fact] + public void ContentIsNotModified_IfNoneMatch_Overrides_IfModifiedSince_ToTrue() + { + var utcNow = DateTimeOffset.UtcNow; + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); + context.CachedResponseHeaders = new HeaderDictionary(); + + // This would fail the IfModifiedSince checks + context.HttpContext.Request.Headers.IfModifiedSince = HeaderUtilities.FormatDate(utcNow); + context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); + + context.HttpContext.Request.Headers.IfNoneMatch = EntityTagHeaderValue.Any.ToString(); + Assert.True(OutputCachingMiddleware.ContentIsNotModified(context)); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.NotModifiedIfNoneMatchStar); + } + + [Fact] + public void ContentIsNotModified_IfNoneMatch_Overrides_IfModifiedSince_ToFalse() + { + var utcNow = DateTimeOffset.UtcNow; + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); + context.CachedResponseHeaders = new HeaderDictionary(); + + // This would pass the IfModifiedSince checks + context.HttpContext.Request.Headers.IfModifiedSince = HeaderUtilities.FormatDate(utcNow); + context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10)); + + context.HttpContext.Request.Headers.IfNoneMatch = "\"E1\""; + Assert.False(OutputCachingMiddleware.ContentIsNotModified(context)); + Assert.Empty(sink.Writes); + } + + [Fact] + public void ContentIsNotModified_IfNoneMatch_AnyWithoutETagInResponse_False() + { + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); + context.CachedResponseHeaders = new HeaderDictionary(); + context.HttpContext.Request.Headers.IfNoneMatch = "\"E1\""; + + Assert.False(OutputCachingMiddleware.ContentIsNotModified(context)); + Assert.Empty(sink.Writes); + } + + public static TheoryData EquivalentWeakETags + { + get + { + return new TheoryData + { + { new EntityTagHeaderValue("\"tag\""), new EntityTagHeaderValue("\"tag\"") }, + { new EntityTagHeaderValue("\"tag\"", true), new EntityTagHeaderValue("\"tag\"") }, + { new EntityTagHeaderValue("\"tag\""), new EntityTagHeaderValue("\"tag\"", true) }, + { new EntityTagHeaderValue("\"tag\"", true), new EntityTagHeaderValue("\"tag\"", true) } + }; + } + } + + [Theory] + [MemberData(nameof(EquivalentWeakETags))] + public void ContentIsNotModified_IfNoneMatch_ExplicitWithMatch_True(EntityTagHeaderValue responseETag, EntityTagHeaderValue requestETag) + { + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); + context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponseHeaders[HeaderNames.ETag] = responseETag.ToString(); + context.HttpContext.Request.Headers.IfNoneMatch = requestETag.ToString(); + + Assert.True(OutputCachingMiddleware.ContentIsNotModified(context)); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.NotModifiedIfNoneMatchMatched); + } + + [Fact] + public void ContentIsNotModified_IfNoneMatch_ExplicitWithoutMatch_False() + { + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); + context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponseHeaders[HeaderNames.ETag] = "\"E2\""; + context.HttpContext.Request.Headers.IfNoneMatch = "\"E1\""; + + Assert.False(OutputCachingMiddleware.ContentIsNotModified(context)); + Assert.Empty(sink.Writes); + } + + [Fact] + public void ContentIsNotModified_IfNoneMatch_MatchesAtLeastOneValue_True() + { + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); + context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponseHeaders[HeaderNames.ETag] = "\"E2\""; + context.HttpContext.Request.Headers.IfNoneMatch = new string[] { "\"E0\", \"E1\"", "\"E1\", \"E2\"" }; + + Assert.True(OutputCachingMiddleware.ContentIsNotModified(context)); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.NotModifiedIfNoneMatchMatched); + } + + [Fact] + public void StartResponsegAsync_IfAllowResponseCaptureIsTrue_SetsResponseTime() + { + var clock = new TestClock + { + UtcNow = DateTimeOffset.UtcNow + }; + var middleware = TestUtils.CreateTestMiddleware(options: new OutputCachingOptions + { + SystemClock = clock + }); + var context = TestUtils.CreateTestContext(); + context.ResponseTime = null; + + middleware.StartResponse(context); + + Assert.Equal(clock.UtcNow, context.ResponseTime); + } + + [Fact] + public void StartResponseAsync_IfAllowResponseCaptureIsTrue_SetsResponseTimeOnlyOnce() + { + var clock = new TestClock + { + UtcNow = DateTimeOffset.UtcNow + }; + var middleware = TestUtils.CreateTestMiddleware(options: new OutputCachingOptions + { + SystemClock = clock + }); + var context = TestUtils.CreateTestContext(); + var initialTime = clock.UtcNow; + context.ResponseTime = null; + + middleware.StartResponse(context); + Assert.Equal(initialTime, context.ResponseTime); + + clock.UtcNow += TimeSpan.FromSeconds(10); + + middleware.StartResponse(context); + Assert.NotEqual(clock.UtcNow, context.ResponseTime); + Assert.Equal(initialTime, context.ResponseTime); + } + + //[Fact] + //public void FinalizeCacheHeadersAsync_UpdateAllowCacheStorage_IfResponseCacheable() + //{ + // var sink = new TestSink(); + // var middleware = TestUtils.CreateTestMiddleware(testSink: sink, policyProvider: new ResponseCachingPolicyProvider()); + // var context = TestUtils.CreateTestContext(); + + // context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() + // { + // Public = true + // }.ToString(); + + // Assert.False(context.AllowCacheStorage); + + // middleware.FinalizeCacheHeaders(context); + + // Assert.True(context.AllowCacheStorage); + // Assert.Empty(sink.Writes); + //} + + //[Fact] + //public void FinalizeCacheHeadersAsync_DoNotUpdateAllowCacheStorage_IfResponseIsNotCacheable() + //{ + // var sink = new TestSink(); + // var middleware = TestUtils.CreateTestMiddleware(testSink: sink, policyProvider: new ResponseCachingPolicyProvider()); + // var context = TestUtils.CreateTestContext(); + + // middleware.ShimResponseStream(context); + + // middleware.FinalizeCacheHeaders(context); + + // Assert.False(context.AllowCacheStorage); + // Assert.Empty(sink.Writes); + //} + + [Fact] + public void FinalizeCacheHeadersAsync_DefaultResponseValidity_Is10Seconds() + { + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink); + var context = TestUtils.CreateTestContext(); + + middleware.FinalizeCacheHeaders(context); + + Assert.Equal(TimeSpan.FromSeconds(10), context.CachedResponseValidFor); + Assert.Empty(sink.Writes); + } + + [Fact] + public void FinalizeCacheHeadersAsync_ResponseValidity_UseExpiryIfAvailable() + { + var clock = new TestClock + { + UtcNow = DateTimeOffset.MinValue + }; + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, options: new OutputCachingOptions + { + SystemClock = clock + }); + var context = TestUtils.CreateTestContext(); + + context.ResponseTime = clock.UtcNow; + context.HttpContext.Response.Headers.Expires = HeaderUtilities.FormatDate(clock.UtcNow + TimeSpan.FromSeconds(11)); + + middleware.FinalizeCacheHeaders(context); + + Assert.Equal(TimeSpan.FromSeconds(11), context.CachedResponseValidFor); + Assert.Empty(sink.Writes); + } + + [Fact] + public void FinalizeCacheHeadersAsync_ResponseValidity_UseMaxAgeIfAvailable() + { + var clock = new TestClock + { + UtcNow = DateTimeOffset.UtcNow + }; + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, options: new OutputCachingOptions + { + SystemClock = clock + }); + var context = TestUtils.CreateTestContext(); + + context.ResponseTime = clock.UtcNow; + context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() + { + MaxAge = TimeSpan.FromSeconds(12) + }.ToString(); + + context.HttpContext.Response.Headers.Expires = HeaderUtilities.FormatDate(clock.UtcNow + TimeSpan.FromSeconds(11)); + + middleware.FinalizeCacheHeaders(context); + + Assert.Equal(TimeSpan.FromSeconds(12), context.CachedResponseValidFor); + Assert.Empty(sink.Writes); + } + + [Fact] + public void FinalizeCacheHeadersAsync_ResponseValidity_UseSharedMaxAgeIfAvailable() + { + var clock = new TestClock + { + UtcNow = DateTimeOffset.UtcNow + }; + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, options: new OutputCachingOptions + { + SystemClock = clock + }); + var context = TestUtils.CreateTestContext(); + + context.ResponseTime = clock.UtcNow; + context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() + { + MaxAge = TimeSpan.FromSeconds(12), + SharedMaxAge = TimeSpan.FromSeconds(13) + }.ToString(); + context.HttpContext.Response.Headers.Expires = HeaderUtilities.FormatDate(clock.UtcNow + TimeSpan.FromSeconds(11)); + + middleware.FinalizeCacheHeaders(context); + + Assert.Equal(TimeSpan.FromSeconds(13), context.CachedResponseValidFor); + Assert.Empty(sink.Writes); + } + + //[Fact] + //public void FinalizeCacheHeadersAsync_UpdateCachedVaryByRules_IfNotEquivalentToPrevious() + //{ + // var cache = new TestOutputCache(); + // var sink = new TestSink(); + // var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); + // var context = TestUtils.CreateTestContext(); + + // context.HttpContext.Response.Headers.Vary = new StringValues(new[] { "headerA", "HEADERB", "HEADERc" }); + // context.HttpContext.Features.Set(new OutputCachingFeature() + // { + // VaryByQueryKeys = new StringValues(new[] { "queryB", "QUERYA" }) + // }); + // var cachedVaryByRules = new CachedVaryByRules() + // { + // Headers = new StringValues(new[] { "HeaderA", "HeaderB" }), + // QueryKeys = new StringValues(new[] { "QueryA", "QueryB" }) + // }; + // context.CachedVaryByRules = cachedVaryByRules; + + // middleware.FinalizeCacheHeaders(context); + + // Assert.Equal(1, cache.SetCount); + // Assert.NotSame(cachedVaryByRules, context.CachedVaryByRules); + // TestUtils.AssertLoggedMessages( + // sink.Writes, + // LoggedMessage.VaryByRulesUpdated); + //} + + //[Fact] + //public void FinalizeCacheHeadersAsync_UpdateCachedVaryByRules_IfEquivalentToPrevious() + //{ + // var cache = new TestOutputCache(); + // var sink = new TestSink(); + // var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); + // var context = TestUtils.CreateTestContext(); + + // context.HttpContext.Response.Headers.Vary = new StringValues(new[] { "headerA", "HEADERB" }); + // context.HttpContext.Features.Set(new OutputCachingFeature() + // { + // VaryByQueryKeys = new StringValues(new[] { "queryB", "QUERYA" }) + // }); + // var cachedVaryByRules = new CachedVaryByRules() + // { + // VaryByKeyPrefix = FastGuid.NewGuid().IdString, + // Headers = new StringValues(new[] { "HEADERA", "HEADERB" }), + // QueryKeys = new StringValues(new[] { "QUERYA", "QUERYB" }) + // }; + // context.CachedVaryByRules = cachedVaryByRules; + + // middleware.FinalizeCacheHeaders(context); + + // // An update to the cache is always made but the entry should be the same + // Assert.Equal(1, cache.SetCount); + // Assert.Same(cachedVaryByRules, context.CachedVaryByRules); + // TestUtils.AssertLoggedMessages( + // sink.Writes, + // LoggedMessage.VaryByRulesUpdated); + //} + + public static TheoryData NullOrEmptyVaryRules + { + get + { + return new TheoryData + { + default(StringValues), + StringValues.Empty, + new StringValues((string)null), + new StringValues(string.Empty), + new StringValues((string[])null), + new StringValues(new string[0]), + new StringValues(new string[] { null }), + new StringValues(new string[] { string.Empty }) + }; + } + } + + [Theory] + [MemberData(nameof(NullOrEmptyVaryRules))] + public void FinalizeCacheHeadersAsync_UpdateCachedVaryByRules_NullOrEmptyRules(StringValues vary) + { + var cache = new TestOutputCache(); + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); + var context = TestUtils.CreateTestContext(); + + context.HttpContext.Response.Headers.Vary = vary; + context.HttpContext.Features.Set(new OutputCachingFeature(context)); + context.CachedVaryByRules.QueryKeys = vary; + + middleware.FinalizeCacheHeaders(context); + + // Vary rules should not be updated + Assert.Equal(0, cache.SetCount); + Assert.Empty(sink.Writes); + } + + [Fact] + public void FinalizeCacheHeadersAsync_AddsDate_IfNoneSpecified() + { + var utcNow = DateTimeOffset.UtcNow; + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink); + var context = TestUtils.CreateTestContext(); + // ResponseTime is the actual value that's used to set the Date header in FinalizeCacheHeadersAsync + context.ResponseTime = utcNow; + + Assert.True(StringValues.IsNullOrEmpty(context.HttpContext.Response.Headers.Date)); + + middleware.FinalizeCacheHeaders(context); + + Assert.Equal(HeaderUtilities.FormatDate(utcNow), context.HttpContext.Response.Headers.Date); + Assert.Empty(sink.Writes); + } + + [Fact] + public void FinalizeCacheHeadersAsync_DoNotAddDate_IfSpecified() + { + var utcNow = DateTimeOffset.MinValue; + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink); + var context = TestUtils.CreateTestContext(); + + context.HttpContext.Response.Headers.Date = HeaderUtilities.FormatDate(utcNow); + context.ResponseTime = utcNow + TimeSpan.FromSeconds(10); + + Assert.Equal(HeaderUtilities.FormatDate(utcNow), context.HttpContext.Response.Headers.Date); + + middleware.FinalizeCacheHeaders(context); + + Assert.Equal(HeaderUtilities.FormatDate(utcNow), context.HttpContext.Response.Headers.Date); + Assert.Empty(sink.Writes); + } + + [Fact] + public void FinalizeCacheHeadersAsync_StoresCachedResponse_InState() + { + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink); + var context = TestUtils.CreateTestContext(); + + Assert.Null(context.CachedResponse); + + middleware.FinalizeCacheHeaders(context); + + Assert.NotNull(context.CachedResponse); + Assert.Empty(sink.Writes); + } + + [Fact] + public void FinalizeCacheHeadersAsync_SplitsVaryHeaderByCommas() + { + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink); + var context = TestUtils.CreateTestContext(); + + context.HttpContext.Response.Headers.Vary = "HeaderB, heaDera"; + + middleware.FinalizeCacheHeaders(context); + + Assert.Equal(new StringValues(new[] { "HEADERA", "HEADERB" }), context.CachedVaryByRules.Headers); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.VaryByRulesUpdated); + } + + [Fact] + public async Task FinalizeCacheBody_Cache_IfContentLengthMatches() + { + var cache = new TestOutputCache(); + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); + var context = TestUtils.CreateTestContext(); + + context.AllowCacheStorage = true; + middleware.ShimResponseStream(context); + context.HttpContext.Response.ContentLength = 20; + + await context.HttpContext.Response.WriteAsync(new string('0', 20)); + + context.CachedResponse = new OutputCacheEntry(); + context.CacheKey = "BaseKey"; + context.CachedResponseValidFor = TimeSpan.FromSeconds(10); + + await middleware.FinalizeCacheBodyAsync(context); + + Assert.Equal(1, cache.SetCount); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.ResponseCached); + } + + [Theory] + [InlineData("GET")] + [InlineData("HEAD")] + public async Task FinalizeCacheBody_DoNotCache_IfContentLengthMismatches(string method) + { + var cache = new TestOutputCache(); + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); + var context = TestUtils.CreateTestContext(); + + context.AllowCacheStorage = true; + middleware.ShimResponseStream(context); + context.HttpContext.Response.ContentLength = 9; + context.HttpContext.Request.Method = method; + + await context.HttpContext.Response.WriteAsync(new string('0', 10)); + + context.CachedResponse = new OutputCacheEntry(); + context.CacheKey = "BaseKey"; + context.CachedResponseValidFor = TimeSpan.FromSeconds(10); + + await middleware.FinalizeCacheBodyAsync(context); + + Assert.Equal(0, cache.SetCount); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.ResponseContentLengthMismatchNotCached); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task FinalizeCacheBody_RequestHead_Cache_IfContentLengthPresent_AndBodyAbsentOrOfSameLength(bool includeBody) + { + var cache = new TestOutputCache(); + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); + var context = TestUtils.CreateTestContext(); + + context.AllowCacheStorage = true; + middleware.ShimResponseStream(context); + context.HttpContext.Response.ContentLength = 10; + context.HttpContext.Request.Method = "HEAD"; + + if (includeBody) + { + // A response to HEAD should not include a body, but it may be present + await context.HttpContext.Response.WriteAsync(new string('0', 10)); + } + + context.CachedResponse = new OutputCacheEntry(); + context.CacheKey = "BaseKey"; + context.CachedResponseValidFor = TimeSpan.FromSeconds(10); + + await middleware.FinalizeCacheBodyAsync(context); + + Assert.Equal(1, cache.SetCount); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.ResponseCached); + } + + [Fact] + public async Task FinalizeCacheBody_Cache_IfContentLengthAbsent() + { + var cache = new TestOutputCache(); + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); + var context = TestUtils.CreateTestContext(); + + context.AllowCacheStorage = true; + middleware.ShimResponseStream(context); + + await context.HttpContext.Response.WriteAsync(new string('0', 10)); + + context.CachedResponse = new OutputCacheEntry() + { + Headers = new HeaderDictionary() + }; + context.CacheKey = "BaseKey"; + context.CachedResponseValidFor = TimeSpan.FromSeconds(10); + + await middleware.FinalizeCacheBodyAsync(context); + + Assert.Equal(1, cache.SetCount); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.ResponseCached); + } + + [Fact] + public async Task FinalizeCacheBody_DoNotCache_IfAllowCacheStorageFalse() + { + var cache = new TestOutputCache(); + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); + var context = TestUtils.CreateTestContext(); + + middleware.ShimResponseStream(context); + await context.HttpContext.Response.WriteAsync(new string('0', 10)); + context.AllowCacheStorage = false; + + await middleware.FinalizeCacheBodyAsync(context); + + Assert.Equal(0, cache.SetCount); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.ResponseNotCached); + } + + [Fact] + public async Task FinalizeCacheBody_DoNotCache_IfBufferingDisabled() + { + var cache = new TestOutputCache(); + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); + var context = TestUtils.CreateTestContext(); + + context.AllowCacheStorage = true; + middleware.ShimResponseStream(context); + await context.HttpContext.Response.WriteAsync(new string('0', 10)); + + context.OutputCachingStream.DisableBuffering(); + + await middleware.FinalizeCacheBodyAsync(context); + + Assert.Equal(0, cache.SetCount); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.ResponseNotCached); + } + + [Fact] + public async Task FinalizeCacheBody_DoNotCache_IfSizeTooBig() + { + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware( + testSink: sink, + keyProvider: new TestResponseCachingKeyProvider("BaseKey"), + cache: new MemoryOutputCacheStore(new MemoryCache(new MemoryCacheOptions + { + SizeLimit = 100 + }))); + var context = TestUtils.CreateTestContext(); + + context.AllowCacheStorage = true; + middleware.ShimResponseStream(context); + + await context.HttpContext.Response.WriteAsync(new string('0', 101)); + + context.CachedResponse = new OutputCacheEntry() { Headers = new HeaderDictionary() }; + context.CachedResponseValidFor = TimeSpan.FromSeconds(10); + + await middleware.FinalizeCacheBodyAsync(context); + + // The response cached message will be logged but the adding of the entry will no-op + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.ResponseCached); + + // The entry cannot be retrieved + Assert.False(await middleware.TryServeFromCacheAsync(context)); + } + + [Fact] + public void AddOutputCachingFeature_SecondInvocation_Throws() + { + var httpContext = new DefaultHttpContext(); + var context = TestUtils.CreateTestContext(httpContext); + + // Should not throw + OutputCachingMiddleware.AddOutputCachingFeature(context); + + // Should throw + Assert.ThrowsAny(() => OutputCachingMiddleware.AddOutputCachingFeature(context)); + } + + private class FakeResponseFeature : HttpResponseFeature + { + public override void OnStarting(Func callback, object state) { } + } + + [Theory] + // If allowResponseCaching is false, other settings will not matter but are included for completeness + [InlineData(false, false, false)] + [InlineData(false, false, true)] + [InlineData(false, true, false)] + [InlineData(false, true, true)] + [InlineData(true, false, false)] + [InlineData(true, false, true)] + [InlineData(true, true, false)] + [InlineData(true, true, true)] + public async Task Invoke_AddsOutputCachingFeature_Always(bool allowResponseCaching, bool allowCacheLookup, bool allowCacheStorage) + { + var responseCachingFeatureAdded = false; + var middleware = TestUtils.CreateTestMiddleware(next: httpContext => + { + responseCachingFeatureAdded = httpContext.Features.Get() != null; + return Task.CompletedTask; + }, + policyProvider: new TestOutputCachingPolicyProvider + { + AttemptOutputCachingValue = allowResponseCaching, + AllowCacheLookupValue = allowCacheLookup, + AllowCacheStorageValue = allowCacheStorage + }); + + var context = new DefaultHttpContext(); + context.Features.Set(new FakeResponseFeature()); + await middleware.Invoke(context); + + Assert.True(responseCachingFeatureAdded); + } + + [Fact] + public void GetOrderCasingNormalizedStringValues_NormalizesCasingToUpper() + { + var uppercaseStrings = new StringValues(new[] { "STRINGA", "STRINGB" }); + var lowercaseStrings = new StringValues(new[] { "stringA", "stringB" }); + + var normalizedStrings = OutputCachingMiddleware.GetOrderCasingNormalizedStringValues(lowercaseStrings); + + Assert.Equal(uppercaseStrings, normalizedStrings); + } + + [Fact] + public void GetOrderCasingNormalizedStringValues_NormalizesOrder() + { + var orderedStrings = new StringValues(new[] { "STRINGA", "STRINGB" }); + var reverseOrderStrings = new StringValues(new[] { "STRINGB", "STRINGA" }); + + var normalizedStrings = OutputCachingMiddleware.GetOrderCasingNormalizedStringValues(reverseOrderStrings); + + Assert.Equal(orderedStrings, normalizedStrings); + } + + [Fact] + public void GetOrderCasingNormalizedStringValues_PreservesCommas() + { + var originalStrings = new StringValues(new[] { "STRINGA, STRINGB" }); + + var normalizedStrings = OutputCachingMiddleware.GetOrderCasingNormalizedStringValues(originalStrings); + + Assert.Equal(originalStrings, normalizedStrings); + } +} diff --git a/src/Middleware/OutputCaching/test/ResponseCachingFeatureTests.cs b/src/Middleware/OutputCaching/test/ResponseCachingFeatureTests.cs deleted file mode 100644 index d5f52327ab06..000000000000 --- a/src/Middleware/OutputCaching/test/ResponseCachingFeatureTests.cs +++ /dev/null @@ -1,54 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -//namespace Microsoft.AspNetCore.ResponseCaching.Tests; - -//public class ResponseCachingFeatureTests -//{ -// public static TheoryData ValidNullOrEmptyVaryRules -// { -// get -// { -// return new TheoryData -// { -// null, -// new string[0], -// new string[] { null }, -// new string[] { string.Empty } -// }; -// } -// } - -// [Theory] -// [MemberData(nameof(ValidNullOrEmptyVaryRules))] -// public void VaryByQueryKeys_Set_ValidEmptyValues_Succeeds(string[] value) -// { -// // Does not throw -// new ResponseCachingFeature().VaryByQueryKeys = value; -// } - -// public static TheoryData InvalidVaryRules -// { -// get -// { -// return new TheoryData -// { -// new string[] { null, null }, -// new string[] { null, string.Empty }, -// new string[] { string.Empty, null }, -// new string[] { string.Empty, "Valid" }, -// new string[] { "Valid", string.Empty }, -// new string[] { null, "Valid" }, -// new string[] { "Valid", null } -// }; -// } -// } - -// [Theory] -// [MemberData(nameof(InvalidVaryRules))] -// public void VaryByQueryKeys_Set_InValidEmptyValues_Throws(string[] value) -// { -// // Throws -// Assert.Throws(() => new ResponseCachingFeature().VaryByQueryKeys = value); -// } -//} diff --git a/src/Middleware/OutputCaching/test/ResponseCachingKeyProviderTests.cs b/src/Middleware/OutputCaching/test/ResponseCachingKeyProviderTests.cs deleted file mode 100644 index dc3d73c6096f..000000000000 --- a/src/Middleware/OutputCaching/test/ResponseCachingKeyProviderTests.cs +++ /dev/null @@ -1,214 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -//using Microsoft.AspNetCore.Http; - -//namespace Microsoft.AspNetCore.ResponseCaching.Tests; - -//public class ResponseCachingKeyProviderTests -//{ -// private static readonly char KeyDelimiter = '\x1e'; -// private static readonly char KeySubDelimiter = '\x1f'; - -// [Fact] -// public void ResponseCachingKeyProvider_CreateStorageBaseKey_IncludesOnlyNormalizedMethodSchemeHostPortAndPath() -// { -// var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); -// var context = TestUtils.CreateTestContext(); -// context.HttpContext.Request.Method = "head"; -// context.HttpContext.Request.Path = "/path/subpath"; -// context.HttpContext.Request.Scheme = "https"; -// context.HttpContext.Request.Host = new HostString("example.com", 80); -// context.HttpContext.Request.PathBase = "/pathBase"; -// context.HttpContext.Request.QueryString = new QueryString("?query.Key=a&query.Value=b"); - -// Assert.Equal($"HEAD{KeyDelimiter}HTTPS{KeyDelimiter}EXAMPLE.COM:80/PATHBASE/PATH/SUBPATH", cacheKeyProvider.CreateBaseKey(context)); -// } - -// [Fact] -// public void ResponseCachingKeyProvider_CreateStorageBaseKey_CaseInsensitivePath_NormalizesPath() -// { -// var cacheKeyProvider = TestUtils.CreateTestKeyProvider(new ResponseCachingOptions() -// { -// UseCaseSensitivePaths = false -// }); -// var context = TestUtils.CreateTestContext(); -// context.HttpContext.Request.Method = HttpMethods.Get; -// context.HttpContext.Request.Path = "/Path"; - -// Assert.Equal($"{HttpMethods.Get}{KeyDelimiter}{KeyDelimiter}/PATH", cacheKeyProvider.CreateBaseKey(context)); -// } - -// [Fact] -// public void ResponseCachingKeyProvider_CreateStorageBaseKey_CaseSensitivePath_PreservesPathCase() -// { -// var cacheKeyProvider = TestUtils.CreateTestKeyProvider(new ResponseCachingOptions() -// { -// UseCaseSensitivePaths = true -// }); -// var context = TestUtils.CreateTestContext(); -// context.HttpContext.Request.Method = HttpMethods.Get; -// context.HttpContext.Request.Path = "/Path"; - -// Assert.Equal($"{HttpMethods.Get}{KeyDelimiter}{KeyDelimiter}/Path", cacheKeyProvider.CreateBaseKey(context)); -// } - -// [Fact] -// public void ResponseCachingKeyProvider_CreateStorageVaryByKey_Throws_IfVaryByRulesIsNull() -// { -// var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); -// var context = TestUtils.CreateTestContext(); - -// Assert.Throws(() => cacheKeyProvider.CreateStorageVaryByKey(context)); -// } - -// [Fact] -// public void ResponseCachingKeyProvider_CreateStorageVaryKey_ReturnsCachedVaryByGuid_IfVaryByRulesIsEmpty() -// { -// var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); -// var context = TestUtils.CreateTestContext(); -// context.CachedVaryByRules = new CachedVaryByRules() -// { -// VaryByKeyPrefix = FastGuid.NewGuid().IdString -// }; - -// Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}", cacheKeyProvider.CreateStorageVaryByKey(context)); -// } - -// [Fact] -// public void ResponseCachingKeyProvider_CreateStorageVaryKey_IncludesListedHeadersOnly() -// { -// var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); -// var context = TestUtils.CreateTestContext(); -// context.HttpContext.Request.Headers["HeaderA"] = "ValueA"; -// context.HttpContext.Request.Headers["HeaderB"] = "ValueB"; -// context.CachedVaryByRules = new CachedVaryByRules() -// { -// Headers = new string[] { "HeaderA", "HeaderC" } -// }; - -// Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=", -// cacheKeyProvider.CreateStorageVaryByKey(context)); -// } - -// [Fact] -// public void ResponseCachingKeyProvider_CreateStorageVaryKey_HeaderValuesAreSorted() -// { -// var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); -// var context = TestUtils.CreateTestContext(); -// context.HttpContext.Request.Headers["HeaderA"] = "ValueB"; -// context.HttpContext.Request.Headers.Append("HeaderA", "ValueA"); -// context.CachedVaryByRules = new CachedVaryByRules() -// { -// Headers = new string[] { "HeaderA", "HeaderC" } -// }; - -// Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueAValueB{KeyDelimiter}HeaderC=", -// cacheKeyProvider.CreateStorageVaryByKey(context)); -// } - -// [Fact] -// public void ResponseCachingKeyProvider_CreateStorageVaryKey_IncludesListedQueryKeysOnly() -// { -// var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); -// var context = TestUtils.CreateTestContext(); -// context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueA&QueryB=ValueB"); -// context.CachedVaryByRules = new CachedVaryByRules() -// { -// VaryByKeyPrefix = FastGuid.NewGuid().IdString, -// QueryKeys = new string[] { "QueryA", "QueryC" } -// }; - -// Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}QueryA=ValueA{KeyDelimiter}QueryC=", -// cacheKeyProvider.CreateStorageVaryByKey(context)); -// } - -// [Fact] -// public void ResponseCachingKeyProvider_CreateStorageVaryKey_IncludesQueryKeys_QueryKeyCaseInsensitive_UseQueryKeyCasing() -// { -// var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); -// var context = TestUtils.CreateTestContext(); -// context.HttpContext.Request.QueryString = new QueryString("?queryA=ValueA&queryB=ValueB"); -// context.CachedVaryByRules = new CachedVaryByRules() -// { -// VaryByKeyPrefix = FastGuid.NewGuid().IdString, -// QueryKeys = new string[] { "QueryA", "QueryC" } -// }; - -// Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}QueryA=ValueA{KeyDelimiter}QueryC=", -// cacheKeyProvider.CreateStorageVaryByKey(context)); -// } - -// [Fact] -// public void ResponseCachingKeyProvider_CreateStorageVaryKey_IncludesAllQueryKeysGivenAsterisk() -// { -// var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); -// var context = TestUtils.CreateTestContext(); -// context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueA&QueryB=ValueB"); -// context.CachedVaryByRules = new CachedVaryByRules() -// { -// VaryByKeyPrefix = FastGuid.NewGuid().IdString, -// QueryKeys = new string[] { "*" } -// }; - -// // To support case insensitivity, all query keys are converted to upper case. -// // Explicit query keys uses the casing specified in the setting. -// Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}QUERYA=ValueA{KeyDelimiter}QUERYB=ValueB", -// cacheKeyProvider.CreateStorageVaryByKey(context)); -// } - -// [Fact] -// public void ResponseCachingKeyProvider_CreateStorageVaryKey_QueryKeysValuesNotConsolidated() -// { -// var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); -// var context = TestUtils.CreateTestContext(); -// context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueA&QueryA=ValueB"); -// context.CachedVaryByRules = new CachedVaryByRules() -// { -// VaryByKeyPrefix = FastGuid.NewGuid().IdString, -// QueryKeys = new string[] { "*" } -// }; - -// // To support case insensitivity, all query keys are converted to upper case. -// // Explicit query keys uses the casing specified in the setting. -// Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}QUERYA=ValueA{KeySubDelimiter}ValueB", -// cacheKeyProvider.CreateStorageVaryByKey(context)); -// } - -// [Fact] -// public void ResponseCachingKeyProvider_CreateStorageVaryKey_QueryKeysValuesAreSorted() -// { -// var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); -// var context = TestUtils.CreateTestContext(); -// context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueB&QueryA=ValueA"); -// context.CachedVaryByRules = new CachedVaryByRules() -// { -// VaryByKeyPrefix = FastGuid.NewGuid().IdString, -// QueryKeys = new string[] { "*" } -// }; - -// // To support case insensitivity, all query keys are converted to upper case. -// // Explicit query keys uses the casing specified in the setting. -// Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}QUERYA=ValueA{KeySubDelimiter}ValueB", -// cacheKeyProvider.CreateStorageVaryByKey(context)); -// } - -// [Fact] -// public void ResponseCachingKeyProvider_CreateStorageVaryKey_IncludesListedHeadersAndQueryKeys() -// { -// var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); -// var context = TestUtils.CreateTestContext(); -// context.HttpContext.Request.Headers["HeaderA"] = "ValueA"; -// context.HttpContext.Request.Headers["HeaderB"] = "ValueB"; -// context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueA&QueryB=ValueB"); -// context.CachedVaryByRules = new CachedVaryByRules() -// { -// VaryByKeyPrefix = FastGuid.NewGuid().IdString, -// Headers = new string[] { "HeaderA", "HeaderC" }, -// QueryKeys = new string[] { "QueryA", "QueryC" } -// }; - -// Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC={KeyDelimiter}Q{KeyDelimiter}QueryA=ValueA{KeyDelimiter}QueryC=", -// cacheKeyProvider.CreateStorageVaryByKey(context)); -// } -//} diff --git a/src/Middleware/OutputCaching/test/ResponseCachingMiddlewareTests.cs b/src/Middleware/OutputCaching/test/ResponseCachingMiddlewareTests.cs deleted file mode 100644 index 8315d2e47af2..000000000000 --- a/src/Middleware/OutputCaching/test/ResponseCachingMiddlewareTests.cs +++ /dev/null @@ -1,966 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -//using Microsoft.AspNetCore.Http; -//using Microsoft.AspNetCore.Http.Features; -//using Microsoft.Extensions.Caching.Memory; -//using Microsoft.Extensions.Logging.Testing; -//using Microsoft.Extensions.Primitives; -//using Microsoft.Net.Http.Headers; - -//namespace Microsoft.AspNetCore.ResponseCaching.Tests; - -//public class ResponseCachingMiddlewareTests -//{ -// [Fact] -// public async Task TryServeFromCacheAsync_OnlyIfCached_Serves504() -// { -// var cache = new TestResponseCache(); -// var sink = new TestSink(); -// var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider()); -// var context = TestUtils.CreateTestContext(); -// context.HttpContext.Request.Headers.CacheControl = new CacheControlHeaderValue() -// { -// OnlyIfCached = true -// }.ToString(); - -// Assert.True(await middleware.TryServeFromCacheAsync(context)); -// Assert.Equal(StatusCodes.Status504GatewayTimeout, context.HttpContext.Response.StatusCode); -// TestUtils.AssertLoggedMessages( -// sink.Writes, -// LoggedMessage.GatewayTimeoutServed); -// } - -// [Fact] -// public async Task TryServeFromCacheAsync_CachedResponseNotFound_Fails() -// { -// var cache = new TestResponseCache(); -// var sink = new TestSink(); -// var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider("BaseKey")); -// var context = TestUtils.CreateTestContext(); - -// Assert.False(await middleware.TryServeFromCacheAsync(context)); -// Assert.Equal(1, cache.GetCount); -// TestUtils.AssertLoggedMessages( -// sink.Writes, -// LoggedMessage.NoResponseServed); -// } - -// [Fact] -// public async Task TryServeFromCacheAsync_CachedResponseFound_Succeeds() -// { -// var cache = new TestResponseCache(); -// var sink = new TestSink(); -// var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider("BaseKey")); -// var context = TestUtils.CreateTestContext(); - -// cache.Set( -// "BaseKey", -// new CachedResponse() -// { -// Headers = new HeaderDictionary(), -// Body = new CachedResponseBody(new List(0), 0) -// }, -// TimeSpan.Zero); - -// Assert.True(await middleware.TryServeFromCacheAsync(context)); -// Assert.Equal(1, cache.GetCount); -// TestUtils.AssertLoggedMessages( -// sink.Writes, -// LoggedMessage.CachedResponseServed); -// } - -// [Fact] -// public async Task TryServeFromCacheAsync_CachedResponseFound_OverwritesExistingHeaders() -// { -// var cache = new TestResponseCache(); -// var sink = new TestSink(); -// var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider("BaseKey")); -// var context = TestUtils.CreateTestContext(); - -// context.HttpContext.Response.Headers["MyHeader"] = "OldValue"; -// cache.Set( -// "BaseKey", -// new CachedResponse() -// { -// Headers = new HeaderDictionary() -// { -// { "MyHeader", "NewValue" } -// }, -// Body = new CachedResponseBody(new List(0), 0) -// }, -// TimeSpan.Zero); - -// Assert.True(await middleware.TryServeFromCacheAsync(context)); -// Assert.Equal("NewValue", context.HttpContext.Response.Headers["MyHeader"]); -// Assert.Equal(1, cache.GetCount); -// TestUtils.AssertLoggedMessages( -// sink.Writes, -// LoggedMessage.CachedResponseServed); -// } - -// [Fact] -// public async Task TryServeFromCacheAsync_VaryByRuleFound_CachedResponseNotFound_Fails() -// { -// var cache = new TestResponseCache(); -// var sink = new TestSink(); -// var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider("BaseKey", "VaryKey")); -// var context = TestUtils.CreateTestContext(); - -// cache.Set( -// "BaseKey", -// new CachedVaryByRules(), -// TimeSpan.Zero); - -// Assert.False(await middleware.TryServeFromCacheAsync(context)); -// Assert.Equal(2, cache.GetCount); -// TestUtils.AssertLoggedMessages( -// sink.Writes, -// LoggedMessage.NoResponseServed); -// } - -// [Fact] -// public async Task TryServeFromCacheAsync_VaryByRuleFound_CachedResponseFound_Succeeds() -// { -// var cache = new TestResponseCache(); -// var sink = new TestSink(); -// var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider("BaseKey", new[] { "VaryKey", "VaryKey2" })); -// var context = TestUtils.CreateTestContext(); - -// cache.Set( -// "BaseKey", -// new CachedVaryByRules(), -// TimeSpan.Zero); -// cache.Set( -// "BaseKeyVaryKey2", -// new CachedResponse() -// { -// Headers = new HeaderDictionary(), -// Body = new CachedResponseBody(new List(0), 0) -// }, -// TimeSpan.Zero); - -// Assert.True(await middleware.TryServeFromCacheAsync(context)); -// Assert.Equal(3, cache.GetCount); -// TestUtils.AssertLoggedMessages( -// sink.Writes, -// LoggedMessage.CachedResponseServed); -// } - -// [Fact] -// public async Task TryServeFromCacheAsync_CachedResponseFound_Serves304IfPossible() -// { -// var cache = new TestResponseCache(); -// var sink = new TestSink(); -// var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider("BaseKey")); -// var context = TestUtils.CreateTestContext(); -// context.HttpContext.Request.Headers.IfNoneMatch = "*"; - -// cache.Set( -// "BaseKey", -// new CachedResponse() -// { -// Body = new CachedResponseBody(new List(0), 0) -// }, -// TimeSpan.Zero); - -// Assert.True(await middleware.TryServeFromCacheAsync(context)); -// Assert.Equal(1, cache.GetCount); -// TestUtils.AssertLoggedMessages( -// sink.Writes, -// LoggedMessage.NotModifiedServed); -// } - -// [Fact] -// public void ContentIsNotModified_NotConditionalRequest_False() -// { -// var sink = new TestSink(); -// var context = TestUtils.CreateTestContext(sink); -// context.CachedResponseHeaders = new HeaderDictionary(); - -// Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context)); -// Assert.Empty(sink.Writes); -// } - -// [Fact] -// public void ContentIsNotModified_IfModifiedSince_FallsbackToDateHeader() -// { -// var utcNow = DateTimeOffset.UtcNow; -// var sink = new TestSink(); -// var context = TestUtils.CreateTestContext(sink); -// context.CachedResponseHeaders = new HeaderDictionary(); - -// context.HttpContext.Request.Headers.IfModifiedSince = HeaderUtilities.FormatDate(utcNow); - -// // Verify modifications in the past succeeds -// context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10)); -// Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context)); -// Assert.Single(sink.Writes); - -// // Verify modifications at present succeeds -// context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow); -// Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context)); -// Assert.Equal(2, sink.Writes.Count); - -// // Verify modifications in the future fails -// context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); -// Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context)); - -// // Verify logging -// TestUtils.AssertLoggedMessages( -// sink.Writes, -// LoggedMessage.NotModifiedIfModifiedSinceSatisfied, -// LoggedMessage.NotModifiedIfModifiedSinceSatisfied); -// } - -// [Fact] -// public void ContentIsNotModified_IfModifiedSince_LastModifiedOverridesDateHeader() -// { -// var utcNow = DateTimeOffset.UtcNow; -// var sink = new TestSink(); -// var context = TestUtils.CreateTestContext(sink); -// context.CachedResponseHeaders = new HeaderDictionary(); - -// context.HttpContext.Request.Headers.IfModifiedSince = HeaderUtilities.FormatDate(utcNow); - -// // Verify modifications in the past succeeds -// context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); -// context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10)); -// Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context)); -// Assert.Single(sink.Writes); - -// // Verify modifications at present -// context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); -// context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow); -// Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context)); -// Assert.Equal(2, sink.Writes.Count); - -// // Verify modifications in the future fails -// context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10)); -// context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); -// Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context)); - -// // Verify logging -// TestUtils.AssertLoggedMessages( -// sink.Writes, -// LoggedMessage.NotModifiedIfModifiedSinceSatisfied, -// LoggedMessage.NotModifiedIfModifiedSinceSatisfied); -// } - -// [Fact] -// public void ContentIsNotModified_IfNoneMatch_Overrides_IfModifiedSince_ToTrue() -// { -// var utcNow = DateTimeOffset.UtcNow; -// var sink = new TestSink(); -// var context = TestUtils.CreateTestContext(sink); -// context.CachedResponseHeaders = new HeaderDictionary(); - -// // This would fail the IfModifiedSince checks -// context.HttpContext.Request.Headers.IfModifiedSince = HeaderUtilities.FormatDate(utcNow); -// context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); - -// context.HttpContext.Request.Headers.IfNoneMatch = EntityTagHeaderValue.Any.ToString(); -// Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context)); -// TestUtils.AssertLoggedMessages( -// sink.Writes, -// LoggedMessage.NotModifiedIfNoneMatchStar); -// } - -// [Fact] -// public void ContentIsNotModified_IfNoneMatch_Overrides_IfModifiedSince_ToFalse() -// { -// var utcNow = DateTimeOffset.UtcNow; -// var sink = new TestSink(); -// var context = TestUtils.CreateTestContext(sink); -// context.CachedResponseHeaders = new HeaderDictionary(); - -// // This would pass the IfModifiedSince checks -// context.HttpContext.Request.Headers.IfModifiedSince = HeaderUtilities.FormatDate(utcNow); -// context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10)); - -// context.HttpContext.Request.Headers.IfNoneMatch = "\"E1\""; -// Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context)); -// Assert.Empty(sink.Writes); -// } - -// [Fact] -// public void ContentIsNotModified_IfNoneMatch_AnyWithoutETagInResponse_False() -// { -// var sink = new TestSink(); -// var context = TestUtils.CreateTestContext(sink); -// context.CachedResponseHeaders = new HeaderDictionary(); -// context.HttpContext.Request.Headers.IfNoneMatch = "\"E1\""; - -// Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context)); -// Assert.Empty(sink.Writes); -// } - -// public static TheoryData EquivalentWeakETags -// { -// get -// { -// return new TheoryData -// { -// { new EntityTagHeaderValue("\"tag\""), new EntityTagHeaderValue("\"tag\"") }, -// { new EntityTagHeaderValue("\"tag\"", true), new EntityTagHeaderValue("\"tag\"") }, -// { new EntityTagHeaderValue("\"tag\""), new EntityTagHeaderValue("\"tag\"", true) }, -// { new EntityTagHeaderValue("\"tag\"", true), new EntityTagHeaderValue("\"tag\"", true) } -// }; -// } -// } - -// [Theory] -// [MemberData(nameof(EquivalentWeakETags))] -// public void ContentIsNotModified_IfNoneMatch_ExplicitWithMatch_True(EntityTagHeaderValue responseETag, EntityTagHeaderValue requestETag) -// { -// var sink = new TestSink(); -// var context = TestUtils.CreateTestContext(sink); -// context.CachedResponseHeaders = new HeaderDictionary(); -// context.CachedResponseHeaders[HeaderNames.ETag] = responseETag.ToString(); -// context.HttpContext.Request.Headers.IfNoneMatch = requestETag.ToString(); - -// Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context)); -// TestUtils.AssertLoggedMessages( -// sink.Writes, -// LoggedMessage.NotModifiedIfNoneMatchMatched); -// } - -// [Fact] -// public void ContentIsNotModified_IfNoneMatch_ExplicitWithoutMatch_False() -// { -// var sink = new TestSink(); -// var context = TestUtils.CreateTestContext(sink); -// context.CachedResponseHeaders = new HeaderDictionary(); -// context.CachedResponseHeaders[HeaderNames.ETag] = "\"E2\""; -// context.HttpContext.Request.Headers.IfNoneMatch = "\"E1\""; - -// Assert.False(ResponseCachingMiddleware.ContentIsNotModified(context)); -// Assert.Empty(sink.Writes); -// } - -// [Fact] -// public void ContentIsNotModified_IfNoneMatch_MatchesAtLeastOneValue_True() -// { -// var sink = new TestSink(); -// var context = TestUtils.CreateTestContext(sink); -// context.CachedResponseHeaders = new HeaderDictionary(); -// context.CachedResponseHeaders[HeaderNames.ETag] = "\"E2\""; -// context.HttpContext.Request.Headers.IfNoneMatch = new string[] { "\"E0\", \"E1\"", "\"E1\", \"E2\"" }; - -// Assert.True(ResponseCachingMiddleware.ContentIsNotModified(context)); -// TestUtils.AssertLoggedMessages( -// sink.Writes, -// LoggedMessage.NotModifiedIfNoneMatchMatched); -// } - -// [Fact] -// public void StartResponsegAsync_IfAllowResponseCaptureIsTrue_SetsResponseTime() -// { -// var clock = new TestClock -// { -// UtcNow = DateTimeOffset.UtcNow -// }; -// var middleware = TestUtils.CreateTestMiddleware(options: new ResponseCachingOptions -// { -// SystemClock = clock -// }); -// var context = TestUtils.CreateTestContext(); -// context.ResponseTime = null; - -// middleware.StartResponse(context); - -// Assert.Equal(clock.UtcNow, context.ResponseTime); -// } - -// [Fact] -// public void StartResponseAsync_IfAllowResponseCaptureIsTrue_SetsResponseTimeOnlyOnce() -// { -// var clock = new TestClock -// { -// UtcNow = DateTimeOffset.UtcNow -// }; -// var middleware = TestUtils.CreateTestMiddleware(options: new ResponseCachingOptions -// { -// SystemClock = clock -// }); -// var context = TestUtils.CreateTestContext(); -// var initialTime = clock.UtcNow; -// context.ResponseTime = null; - -// middleware.StartResponse(context); -// Assert.Equal(initialTime, context.ResponseTime); - -// clock.UtcNow += TimeSpan.FromSeconds(10); - -// middleware.StartResponse(context); -// Assert.NotEqual(clock.UtcNow, context.ResponseTime); -// Assert.Equal(initialTime, context.ResponseTime); -// } - -// [Fact] -// public void FinalizeCacheHeadersAsync_UpdateShouldCacheResponse_IfResponseCacheable() -// { -// var sink = new TestSink(); -// var middleware = TestUtils.CreateTestMiddleware(testSink: sink, policyProvider: new ResponseCachingPolicyProvider()); -// var context = TestUtils.CreateTestContext(); - -// context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() -// { -// Public = true -// }.ToString(); - -// Assert.False(context.ShouldCacheResponse); - -// middleware.FinalizeCacheHeaders(context); - -// Assert.True(context.ShouldCacheResponse); -// Assert.Empty(sink.Writes); -// } - -// [Fact] -// public void FinalizeCacheHeadersAsync_DoNotUpdateShouldCacheResponse_IfResponseIsNotCacheable() -// { -// var sink = new TestSink(); -// var middleware = TestUtils.CreateTestMiddleware(testSink: sink, policyProvider: new ResponseCachingPolicyProvider()); -// var context = TestUtils.CreateTestContext(); - -// middleware.ShimResponseStream(context); - -// middleware.FinalizeCacheHeaders(context); - -// Assert.False(context.ShouldCacheResponse); -// Assert.Empty(sink.Writes); -// } - -// [Fact] -// public void FinalizeCacheHeadersAsync_DefaultResponseValidity_Is10Seconds() -// { -// var sink = new TestSink(); -// var middleware = TestUtils.CreateTestMiddleware(testSink: sink); -// var context = TestUtils.CreateTestContext(); - -// middleware.FinalizeCacheHeaders(context); - -// Assert.Equal(TimeSpan.FromSeconds(10), context.CachedResponseValidFor); -// Assert.Empty(sink.Writes); -// } - -// [Fact] -// public void FinalizeCacheHeadersAsync_ResponseValidity_UseExpiryIfAvailable() -// { -// var clock = new TestClock -// { -// UtcNow = DateTimeOffset.MinValue -// }; -// var sink = new TestSink(); -// var middleware = TestUtils.CreateTestMiddleware(testSink: sink, options: new ResponseCachingOptions -// { -// SystemClock = clock -// }); -// var context = TestUtils.CreateTestContext(); - -// context.ResponseTime = clock.UtcNow; -// context.HttpContext.Response.Headers.Expires = HeaderUtilities.FormatDate(clock.UtcNow + TimeSpan.FromSeconds(11)); - -// middleware.FinalizeCacheHeaders(context); - -// Assert.Equal(TimeSpan.FromSeconds(11), context.CachedResponseValidFor); -// Assert.Empty(sink.Writes); -// } - -// [Fact] -// public void FinalizeCacheHeadersAsync_ResponseValidity_UseMaxAgeIfAvailable() -// { -// var clock = new TestClock -// { -// UtcNow = DateTimeOffset.UtcNow -// }; -// var sink = new TestSink(); -// var middleware = TestUtils.CreateTestMiddleware(testSink: sink, options: new ResponseCachingOptions -// { -// SystemClock = clock -// }); -// var context = TestUtils.CreateTestContext(); - -// context.ResponseTime = clock.UtcNow; -// context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() -// { -// MaxAge = TimeSpan.FromSeconds(12) -// }.ToString(); - -// context.HttpContext.Response.Headers.Expires = HeaderUtilities.FormatDate(clock.UtcNow + TimeSpan.FromSeconds(11)); - -// middleware.FinalizeCacheHeaders(context); - -// Assert.Equal(TimeSpan.FromSeconds(12), context.CachedResponseValidFor); -// Assert.Empty(sink.Writes); -// } - -// [Fact] -// public void FinalizeCacheHeadersAsync_ResponseValidity_UseSharedMaxAgeIfAvailable() -// { -// var clock = new TestClock -// { -// UtcNow = DateTimeOffset.UtcNow -// }; -// var sink = new TestSink(); -// var middleware = TestUtils.CreateTestMiddleware(testSink: sink, options: new ResponseCachingOptions -// { -// SystemClock = clock -// }); -// var context = TestUtils.CreateTestContext(); - -// context.ResponseTime = clock.UtcNow; -// context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() -// { -// MaxAge = TimeSpan.FromSeconds(12), -// SharedMaxAge = TimeSpan.FromSeconds(13) -// }.ToString(); -// context.HttpContext.Response.Headers.Expires = HeaderUtilities.FormatDate(clock.UtcNow + TimeSpan.FromSeconds(11)); - -// middleware.FinalizeCacheHeaders(context); - -// Assert.Equal(TimeSpan.FromSeconds(13), context.CachedResponseValidFor); -// Assert.Empty(sink.Writes); -// } - -// [Fact] -// public void FinalizeCacheHeadersAsync_UpdateCachedVaryByRules_IfNotEquivalentToPrevious() -// { -// var cache = new TestResponseCache(); -// var sink = new TestSink(); -// var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); -// var context = TestUtils.CreateTestContext(); - -// context.HttpContext.Response.Headers.Vary = new StringValues(new[] { "headerA", "HEADERB", "HEADERc" }); -// context.HttpContext.Features.Set(new ResponseCachingFeature() -// { -// VaryByQueryKeys = new StringValues(new[] { "queryB", "QUERYA" }) -// }); -// var cachedVaryByRules = new CachedVaryByRules() -// { -// Headers = new StringValues(new[] { "HeaderA", "HeaderB" }), -// QueryKeys = new StringValues(new[] { "QueryA", "QueryB" }) -// }; -// context.CachedVaryByRules = cachedVaryByRules; - -// middleware.FinalizeCacheHeaders(context); - -// Assert.Equal(1, cache.SetCount); -// Assert.NotSame(cachedVaryByRules, context.CachedVaryByRules); -// TestUtils.AssertLoggedMessages( -// sink.Writes, -// LoggedMessage.VaryByRulesUpdated); -// } - -// [Fact] -// public void FinalizeCacheHeadersAsync_UpdateCachedVaryByRules_IfEquivalentToPrevious() -// { -// var cache = new TestResponseCache(); -// var sink = new TestSink(); -// var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); -// var context = TestUtils.CreateTestContext(); - -// context.HttpContext.Response.Headers.Vary = new StringValues(new[] { "headerA", "HEADERB" }); -// context.HttpContext.Features.Set(new ResponseCachingFeature() -// { -// VaryByQueryKeys = new StringValues(new[] { "queryB", "QUERYA" }) -// }); -// var cachedVaryByRules = new CachedVaryByRules() -// { -// VaryByKeyPrefix = FastGuid.NewGuid().IdString, -// Headers = new StringValues(new[] { "HEADERA", "HEADERB" }), -// QueryKeys = new StringValues(new[] { "QUERYA", "QUERYB" }) -// }; -// context.CachedVaryByRules = cachedVaryByRules; - -// middleware.FinalizeCacheHeaders(context); - -// // An update to the cache is always made but the entry should be the same -// Assert.Equal(1, cache.SetCount); -// Assert.Same(cachedVaryByRules, context.CachedVaryByRules); -// TestUtils.AssertLoggedMessages( -// sink.Writes, -// LoggedMessage.VaryByRulesUpdated); -// } - -// public static TheoryData NullOrEmptyVaryRules -// { -// get -// { -// return new TheoryData -// { -// default(StringValues), -// StringValues.Empty, -// new StringValues((string)null), -// new StringValues(string.Empty), -// new StringValues((string[])null), -// new StringValues(new string[0]), -// new StringValues(new string[] { null }), -// new StringValues(new string[] { string.Empty }) -// }; -// } -// } - -// [Theory] -// [MemberData(nameof(NullOrEmptyVaryRules))] -// public void FinalizeCacheHeadersAsync_UpdateCachedVaryByRules_NullOrEmptyRules(StringValues vary) -// { -// var cache = new TestResponseCache(); -// var sink = new TestSink(); -// var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); -// var context = TestUtils.CreateTestContext(); - -// context.HttpContext.Response.Headers.Vary = vary; -// context.HttpContext.Features.Set(new ResponseCachingFeature() -// { -// VaryByQueryKeys = vary -// }); - -// middleware.FinalizeCacheHeaders(context); - -// // Vary rules should not be updated -// Assert.Equal(0, cache.SetCount); -// Assert.Empty(sink.Writes); -// } - -// [Fact] -// public void FinalizeCacheHeadersAsync_AddsDate_IfNoneSpecified() -// { -// var utcNow = DateTimeOffset.UtcNow; -// var sink = new TestSink(); -// var middleware = TestUtils.CreateTestMiddleware(testSink: sink); -// var context = TestUtils.CreateTestContext(); -// // ResponseTime is the actual value that's used to set the Date header in FinalizeCacheHeadersAsync -// context.ResponseTime = utcNow; - -// Assert.True(StringValues.IsNullOrEmpty(context.HttpContext.Response.Headers.Date)); - -// middleware.FinalizeCacheHeaders(context); - -// Assert.Equal(HeaderUtilities.FormatDate(utcNow), context.HttpContext.Response.Headers.Date); -// Assert.Empty(sink.Writes); -// } - -// [Fact] -// public void FinalizeCacheHeadersAsync_DoNotAddDate_IfSpecified() -// { -// var utcNow = DateTimeOffset.MinValue; -// var sink = new TestSink(); -// var middleware = TestUtils.CreateTestMiddleware(testSink: sink); -// var context = TestUtils.CreateTestContext(); - -// context.HttpContext.Response.Headers.Date = HeaderUtilities.FormatDate(utcNow); -// context.ResponseTime = utcNow + TimeSpan.FromSeconds(10); - -// Assert.Equal(HeaderUtilities.FormatDate(utcNow), context.HttpContext.Response.Headers.Date); - -// middleware.FinalizeCacheHeaders(context); - -// Assert.Equal(HeaderUtilities.FormatDate(utcNow), context.HttpContext.Response.Headers.Date); -// Assert.Empty(sink.Writes); -// } - -// [Fact] -// public void FinalizeCacheHeadersAsync_StoresCachedResponse_InState() -// { -// var sink = new TestSink(); -// var middleware = TestUtils.CreateTestMiddleware(testSink: sink); -// var context = TestUtils.CreateTestContext(); - -// Assert.Null(context.CachedResponse); - -// middleware.FinalizeCacheHeaders(context); - -// Assert.NotNull(context.CachedResponse); -// Assert.Empty(sink.Writes); -// } - -// [Fact] -// public void FinalizeCacheHeadersAsync_SplitsVaryHeaderByCommas() -// { -// var sink = new TestSink(); -// var middleware = TestUtils.CreateTestMiddleware(testSink: sink); -// var context = TestUtils.CreateTestContext(); - -// context.HttpContext.Response.Headers.Vary = "HeaderB, heaDera"; - -// middleware.FinalizeCacheHeaders(context); - -// Assert.Equal(new StringValues(new[] { "HEADERA", "HEADERB" }), context.CachedVaryByRules.Headers); -// TestUtils.AssertLoggedMessages( -// sink.Writes, -// LoggedMessage.VaryByRulesUpdated); -// } - -// [Fact] -// public async Task FinalizeCacheBody_Cache_IfContentLengthMatches() -// { -// var cache = new TestResponseCache(); -// var sink = new TestSink(); -// var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); -// var context = TestUtils.CreateTestContext(); - -// context.ShouldCacheResponse = true; -// middleware.ShimResponseStream(context); -// context.HttpContext.Response.ContentLength = 20; - -// await context.HttpContext.Response.WriteAsync(new string('0', 20)); - -// context.CachedResponse = new CachedResponse(); -// context.BaseKey = "BaseKey"; -// context.CachedResponseValidFor = TimeSpan.FromSeconds(10); - -// middleware.FinalizeCacheBody(context); - -// Assert.Equal(1, cache.SetCount); -// TestUtils.AssertLoggedMessages( -// sink.Writes, -// LoggedMessage.ResponseCached); -// } - -// [Theory] -// [InlineData("GET")] -// [InlineData("HEAD")] -// public async Task FinalizeCacheBody_DoNotCache_IfContentLengthMismatches(string method) -// { -// var cache = new TestResponseCache(); -// var sink = new TestSink(); -// var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); -// var context = TestUtils.CreateTestContext(); - -// context.ShouldCacheResponse = true; -// middleware.ShimResponseStream(context); -// context.HttpContext.Response.ContentLength = 9; -// context.HttpContext.Request.Method = method; - -// await context.HttpContext.Response.WriteAsync(new string('0', 10)); - -// context.CachedResponse = new CachedResponse(); -// context.BaseKey = "BaseKey"; -// context.CachedResponseValidFor = TimeSpan.FromSeconds(10); - -// middleware.FinalizeCacheBody(context); - -// Assert.Equal(0, cache.SetCount); -// TestUtils.AssertLoggedMessages( -// sink.Writes, -// LoggedMessage.ResponseContentLengthMismatchNotCached); -// } - -// [Theory] -// [InlineData(false)] -// [InlineData(true)] -// public async Task FinalizeCacheBody_RequestHead_Cache_IfContentLengthPresent_AndBodyAbsentOrOfSameLength(bool includeBody) -// { -// var cache = new TestResponseCache(); -// var sink = new TestSink(); -// var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); -// var context = TestUtils.CreateTestContext(); - -// context.ShouldCacheResponse = true; -// middleware.ShimResponseStream(context); -// context.HttpContext.Response.ContentLength = 10; -// context.HttpContext.Request.Method = "HEAD"; - -// if (includeBody) -// { -// // A response to HEAD should not include a body, but it may be present -// await context.HttpContext.Response.WriteAsync(new string('0', 10)); -// } - -// context.CachedResponse = new CachedResponse(); -// context.BaseKey = "BaseKey"; -// context.CachedResponseValidFor = TimeSpan.FromSeconds(10); - -// middleware.FinalizeCacheBody(context); - -// Assert.Equal(1, cache.SetCount); -// TestUtils.AssertLoggedMessages( -// sink.Writes, -// LoggedMessage.ResponseCached); -// } - -// [Fact] -// public async Task FinalizeCacheBody_Cache_IfContentLengthAbsent() -// { -// var cache = new TestResponseCache(); -// var sink = new TestSink(); -// var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); -// var context = TestUtils.CreateTestContext(); - -// context.ShouldCacheResponse = true; -// middleware.ShimResponseStream(context); - -// await context.HttpContext.Response.WriteAsync(new string('0', 10)); - -// context.CachedResponse = new CachedResponse() -// { -// Headers = new HeaderDictionary() -// }; -// context.BaseKey = "BaseKey"; -// context.CachedResponseValidFor = TimeSpan.FromSeconds(10); - -// middleware.FinalizeCacheBody(context); - -// Assert.Equal(1, cache.SetCount); -// TestUtils.AssertLoggedMessages( -// sink.Writes, -// LoggedMessage.ResponseCached); -// } - -// [Fact] -// public async Task FinalizeCacheBody_DoNotCache_IfShouldCacheResponseFalse() -// { -// var cache = new TestResponseCache(); -// var sink = new TestSink(); -// var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); -// var context = TestUtils.CreateTestContext(); - -// middleware.ShimResponseStream(context); -// await context.HttpContext.Response.WriteAsync(new string('0', 10)); -// context.ShouldCacheResponse = false; - -// middleware.FinalizeCacheBody(context); - -// Assert.Equal(0, cache.SetCount); -// TestUtils.AssertLoggedMessages( -// sink.Writes, -// LoggedMessage.ResponseNotCached); -// } - -// [Fact] -// public async Task FinalizeCacheBody_DoNotCache_IfBufferingDisabled() -// { -// var cache = new TestResponseCache(); -// var sink = new TestSink(); -// var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); -// var context = TestUtils.CreateTestContext(); - -// context.ShouldCacheResponse = true; -// middleware.ShimResponseStream(context); -// await context.HttpContext.Response.WriteAsync(new string('0', 10)); - -// context.ResponseCachingStream.DisableBuffering(); - -// middleware.FinalizeCacheBody(context); - -// Assert.Equal(0, cache.SetCount); -// TestUtils.AssertLoggedMessages( -// sink.Writes, -// LoggedMessage.ResponseNotCached); -// } - -// [Fact] -// public async Task FinalizeCacheBody_DoNotCache_IfSizeTooBig() -// { -// var sink = new TestSink(); -// var middleware = TestUtils.CreateTestMiddleware( -// testSink: sink, -// keyProvider: new TestResponseCachingKeyProvider("BaseKey"), -// cache: new MemoryResponseCache(new MemoryCache(new MemoryCacheOptions -// { -// SizeLimit = 100 -// }))); -// var context = TestUtils.CreateTestContext(); - -// context.ShouldCacheResponse = true; -// middleware.ShimResponseStream(context); - -// await context.HttpContext.Response.WriteAsync(new string('0', 101)); - -// context.CachedResponse = new CachedResponse() { Headers = new HeaderDictionary() }; -// context.CachedResponseValidFor = TimeSpan.FromSeconds(10); - -// middleware.FinalizeCacheBody(context); - -// // The response cached message will be logged but the adding of the entry will no-op -// TestUtils.AssertLoggedMessages( -// sink.Writes, -// LoggedMessage.ResponseCached); - -// // The entry cannot be retrieved -// Assert.False(await middleware.TryServeFromCacheAsync(context)); -// } - -// [Fact] -// public void AddResponseCachingFeature_SecondInvocation_Throws() -// { -// var httpContext = new DefaultHttpContext(); - -// // Should not throw -// ResponseCachingMiddleware.AddResponseCachingFeature(httpContext); - -// // Should throw -// Assert.ThrowsAny(() => ResponseCachingMiddleware.AddResponseCachingFeature(httpContext)); -// } - -// private class FakeResponseFeature : HttpResponseFeature -// { -// public override void OnStarting(Func callback, object state) { } -// } - -// [Theory] -// // If allowResponseCaching is false, other settings will not matter but are included for completeness -// [InlineData(false, false, false)] -// [InlineData(false, false, true)] -// [InlineData(false, true, false)] -// [InlineData(false, true, true)] -// [InlineData(true, false, false)] -// [InlineData(true, false, true)] -// [InlineData(true, true, false)] -// [InlineData(true, true, true)] -// public async Task Invoke_AddsResponseCachingFeature_Always(bool allowResponseCaching, bool allowCacheLookup, bool allowCacheStorage) -// { -// var responseCachingFeatureAdded = false; -// var middleware = TestUtils.CreateTestMiddleware(next: httpContext => -// { -// responseCachingFeatureAdded = httpContext.Features.Get() != null; -// return Task.CompletedTask; -// }, -// policyProvider: new TestResponseCachingPolicyProvider -// { -// AttemptResponseCachingValue = allowResponseCaching, -// AllowCacheLookupValue = allowCacheLookup, -// AllowCacheStorageValue = allowCacheStorage -// }); - -// var context = new DefaultHttpContext(); -// context.Features.Set(new FakeResponseFeature()); -// await middleware.Invoke(context); - -// Assert.True(responseCachingFeatureAdded); -// } - -// [Fact] -// public void GetOrderCasingNormalizedStringValues_NormalizesCasingToUpper() -// { -// var uppercaseStrings = new StringValues(new[] { "STRINGA", "STRINGB" }); -// var lowercaseStrings = new StringValues(new[] { "stringA", "stringB" }); - -// var normalizedStrings = ResponseCachingMiddleware.GetOrderCasingNormalizedStringValues(lowercaseStrings); - -// Assert.Equal(uppercaseStrings, normalizedStrings); -// } - -// [Fact] -// public void GetOrderCasingNormalizedStringValues_NormalizesOrder() -// { -// var orderedStrings = new StringValues(new[] { "STRINGA", "STRINGB" }); -// var reverseOrderStrings = new StringValues(new[] { "STRINGB", "STRINGA" }); - -// var normalizedStrings = ResponseCachingMiddleware.GetOrderCasingNormalizedStringValues(reverseOrderStrings); - -// Assert.Equal(orderedStrings, normalizedStrings); -// } - -// [Fact] -// public void GetOrderCasingNormalizedStringValues_PreservesCommas() -// { -// var originalStrings = new StringValues(new[] { "STRINGA, STRINGB" }); - -// var normalizedStrings = ResponseCachingMiddleware.GetOrderCasingNormalizedStringValues(originalStrings); - -// Assert.Equal(originalStrings, normalizedStrings); -// } -//} diff --git a/src/Middleware/OutputCaching/test/SegmentWriteStreamTests.cs b/src/Middleware/OutputCaching/test/SegmentWriteStreamTests.cs index 2277bf2b5564..cb8d4bcfbb69 100644 --- a/src/Middleware/OutputCaching/test/SegmentWriteStreamTests.cs +++ b/src/Middleware/OutputCaching/test/SegmentWriteStreamTests.cs @@ -1,105 +1,105 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -//namespace Microsoft.AspNetCore.ResponseCaching.Tests; - -//public class SegmentWriteStreamTests -//{ -// private static readonly byte[] WriteData = new byte[] -// { -// 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14 -// }; - -// [Theory] -// [InlineData(0)] -// [InlineData(-1)] -// public void SegmentWriteStream_InvalidSegmentSize_Throws(int segmentSize) -// { -// Assert.Throws(() => new SegmentWriteStream(segmentSize)); -// } - -// [Fact] -// public void ReadAndSeekOperations_Throws() -// { -// var stream = new SegmentWriteStream(1); - -// Assert.Throws(() => stream.Read(new byte[1], 0, 0)); -// Assert.Throws(() => stream.Position = 0); -// Assert.Throws(() => stream.Seek(0, SeekOrigin.Begin)); -// } - -// [Fact] -// public void GetSegments_ExtractionDisablesWriting() -// { -// var stream = new SegmentWriteStream(1); - -// Assert.True(stream.CanWrite); -// Assert.Empty(stream.GetSegments()); -// Assert.False(stream.CanWrite); -// } - -// [Theory] -// [InlineData(4)] -// [InlineData(5)] -// [InlineData(6)] -// public void WriteByte_CanWriteAllBytes(int segmentSize) -// { -// var stream = new SegmentWriteStream(segmentSize); - -// foreach (var datum in WriteData) -// { -// stream.WriteByte(datum); -// } -// var segments = stream.GetSegments(); - -// Assert.Equal(WriteData.Length, stream.Length); -// Assert.Equal((WriteData.Length + segmentSize - 1) / segmentSize, segments.Count); - -// for (var i = 0; i < WriteData.Length; i += segmentSize) -// { -// var expectedSegmentSize = Math.Min(segmentSize, WriteData.Length - i); -// var expectedSegment = new byte[expectedSegmentSize]; -// for (int j = 0; j < expectedSegmentSize; j++) -// { -// expectedSegment[j] = (byte)(i + j); -// } -// var segment = segments[i / segmentSize]; - -// Assert.Equal(expectedSegmentSize, segment.Length); -// Assert.True(expectedSegment.SequenceEqual(segment)); -// } -// } - -// [Theory] -// [InlineData(4)] -// [InlineData(5)] -// [InlineData(6)] -// public void Write_CanWriteAllBytes(int writeSize) -// { -// var segmentSize = 5; -// var stream = new SegmentWriteStream(segmentSize); - -// for (var i = 0; i < WriteData.Length; i += writeSize) -// { -// stream.Write(WriteData, i, Math.Min(writeSize, WriteData.Length - i)); -// } -// var segments = stream.GetSegments(); - -// Assert.Equal(WriteData.Length, stream.Length); -// Assert.Equal((WriteData.Length + segmentSize - 1) / segmentSize, segments.Count); - -// for (var i = 0; i < WriteData.Length; i += segmentSize) -// { -// var expectedSegmentSize = Math.Min(segmentSize, WriteData.Length - i); -// var expectedSegment = new byte[expectedSegmentSize]; -// for (int j = 0; j < expectedSegmentSize; j++) -// { -// expectedSegment[j] = (byte)(i + j); -// } -// var segment = segments[i / segmentSize]; - -// Assert.Equal(expectedSegmentSize, segment.Length); -// Assert.True(expectedSegment.SequenceEqual(segment)); -// } -// } -//} +namespace Microsoft.AspNetCore.OutputCaching.Tests; + +public class SegmentWriteStreamTests +{ + private static readonly byte[] WriteData = new byte[] + { + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14 + }; + + [Theory] + [InlineData(0)] + [InlineData(-1)] + public void SegmentWriteStream_InvalidSegmentSize_Throws(int segmentSize) + { + Assert.Throws(() => new SegmentWriteStream(segmentSize)); + } + + [Fact] + public void ReadAndSeekOperations_Throws() + { + var stream = new SegmentWriteStream(1); + + Assert.Throws(() => stream.Read(new byte[1], 0, 0)); + Assert.Throws(() => stream.Position = 0); + Assert.Throws(() => stream.Seek(0, SeekOrigin.Begin)); + } + + [Fact] + public void GetSegments_ExtractionDisablesWriting() + { + var stream = new SegmentWriteStream(1); + + Assert.True(stream.CanWrite); + Assert.Empty(stream.GetSegments()); + Assert.False(stream.CanWrite); + } + + [Theory] + [InlineData(4)] + [InlineData(5)] + [InlineData(6)] + public void WriteByte_CanWriteAllBytes(int segmentSize) + { + var stream = new SegmentWriteStream(segmentSize); + + foreach (var datum in WriteData) + { + stream.WriteByte(datum); + } + var segments = stream.GetSegments(); + + Assert.Equal(WriteData.Length, stream.Length); + Assert.Equal((WriteData.Length + segmentSize - 1) / segmentSize, segments.Count); + + for (var i = 0; i < WriteData.Length; i += segmentSize) + { + var expectedSegmentSize = Math.Min(segmentSize, WriteData.Length - i); + var expectedSegment = new byte[expectedSegmentSize]; + for (int j = 0; j < expectedSegmentSize; j++) + { + expectedSegment[j] = (byte)(i + j); + } + var segment = segments[i / segmentSize]; + + Assert.Equal(expectedSegmentSize, segment.Length); + Assert.True(expectedSegment.SequenceEqual(segment)); + } + } + + [Theory] + [InlineData(4)] + [InlineData(5)] + [InlineData(6)] + public void Write_CanWriteAllBytes(int writeSize) + { + var segmentSize = 5; + var stream = new SegmentWriteStream(segmentSize); + + for (var i = 0; i < WriteData.Length; i += writeSize) + { + stream.Write(WriteData, i, Math.Min(writeSize, WriteData.Length - i)); + } + var segments = stream.GetSegments(); + + Assert.Equal(WriteData.Length, stream.Length); + Assert.Equal((WriteData.Length + segmentSize - 1) / segmentSize, segments.Count); + + for (var i = 0; i < WriteData.Length; i += segmentSize) + { + var expectedSegmentSize = Math.Min(segmentSize, WriteData.Length - i); + var expectedSegment = new byte[expectedSegmentSize]; + for (int j = 0; j < expectedSegmentSize; j++) + { + expectedSegment[j] = (byte)(i + j); + } + var segment = segments[i / segmentSize]; + + Assert.Equal(expectedSegmentSize, segment.Length); + Assert.True(expectedSegment.SequenceEqual(segment)); + } + } +} diff --git a/src/Middleware/OutputCaching/test/TestUtils.cs b/src/Middleware/OutputCaching/test/TestUtils.cs index 27f4c2c7619e..2d57d882cb03 100644 --- a/src/Middleware/OutputCaching/test/TestUtils.cs +++ b/src/Middleware/OutputCaching/test/TestUtils.cs @@ -1,403 +1,415 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -//using System.Globalization; -//using System.Net.Http; -//using System.Text; -//using Microsoft.AspNetCore.Builder; -//using Microsoft.AspNetCore.Hosting; -//using Microsoft.AspNetCore.Http; -//using Microsoft.AspNetCore.Http.Features; -//using Microsoft.AspNetCore.TestHost; -//using Microsoft.Extensions.DependencyInjection; -//using Microsoft.Extensions.Hosting; -//using Microsoft.Extensions.Logging; -//using Microsoft.Extensions.Logging.Abstractions; -//using Microsoft.Extensions.Logging.Testing; -//using Microsoft.Extensions.ObjectPool; -//using Microsoft.Extensions.Options; -//using Microsoft.Extensions.Primitives; -//using Microsoft.Net.Http.Headers; - -//namespace Microsoft.AspNetCore.ResponseCaching.Tests; - -//internal class TestUtils -//{ -// static TestUtils() -// { -// // Force sharding in tests -// StreamUtilities.BodySegmentSize = 10; -// } - -// private static bool TestRequestDelegate(HttpContext context, string guid) -// { -// var headers = context.Response.GetTypedHeaders(); - -// var expires = context.Request.Query["Expires"]; -// if (!string.IsNullOrEmpty(expires)) -// { -// headers.Expires = DateTimeOffset.Now.AddSeconds(int.Parse(expires, CultureInfo.InvariantCulture)); -// } - -// if (headers.CacheControl == null) -// { -// headers.CacheControl = new CacheControlHeaderValue -// { -// Public = true, -// MaxAge = string.IsNullOrEmpty(expires) ? TimeSpan.FromSeconds(10) : (TimeSpan?)null -// }; -// } -// else -// { -// headers.CacheControl.Public = true; -// headers.CacheControl.MaxAge = string.IsNullOrEmpty(expires) ? TimeSpan.FromSeconds(10) : (TimeSpan?)null; -// } -// headers.Date = DateTimeOffset.UtcNow; -// headers.Headers["X-Value"] = guid; - -// var contentLength = context.Request.Query["ContentLength"]; -// if (!string.IsNullOrEmpty(contentLength)) -// { -// headers.ContentLength = long.Parse(contentLength, CultureInfo.InvariantCulture); -// } - -// if (context.Request.Method != "HEAD") -// { -// return true; -// } -// return false; -// } - -// internal static async Task TestRequestDelegateWriteAsync(HttpContext context) -// { -// var uniqueId = Guid.NewGuid().ToString(); -// if (TestRequestDelegate(context, uniqueId)) -// { -// await context.Response.WriteAsync(uniqueId); -// } -// } - -// internal static async Task TestRequestDelegateSendFileAsync(HttpContext context) -// { -// var path = Path.Combine(AppContext.BaseDirectory, "TestDocument.txt"); -// var uniqueId = Guid.NewGuid().ToString(); -// if (TestRequestDelegate(context, uniqueId)) -// { -// await context.Response.SendFileAsync(path, 0, null); -// await context.Response.WriteAsync(uniqueId); -// } -// } - -// internal static Task TestRequestDelegateWrite(HttpContext context) -// { -// var uniqueId = Guid.NewGuid().ToString(); -// if (TestRequestDelegate(context, uniqueId)) -// { -// var feature = context.Features.Get(); -// if (feature != null) -// { -// feature.AllowSynchronousIO = true; -// } -// context.Response.Write(uniqueId); -// } -// return Task.CompletedTask; -// } - -// internal static IResponseCachingKeyProvider CreateTestKeyProvider() -// { -// return CreateTestKeyProvider(new ResponseCachingOptions()); -// } - -// internal static IResponseCachingKeyProvider CreateTestKeyProvider(ResponseCachingOptions options) -// { -// return new ResponseCachingKeyProvider(new DefaultObjectPoolProvider(), Options.Create(options)); -// } - -// internal static IEnumerable CreateBuildersWithResponseCaching( -// Action configureDelegate = null, -// ResponseCachingOptions options = null, -// Action contextAction = null) -// { -// return CreateBuildersWithResponseCaching(configureDelegate, options, new RequestDelegate[] -// { -// context => -// { -// contextAction?.Invoke(context); -// return TestRequestDelegateWrite(context); -// }, -// context => -// { -// contextAction?.Invoke(context); -// return TestRequestDelegateWriteAsync(context); -// }, -// context => -// { -// contextAction?.Invoke(context); -// return TestRequestDelegateSendFileAsync(context); -// }, -// }); -// } - -// private static IEnumerable CreateBuildersWithResponseCaching( -// Action configureDelegate = null, -// ResponseCachingOptions options = null, -// IEnumerable requestDelegates = null) -// { -// if (configureDelegate == null) -// { -// configureDelegate = app => { }; -// } -// if (requestDelegates == null) -// { -// requestDelegates = new RequestDelegate[] -// { -// TestRequestDelegateWriteAsync, -// TestRequestDelegateWrite -// }; -// } - -// foreach (var requestDelegate in requestDelegates) -// { -// // Test with in memory ResponseCache -// yield return new HostBuilder() -// .ConfigureWebHost(webHostBuilder => -// { -// webHostBuilder -// .UseTestServer() -// .ConfigureServices(services => -// { -// services.AddResponseCaching(responseCachingOptions => -// { -// if (options != null) -// { -// responseCachingOptions.MaximumBodySize = options.MaximumBodySize; -// responseCachingOptions.UseCaseSensitivePaths = options.UseCaseSensitivePaths; -// responseCachingOptions.SystemClock = options.SystemClock; -// } -// }); -// }) -// .Configure(app => -// { -// configureDelegate(app); -// app.UseResponseCaching(); -// app.Run(requestDelegate); -// }); -// }); -// } -// } - -// internal static ResponseCachingMiddleware CreateTestMiddleware( -// RequestDelegate next = null, -// IResponseCache cache = null, -// ResponseCachingOptions options = null, -// TestSink testSink = null, -// IResponseCachingKeyProvider keyProvider = null, -// IResponseCachingPolicyProvider policyProvider = null) -// { -// if (next == null) -// { -// next = httpContext => Task.CompletedTask; -// } -// if (cache == null) -// { -// cache = new TestResponseCache(); -// } -// if (options == null) -// { -// options = new ResponseCachingOptions(); -// } -// if (keyProvider == null) -// { -// keyProvider = new ResponseCachingKeyProvider(new DefaultObjectPoolProvider(), Options.Create(options)); -// } -// if (policyProvider == null) -// { -// policyProvider = new TestResponseCachingPolicyProvider(); -// } - -// return new ResponseCachingMiddleware( -// next, -// Options.Create(options), -// testSink == null ? (ILoggerFactory)NullLoggerFactory.Instance : new TestLoggerFactory(testSink, true), -// policyProvider, -// cache, -// keyProvider); -// } - -// internal static ResponseCachingContext CreateTestContext() -// { -// return new ResponseCachingContext(new DefaultHttpContext(), NullLogger.Instance) -// { -// ResponseTime = DateTimeOffset.UtcNow -// }; -// } - -// internal static ResponseCachingContext CreateTestContext(ITestSink testSink) -// { -// return new ResponseCachingContext(new DefaultHttpContext(), new TestLogger("ResponseCachingTests", testSink, true)) -// { -// ResponseTime = DateTimeOffset.UtcNow -// }; -// } - -// internal static void AssertLoggedMessages(IEnumerable messages, params LoggedMessage[] expectedMessages) -// { -// var messageList = messages.ToList(); -// Assert.Equal(messageList.Count, expectedMessages.Length); - -// for (var i = 0; i < messageList.Count; i++) -// { -// Assert.Equal(expectedMessages[i].EventId, messageList[i].EventId); -// Assert.Equal(expectedMessages[i].LogLevel, messageList[i].LogLevel); -// } -// } - -// public static HttpRequestMessage CreateRequest(string method, string requestUri) -// { -// return new HttpRequestMessage(new HttpMethod(method), requestUri); -// } -//} - -//internal static class HttpResponseWritingExtensions -//{ -// internal static void Write(this HttpResponse response, string text) -// { -// if (response == null) -// { -// throw new ArgumentNullException(nameof(response)); -// } - -// if (text == null) -// { -// throw new ArgumentNullException(nameof(text)); -// } - -// byte[] data = Encoding.UTF8.GetBytes(text); -// response.Body.Write(data, 0, data.Length); -// } -//} - -//internal class LoggedMessage -//{ -// internal static LoggedMessage RequestMethodNotCacheable => new LoggedMessage(1, LogLevel.Debug); -// internal static LoggedMessage RequestWithAuthorizationNotCacheable => new LoggedMessage(2, LogLevel.Debug); -// internal static LoggedMessage RequestWithNoCacheNotCacheable => new LoggedMessage(3, LogLevel.Debug); -// internal static LoggedMessage RequestWithPragmaNoCacheNotCacheable => new LoggedMessage(4, LogLevel.Debug); -// internal static LoggedMessage ExpirationMinFreshAdded => new LoggedMessage(5, LogLevel.Debug); -// internal static LoggedMessage ExpirationSharedMaxAgeExceeded => new LoggedMessage(6, LogLevel.Debug); -// internal static LoggedMessage ExpirationMustRevalidate => new LoggedMessage(7, LogLevel.Debug); -// internal static LoggedMessage ExpirationMaxStaleSatisfied => new LoggedMessage(8, LogLevel.Debug); -// internal static LoggedMessage ExpirationMaxAgeExceeded => new LoggedMessage(9, LogLevel.Debug); -// internal static LoggedMessage ExpirationExpiresExceeded => new LoggedMessage(10, LogLevel.Debug); -// internal static LoggedMessage ResponseWithoutPublicNotCacheable => new LoggedMessage(11, LogLevel.Debug); -// internal static LoggedMessage ResponseWithNoStoreNotCacheable => new LoggedMessage(12, LogLevel.Debug); -// internal static LoggedMessage ResponseWithNoCacheNotCacheable => new LoggedMessage(13, LogLevel.Debug); -// internal static LoggedMessage ResponseWithSetCookieNotCacheable => new LoggedMessage(14, LogLevel.Debug); -// internal static LoggedMessage ResponseWithVaryStarNotCacheable => new LoggedMessage(15, LogLevel.Debug); -// internal static LoggedMessage ResponseWithPrivateNotCacheable => new LoggedMessage(16, LogLevel.Debug); -// internal static LoggedMessage ResponseWithUnsuccessfulStatusCodeNotCacheable => new LoggedMessage(17, LogLevel.Debug); -// internal static LoggedMessage NotModifiedIfNoneMatchStar => new LoggedMessage(18, LogLevel.Debug); -// internal static LoggedMessage NotModifiedIfNoneMatchMatched => new LoggedMessage(19, LogLevel.Debug); -// internal static LoggedMessage NotModifiedIfModifiedSinceSatisfied => new LoggedMessage(20, LogLevel.Debug); -// internal static LoggedMessage NotModifiedServed => new LoggedMessage(21, LogLevel.Information); -// internal static LoggedMessage CachedResponseServed => new LoggedMessage(22, LogLevel.Information); -// internal static LoggedMessage GatewayTimeoutServed => new LoggedMessage(23, LogLevel.Information); -// internal static LoggedMessage NoResponseServed => new LoggedMessage(24, LogLevel.Information); -// internal static LoggedMessage VaryByRulesUpdated => new LoggedMessage(25, LogLevel.Debug); -// internal static LoggedMessage ResponseCached => new LoggedMessage(26, LogLevel.Information); -// internal static LoggedMessage ResponseNotCached => new LoggedMessage(27, LogLevel.Information); -// internal static LoggedMessage ResponseContentLengthMismatchNotCached => new LoggedMessage(28, LogLevel.Warning); -// internal static LoggedMessage ExpirationInfiniteMaxStaleSatisfied => new LoggedMessage(29, LogLevel.Debug); - -// private LoggedMessage(int evenId, LogLevel logLevel) -// { -// EventId = evenId; -// LogLevel = logLevel; -// } - -// internal int EventId { get; } -// internal LogLevel LogLevel { get; } -//} - -//internal class TestResponseCachingPolicyProvider : IResponseCachingPolicyProvider -//{ -// public bool AllowCacheLookupValue { get; set; } = false; -// public bool AllowCacheStorageValue { get; set; } = false; -// public bool AttemptResponseCachingValue { get; set; } = false; -// public bool IsCachedEntryFreshValue { get; set; } = true; -// public bool IsResponseCacheableValue { get; set; } = true; - -// public bool AllowCacheLookup(ResponseCachingContext context) => AllowCacheLookupValue; - -// public bool AllowCacheStorage(ResponseCachingContext context) => AllowCacheStorageValue; - -// public bool AttemptResponseCaching(ResponseCachingContext context) => AttemptResponseCachingValue; - -// public bool IsCachedEntryFresh(ResponseCachingContext context) => IsCachedEntryFreshValue; - -// public bool IsResponseCacheable(ResponseCachingContext context) => IsResponseCacheableValue; -//} - -//internal class TestResponseCachingKeyProvider : IResponseCachingKeyProvider -//{ -// private readonly string _baseKey; -// private readonly StringValues _varyKey; - -// public TestResponseCachingKeyProvider(string lookupBaseKey = null, StringValues? lookupVaryKey = null) -// { -// _baseKey = lookupBaseKey; -// if (lookupVaryKey.HasValue) -// { -// _varyKey = lookupVaryKey.Value; -// } -// } - -// public IEnumerable CreateLookupVaryByKeys(ResponseCachingContext context) -// { -// foreach (var varyKey in _varyKey) -// { -// yield return _baseKey + varyKey; -// } -// } - -// public string CreateBaseKey(ResponseCachingContext context) -// { -// return _baseKey; -// } - -// public string CreateStorageVaryByKey(ResponseCachingContext context) -// { -// throw new NotImplementedException(); -// } -//} - -//internal class TestResponseCache : IResponseCache -//{ -// private readonly IDictionary _storage = new Dictionary(); -// public int GetCount { get; private set; } -// public int SetCount { get; private set; } - -// public IResponseCacheEntry Get(string key) -// { -// GetCount++; -// try -// { -// return _storage[key]; -// } -// catch -// { -// return null; -// } -// } - -// public void Set(string key, IResponseCacheEntry entry, TimeSpan validFor) -// { -// SetCount++; -// _storage[key] = entry; -// } -//} - -//internal class TestClock : ISystemClock -//{ -// public DateTimeOffset UtcNow { get; set; } -//} +using System.Globalization; +using System.Net.Http; +using System.Text; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Logging.Testing; +using Microsoft.Extensions.ObjectPool; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.OutputCaching.Tests; + +internal class TestUtils +{ + static TestUtils() + { + // Force sharding in tests + StreamUtilities.BodySegmentSize = 10; + } + + private static bool TestRequestDelegate(HttpContext context, string guid) + { + var headers = context.Response.GetTypedHeaders(); + + var expires = context.Request.Query["Expires"]; + if (!string.IsNullOrEmpty(expires)) + { + headers.Expires = DateTimeOffset.Now.AddSeconds(int.Parse(expires, CultureInfo.InvariantCulture)); + } + + if (headers.CacheControl == null) + { + headers.CacheControl = new CacheControlHeaderValue + { + Public = true, + MaxAge = string.IsNullOrEmpty(expires) ? TimeSpan.FromSeconds(10) : (TimeSpan?)null + }; + } + else + { + headers.CacheControl.Public = true; + headers.CacheControl.MaxAge = string.IsNullOrEmpty(expires) ? TimeSpan.FromSeconds(10) : (TimeSpan?)null; + } + headers.Date = DateTimeOffset.UtcNow; + headers.Headers["X-Value"] = guid; + + var contentLength = context.Request.Query["ContentLength"]; + if (!string.IsNullOrEmpty(contentLength)) + { + headers.ContentLength = long.Parse(contentLength, CultureInfo.InvariantCulture); + } + + if (context.Request.Method != "HEAD") + { + return true; + } + return false; + } + + internal static async Task TestRequestDelegateWriteAsync(HttpContext context) + { + var uniqueId = Guid.NewGuid().ToString(); + if (TestRequestDelegate(context, uniqueId)) + { + await context.Response.WriteAsync(uniqueId); + } + } + + internal static async Task TestRequestDelegateSendFileAsync(HttpContext context) + { + var path = Path.Combine(AppContext.BaseDirectory, "TestDocument.txt"); + var uniqueId = Guid.NewGuid().ToString(); + if (TestRequestDelegate(context, uniqueId)) + { + await context.Response.SendFileAsync(path, 0, null); + await context.Response.WriteAsync(uniqueId); + } + } + + internal static Task TestRequestDelegateWrite(HttpContext context) + { + var uniqueId = Guid.NewGuid().ToString(); + if (TestRequestDelegate(context, uniqueId)) + { + var feature = context.Features.Get(); + if (feature != null) + { + feature.AllowSynchronousIO = true; + } + context.Response.Write(uniqueId); + } + return Task.CompletedTask; + } + + internal static IOutputCachingKeyProvider CreateTestKeyProvider() + { + return CreateTestKeyProvider(new OutputCachingOptions()); + } + + internal static IOutputCachingKeyProvider CreateTestKeyProvider(OutputCachingOptions options) + { + return new OutputCachingKeyProvider(new DefaultObjectPoolProvider(), Options.Create(options)); + } + + internal static IEnumerable CreateBuildersWithOutputCaching( + Action configureDelegate = null, + OutputCachingOptions options = null, + Action contextAction = null) + { + return CreateBuildersWithOutputCaching(configureDelegate, options, new RequestDelegate[] + { + context => + { + contextAction?.Invoke(context); + return TestRequestDelegateWrite(context); + }, + context => + { + contextAction?.Invoke(context); + return TestRequestDelegateWriteAsync(context); + }, + context => + { + contextAction?.Invoke(context); + return TestRequestDelegateSendFileAsync(context); + }, + }); + } + + private static IEnumerable CreateBuildersWithOutputCaching( + Action configureDelegate = null, + OutputCachingOptions options = null, + IEnumerable requestDelegates = null) + { + if (configureDelegate == null) + { + configureDelegate = app => { }; + } + if (requestDelegates == null) + { + requestDelegates = new RequestDelegate[] + { + TestRequestDelegateWriteAsync, + TestRequestDelegateWrite + }; + } + + foreach (var requestDelegate in requestDelegates) + { + // Test with in memory OutputCache + yield return new HostBuilder() + .ConfigureWebHost(webHostBuilder => + { + webHostBuilder + .UseTestServer() + .ConfigureServices(services => + { + services.AddOutputCaching(outputCachingOptions => + { + if (options != null) + { + outputCachingOptions.MaximumBodySize = options.MaximumBodySize; + outputCachingOptions.UseCaseSensitivePaths = options.UseCaseSensitivePaths; + outputCachingOptions.SystemClock = options.SystemClock; + } + }); + }) + .Configure(app => + { + configureDelegate(app); + app.UseOutputCaching(); + app.Run(requestDelegate); + }); + }); + } + } + + internal static OutputCachingMiddleware CreateTestMiddleware( + RequestDelegate next = null, + IOutputCacheStore cache = null, + OutputCachingOptions options = null, + TestSink testSink = null, + IOutputCachingKeyProvider keyProvider = null, + IOutputCachingPolicyProvider policyProvider = null) + { + if (next == null) + { + next = httpContext => Task.CompletedTask; + } + if (cache == null) + { + cache = new TestOutputCache(); + } + if (options == null) + { + options = new OutputCachingOptions(); + } + if (keyProvider == null) + { + keyProvider = new OutputCachingKeyProvider(new DefaultObjectPoolProvider(), Options.Create(options)); + } + if (policyProvider == null) + { + policyProvider = new TestOutputCachingPolicyProvider(); + } + + return new OutputCachingMiddleware( + next, + Options.Create(options), + testSink == null ? (ILoggerFactory)NullLoggerFactory.Instance : new TestLoggerFactory(testSink, true), + policyProvider, + cache, + keyProvider); + } + + internal static OutputCachingContext CreateTestContext() + { + return new OutputCachingContext(new DefaultHttpContext(), NullLogger.Instance) + { + ResponseTime = DateTimeOffset.UtcNow + }; + } + + internal static OutputCachingContext CreateTestContext(HttpContext httpContext) + { + return new OutputCachingContext(httpContext, NullLogger.Instance) + { + ResponseTime = DateTimeOffset.UtcNow + }; + } + + internal static OutputCachingContext CreateTestContext(ITestSink testSink) + { + return new OutputCachingContext(new DefaultHttpContext(), new TestLogger("OutputCachingTests", testSink, true)) + { + ResponseTime = DateTimeOffset.UtcNow + }; + } + + internal static void AssertLoggedMessages(IEnumerable messages, params LoggedMessage[] expectedMessages) + { + var messageList = messages.ToList(); + Assert.Equal(messageList.Count, expectedMessages.Length); + + for (var i = 0; i < messageList.Count; i++) + { + Assert.Equal(expectedMessages[i].EventId, messageList[i].EventId); + Assert.Equal(expectedMessages[i].LogLevel, messageList[i].LogLevel); + } + } + + public static HttpRequestMessage CreateRequest(string method, string requestUri) + { + return new HttpRequestMessage(new HttpMethod(method), requestUri); + } +} + +internal static class HttpResponseWritingExtensions +{ + internal static void Write(this HttpResponse response, string text) + { + if (response == null) + { + throw new ArgumentNullException(nameof(response)); + } + + if (text == null) + { + throw new ArgumentNullException(nameof(text)); + } + + byte[] data = Encoding.UTF8.GetBytes(text); + response.Body.Write(data, 0, data.Length); + } +} + +internal class LoggedMessage +{ + internal static LoggedMessage RequestMethodNotCacheable => new LoggedMessage(1, LogLevel.Debug); + internal static LoggedMessage RequestWithAuthorizationNotCacheable => new LoggedMessage(2, LogLevel.Debug); + internal static LoggedMessage RequestWithNoCacheNotCacheable => new LoggedMessage(3, LogLevel.Debug); + internal static LoggedMessage RequestWithPragmaNoCacheNotCacheable => new LoggedMessage(4, LogLevel.Debug); + internal static LoggedMessage ExpirationMinFreshAdded => new LoggedMessage(5, LogLevel.Debug); + internal static LoggedMessage ExpirationSharedMaxAgeExceeded => new LoggedMessage(6, LogLevel.Debug); + internal static LoggedMessage ExpirationMustRevalidate => new LoggedMessage(7, LogLevel.Debug); + internal static LoggedMessage ExpirationMaxStaleSatisfied => new LoggedMessage(8, LogLevel.Debug); + internal static LoggedMessage ExpirationMaxAgeExceeded => new LoggedMessage(9, LogLevel.Debug); + internal static LoggedMessage ExpirationExpiresExceeded => new LoggedMessage(10, LogLevel.Debug); + internal static LoggedMessage ResponseWithoutPublicNotCacheable => new LoggedMessage(11, LogLevel.Debug); + internal static LoggedMessage ResponseWithNoStoreNotCacheable => new LoggedMessage(12, LogLevel.Debug); + internal static LoggedMessage ResponseWithNoCacheNotCacheable => new LoggedMessage(13, LogLevel.Debug); + internal static LoggedMessage ResponseWithSetCookieNotCacheable => new LoggedMessage(14, LogLevel.Debug); + internal static LoggedMessage ResponseWithVaryStarNotCacheable => new LoggedMessage(15, LogLevel.Debug); + internal static LoggedMessage ResponseWithPrivateNotCacheable => new LoggedMessage(16, LogLevel.Debug); + internal static LoggedMessage ResponseWithUnsuccessfulStatusCodeNotCacheable => new LoggedMessage(17, LogLevel.Debug); + internal static LoggedMessage NotModifiedIfNoneMatchStar => new LoggedMessage(18, LogLevel.Debug); + internal static LoggedMessage NotModifiedIfNoneMatchMatched => new LoggedMessage(19, LogLevel.Debug); + internal static LoggedMessage NotModifiedIfModifiedSinceSatisfied => new LoggedMessage(20, LogLevel.Debug); + internal static LoggedMessage NotModifiedServed => new LoggedMessage(21, LogLevel.Information); + internal static LoggedMessage CachedResponseServed => new LoggedMessage(22, LogLevel.Information); + internal static LoggedMessage GatewayTimeoutServed => new LoggedMessage(23, LogLevel.Information); + internal static LoggedMessage NoResponseServed => new LoggedMessage(24, LogLevel.Information); + internal static LoggedMessage VaryByRulesUpdated => new LoggedMessage(25, LogLevel.Debug); + internal static LoggedMessage ResponseCached => new LoggedMessage(26, LogLevel.Information); + internal static LoggedMessage ResponseNotCached => new LoggedMessage(27, LogLevel.Information); + internal static LoggedMessage ResponseContentLengthMismatchNotCached => new LoggedMessage(28, LogLevel.Warning); + internal static LoggedMessage ExpirationInfiniteMaxStaleSatisfied => new LoggedMessage(29, LogLevel.Debug); + + private LoggedMessage(int evenId, LogLevel logLevel) + { + EventId = evenId; + LogLevel = logLevel; + } + + internal int EventId { get; } + internal LogLevel LogLevel { get; } +} + +internal class TestOutputCachingPolicyProvider : IOutputCachingPolicyProvider +{ + public bool AllowCacheLookupValue { get; set; } = false; + public bool AllowCacheStorageValue { get; set; } = false; + public bool AttemptOutputCachingValue { get; set; } = false; + public bool EnableOutputCaching { get; set; } = true; + public bool IsCachedEntryFreshValue { get; set; } = true; + public bool IsResponseCacheableValue { get; set; } = true; + + public Task OnRequestAsync(IOutputCachingContext context) + { + context.EnableOutputCaching = EnableOutputCaching; + context.AttemptOutputCaching = AttemptOutputCachingValue; + + return Task.CompletedTask; + } + + public Task OnServeFromCacheAsync(IOutputCachingContext context) + { + context.AllowCacheLookup = AllowCacheLookupValue; + context.IsCacheEntryFresh = IsCachedEntryFreshValue; + + return Task.CompletedTask; + } + + public Task OnServeResponseAsync(IOutputCachingContext context) + { + context.IsResponseCacheable = IsResponseCacheableValue; + context.AllowCacheStorage = AllowCacheStorageValue; + + return Task.CompletedTask; + } +} + +internal class TestResponseCachingKeyProvider : IOutputCachingKeyProvider +{ + private readonly string _key; + + public TestResponseCachingKeyProvider(string key = null) + { + _key = key; + } + + public string CreateStorageKey(OutputCachingContext context) + { + return _key; + } +} + +internal class TestOutputCache : IOutputCacheStore +{ + private readonly IDictionary _storage = new Dictionary(); + public int GetCount { get; private set; } + public int SetCount { get; private set; } + + public ValueTask EvictByTagAsync(string tag) + { + throw new NotImplementedException(); + } + + public ValueTask GetAsync(string key) + { + GetCount++; + try + { + return new ValueTask(_storage[key]); + } + catch + { + return new ValueTask(default(IOutputCacheEntry)); + } + } + + public ValueTask SetAsync(string key, IOutputCacheEntry entry, TimeSpan validFor) + { + SetCount++; + _storage[key] = entry; + + return ValueTask.CompletedTask; + } +} + +internal class TestClock : ISystemClock +{ + public DateTimeOffset UtcNow { get; set; } +} From 7155379a27484eebfeb82a8e573e0fa191b6553e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Ros?= Date: Tue, 17 May 2022 14:28:09 -0700 Subject: [PATCH 22/48] Update src/Middleware/OutputCaching.Abstractions/src/Microsoft.AspNetCore.OutputCaching.Abstractions.csproj Co-authored-by: James Newton-King --- .../src/Microsoft.AspNetCore.OutputCaching.Abstractions.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Middleware/OutputCaching.Abstractions/src/Microsoft.AspNetCore.OutputCaching.Abstractions.csproj b/src/Middleware/OutputCaching.Abstractions/src/Microsoft.AspNetCore.OutputCaching.Abstractions.csproj index d1b27995a24e..545b77910f09 100644 --- a/src/Middleware/OutputCaching.Abstractions/src/Microsoft.AspNetCore.OutputCaching.Abstractions.csproj +++ b/src/Middleware/OutputCaching.Abstractions/src/Microsoft.AspNetCore.OutputCaching.Abstractions.csproj @@ -7,7 +7,7 @@ true aspnetcore;cache;caching false - enable + true From 1318b0faf2e2077bbc5be8fdb54c382774f19a01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Ros?= Date: Tue, 17 May 2022 14:28:16 -0700 Subject: [PATCH 23/48] Update src/Middleware/OutputCaching/src/Microsoft.AspNetCore.OutputCaching.csproj Co-authored-by: James Newton-King --- .../OutputCaching/src/Microsoft.AspNetCore.OutputCaching.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Middleware/OutputCaching/src/Microsoft.AspNetCore.OutputCaching.csproj b/src/Middleware/OutputCaching/src/Microsoft.AspNetCore.OutputCaching.csproj index 4d81e6a62ceb..eab2febfb3ad 100644 --- a/src/Middleware/OutputCaching/src/Microsoft.AspNetCore.OutputCaching.csproj +++ b/src/Middleware/OutputCaching/src/Microsoft.AspNetCore.OutputCaching.csproj @@ -8,7 +8,7 @@ true aspnetcore;cache;caching false - enable + true From 89b58f1fb2f3b778890ef7b94aa9b468e0385376 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Wed, 18 May 2022 08:33:32 -0700 Subject: [PATCH 24/48] Update TrimmableProjects --- eng/TrimmableProjects.props | 2 ++ 1 file changed, 2 insertions(+) diff --git a/eng/TrimmableProjects.props b/eng/TrimmableProjects.props index 5f86c590f41a..fd635215fc40 100644 --- a/eng/TrimmableProjects.props +++ b/eng/TrimmableProjects.props @@ -66,6 +66,8 @@ + + From ec5167e75e3d802b4c1f2eb809cec0ca31aa9772 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Wed, 18 May 2022 08:37:26 -0700 Subject: [PATCH 25/48] Fix solution --- AspNetCore.sln | 2 -- 1 file changed, 2 deletions(-) diff --git a/AspNetCore.sln b/AspNetCore.sln index 8453a698e181..6971e848cee7 100644 --- a/AspNetCore.sln +++ b/AspNetCore.sln @@ -1664,8 +1664,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Output EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{B5AC1D8B-9D43-4261-AE0F-6B7574656F2C}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.ResponseCaching.Tests", "src\Middleware\OutputCaching\test\Microsoft.AspNetCore.ResponseCaching.Tests.csproj", "{6EBAD7A2-0FA8-4314-89F5-54009E86EE2F}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OutputCachingSample", "src\Middleware\OutputCaching\samples\OutputCachingSample\OutputCachingSample.csproj", "{C3FFA4E4-0E7E-4866-A15F-034245BFD800}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "LinkabilityChecker", "LinkabilityChecker", "{94F95276-7CDF-44A8-B159-D09702EF6794}" From 3dca7ba62378a24ac595a4bf5e8c33b343a2d1c3 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Wed, 18 May 2022 09:04:22 -0700 Subject: [PATCH 26/48] Improve ThrowIfNull usage --- .../src/CachedResponseBody.cs | 2 +- .../OutputCaching/src/DispatcherExtensions.cs | 4 ++-- .../src/Memory/MemoryOutputCacheStore.cs | 2 +- .../src/OutputCachingExtensions.cs | 2 +- .../src/OutputCachingKeyProvider.cs | 6 ++--- .../src/OutputCachingMiddleware.cs | 12 +++++----- .../src/OutputCachingServicesExtensions.cs | 6 ++--- .../src/Policies/OutputCachePolicyBuilder.cs | 22 +++++++++---------- .../src/Policies/PolicyExtensions.cs | 8 +++---- .../src/Streams/SegmentWriteStream.cs | 2 +- 10 files changed, 33 insertions(+), 33 deletions(-) diff --git a/src/Middleware/OutputCaching.Abstractions/src/CachedResponseBody.cs b/src/Middleware/OutputCaching.Abstractions/src/CachedResponseBody.cs index b354fbe850eb..d17627bdcd20 100644 --- a/src/Middleware/OutputCaching.Abstractions/src/CachedResponseBody.cs +++ b/src/Middleware/OutputCaching.Abstractions/src/CachedResponseBody.cs @@ -39,7 +39,7 @@ public CachedResponseBody(List segments, long length) /// public async Task CopyToAsync(PipeWriter destination, CancellationToken cancellationToken) { - ArgumentNullException.ThrowIfNull(destination, nameof(destination)); + ArgumentNullException.ThrowIfNull(destination); foreach (var segment in Segments) { diff --git a/src/Middleware/OutputCaching/src/DispatcherExtensions.cs b/src/Middleware/OutputCaching/src/DispatcherExtensions.cs index ad72d388dfcd..173b7147d120 100644 --- a/src/Middleware/OutputCaching/src/DispatcherExtensions.cs +++ b/src/Middleware/OutputCaching/src/DispatcherExtensions.cs @@ -11,7 +11,7 @@ internal sealed class WorkDispatcher where TKey : notnull public async Task ScheduleAsync(TKey key, Func> valueFactory) { - ArgumentNullException.ThrowIfNull(key, nameof(key)); + ArgumentNullException.ThrowIfNull(key); while (true) { @@ -50,7 +50,7 @@ internal sealed class WorkDispatcher where TKey : notnull public async Task ScheduleAsync(TKey key, TState state, Func> valueFactory) { - ArgumentNullException.ThrowIfNull(key, nameof(key)); + ArgumentNullException.ThrowIfNull(key); while (true) { diff --git a/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs b/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs index 19e0e6bef1f9..186140fcfac8 100644 --- a/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs +++ b/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs @@ -13,7 +13,7 @@ internal sealed class MemoryOutputCacheStore : IOutputCacheStore internal MemoryOutputCacheStore(IMemoryCache cache) { - ArgumentNullException.ThrowIfNull(cache, nameof(cache)); + ArgumentNullException.ThrowIfNull(cache); _cache = cache; } diff --git a/src/Middleware/OutputCaching/src/OutputCachingExtensions.cs b/src/Middleware/OutputCaching/src/OutputCachingExtensions.cs index 8da224853995..2b2c336b9f91 100644 --- a/src/Middleware/OutputCaching/src/OutputCachingExtensions.cs +++ b/src/Middleware/OutputCaching/src/OutputCachingExtensions.cs @@ -16,7 +16,7 @@ public static class OutputCachingExtensions /// The . public static IApplicationBuilder UseOutputCaching(this IApplicationBuilder app) { - ArgumentNullException.ThrowIfNull(app, nameof(app)); + ArgumentNullException.ThrowIfNull(app); return app.UseMiddleware(); } diff --git a/src/Middleware/OutputCaching/src/OutputCachingKeyProvider.cs b/src/Middleware/OutputCaching/src/OutputCachingKeyProvider.cs index 9e7fa502150c..27f30190549c 100644 --- a/src/Middleware/OutputCaching/src/OutputCachingKeyProvider.cs +++ b/src/Middleware/OutputCaching/src/OutputCachingKeyProvider.cs @@ -21,8 +21,8 @@ internal sealed class OutputCachingKeyProvider : IOutputCachingKeyProvider internal OutputCachingKeyProvider(ObjectPoolProvider poolProvider, IOptions options) { - ArgumentNullException.ThrowIfNull(poolProvider, nameof(poolProvider)); - ArgumentNullException.ThrowIfNull(options, nameof(options)); + ArgumentNullException.ThrowIfNull(poolProvider); + ArgumentNullException.ThrowIfNull(options); _builderPool = poolProvider.CreateStringBuilderPool(); _options = options.Value; @@ -31,7 +31,7 @@ internal OutputCachingKeyProvider(ObjectPoolProvider poolProvider, IOptionsSCHEMEHOST:PORT/PATHBASE/PATHHHeaderName=HeaderValueQQueryName=QueryValue1QueryValue2 public string CreateStorageKey(OutputCachingContext context) { - ArgumentNullException.ThrowIfNull(_builderPool, nameof(context)); + ArgumentNullException.ThrowIfNull(_builderPool); var varyByRules = context.CachedVaryByRules; if (varyByRules == null) diff --git a/src/Middleware/OutputCaching/src/OutputCachingMiddleware.cs b/src/Middleware/OutputCaching/src/OutputCachingMiddleware.cs index bc7f7e6afaa5..000a998ba14e 100644 --- a/src/Middleware/OutputCaching/src/OutputCachingMiddleware.cs +++ b/src/Middleware/OutputCaching/src/OutputCachingMiddleware.cs @@ -62,12 +62,12 @@ internal OutputCachingMiddleware( IOutputCacheStore cache, IOutputCachingKeyProvider keyProvider) { - ArgumentNullException.ThrowIfNull(next, nameof(next)); - ArgumentNullException.ThrowIfNull(options, nameof(options)); - ArgumentNullException.ThrowIfNull(loggerFactory, nameof(loggerFactory)); - ArgumentNullException.ThrowIfNull(policyProvider, nameof(policyProvider)); - ArgumentNullException.ThrowIfNull(cache, nameof(cache)); - ArgumentNullException.ThrowIfNull(keyProvider, nameof(keyProvider)); + ArgumentNullException.ThrowIfNull(next); + ArgumentNullException.ThrowIfNull(options); + ArgumentNullException.ThrowIfNull(loggerFactory); + ArgumentNullException.ThrowIfNull(policyProvider); + ArgumentNullException.ThrowIfNull(cache); + ArgumentNullException.ThrowIfNull(keyProvider); _next = next; _options = options.Value; diff --git a/src/Middleware/OutputCaching/src/OutputCachingServicesExtensions.cs b/src/Middleware/OutputCaching/src/OutputCachingServicesExtensions.cs index eba05ea1150f..064b07d731f5 100644 --- a/src/Middleware/OutputCaching/src/OutputCachingServicesExtensions.cs +++ b/src/Middleware/OutputCaching/src/OutputCachingServicesExtensions.cs @@ -22,7 +22,7 @@ public static class OutputCachingServicesExtensions /// public static IServiceCollection AddOutputCaching(this IServiceCollection services) { - ArgumentNullException.ThrowIfNull(nameof(services)); + ArgumentNullException.ThrowIfNull(services); services.TryAddSingleton(); @@ -45,8 +45,8 @@ public static IServiceCollection AddOutputCaching(this IServiceCollection servic /// public static IServiceCollection AddOutputCaching(this IServiceCollection services, Action configureOptions) { - ArgumentNullException.ThrowIfNull(services, nameof(services)); - ArgumentNullException.ThrowIfNull(configureOptions, nameof(configureOptions)); + ArgumentNullException.ThrowIfNull(services); + ArgumentNullException.ThrowIfNull(configureOptions); services.Configure(configureOptions); services.AddOutputCaching(); diff --git a/src/Middleware/OutputCaching/src/Policies/OutputCachePolicyBuilder.cs b/src/Middleware/OutputCaching/src/Policies/OutputCachePolicyBuilder.cs index 35617e1b73c3..c5b6bb609957 100644 --- a/src/Middleware/OutputCaching/src/Policies/OutputCachePolicyBuilder.cs +++ b/src/Middleware/OutputCaching/src/Policies/OutputCachePolicyBuilder.cs @@ -31,7 +31,7 @@ public OutputCachePolicyBuilder When(Func> pre /// The base path to limit the policy to. public OutputCachePolicyBuilder Path(PathString pathBase) { - ArgumentNullException.ThrowIfNull(pathBase, nameof(pathBase)); + ArgumentNullException.ThrowIfNull(pathBase); Requirements.Add(context => { @@ -47,7 +47,7 @@ public OutputCachePolicyBuilder Path(PathString pathBase) /// The base paths to limit the policy to. public OutputCachePolicyBuilder Path(params PathString[] pathBases) { - ArgumentNullException.ThrowIfNull(pathBases, nameof(pathBases)); + ArgumentNullException.ThrowIfNull(pathBases); Requirements.Add(context => { @@ -63,7 +63,7 @@ public OutputCachePolicyBuilder Path(params PathString[] pathBases) /// The method to limit the policy to. public OutputCachePolicyBuilder Method(string method) { - ArgumentNullException.ThrowIfNull(method, nameof(method)); + ArgumentNullException.ThrowIfNull(method); Requirements.Add(context => { @@ -80,7 +80,7 @@ public OutputCachePolicyBuilder Method(string method) /// The methods to limit the policy to. public OutputCachePolicyBuilder Method(params string[] methods) { - ArgumentNullException.ThrowIfNull(methods, nameof(methods)); + ArgumentNullException.ThrowIfNull(methods); Requirements.Add(context => { @@ -97,7 +97,7 @@ public OutputCachePolicyBuilder Method(params string[] methods) /// The query keys to vary the cached responses by. public OutputCachePolicyBuilder VaryByQuery(params string[] queryKeys) { - ArgumentNullException.ThrowIfNull(queryKeys, nameof(queryKeys)); + ArgumentNullException.ThrowIfNull(queryKeys); Policies.Add(new VaryByQueryPolicy(queryKeys)); return this; @@ -109,7 +109,7 @@ public OutputCachePolicyBuilder VaryByQuery(params string[] queryKeys) /// The value to vary the cached responses by. public OutputCachePolicyBuilder VaryByValue(Func> varyBy) { - ArgumentNullException.ThrowIfNull(varyBy, nameof(varyBy)); + ArgumentNullException.ThrowIfNull(varyBy); Policies.Add(new VaryByValuePolicy(varyBy)); return this; @@ -121,7 +121,7 @@ public OutputCachePolicyBuilder VaryByValue(Func> varyBy) /// The key/value to vary the cached responses by. public OutputCachePolicyBuilder VaryByValue(Func> varyBy) { - ArgumentNullException.ThrowIfNull(varyBy, nameof(varyBy)); + ArgumentNullException.ThrowIfNull(varyBy); Policies.Add(new VaryByValuePolicy(varyBy)); return this; @@ -133,7 +133,7 @@ public OutputCachePolicyBuilder VaryByValue(Func> varyBy) /// The value to vary the cached responses by. public OutputCachePolicyBuilder VaryByValue(Func varyBy) { - ArgumentNullException.ThrowIfNull(varyBy, nameof(varyBy)); + ArgumentNullException.ThrowIfNull(varyBy); Policies.Add(new VaryByValuePolicy(varyBy)); return this; @@ -145,7 +145,7 @@ public OutputCachePolicyBuilder VaryByValue(Func varyBy) /// The key/value to vary the cached responses by. public OutputCachePolicyBuilder VaryByValue(Func<(string, string)> varyBy) { - ArgumentNullException.ThrowIfNull(varyBy, nameof(varyBy)); + ArgumentNullException.ThrowIfNull(varyBy); Policies.Add(new VaryByValuePolicy(varyBy)); return this; @@ -157,7 +157,7 @@ public OutputCachePolicyBuilder VaryByValue(Func<(string, string)> varyBy) /// The name of the policy to add. public OutputCachePolicyBuilder Profile(string profileName) { - ArgumentNullException.ThrowIfNull(profileName, nameof(profileName)); + ArgumentNullException.ThrowIfNull(profileName); Policies.Add(new ProfilePolicy(profileName)); @@ -170,7 +170,7 @@ public OutputCachePolicyBuilder Profile(string profileName) /// The tags to add to the cached reponse. public OutputCachePolicyBuilder Tag(params string[] tags) { - ArgumentNullException.ThrowIfNull(tags, nameof(tags)); + ArgumentNullException.ThrowIfNull(tags); Policies.Add(new TagsPolicy(tags)); return this; diff --git a/src/Middleware/OutputCaching/src/Policies/PolicyExtensions.cs b/src/Middleware/OutputCaching/src/Policies/PolicyExtensions.cs index 7fca54d2480c..d8099a2fa892 100644 --- a/src/Middleware/OutputCaching/src/Policies/PolicyExtensions.cs +++ b/src/Middleware/OutputCaching/src/Policies/PolicyExtensions.cs @@ -15,7 +15,7 @@ public static class PolicyExtensions /// public static TBuilder OutputCache(this TBuilder builder) where TBuilder : IEndpointConventionBuilder { - ArgumentNullException.ThrowIfNull(builder, nameof(builder)); + ArgumentNullException.ThrowIfNull(builder); var policiesMetadata = new PoliciesMetadata(); @@ -34,8 +34,8 @@ public static TBuilder OutputCache(this TBuilder builder) where TBuild /// public static TBuilder OutputCache(this TBuilder builder, params IOutputCachingPolicy[] items) where TBuilder : IEndpointConventionBuilder { - ArgumentNullException.ThrowIfNull(builder, nameof(builder)); - ArgumentNullException.ThrowIfNull(items, nameof(items)); + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(items); var policiesMetadata = new PoliciesMetadata(); @@ -59,7 +59,7 @@ public static TBuilder OutputCache(this TBuilder builder, params IOutp /// public static TBuilder OutputCache(this TBuilder builder, Action policy) where TBuilder : IEndpointConventionBuilder { - ArgumentNullException.ThrowIfNull(builder, nameof(builder)); + ArgumentNullException.ThrowIfNull(builder); var outputCachePolicyBuilder = new OutputCachePolicyBuilder(); policy?.Invoke(outputCachePolicyBuilder); diff --git a/src/Middleware/OutputCaching/src/Streams/SegmentWriteStream.cs b/src/Middleware/OutputCaching/src/Streams/SegmentWriteStream.cs index caf5a518e5ca..b7491fc174ef 100644 --- a/src/Middleware/OutputCaching/src/Streams/SegmentWriteStream.cs +++ b/src/Middleware/OutputCaching/src/Streams/SegmentWriteStream.cs @@ -122,7 +122,7 @@ public override void SetLength(long value) public override void Write(byte[] buffer, int offset, int count) { - ArgumentNullException.ThrowIfNull(buffer, nameof(buffer)); + ArgumentNullException.ThrowIfNull(buffer); if (offset < 0) { From 8c745ba7189155dea0615f819dfba00f22c0014c Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Wed, 18 May 2022 10:00:59 -0700 Subject: [PATCH 27/48] API review feedback --- .../samples/OutputCachingSample/Startup.cs | 32 +++++++++--------- .../OutputCaching/src/OutputCachingOptions.cs | 6 ++-- .../src/OutputCachingPolicyProvider.cs | 12 +++---- .../src/Policies/EnableCachingPolicy.cs | 2 +- .../src/Policies/OutputCachePolicyBuilder.cs | 33 +++++++++++++++++++ .../src/Policies/PolicyExtensions.cs | 8 ++--- .../src/Policies/ProfilePolicy.cs | 2 +- .../OutputCaching/src/PublicAPI.Unshipped.txt | 16 +++++---- 8 files changed, 73 insertions(+), 38 deletions(-) diff --git a/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs b/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs index 07b096e6203c..c3f90d6fe8f6 100644 --- a/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs +++ b/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs @@ -9,13 +9,10 @@ builder.Services.AddOutputCaching(options => { - options.Policies.Add(new EnableCachingPolicy()); - - options.Profiles["NoCache"] = new OutputCachePolicyBuilder().NoStore().Build(); - - // Tag any request to "/blog** - options.Policies.Add(new OutputCachePolicyBuilder().Path("/blog").Tag("blog").Build()); + // Enable caching on all requests + // options.DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().Build(); + options.Policies["NoCache"] = new OutputCachePolicyBuilder().NoStore().Build(); }); var app = builder.Build(); @@ -24,16 +21,17 @@ app.MapGet("/", Gravatar.WriteGravatar); -app.MapGet("/cached", Gravatar.WriteGravatar).OutputCache(); +app.MapGet("/cached", Gravatar.WriteGravatar).CacheOutput(); -app.MapGet("/nocache", Gravatar.WriteGravatar).OutputCache(x => x.NoStore()); +app.MapGet("/nocache", Gravatar.WriteGravatar).CacheOutput(x => x.NoStore()); -app.MapGet("/profile", Gravatar.WriteGravatar).OutputCache(x => x.Profile("NoCache")); +app.MapGet("/profile", Gravatar.WriteGravatar).CacheOutput(x => x.Profile("NoCache")); app.MapGet("/attribute", [OutputCache(Profile = "NoCache")] (c) => Gravatar.WriteGravatar(c)); -app.MapGet("/blog", Gravatar.WriteGravatar); -app.MapGet("/blog/post/{id}", Gravatar.WriteGravatar); +var blog = app.MapGroup("blog").CacheOutput(x => x.Tag("blog")); +blog.MapGet("/", Gravatar.WriteGravatar); +blog.MapGet("/post/{id}", Gravatar.WriteGravatar); app.MapPost("/purge/{tag}", async (IOutputCacheStore cache, string tag) => { @@ -43,9 +41,9 @@ }); // Cached entries will vary by culture, but any other additional query is ignored and returns the same cached content -app.MapGet("/query", Gravatar.WriteGravatar).OutputCache(p => p.VaryByQuery("culture")); +app.MapGet("/query", Gravatar.WriteGravatar).CacheOutput(p => p.VaryByQuery("culture")); -app.MapGet("/vary", Gravatar.WriteGravatar).OutputCache(c => c.VaryByValue(() => ("time", (DateTime.Now.Second % 2).ToString(CultureInfo.InvariantCulture)))); +app.MapGet("/vary", Gravatar.WriteGravatar).CacheOutput(c => c.VaryByValue(() => ("time", (DateTime.Now.Second % 2).ToString(CultureInfo.InvariantCulture)))); long requests = 0; @@ -54,7 +52,7 @@ { await Task.Delay(1000); await context.Response.WriteAsync($"
{requests++}
"); -}).OutputCache(p => p.Lock(false).Expires(TimeSpan.FromMilliseconds(1))); +}).CacheOutput(p => p.Lock(false).Expires(TimeSpan.FromMilliseconds(1))); // Cached because Response Caching policy and contains "Cache-Control: public" app.MapGet("/headers", async context => @@ -62,7 +60,7 @@ // From a browser this endpoint won't be cached because of max-age: 0 context.Response.Headers.CacheControl = "public"; await Gravatar.WriteGravatar(context); -}).OutputCache(new ResponseCachingPolicy()); +}).CacheOutput(new ResponseCachingPolicy()); // Etag app.MapGet("/etag", async (context) => @@ -74,9 +72,9 @@ context.Response.Headers.ETag = etag; await Gravatar.WriteGravatar(context); -}).OutputCache(); +}).CacheOutput(); // When the request header If-Modified-Since is provided, return 304 if the cached entry is older -app.MapGet("/ims", Gravatar.WriteGravatar).OutputCache(); +app.MapGet("/ims", Gravatar.WriteGravatar).CacheOutput(); await app.RunAsync(); diff --git a/src/Middleware/OutputCaching/src/OutputCachingOptions.cs b/src/Middleware/OutputCaching/src/OutputCachingOptions.cs index 8cd5ea8ac378..cf839b14cd8a 100644 --- a/src/Middleware/OutputCaching/src/OutputCachingOptions.cs +++ b/src/Middleware/OutputCaching/src/OutputCachingOptions.cs @@ -34,15 +34,15 @@ public class OutputCachingOptions public bool UseCaseSensitivePaths { get; set; } /// - /// Gets the policies applied to all requests. + /// Gets the policy applied to all requests. /// - public List Policies { get; } = new() { new DefaultOutputCachePolicy() }; + public IOutputCachingPolicy? DefaultPolicy { get; set; } = new DefaultOutputCachePolicy(); /// /// Gets a Dictionary of policy names, which are pre-defined settings for /// output caching. /// - public IDictionary Profiles { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); + public IDictionary Policies { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); /// /// For testing purposes only. diff --git a/src/Middleware/OutputCaching/src/OutputCachingPolicyProvider.cs b/src/Middleware/OutputCaching/src/OutputCachingPolicyProvider.cs index 73211a2ff5ae..6bc1a09b1c92 100644 --- a/src/Middleware/OutputCaching/src/OutputCachingPolicyProvider.cs +++ b/src/Middleware/OutputCaching/src/OutputCachingPolicyProvider.cs @@ -17,9 +17,9 @@ public OutputCachingPolicyProvider(IOptions options) public async Task OnRequestAsync(IOutputCachingContext context) { - foreach (var policy in _options.Policies) + if (_options.DefaultPolicy != null) { - await policy.OnRequestAsync(context); + await _options.DefaultPolicy.OnRequestAsync(context); } var policiesMetadata = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata(); @@ -42,9 +42,9 @@ public async Task OnRequestAsync(IOutputCachingContext context) public async Task OnServeFromCacheAsync(IOutputCachingContext context) { - foreach (var policy in _options.Policies) + if (_options.DefaultPolicy != null) { - await policy.OnServeFromCacheAsync(context); + await _options.DefaultPolicy.OnServeFromCacheAsync(context); } // Apply response policies defined on the feature, e.g. from action attributes @@ -72,9 +72,9 @@ public async Task OnServeFromCacheAsync(IOutputCachingContext context) public async Task OnServeResponseAsync(IOutputCachingContext context) { - foreach (var policy in _options.Policies) + if (_options.DefaultPolicy != null) { - await policy.OnServeResponseAsync(context); + await _options.DefaultPolicy.OnServeResponseAsync(context); } // Apply response policies defined on the feature, e.g. from action attributes diff --git a/src/Middleware/OutputCaching/src/Policies/EnableCachingPolicy.cs b/src/Middleware/OutputCaching/src/Policies/EnableCachingPolicy.cs index 10c533edf32b..1857196e3434 100644 --- a/src/Middleware/OutputCaching/src/Policies/EnableCachingPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/EnableCachingPolicy.cs @@ -11,7 +11,7 @@ public sealed class EnableCachingPolicy : IOutputCachingPolicy /// /// Default instance of . /// - public static EnableCachingPolicy Instance = new EnableCachingPolicy(); + public static readonly EnableCachingPolicy Instance = new(); /// public Task OnRequestAsync(IOutputCachingContext context) diff --git a/src/Middleware/OutputCaching/src/Policies/OutputCachePolicyBuilder.cs b/src/Middleware/OutputCaching/src/Policies/OutputCachePolicyBuilder.cs index c5b6bb609957..2aa1c5bc4ffa 100644 --- a/src/Middleware/OutputCaching/src/Policies/OutputCachePolicyBuilder.cs +++ b/src/Middleware/OutputCaching/src/Policies/OutputCachePolicyBuilder.cs @@ -15,6 +15,39 @@ public class OutputCachePolicyBuilder private List Policies { get; } = new(); private List>> Requirements { get; } = new(); + /// + /// Gets an initialized with a instance. + /// + public static OutputCachePolicyBuilder Default + { + get + { + var builder = new OutputCachePolicyBuilder(); + builder.Policies.Add(new DefaultOutputCachePolicy()); + return builder; + } + } + + /// + /// Gets an empty . + /// + public static OutputCachePolicyBuilder Empty + { + get + { + return new OutputCachePolicyBuilder(); + } + } + + /// + /// Enables caching. + /// + public OutputCachePolicyBuilder Enable() + { + Policies.Add(new EnableCachingPolicy()); + return this; + } + /// /// Adds a requirement to the current policy. /// diff --git a/src/Middleware/OutputCaching/src/Policies/PolicyExtensions.cs b/src/Middleware/OutputCaching/src/Policies/PolicyExtensions.cs index d8099a2fa892..3aea6a271d0f 100644 --- a/src/Middleware/OutputCaching/src/Policies/PolicyExtensions.cs +++ b/src/Middleware/OutputCaching/src/Policies/PolicyExtensions.cs @@ -13,7 +13,7 @@ public static class PolicyExtensions /// /// Marks an endpoint to be cached. /// - public static TBuilder OutputCache(this TBuilder builder) where TBuilder : IEndpointConventionBuilder + public static TBuilder CacheOutput(this TBuilder builder) where TBuilder : IEndpointConventionBuilder { ArgumentNullException.ThrowIfNull(builder); @@ -32,7 +32,7 @@ public static TBuilder OutputCache(this TBuilder builder) where TBuild /// /// Marks an endpoint to be cached with the specified policies. /// - public static TBuilder OutputCache(this TBuilder builder, params IOutputCachingPolicy[] items) where TBuilder : IEndpointConventionBuilder + public static TBuilder CacheOutput(this TBuilder builder, params IOutputCachingPolicy[] items) where TBuilder : IEndpointConventionBuilder { ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(items); @@ -55,9 +55,9 @@ public static TBuilder OutputCache(this TBuilder builder, params IOutp } /// - /// Marks an endpoint to be cached with the specified policies. + /// Marks an endpoint to be cached with the specified policy. /// - public static TBuilder OutputCache(this TBuilder builder, Action policy) where TBuilder : IEndpointConventionBuilder + public static TBuilder CacheOutput(this TBuilder builder, Action policy) where TBuilder : IEndpointConventionBuilder { ArgumentNullException.ThrowIfNull(builder); diff --git a/src/Middleware/OutputCaching/src/Policies/ProfilePolicy.cs b/src/Middleware/OutputCaching/src/Policies/ProfilePolicy.cs index ca8fb2d6cda0..38edfff833ed 100644 --- a/src/Middleware/OutputCaching/src/Policies/ProfilePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/ProfilePolicy.cs @@ -65,7 +65,7 @@ public Task OnRequestAsync(IOutputCachingContext context) { var options = context.HttpContext.RequestServices.GetRequiredService>(); - return options.Value.Profiles.TryGetValue(_profileName, out var cacheProfile) + return options.Value.Policies.TryGetValue(_profileName, out var cacheProfile) ? cacheProfile : null; } diff --git a/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt b/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt index 0bbb2de3b35a..29611a4628d7 100644 --- a/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt @@ -38,6 +38,7 @@ Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.VaryByQueryKeys.set -> v Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Build() -> Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Clear() -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Enable() -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Expires(System.TimeSpan expiration) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Lock(bool lockResponse = true) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Method(params string![]! methods) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! @@ -64,11 +65,12 @@ Microsoft.AspNetCore.OutputCaching.OutputCachingMiddleware.OutputCachingMiddlewa Microsoft.AspNetCore.OutputCaching.OutputCachingOptions Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.DefaultExpirationTimeSpan.get -> System.TimeSpan Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.DefaultExpirationTimeSpan.set -> void +Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.DefaultPolicy.get -> Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy? +Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.DefaultPolicy.set -> void Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.MaximumBodySize.get -> long Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.MaximumBodySize.set -> void Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.OutputCachingOptions() -> void -Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.Policies.get -> System.Collections.Generic.List! -Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.Profiles.get -> System.Collections.Generic.IDictionary! +Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.Policies.get -> System.Collections.Generic.IDictionary! Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.SizeLimit.get -> long Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.SizeLimit.set -> void Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.UseCaseSensitivePaths.get -> bool @@ -117,11 +119,13 @@ Microsoft.AspNetCore.OutputCaching.VaryByValuePolicy.VaryByValuePolicy(System.Fu Microsoft.AspNetCore.OutputCaching.VaryByValuePolicy.VaryByValuePolicy(System.Func! varyBy) -> void Microsoft.Extensions.DependencyInjection.OutputCachingServicesExtensions static Microsoft.AspNetCore.Builder.OutputCachingExtensions.UseOutputCaching(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! -static Microsoft.AspNetCore.OutputCaching.EnableCachingPolicy.Instance -> Microsoft.AspNetCore.OutputCaching.EnableCachingPolicy! -static Microsoft.AspNetCore.OutputCaching.Policies.PolicyExtensions.OutputCache(this TBuilder builder) -> TBuilder -static Microsoft.AspNetCore.OutputCaching.Policies.PolicyExtensions.OutputCache(this TBuilder builder, System.Action! policy) -> TBuilder -static Microsoft.AspNetCore.OutputCaching.Policies.PolicyExtensions.OutputCache(this TBuilder builder, params Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy![]! items) -> TBuilder +static Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Default.get -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +static Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Empty.get -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +static Microsoft.AspNetCore.OutputCaching.Policies.PolicyExtensions.CacheOutput(this TBuilder builder) -> TBuilder +static Microsoft.AspNetCore.OutputCaching.Policies.PolicyExtensions.CacheOutput(this TBuilder builder, System.Action! policy) -> TBuilder +static Microsoft.AspNetCore.OutputCaching.Policies.PolicyExtensions.CacheOutput(this TBuilder builder, params Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy![]! items) -> TBuilder static Microsoft.Extensions.DependencyInjection.OutputCachingServicesExtensions.AddOutputCaching(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.Extensions.DependencyInjection.OutputCachingServicesExtensions.AddOutputCaching(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static readonly Microsoft.AspNetCore.OutputCaching.EnableCachingPolicy.Instance -> Microsoft.AspNetCore.OutputCaching.EnableCachingPolicy! static readonly Microsoft.AspNetCore.OutputCaching.LockingPolicy.Disabled -> Microsoft.AspNetCore.OutputCaching.LockingPolicy! static readonly Microsoft.AspNetCore.OutputCaching.LockingPolicy.Enabled -> Microsoft.AspNetCore.OutputCaching.LockingPolicy! From f19b5979f898d57cc771bcd5ebc788ff735aef4b Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Tue, 24 May 2022 16:00:10 -0700 Subject: [PATCH 28/48] Add more tests --- .../src/IOutputCachingContext.cs | 12 +- .../OutputCaching/src/LoggerExtensions.cs | 7 +- .../OutputCaching/src/OutputCacheAttribute.cs | 13 + .../OutputCaching/src/OutputCachingContext.cs | 2 +- .../src/OutputCachingKeyProvider.cs | 2 +- .../src/OutputCachingMiddleware.cs | 4 +- .../src/Policies/CompositePolicy.cs | 6 +- .../src/Policies/DefaultOutputCachePolicy.cs | 32 +- .../src/Policies/EnableCachingPolicy.cs | 6 +- .../src/Policies/ExpirationPolicy.cs | 6 +- .../src/Policies/LockingPolicy.cs | 6 +- .../src/Policies/NoStorePolicy.cs | 6 +- .../src/Policies/OutputCachePolicyBuilder.cs | 12 + .../src/Policies/PredicatePolicy.cs | 6 +- .../src/Policies/ProfilePolicy.cs | 6 +- .../src/Policies/ResponseCachingPolicy.cs | 6 +- .../OutputCaching/src/Policies/TagsPolicy.cs | 6 +- .../src/Policies/VaryByHeaderPolicy.cs | 64 ++ .../src/Policies/VaryByQueryPolicy.cs | 10 +- .../src/Policies/VaryByValuePolicy.cs | 6 +- .../OutputCaching/src/PublicAPI.Unshipped.txt | 43 +- .../test/OutputCachingMiddlewareTests.cs | 130 +-- .../test/OutputCachingPolicyProviderTests.cs | 476 +++++++++ .../OutputCaching/test/OutputCachingTests.cs | 978 +++++++++++++++++ .../ResponseCachingPolicyProviderTests.cs | 790 -------------- .../test/ResponseCachingTests.cs | 984 ------------------ .../OutputCaching/test/TestUtils.cs | 74 +- 27 files changed, 1687 insertions(+), 2006 deletions(-) create mode 100644 src/Middleware/OutputCaching/src/Policies/VaryByHeaderPolicy.cs create mode 100644 src/Middleware/OutputCaching/test/OutputCachingPolicyProviderTests.cs create mode 100644 src/Middleware/OutputCaching/test/OutputCachingTests.cs delete mode 100644 src/Middleware/OutputCaching/test/ResponseCachingPolicyProviderTests.cs delete mode 100644 src/Middleware/OutputCaching/test/ResponseCachingTests.cs diff --git a/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingContext.cs b/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingContext.cs index 0f5f969a8338..4e9561100008 100644 --- a/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingContext.cs +++ b/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingContext.cs @@ -46,11 +46,6 @@ public interface IOutputCachingContext /// TimeSpan? ResponseMaxAge { get; } - /// - /// The custom expiration timespan for the response - /// - public TimeSpan? ResponseExpirationTimeSpan { get; set; } - /// /// Gets the cached response headers. /// @@ -71,6 +66,11 @@ public interface IOutputCachingContext ///
ILogger Logger { get; } + /// + /// Gets or sets the custom expiration timespan for the response + /// + public TimeSpan? ResponseExpirationTimeSpan { get; set; } + /// /// Determines whether the output caching logic should be configured for the incoming HTTP request. /// @@ -102,7 +102,7 @@ public interface IOutputCachingContext bool IsResponseCacheable { get; set; } /// - /// Determines whether the response retrieved from the response cache is fresh and can be served. + /// Determines whether the response retrieved from the cache store is fresh and can be served. /// bool IsCacheEntryFresh { get; set; } } diff --git a/src/Middleware/OutputCaching/src/LoggerExtensions.cs b/src/Middleware/OutputCaching/src/LoggerExtensions.cs index eee6d87602fd..4b4db1455d35 100644 --- a/src/Middleware/OutputCaching/src/LoggerExtensions.cs +++ b/src/Middleware/OutputCaching/src/LoggerExtensions.cs @@ -48,7 +48,7 @@ internal static partial class LoggerExtensions [LoggerMessage(9, LogLevel.Debug, "The age of the entry is {Age} and has exceeded the maximum age of {MaxAge} specified by the 'max-age' cache directive.", EventName = "ExpirationMaxAgeExceeded")] internal static partial void ExpirationMaxAgeExceeded(this ILogger logger, TimeSpan age, TimeSpan maxAge); - [LoggerMessage(10, LogLevel.Debug, "The response time of the entry is {ResponseTime} and has exceeded the expiry date of {Expired} specified by the 'Expires' header.", + [LoggerMessage(10, LogLevel.Debug, "The response time of the entry is {ResponseTime} and has exceeded its expiry date of {Expired} specified by the 'Expires' header.", EventName = "ExpirationExpiresExceeded")] internal static partial void ExpirationExpiresExceeded(this ILogger logger, DateTimeOffset responseTime, DateTimeOffset expired); @@ -120,4 +120,9 @@ internal static partial class LoggerExtensions "However, the 'max-stale' cache directive was specified without an assigned value and a stale response of any age is accepted.", EventName = "ExpirationInfiniteMaxStaleSatisfied")] internal static partial void ExpirationInfiniteMaxStaleSatisfied(this ILogger logger, TimeSpan age, TimeSpan maxAge); + + [LoggerMessage(30, LogLevel.Debug, "The response time of the entry is {ResponseTime} and has exceeded its expiry date.", + EventName = "ExpirationExpiresExceeded")] + internal static partial void ExpirationExpiresExceeded(this ILogger logger, DateTimeOffset responseTime); + } diff --git a/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs b/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs index be83e05c58a6..69fd87d1e73d 100644 --- a/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs @@ -44,6 +44,14 @@ public bool NoStore /// public string[]? VaryByQueryKeys { get; set; } + /// + /// Gets or sets the headers to vary by. + /// + /// + /// requires the output cache middleware. + /// + public string[]? VaryByHeaders { get; set; } + /// /// Gets or sets the value of the cache profile name. /// @@ -73,6 +81,11 @@ private List GetPolicies() policies.Add(new VaryByQueryPolicy(VaryByQueryKeys)); } + if (VaryByHeaders != null) + { + policies.Add(new VaryByHeaderPolicy(VaryByHeaders)); + } + if (_duration != null) { policies.Add(new ExpirationPolicy(TimeSpan.FromSeconds(_duration.Value))); diff --git a/src/Middleware/OutputCaching/src/OutputCachingContext.cs b/src/Middleware/OutputCaching/src/OutputCachingContext.cs index 0be6004c6b4a..d5dd89fb8938 100644 --- a/src/Middleware/OutputCaching/src/OutputCachingContext.cs +++ b/src/Middleware/OutputCaching/src/OutputCachingContext.cs @@ -57,7 +57,7 @@ internal OutputCachingContext(HttpContext httpContext, ILogger logger) public bool IsResponseCacheable { get; set; } /// - /// Determine whether the response retrieved from the response cache is fresh and can be served. + /// Determine whether the response retrieved from the cache store is fresh and can be served. /// public bool IsCacheEntryFresh { get; set; } diff --git a/src/Middleware/OutputCaching/src/OutputCachingKeyProvider.cs b/src/Middleware/OutputCaching/src/OutputCachingKeyProvider.cs index 27f30190549c..40a21e7e26a7 100644 --- a/src/Middleware/OutputCaching/src/OutputCachingKeyProvider.cs +++ b/src/Middleware/OutputCaching/src/OutputCachingKeyProvider.cs @@ -113,7 +113,7 @@ public string CreateStorageKey(OutputCachingContext context) builder.Append(KeyDelimiter) .Append('Q'); - if (varyByRules.QueryKeys.Count == 1 && string.Equals(varyByRules.QueryKeys[0], "*", StringComparison.Ordinal)) + if (varyByRules.QueryKeys.Count == 1 && string.Equals(varyByRules.QueryKeys[0], "*", StringComparison.Ordinal) && context.HttpContext.Request.Query.Count > 0) { // Vary by all available query keys var queryArray = context.HttpContext.Request.Query.ToArray(); diff --git a/src/Middleware/OutputCaching/src/OutputCachingMiddleware.cs b/src/Middleware/OutputCaching/src/OutputCachingMiddleware.cs index 000a998ba14e..df1437298b85 100644 --- a/src/Middleware/OutputCaching/src/OutputCachingMiddleware.cs +++ b/src/Middleware/OutputCaching/src/OutputCachingMiddleware.cs @@ -271,9 +271,9 @@ internal async Task TryServeFromCacheAsync(OutputCachingContext context) return false; } - private void CreateCacheKey(OutputCachingContext context) + internal void CreateCacheKey(OutputCachingContext context) { - var varyHeaders = new StringValues(context.HttpContext.Response.Headers.GetCommaSeparatedValues(HeaderNames.Vary)); + var varyHeaders = context.CachedVaryByRules.Headers; var varyQueryKeys = context.CachedVaryByRules.QueryKeys; var varyByCustomKeys = context.CachedVaryByRules.VaryByCustom; var varyByPrefix = context.CachedVaryByRules.VaryByPrefix; diff --git a/src/Middleware/OutputCaching/src/Policies/CompositePolicy.cs b/src/Middleware/OutputCaching/src/Policies/CompositePolicy.cs index 375f284bc352..2ba4ef40f2fa 100644 --- a/src/Middleware/OutputCaching/src/Policies/CompositePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/CompositePolicy.cs @@ -20,7 +20,7 @@ public CompositePolicy(params IOutputCachingPolicy[] policies!!) } /// - public async Task OnRequestAsync(IOutputCachingContext context) + async Task IOutputCachingPolicy.OnRequestAsync(IOutputCachingContext context) { foreach (var policy in _policies) { @@ -29,7 +29,7 @@ public async Task OnRequestAsync(IOutputCachingContext context) } /// - public async Task OnServeFromCacheAsync(IOutputCachingContext context) + async Task IOutputCachingPolicy.OnServeFromCacheAsync(IOutputCachingContext context) { foreach (var policy in _policies) { @@ -38,7 +38,7 @@ public async Task OnServeFromCacheAsync(IOutputCachingContext context) } /// - public async Task OnServeResponseAsync(IOutputCachingContext context) + async Task IOutputCachingPolicy.OnServeResponseAsync(IOutputCachingContext context) { foreach (var policy in _policies) { diff --git a/src/Middleware/OutputCaching/src/Policies/DefaultOutputCachePolicy.cs b/src/Middleware/OutputCaching/src/Policies/DefaultOutputCachePolicy.cs index e67de21ee518..a3765358a0a6 100644 --- a/src/Middleware/OutputCaching/src/Policies/DefaultOutputCachePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/DefaultOutputCachePolicy.cs @@ -12,7 +12,7 @@ namespace Microsoft.AspNetCore.OutputCaching; public sealed class DefaultOutputCachePolicy : IOutputCachingPolicy { /// - public Task OnRequestAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnRequestAsync(IOutputCachingContext context) { context.AttemptOutputCaching = AttemptOutputCaching(context); context.AllowCacheLookup = true; @@ -27,15 +27,41 @@ public Task OnRequestAsync(IOutputCachingContext context) } /// - public Task OnServeFromCacheAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnServeFromCacheAsync(IOutputCachingContext context) { context.IsCacheEntryFresh = true; + + // Validate expiration + if (context.CachedEntryAge <= TimeSpan.Zero) + { + context.Logger.ExpirationExpiresExceeded(context.ResponseTime!.Value); + context.IsCacheEntryFresh = false; + } + return Task.CompletedTask; } /// - public Task OnServeResponseAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnServeResponseAsync(IOutputCachingContext context) { + var response = context.HttpContext.Response; + + // Verify existence of cookie headers + if (!StringValues.IsNullOrEmpty(response.Headers.SetCookie)) + { + context.Logger.ResponseWithSetCookieNotCacheable(); + context.IsResponseCacheable = false; + return Task.CompletedTask; + } + + // Check response code + if (response.StatusCode != StatusCodes.Status200OK) + { + context.Logger.ResponseWithUnsuccessfulStatusCodeNotCacheable(response.StatusCode); + context.IsResponseCacheable = false; + return Task.CompletedTask; + } + context.IsResponseCacheable = true; return Task.CompletedTask; } diff --git a/src/Middleware/OutputCaching/src/Policies/EnableCachingPolicy.cs b/src/Middleware/OutputCaching/src/Policies/EnableCachingPolicy.cs index 1857196e3434..2bb9bf6291a1 100644 --- a/src/Middleware/OutputCaching/src/Policies/EnableCachingPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/EnableCachingPolicy.cs @@ -14,7 +14,7 @@ public sealed class EnableCachingPolicy : IOutputCachingPolicy public static readonly EnableCachingPolicy Instance = new(); /// - public Task OnRequestAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnRequestAsync(IOutputCachingContext context) { context.EnableOutputCaching = true; @@ -22,13 +22,13 @@ public Task OnRequestAsync(IOutputCachingContext context) } /// - public Task OnServeResponseAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnServeResponseAsync(IOutputCachingContext context) { return Task.CompletedTask; } /// - public Task OnServeFromCacheAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnServeFromCacheAsync(IOutputCachingContext context) { return Task.CompletedTask; } diff --git a/src/Middleware/OutputCaching/src/Policies/ExpirationPolicy.cs b/src/Middleware/OutputCaching/src/Policies/ExpirationPolicy.cs index ca4c82505823..023ff05da3f4 100644 --- a/src/Middleware/OutputCaching/src/Policies/ExpirationPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/ExpirationPolicy.cs @@ -20,7 +20,7 @@ public ExpirationPolicy(TimeSpan expiration) } /// - public Task OnRequestAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnRequestAsync(IOutputCachingContext context) { context.ResponseExpirationTimeSpan = _expiration; @@ -28,13 +28,13 @@ public Task OnRequestAsync(IOutputCachingContext context) } /// - public Task OnServeFromCacheAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnServeFromCacheAsync(IOutputCachingContext context) { return Task.CompletedTask; } /// - public Task OnServeResponseAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnServeResponseAsync(IOutputCachingContext context) { return Task.CompletedTask; } diff --git a/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs b/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs index 257154491650..3c8022fc9b30 100644 --- a/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs @@ -26,7 +26,7 @@ private LockingPolicy(bool lockResponse) public static readonly LockingPolicy Disabled = new(false); /// - public Task OnRequestAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnRequestAsync(IOutputCachingContext context) { context.AllowLocking = _lockResponse; @@ -34,13 +34,13 @@ public Task OnRequestAsync(IOutputCachingContext context) } /// - public Task OnServeFromCacheAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnServeFromCacheAsync(IOutputCachingContext context) { return Task.CompletedTask; } /// - public Task OnServeResponseAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnServeResponseAsync(IOutputCachingContext context) { return Task.CompletedTask; } diff --git a/src/Middleware/OutputCaching/src/Policies/NoStorePolicy.cs b/src/Middleware/OutputCaching/src/Policies/NoStorePolicy.cs index 00b4d7cc1d4c..11f8401111c7 100644 --- a/src/Middleware/OutputCaching/src/Policies/NoStorePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/NoStorePolicy.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.OutputCaching; public class NoStorePolicy : IOutputCachingPolicy { /// - public Task OnServeResponseAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnServeResponseAsync(IOutputCachingContext context) { context.IsResponseCacheable = false; @@ -17,13 +17,13 @@ public Task OnServeResponseAsync(IOutputCachingContext context) } /// - public Task OnServeFromCacheAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnServeFromCacheAsync(IOutputCachingContext context) { return Task.CompletedTask; } /// - public Task OnRequestAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnRequestAsync(IOutputCachingContext context) { return Task.CompletedTask; } diff --git a/src/Middleware/OutputCaching/src/Policies/OutputCachePolicyBuilder.cs b/src/Middleware/OutputCaching/src/Policies/OutputCachePolicyBuilder.cs index 2aa1c5bc4ffa..f64d64cd411a 100644 --- a/src/Middleware/OutputCaching/src/Policies/OutputCachePolicyBuilder.cs +++ b/src/Middleware/OutputCaching/src/Policies/OutputCachePolicyBuilder.cs @@ -136,6 +136,18 @@ public OutputCachePolicyBuilder VaryByQuery(params string[] queryKeys) return this; } + /// + /// Adds a policy to vary the cached responses by header. + /// + /// The headers to vary the cached responses by. + public OutputCachePolicyBuilder VaryByHeader(params string[] headers) + { + ArgumentNullException.ThrowIfNull(headers); + + Policies.Add(new VaryByHeaderPolicy(headers)); + return this; + } + /// /// Adds a policy to vary the cached responses by custom values. /// diff --git a/src/Middleware/OutputCaching/src/Policies/PredicatePolicy.cs b/src/Middleware/OutputCaching/src/Policies/PredicatePolicy.cs index 8d65f4ea0335..171c1f66cfd0 100644 --- a/src/Middleware/OutputCaching/src/Policies/PredicatePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/PredicatePolicy.cs @@ -25,7 +25,7 @@ public PredicatePolicy(Func> predicate, IOutpu } /// - public Task OnRequestAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnRequestAsync(IOutputCachingContext context) { if (_predicate == null) { @@ -56,13 +56,13 @@ async static Task Awaited(Task task, IOutputCachingPolicy policy, IOutputC } /// - public Task OnServeFromCacheAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnServeFromCacheAsync(IOutputCachingContext context) { return Task.CompletedTask; } /// - public Task OnServeResponseAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnServeResponseAsync(IOutputCachingContext context) { return Task.CompletedTask; } diff --git a/src/Middleware/OutputCaching/src/Policies/ProfilePolicy.cs b/src/Middleware/OutputCaching/src/Policies/ProfilePolicy.cs index 38edfff833ed..08f9abea8f6c 100644 --- a/src/Middleware/OutputCaching/src/Policies/ProfilePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/ProfilePolicy.cs @@ -23,7 +23,7 @@ public ProfilePolicy(string profileName) } /// - public Task OnServeResponseAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnServeResponseAsync(IOutputCachingContext context) { var policy = GetProfilePolicy(context); @@ -36,7 +36,7 @@ public Task OnServeResponseAsync(IOutputCachingContext context) } /// - public Task OnServeFromCacheAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnServeFromCacheAsync(IOutputCachingContext context) { var policy = GetProfilePolicy(context); @@ -49,7 +49,7 @@ public Task OnServeFromCacheAsync(IOutputCachingContext context) } /// - public Task OnRequestAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnRequestAsync(IOutputCachingContext context) { var policy = GetProfilePolicy(context); diff --git a/src/Middleware/OutputCaching/src/Policies/ResponseCachingPolicy.cs b/src/Middleware/OutputCaching/src/Policies/ResponseCachingPolicy.cs index b77b988e9222..eb6a21e74c6b 100644 --- a/src/Middleware/OutputCaching/src/Policies/ResponseCachingPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/ResponseCachingPolicy.cs @@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.OutputCaching; public sealed class ResponseCachingPolicy : IOutputCachingPolicy { /// - public Task OnRequestAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnRequestAsync(IOutputCachingContext context) { context.AttemptOutputCaching = AttemptOutputCaching(context); context.AllowCacheLookup = AllowCacheLookup(context); @@ -27,7 +27,7 @@ public Task OnRequestAsync(IOutputCachingContext context) } /// - public Task OnServeResponseAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnServeResponseAsync(IOutputCachingContext context) { context.IsResponseCacheable = IsResponseCacheable(context); @@ -35,7 +35,7 @@ public Task OnServeResponseAsync(IOutputCachingContext context) } /// - public Task OnServeFromCacheAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnServeFromCacheAsync(IOutputCachingContext context) { context.IsCacheEntryFresh = IsCachedEntryFresh(context); diff --git a/src/Middleware/OutputCaching/src/Policies/TagsPolicy.cs b/src/Middleware/OutputCaching/src/Policies/TagsPolicy.cs index 6906731cd11c..8b3db6076082 100644 --- a/src/Middleware/OutputCaching/src/Policies/TagsPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/TagsPolicy.cs @@ -20,7 +20,7 @@ public TagsPolicy(params string[] tags) } /// - public Task OnRequestAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnRequestAsync(IOutputCachingContext context) { foreach (var tag in _tags) { @@ -31,13 +31,13 @@ public Task OnRequestAsync(IOutputCachingContext context) } /// - public Task OnServeFromCacheAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnServeFromCacheAsync(IOutputCachingContext context) { return Task.CompletedTask; } /// - public Task OnServeResponseAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnServeResponseAsync(IOutputCachingContext context) { return Task.CompletedTask; } diff --git a/src/Middleware/OutputCaching/src/Policies/VaryByHeaderPolicy.cs b/src/Middleware/OutputCaching/src/Policies/VaryByHeaderPolicy.cs new file mode 100644 index 000000000000..f80c56dc07d4 --- /dev/null +++ b/src/Middleware/OutputCaching/src/Policies/VaryByHeaderPolicy.cs @@ -0,0 +1,64 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.OutputCaching; + +/// +/// When applied, the cached content will be different for every value of the provided headers. +/// +public sealed class VaryByHeaderPolicy : IOutputCachingPolicy +{ + private StringValues _headers { get; set; } + + /// + /// Creates a policy that doesn't vary the cached content based on headers. + /// + public VaryByHeaderPolicy() + { + } + + /// + /// Creates a policy that varies the cached content based on the specified header. + /// + public VaryByHeaderPolicy(string header) + { + _headers = header; + } + + /// + /// Creates a policy that varies the cached content based on the specified query string keys. + /// + public VaryByHeaderPolicy(params string[] headers) + { + _headers = headers; + } + + /// + Task IOutputCachingPolicy.OnRequestAsync(IOutputCachingContext context) + { + // No vary by header? + if (_headers.Count == 0) + { + context.CachedVaryByRules.Headers = _headers; + return Task.CompletedTask; + } + + context.CachedVaryByRules.Headers = StringValues.Concat(context.CachedVaryByRules.Headers, _headers); + + return Task.CompletedTask; + } + + /// + Task IOutputCachingPolicy.OnServeFromCacheAsync(IOutputCachingContext context) + { + return Task.CompletedTask; + } + + /// + Task IOutputCachingPolicy.OnServeResponseAsync(IOutputCachingContext context) + { + return Task.CompletedTask; + } +} diff --git a/src/Middleware/OutputCaching/src/Policies/VaryByQueryPolicy.cs b/src/Middleware/OutputCaching/src/Policies/VaryByQueryPolicy.cs index 25a45310b8e8..a6b2086eeb2e 100644 --- a/src/Middleware/OutputCaching/src/Policies/VaryByQueryPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/VaryByQueryPolicy.cs @@ -21,7 +21,7 @@ public VaryByQueryPolicy() } /// - /// Creates a policy that vary the cached content based on the specified query string key. + /// Creates a policy that varies the cached content based on the specified query string key. /// public VaryByQueryPolicy(string queryKey) { @@ -29,7 +29,7 @@ public VaryByQueryPolicy(string queryKey) } /// - /// Creates a policy that vary the cached content based on the specified query string keys. + /// Creates a policy that varies the cached content based on the specified query string keys. /// public VaryByQueryPolicy(params string[] queryKeys) { @@ -37,7 +37,7 @@ public VaryByQueryPolicy(params string[] queryKeys) } /// - public Task OnRequestAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnRequestAsync(IOutputCachingContext context) { // No vary by query? if (_queryKeys.Count == 0) @@ -59,13 +59,13 @@ public Task OnRequestAsync(IOutputCachingContext context) } /// - public Task OnServeFromCacheAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnServeFromCacheAsync(IOutputCachingContext context) { return Task.CompletedTask; } /// - public Task OnServeResponseAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnServeResponseAsync(IOutputCachingContext context) { return Task.CompletedTask; } diff --git a/src/Middleware/OutputCaching/src/Policies/VaryByValuePolicy.cs b/src/Middleware/OutputCaching/src/Policies/VaryByValuePolicy.cs index f143967dacd1..88799213535f 100644 --- a/src/Middleware/OutputCaching/src/Policies/VaryByValuePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/VaryByValuePolicy.cs @@ -59,7 +59,7 @@ public VaryByValuePolicy(Func> varyBy) } /// - public Task OnRequestAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnRequestAsync(IOutputCachingContext context) { _varyBy?.Invoke(context.CachedVaryByRules); @@ -67,13 +67,13 @@ public Task OnRequestAsync(IOutputCachingContext context) } /// - public Task OnServeFromCacheAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnServeFromCacheAsync(IOutputCachingContext context) { return Task.CompletedTask; } /// - public Task OnServeResponseAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnServeResponseAsync(IOutputCachingContext context) { return Task.CompletedTask; } diff --git a/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt b/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt index 29611a4628d7..15930d558c34 100644 --- a/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt @@ -2,28 +2,13 @@ Microsoft.AspNetCore.Builder.OutputCachingExtensions Microsoft.AspNetCore.OutputCaching.DefaultOutputCachePolicy Microsoft.AspNetCore.OutputCaching.DefaultOutputCachePolicy.DefaultOutputCachePolicy() -> void -Microsoft.AspNetCore.OutputCaching.DefaultOutputCachePolicy.OnRequestAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.OutputCaching.DefaultOutputCachePolicy.OnServeFromCacheAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.OutputCaching.DefaultOutputCachePolicy.OnServeResponseAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.OutputCaching.EnableCachingPolicy Microsoft.AspNetCore.OutputCaching.EnableCachingPolicy.EnableCachingPolicy() -> void -Microsoft.AspNetCore.OutputCaching.EnableCachingPolicy.OnRequestAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.OutputCaching.EnableCachingPolicy.OnServeFromCacheAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.OutputCaching.EnableCachingPolicy.OnServeResponseAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.OutputCaching.ExpirationPolicy Microsoft.AspNetCore.OutputCaching.ExpirationPolicy.ExpirationPolicy(System.TimeSpan expiration) -> void -Microsoft.AspNetCore.OutputCaching.ExpirationPolicy.OnRequestAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.OutputCaching.ExpirationPolicy.OnServeFromCacheAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.OutputCaching.ExpirationPolicy.OnServeResponseAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.OutputCaching.LockingPolicy -Microsoft.AspNetCore.OutputCaching.LockingPolicy.OnRequestAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.OutputCaching.LockingPolicy.OnServeFromCacheAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.OutputCaching.LockingPolicy.OnServeResponseAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.OutputCaching.NoStorePolicy Microsoft.AspNetCore.OutputCaching.NoStorePolicy.NoStorePolicy() -> void -Microsoft.AspNetCore.OutputCaching.NoStorePolicy.OnRequestAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.OutputCaching.NoStorePolicy.OnServeFromCacheAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.OutputCaching.NoStorePolicy.OnServeResponseAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.Duration.get -> int Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.Duration.set -> void @@ -33,6 +18,8 @@ Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.OutputCacheAttribute() - Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.Policies.get -> System.Collections.Generic.IReadOnlyList! Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.Profile.get -> string? Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.Profile.set -> void +Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.VaryByHeaders.get -> string![]? +Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.VaryByHeaders.set -> void Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.VaryByQueryKeys.get -> string![]? Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.VaryByQueryKeys.set -> void Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder @@ -49,6 +36,7 @@ Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Path(Microsoft.AspNe Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Path(params Microsoft.AspNetCore.Http.PathString[]! pathBases) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Profile(string! profileName) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Tag(params string![]! tags) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByHeader(params string![]! headers) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByQuery(params string![]! queryKeys) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.Func<(string!, string!)>! varyBy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.Func!>! varyBy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! @@ -77,41 +65,24 @@ Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.UseCaseSensitivePaths.ge Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.UseCaseSensitivePaths.set -> void Microsoft.AspNetCore.OutputCaching.Policies.CompositePolicy Microsoft.AspNetCore.OutputCaching.Policies.CompositePolicy.CompositePolicy(params Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy![]! policies) -> void -Microsoft.AspNetCore.OutputCaching.Policies.CompositePolicy.OnRequestAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.OutputCaching.Policies.CompositePolicy.OnServeFromCacheAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.OutputCaching.Policies.CompositePolicy.OnServeResponseAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.OutputCaching.Policies.PolicyExtensions Microsoft.AspNetCore.OutputCaching.Policies.PredicatePolicy -Microsoft.AspNetCore.OutputCaching.Policies.PredicatePolicy.OnRequestAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.OutputCaching.Policies.PredicatePolicy.OnServeFromCacheAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.OutputCaching.Policies.PredicatePolicy.OnServeResponseAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.OutputCaching.Policies.PredicatePolicy.PredicatePolicy(System.Func!>! predicate, Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy! policy) -> void Microsoft.AspNetCore.OutputCaching.ProfilePolicy -Microsoft.AspNetCore.OutputCaching.ProfilePolicy.OnRequestAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.OutputCaching.ProfilePolicy.OnServeFromCacheAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.OutputCaching.ProfilePolicy.OnServeResponseAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.OutputCaching.ProfilePolicy.ProfilePolicy(string! profileName) -> void Microsoft.AspNetCore.OutputCaching.ResponseCachingPolicy -Microsoft.AspNetCore.OutputCaching.ResponseCachingPolicy.OnRequestAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.OutputCaching.ResponseCachingPolicy.OnServeFromCacheAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.OutputCaching.ResponseCachingPolicy.OnServeResponseAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.OutputCaching.ResponseCachingPolicy.ResponseCachingPolicy() -> void Microsoft.AspNetCore.OutputCaching.TagsPolicy -Microsoft.AspNetCore.OutputCaching.TagsPolicy.OnRequestAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.OutputCaching.TagsPolicy.OnServeFromCacheAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.OutputCaching.TagsPolicy.OnServeResponseAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.OutputCaching.TagsPolicy.TagsPolicy(params string![]! tags) -> void +Microsoft.AspNetCore.OutputCaching.VaryByHeaderPolicy +Microsoft.AspNetCore.OutputCaching.VaryByHeaderPolicy.VaryByHeaderPolicy() -> void +Microsoft.AspNetCore.OutputCaching.VaryByHeaderPolicy.VaryByHeaderPolicy(params string![]! headers) -> void +Microsoft.AspNetCore.OutputCaching.VaryByHeaderPolicy.VaryByHeaderPolicy(string! header) -> void Microsoft.AspNetCore.OutputCaching.VaryByQueryPolicy -Microsoft.AspNetCore.OutputCaching.VaryByQueryPolicy.OnRequestAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.OutputCaching.VaryByQueryPolicy.OnServeFromCacheAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.OutputCaching.VaryByQueryPolicy.OnServeResponseAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.OutputCaching.VaryByQueryPolicy.VaryByQueryPolicy() -> void Microsoft.AspNetCore.OutputCaching.VaryByQueryPolicy.VaryByQueryPolicy(params string![]! queryKeys) -> void Microsoft.AspNetCore.OutputCaching.VaryByQueryPolicy.VaryByQueryPolicy(string! queryKey) -> void Microsoft.AspNetCore.OutputCaching.VaryByValuePolicy -Microsoft.AspNetCore.OutputCaching.VaryByValuePolicy.OnRequestAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.OutputCaching.VaryByValuePolicy.OnServeFromCacheAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.OutputCaching.VaryByValuePolicy.OnServeResponseAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.OutputCaching.VaryByValuePolicy.VaryByValuePolicy() -> void Microsoft.AspNetCore.OutputCaching.VaryByValuePolicy.VaryByValuePolicy(System.Func<(string!, string!)>! varyBy) -> void Microsoft.AspNetCore.OutputCaching.VaryByValuePolicy.VaryByValuePolicy(System.Func!>! varyBy) -> void diff --git a/src/Middleware/OutputCaching/test/OutputCachingMiddlewareTests.cs b/src/Middleware/OutputCaching/test/OutputCachingMiddlewareTests.cs index 7f3ea3e042a3..be79352a27bb 100644 --- a/src/Middleware/OutputCaching/test/OutputCachingMiddlewareTests.cs +++ b/src/Middleware/OutputCaching/test/OutputCachingMiddlewareTests.cs @@ -6,6 +6,7 @@ using Microsoft.AspNetCore.OutputCaching.Memory; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging.Testing; +using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; @@ -350,43 +351,31 @@ public void StartResponseAsync_IfAllowResponseCaptureIsTrue_SetsResponseTimeOnly Assert.Equal(initialTime, context.ResponseTime); } - //[Fact] - //public void FinalizeCacheHeadersAsync_UpdateAllowCacheStorage_IfResponseCacheable() - //{ - // var sink = new TestSink(); - // var middleware = TestUtils.CreateTestMiddleware(testSink: sink, policyProvider: new ResponseCachingPolicyProvider()); - // var context = TestUtils.CreateTestContext(); - - // context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() - // { - // Public = true - // }.ToString(); - - // Assert.False(context.AllowCacheStorage); - - // middleware.FinalizeCacheHeaders(context); + [Fact] + public void FinalizeCacheHeadersAsync_DoesntUpdateAllowCacheStorage_IfResponseCacheable() + { + // Contrary to ResponseCaching which reacts to server headers. - // Assert.True(context.AllowCacheStorage); - // Assert.Empty(sink.Writes); - //} + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink); + var context = TestUtils.CreateTestContext(); + context.AllowCacheStorage = false; - //[Fact] - //public void FinalizeCacheHeadersAsync_DoNotUpdateAllowCacheStorage_IfResponseIsNotCacheable() - //{ - // var sink = new TestSink(); - // var middleware = TestUtils.CreateTestMiddleware(testSink: sink, policyProvider: new ResponseCachingPolicyProvider()); - // var context = TestUtils.CreateTestContext(); + context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() + { + Public = true + }.ToString(); - // middleware.ShimResponseStream(context); + Assert.False(context.AllowCacheStorage); - // middleware.FinalizeCacheHeaders(context); + middleware.FinalizeCacheHeaders(context); - // Assert.False(context.AllowCacheStorage); - // Assert.Empty(sink.Writes); - //} + Assert.False(context.AllowCacheStorage); + Assert.Empty(sink.Writes); + } [Fact] - public void FinalizeCacheHeadersAsync_DefaultResponseValidity_Is10Seconds() + public void FinalizeCacheHeadersAsync_DefaultResponseValidity_Is60Seconds() { var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink); @@ -394,7 +383,7 @@ public void FinalizeCacheHeadersAsync_DefaultResponseValidity_Is10Seconds() middleware.FinalizeCacheHeaders(context); - Assert.Equal(TimeSpan.FromSeconds(10), context.CachedResponseValidFor); + Assert.Equal(TimeSpan.FromSeconds(60), context.CachedResponseValidFor); Assert.Empty(sink.Writes); } @@ -477,66 +466,6 @@ public void FinalizeCacheHeadersAsync_ResponseValidity_UseSharedMaxAgeIfAvailabl Assert.Empty(sink.Writes); } - //[Fact] - //public void FinalizeCacheHeadersAsync_UpdateCachedVaryByRules_IfNotEquivalentToPrevious() - //{ - // var cache = new TestOutputCache(); - // var sink = new TestSink(); - // var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); - // var context = TestUtils.CreateTestContext(); - - // context.HttpContext.Response.Headers.Vary = new StringValues(new[] { "headerA", "HEADERB", "HEADERc" }); - // context.HttpContext.Features.Set(new OutputCachingFeature() - // { - // VaryByQueryKeys = new StringValues(new[] { "queryB", "QUERYA" }) - // }); - // var cachedVaryByRules = new CachedVaryByRules() - // { - // Headers = new StringValues(new[] { "HeaderA", "HeaderB" }), - // QueryKeys = new StringValues(new[] { "QueryA", "QueryB" }) - // }; - // context.CachedVaryByRules = cachedVaryByRules; - - // middleware.FinalizeCacheHeaders(context); - - // Assert.Equal(1, cache.SetCount); - // Assert.NotSame(cachedVaryByRules, context.CachedVaryByRules); - // TestUtils.AssertLoggedMessages( - // sink.Writes, - // LoggedMessage.VaryByRulesUpdated); - //} - - //[Fact] - //public void FinalizeCacheHeadersAsync_UpdateCachedVaryByRules_IfEquivalentToPrevious() - //{ - // var cache = new TestOutputCache(); - // var sink = new TestSink(); - // var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); - // var context = TestUtils.CreateTestContext(); - - // context.HttpContext.Response.Headers.Vary = new StringValues(new[] { "headerA", "HEADERB" }); - // context.HttpContext.Features.Set(new OutputCachingFeature() - // { - // VaryByQueryKeys = new StringValues(new[] { "queryB", "QUERYA" }) - // }); - // var cachedVaryByRules = new CachedVaryByRules() - // { - // VaryByKeyPrefix = FastGuid.NewGuid().IdString, - // Headers = new StringValues(new[] { "HEADERA", "HEADERB" }), - // QueryKeys = new StringValues(new[] { "QUERYA", "QUERYB" }) - // }; - // context.CachedVaryByRules = cachedVaryByRules; - - // middleware.FinalizeCacheHeaders(context); - - // // An update to the cache is always made but the entry should be the same - // Assert.Equal(1, cache.SetCount); - // Assert.Same(cachedVaryByRules, context.CachedVaryByRules); - // TestUtils.AssertLoggedMessages( - // sink.Writes, - // LoggedMessage.VaryByRulesUpdated); - //} - public static TheoryData NullOrEmptyVaryRules { get @@ -628,7 +557,7 @@ public void FinalizeCacheHeadersAsync_StoresCachedResponse_InState() } [Fact] - public void FinalizeCacheHeadersAsync_SplitsVaryHeaderByCommas() + public void FinalizeCacheHeadersAsync_StoresHeaders() { var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink); @@ -638,10 +567,7 @@ public void FinalizeCacheHeadersAsync_SplitsVaryHeaderByCommas() middleware.FinalizeCacheHeaders(context); - Assert.Equal(new StringValues(new[] { "HEADERA", "HEADERB" }), context.CachedVaryByRules.Headers); - TestUtils.AssertLoggedMessages( - sink.Writes, - LoggedMessage.VaryByRulesUpdated); + Assert.Equal(new StringValues(new[] { "HeaderB, heaDera" }), context.CachedResponse.Headers.Vary); } [Fact] @@ -652,7 +578,6 @@ public async Task FinalizeCacheBody_Cache_IfContentLengthMatches() var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); var context = TestUtils.CreateTestContext(); - context.AllowCacheStorage = true; middleware.ShimResponseStream(context); context.HttpContext.Response.ContentLength = 20; @@ -680,7 +605,6 @@ public async Task FinalizeCacheBody_DoNotCache_IfContentLengthMismatches(string var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); var context = TestUtils.CreateTestContext(); - context.AllowCacheStorage = true; middleware.ShimResponseStream(context); context.HttpContext.Response.ContentLength = 9; context.HttpContext.Request.Method = method; @@ -709,7 +633,6 @@ public async Task FinalizeCacheBody_RequestHead_Cache_IfContentLengthPresent_And var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); var context = TestUtils.CreateTestContext(); - context.AllowCacheStorage = true; middleware.ShimResponseStream(context); context.HttpContext.Response.ContentLength = 10; context.HttpContext.Request.Method = "HEAD"; @@ -740,7 +663,6 @@ public async Task FinalizeCacheBody_Cache_IfContentLengthAbsent() var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); var context = TestUtils.CreateTestContext(); - context.AllowCacheStorage = true; middleware.ShimResponseStream(context); await context.HttpContext.Response.WriteAsync(new string('0', 10)); @@ -761,7 +683,7 @@ public async Task FinalizeCacheBody_Cache_IfContentLengthAbsent() } [Fact] - public async Task FinalizeCacheBody_DoNotCache_IfAllowCacheStorageFalse() + public async Task FinalizeCacheBody_DoNotCache_IfIsResponseCacheableFalse() { var cache = new TestOutputCache(); var sink = new TestSink(); @@ -770,7 +692,8 @@ public async Task FinalizeCacheBody_DoNotCache_IfAllowCacheStorageFalse() middleware.ShimResponseStream(context); await context.HttpContext.Response.WriteAsync(new string('0', 10)); - context.AllowCacheStorage = false; + context.IsResponseCacheable = false; + context.CacheKey = "BaseKey"; await middleware.FinalizeCacheBodyAsync(context); @@ -788,7 +711,6 @@ public async Task FinalizeCacheBody_DoNotCache_IfBufferingDisabled() var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); var context = TestUtils.CreateTestContext(); - context.AllowCacheStorage = true; middleware.ShimResponseStream(context); await context.HttpContext.Response.WriteAsync(new string('0', 10)); @@ -815,12 +737,12 @@ public async Task FinalizeCacheBody_DoNotCache_IfSizeTooBig() }))); var context = TestUtils.CreateTestContext(); - context.AllowCacheStorage = true; middleware.ShimResponseStream(context); await context.HttpContext.Response.WriteAsync(new string('0', 101)); context.CachedResponse = new OutputCacheEntry() { Headers = new HeaderDictionary() }; + context.CacheKey = "BaseKey"; context.CachedResponseValidFor = TimeSpan.FromSeconds(10); await middleware.FinalizeCacheBodyAsync(context); diff --git a/src/Middleware/OutputCaching/test/OutputCachingPolicyProviderTests.cs b/src/Middleware/OutputCaching/test/OutputCachingPolicyProviderTests.cs new file mode 100644 index 000000000000..19672b6a2aa9 --- /dev/null +++ b/src/Middleware/OutputCaching/test/OutputCachingPolicyProviderTests.cs @@ -0,0 +1,476 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.Logging.Testing; +using Microsoft.Extensions.Options; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.OutputCaching.Tests; + +public class OutputCachingPolicyProviderTests +{ + public static TheoryData CacheableMethods + { + get + { + return new TheoryData + { + HttpMethods.Get, + HttpMethods.Head + }; + } + } + + public static TheoryData NonCacheableMethods + { + get + { + return new TheoryData + { + HttpMethods.Post, + HttpMethods.Put, + HttpMethods.Delete, + HttpMethods.Trace, + HttpMethods.Connect, + HttpMethods.Options, + "", + null + }; + } + } + + [Theory] + [MemberData(nameof(CacheableMethods))] + public async Task AttemptOutputCaching_CacheableMethods_Allowed(string method) + { + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); + var options = new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().Build() }; + context.HttpContext.Request.Method = method; + + await new OutputCachingPolicyProvider(Options.Create(options)).OnRequestAsync(context); + + Assert.True(context.AttemptOutputCaching); + Assert.Empty(sink.Writes); + } + + [Theory] + [MemberData(nameof(NonCacheableMethods))] + public async Task AttemptOutputCaching_UncacheableMethods_NotAllowed(string method) + { + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); + var options = new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().Build() }; + context.HttpContext.Request.Method = method; + + await new OutputCachingPolicyProvider(Options.Create(options)).OnRequestAsync(context); + + Assert.False(context.AttemptOutputCaching); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.RequestMethodNotCacheable); + } + + [Fact] + public async Task AttemptResponseCaching_AuthorizationHeaders_NotAllowed() + { + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); + context.HttpContext.Request.Method = HttpMethods.Get; + context.HttpContext.Request.Headers.Authorization = "Placeholder"; + + var options = new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().Build() }; + + await new OutputCachingPolicyProvider(Options.Create(options)).OnRequestAsync(context); + + Assert.False(context.AttemptOutputCaching); + + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.RequestWithAuthorizationNotCacheable); + } + + [Fact] + public async Task AllowCacheStorage_NoStore_Allowed() + { + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); + context.HttpContext.Request.Method = HttpMethods.Get; + context.HttpContext.Request.Headers.CacheControl = new CacheControlHeaderValue() + { + NoStore = true + }.ToString(); + + var options = new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().Build() }; + await new OutputCachingPolicyProvider(Options.Create(options)).OnRequestAsync(context); + + Assert.True(context.AllowCacheStorage); + Assert.Empty(sink.Writes); + } + + [Fact] + public async Task AllowCacheLookup_LegacyDirectives_OverridenByCacheControl() + { + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); + context.HttpContext.Request.Method = HttpMethods.Get; + context.HttpContext.Request.Headers.Pragma = "no-cache"; + context.HttpContext.Request.Headers.CacheControl = "max-age=10"; + + var options = new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().Build() }; + await new OutputCachingPolicyProvider(Options.Create(options)).OnRequestAsync(context); + + Assert.True(context.AllowCacheLookup); + Assert.Empty(sink.Writes); + } + + [Fact] + public async Task IsResponseCacheable_NoPublic_Allowed() + { + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); + + var options = new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().Build() }; + await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); + + Assert.True(context.IsResponseCacheable); + Assert.Empty(sink.Writes); + } + + [Fact] + public async Task IsResponseCacheable_Public_Allowed() + { + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); + context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() + { + Public = true + }.ToString(); + + var options = new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().Build() }; + await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); + + Assert.True(context.IsResponseCacheable); + Assert.Empty(sink.Writes); + } + + [Fact] + public async Task IsResponseCacheable_NoCache_Allowed() + { + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); + context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() + { + Public = true, + NoCache = true + }.ToString(); + + var options = new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().Build() }; + await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); + + Assert.True(context.IsResponseCacheable); + Assert.Empty(sink.Writes); + } + + [Fact] + public async Task IsResponseCacheable_ResponseNoStore_Allowed() + { + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); + context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() + { + Public = true, + NoStore = true + }.ToString(); + + var options = new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().Build() }; + await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); + + Assert.True(context.IsResponseCacheable); + Assert.Empty(sink.Writes); + } + + [Fact] + public async Task IsResponseCacheable_SetCookieHeader_NotAllowed() + { + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); + context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() + { + Public = true + }.ToString(); + context.HttpContext.Response.Headers.SetCookie = "cookieName=cookieValue"; + + var options = new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().Build() }; + await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); + + Assert.False(context.IsResponseCacheable); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.ResponseWithSetCookieNotCacheable); + } + + [Fact] + public async Task IsResponseCacheable_VaryHeaderByStar_Allowed() + { + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); + context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() + { + Public = true + }.ToString(); + context.HttpContext.Response.Headers.Vary = "*"; + + var options = new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().Build() }; + await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); + + Assert.True(context.IsResponseCacheable); + Assert.Empty(sink.Writes); + } + + [Fact] + public async Task IsResponseCacheable_Private_Allowed() + { + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); + context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() + { + Public = true, + Private = true + }.ToString(); + + var options = new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().Build() }; + await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); + + Assert.True(context.IsResponseCacheable); + Assert.Empty(sink.Writes); + } + + [Theory] + [InlineData(StatusCodes.Status200OK)] + public async Task IsResponseCacheable_SuccessStatusCodes_Allowed(int statusCode) + { + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); + context.HttpContext.Response.StatusCode = statusCode; + context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() + { + Public = true + }.ToString(); + + var options = new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().Build() }; + await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); + + Assert.True(context.IsResponseCacheable); + Assert.Empty(sink.Writes); + } + + [Theory] + [InlineData(StatusCodes.Status100Continue)] + [InlineData(StatusCodes.Status101SwitchingProtocols)] + [InlineData(StatusCodes.Status102Processing)] + [InlineData(StatusCodes.Status201Created)] + [InlineData(StatusCodes.Status202Accepted)] + [InlineData(StatusCodes.Status203NonAuthoritative)] + [InlineData(StatusCodes.Status204NoContent)] + [InlineData(StatusCodes.Status205ResetContent)] + [InlineData(StatusCodes.Status206PartialContent)] + [InlineData(StatusCodes.Status207MultiStatus)] + [InlineData(StatusCodes.Status208AlreadyReported)] + [InlineData(StatusCodes.Status226IMUsed)] + [InlineData(StatusCodes.Status300MultipleChoices)] + [InlineData(StatusCodes.Status301MovedPermanently)] + [InlineData(StatusCodes.Status302Found)] + [InlineData(StatusCodes.Status303SeeOther)] + [InlineData(StatusCodes.Status304NotModified)] + [InlineData(StatusCodes.Status305UseProxy)] + [InlineData(StatusCodes.Status306SwitchProxy)] + [InlineData(StatusCodes.Status307TemporaryRedirect)] + [InlineData(StatusCodes.Status308PermanentRedirect)] + [InlineData(StatusCodes.Status400BadRequest)] + [InlineData(StatusCodes.Status401Unauthorized)] + [InlineData(StatusCodes.Status402PaymentRequired)] + [InlineData(StatusCodes.Status403Forbidden)] + [InlineData(StatusCodes.Status404NotFound)] + [InlineData(StatusCodes.Status405MethodNotAllowed)] + [InlineData(StatusCodes.Status406NotAcceptable)] + [InlineData(StatusCodes.Status407ProxyAuthenticationRequired)] + [InlineData(StatusCodes.Status408RequestTimeout)] + [InlineData(StatusCodes.Status409Conflict)] + [InlineData(StatusCodes.Status410Gone)] + [InlineData(StatusCodes.Status411LengthRequired)] + [InlineData(StatusCodes.Status412PreconditionFailed)] + [InlineData(StatusCodes.Status413RequestEntityTooLarge)] + [InlineData(StatusCodes.Status414RequestUriTooLong)] + [InlineData(StatusCodes.Status415UnsupportedMediaType)] + [InlineData(StatusCodes.Status416RequestedRangeNotSatisfiable)] + [InlineData(StatusCodes.Status417ExpectationFailed)] + [InlineData(StatusCodes.Status418ImATeapot)] + [InlineData(StatusCodes.Status419AuthenticationTimeout)] + [InlineData(StatusCodes.Status421MisdirectedRequest)] + [InlineData(StatusCodes.Status422UnprocessableEntity)] + [InlineData(StatusCodes.Status423Locked)] + [InlineData(StatusCodes.Status424FailedDependency)] + [InlineData(StatusCodes.Status426UpgradeRequired)] + [InlineData(StatusCodes.Status428PreconditionRequired)] + [InlineData(StatusCodes.Status429TooManyRequests)] + [InlineData(StatusCodes.Status431RequestHeaderFieldsTooLarge)] + [InlineData(StatusCodes.Status451UnavailableForLegalReasons)] + [InlineData(StatusCodes.Status500InternalServerError)] + [InlineData(StatusCodes.Status501NotImplemented)] + [InlineData(StatusCodes.Status502BadGateway)] + [InlineData(StatusCodes.Status503ServiceUnavailable)] + [InlineData(StatusCodes.Status504GatewayTimeout)] + [InlineData(StatusCodes.Status505HttpVersionNotsupported)] + [InlineData(StatusCodes.Status506VariantAlsoNegotiates)] + [InlineData(StatusCodes.Status507InsufficientStorage)] + [InlineData(StatusCodes.Status508LoopDetected)] + [InlineData(StatusCodes.Status510NotExtended)] + [InlineData(StatusCodes.Status511NetworkAuthenticationRequired)] + public async Task IsResponseCacheable_NonSuccessStatusCodes_NotAllowed(int statusCode) + { + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); + context.HttpContext.Response.StatusCode = statusCode; + context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() + { + Public = true + }.ToString(); + + var options = new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().Build() }; + await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); + + Assert.False(context.IsResponseCacheable); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.ResponseWithUnsuccessfulStatusCodeNotCacheable); + } + + [Fact] + public async Task IsResponseCacheable_NoExpiryRequirements_IsAllowed() + { + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); + context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; + context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() + { + Public = true + }.ToString(); + + var utcNow = DateTimeOffset.UtcNow; + context.HttpContext.Response.Headers.Date = HeaderUtilities.FormatDate(utcNow); + context.ResponseTime = DateTimeOffset.MaxValue; + + var options = new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().Build() }; + await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); + + Assert.True(context.IsResponseCacheable); + Assert.Empty(sink.Writes); + } + + [Fact] + public async Task IsResponseCacheable_MaxAgeOverridesExpiry_ToAllowed() + { + var utcNow = DateTimeOffset.UtcNow; + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); + context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; + context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() + { + Public = true, + MaxAge = TimeSpan.FromSeconds(10) + }.ToString(); + context.HttpContext.Response.Headers.Expires = HeaderUtilities.FormatDate(utcNow); + context.HttpContext.Response.Headers.Date = HeaderUtilities.FormatDate(utcNow); + context.ResponseTime = utcNow + TimeSpan.FromSeconds(9); + + var options = new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().Build() }; + await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); + + Assert.True(context.IsResponseCacheable); + Assert.Empty(sink.Writes); + } + + [Fact] + public async Task IsResponseCacheable_SharedMaxAgeOverridesMaxAge_ToAllowed() + { + var utcNow = DateTimeOffset.UtcNow; + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); + context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; + context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() + { + Public = true, + MaxAge = TimeSpan.FromSeconds(10), + SharedMaxAge = TimeSpan.FromSeconds(15) + }.ToString(); + context.HttpContext.Response.Headers.Date = HeaderUtilities.FormatDate(utcNow); + context.ResponseTime = utcNow + TimeSpan.FromSeconds(11); + + var options = new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().Build() }; + await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); + + Assert.True(context.IsResponseCacheable); + Assert.Empty(sink.Writes); + } + + [Fact] + public async Task IsCachedEntryFresh_NoExpiryRequirements_IsFresh() + { + var utcNow = DateTimeOffset.UtcNow; + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); + context.ResponseTime = DateTimeOffset.MaxValue; + context.CachedEntryAge = TimeSpan.MaxValue; + + var options = new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().Build() }; + await new OutputCachingPolicyProvider(Options.Create(options)).OnServeFromCacheAsync(context); + + Assert.True(context.IsCacheEntryFresh); + Assert.Empty(sink.Writes); + } + + [Fact] + public async Task IsCachedEntryFresh_AtExpiry_IsNotFresh() + { + var utcNow = DateTimeOffset.UtcNow; + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); + context.ResponseTime = utcNow; + context.CachedEntryAge = TimeSpan.Zero; + + var options = new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().Build() }; + await new OutputCachingPolicyProvider(Options.Create(options)).OnServeFromCacheAsync(context); + + Assert.False(context.IsCacheEntryFresh); + TestUtils.AssertLoggedMessages( + sink.Writes, + LoggedMessage.ExpirationExpiresExceededNoExpiration); + } + + [Fact] + public async Task IsCachedEntryFresh_SharedMaxAgeOverridesMaxAge_ToFresh() + { + var utcNow = DateTimeOffset.UtcNow; + var sink = new TestSink(); + var context = TestUtils.CreateTestContext(sink); + context.CachedEntryAge = TimeSpan.FromSeconds(11); + context.ResponseTime = utcNow + context.CachedEntryAge; + context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() + { + Public = true, + MaxAge = TimeSpan.FromSeconds(10), + SharedMaxAge = TimeSpan.FromSeconds(15) + }.ToString(); + context.CachedResponseHeaders[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow); + + var options = new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().Build() }; + await new OutputCachingPolicyProvider(Options.Create(options)).OnServeFromCacheAsync(context); + + Assert.True(context.IsCacheEntryFresh); + Assert.Empty(sink.Writes); + } +} diff --git a/src/Middleware/OutputCaching/test/OutputCachingTests.cs b/src/Middleware/OutputCaching/test/OutputCachingTests.cs new file mode 100644 index 000000000000..172dfd1d2e92 --- /dev/null +++ b/src/Middleware/OutputCaching/test/OutputCachingTests.cs @@ -0,0 +1,978 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Http; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.OutputCaching.Tests; + +public class OutputCachingTests +{ + [Theory] + [InlineData("GET")] + [InlineData("HEAD")] + public async Task ServesCachedContent_IfAvailable(string method) + { + var builders = TestUtils.CreateBuildersWithOutputCaching(); + + foreach (var builder in builders) + { + using var host = builder.Build(); + + await host.StartAsync(); + + using (var server = host.GetTestServer()) + { + var client = server.CreateClient(); + var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); + var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); + + await AssertCachedResponseAsync(initialResponse, subsequentResponse); + } + } + } + + [Theory] + [InlineData("GET")] + [InlineData("HEAD")] + public async Task ServesFreshContent_IfNotAvailable(string method) + { + var builders = TestUtils.CreateBuildersWithOutputCaching(); + + foreach (var builder in builders) + { + using var host = builder.Build(); + + await host.StartAsync(); + + using (var server = host.GetTestServer()) + { + var client = server.CreateClient(); + var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); + var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, "different")); + + await AssertFreshResponseAsync(initialResponse, subsequentResponse); + } + } + } + + [Fact] + public async Task ServesFreshContent_Post() + { + var builders = TestUtils.CreateBuildersWithOutputCaching(); + + foreach (var builder in builders) + { + using var host = builder.Build(); + + await host.StartAsync(); + + using (var server = host.GetTestServer()) + { + var client = server.CreateClient(); + var initialResponse = await client.PostAsync("", new StringContent(string.Empty)); + var subsequentResponse = await client.PostAsync("", new StringContent(string.Empty)); + + await AssertFreshResponseAsync(initialResponse, subsequentResponse); + } + } + } + + [Fact] + public async Task ServesFreshContent_Head_Get() + { + var builders = TestUtils.CreateBuildersWithOutputCaching(); + + foreach (var builder in builders) + { + using var host = builder.Build(); + + await host.StartAsync(); + + using (var server = host.GetTestServer()) + { + var client = server.CreateClient(); + var subsequentResponse = await client.SendAsync(new HttpRequestMessage(HttpMethod.Head, "")); + var initialResponse = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "")); + + await AssertFreshResponseAsync(initialResponse, subsequentResponse); + } + } + } + + [Fact] + public async Task ServesFreshContent_Get_Head() + { + var builders = TestUtils.CreateBuildersWithOutputCaching(); + + foreach (var builder in builders) + { + using var host = builder.Build(); + + await host.StartAsync(); + + using (var server = host.GetTestServer()) + { + var client = server.CreateClient(); + var initialResponse = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "")); + var subsequentResponse = await client.SendAsync(new HttpRequestMessage(HttpMethod.Head, "")); + + await AssertFreshResponseAsync(initialResponse, subsequentResponse); + } + } + } + + [Theory] + [InlineData("GET")] + [InlineData("HEAD")] + public async Task ServesCachedContent_If_CacheControlNoCache(string method) + { + var builders = TestUtils.CreateBuildersWithOutputCaching(); + + foreach (var builder in builders) + { + using var host = builder.Build(); + + await host.StartAsync(); + + using (var server = host.GetTestServer()) + { + var client = server.CreateClient(); + + var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); + + // verify the response is cached + var cachedResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); + await AssertCachedResponseAsync(initialResponse, cachedResponse); + + // assert cached response still served + client.DefaultRequestHeaders.CacheControl = + new System.Net.Http.Headers.CacheControlHeaderValue { NoCache = true }; + var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); + + await AssertCachedResponseAsync(initialResponse, subsequentResponse); + } + } + } + + [Theory] + [InlineData("GET")] + [InlineData("HEAD")] + public async Task ServesCachedContent_If_PragmaNoCache(string method) + { + var builders = TestUtils.CreateBuildersWithOutputCaching(); + + foreach (var builder in builders) + { + using var host = builder.Build(); + + await host.StartAsync(); + + using (var server = host.GetTestServer()) + { + var client = server.CreateClient(); + + var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); + + // verify the response is cached + var cachedResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); + await AssertCachedResponseAsync(initialResponse, cachedResponse); + + // assert cached response still served + client.DefaultRequestHeaders.Pragma.Clear(); + client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("no-cache")); + var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); + + await AssertCachedResponseAsync(initialResponse, subsequentResponse); + } + } + } + + [Theory] + [InlineData("GET")] + [InlineData("HEAD")] + public async Task ServesCachedContent_If_PathCasingDiffers(string method) + { + var builders = TestUtils.CreateBuildersWithOutputCaching(); + + foreach (var builder in builders) + { + using var host = builder.Build(); + + await host.StartAsync(); + + using (var server = host.GetTestServer()) + { + var client = server.CreateClient(); + var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, "path")); + var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, "PATH")); + + await AssertCachedResponseAsync(initialResponse, subsequentResponse); + } + } + } + + [Theory] + [InlineData("GET")] + [InlineData("HEAD")] + public async Task ServesFreshContent_If_ResponseExpired(string method) + { + var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions + { + DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().VaryByHeader(HeaderNames.From).Build(), + DefaultExpirationTimeSpan = TimeSpan.FromMicroseconds(100) + }); + + foreach (var builder in builders) + { + using var host = builder.Build(); + + await host.StartAsync(); + + using (var server = host.GetTestServer()) + { + var client = server.CreateClient(); + var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); + await Task.Delay(1); + var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); + + await AssertFreshResponseAsync(initialResponse, subsequentResponse); + } + } + } + + [Theory] + [InlineData("GET")] + [InlineData("HEAD")] + public async Task ServesFreshContent_If_Authorization_HeaderExists(string method) + { + var builders = TestUtils.CreateBuildersWithOutputCaching(); + + foreach (var builder in builders) + { + using var host = builder.Build(); + + await host.StartAsync(); + + using (var server = host.GetTestServer()) + { + var client = server.CreateClient(); + client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("abc"); + var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); + var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); + + await AssertFreshResponseAsync(initialResponse, subsequentResponse); + } + } + } + + [Fact] + public async Task ServesCachedContent_IfVaryHeader_Matches() + { + var builders = TestUtils.CreateBuildersWithOutputCaching(contextAction: context => context.Response.Headers.Vary = HeaderNames.From); + + foreach (var builder in builders) + { + using var host = builder.Build(); + + await host.StartAsync(); + + using (var server = host.GetTestServer()) + { + var client = server.CreateClient(); + client.DefaultRequestHeaders.From = "user@example.com"; + var initialResponse = await client.GetAsync(""); + var subsequentResponse = await client.GetAsync(""); + + await AssertCachedResponseAsync(initialResponse, subsequentResponse); + } + } + } + + [Fact] + public async Task ServesFreshContent_IfVaryHeader_Mismatches() + { + var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().VaryByHeader(HeaderNames.From).Build() }); + + foreach (var builder in builders) + { + using var host = builder.Build(); + + await host.StartAsync(); + + using (var server = host.GetTestServer()) + { + var client = server.CreateClient(); + client.DefaultRequestHeaders.From = "user@example.com"; + var initialResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.From = "user2@example.com"; + var subsequentResponse = await client.GetAsync(""); + + await AssertFreshResponseAsync(initialResponse, subsequentResponse); + } + } + } + + [Fact] + public async Task ServesCachedContent_IfVaryQueryKeys_Matches() + { + var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().VaryByQuery("query").Build() }); + + foreach (var builder in builders) + { + using var host = builder.Build(); + + await host.StartAsync(); + + using (var server = host.GetTestServer()) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync("?query=value"); + var subsequentResponse = await client.GetAsync("?query=value"); + + await AssertCachedResponseAsync(initialResponse, subsequentResponse); + } + } + } + + [Fact] + public async Task ServesCachedContent_IfVaryQueryKeysExplicit_Matches_QueryKeyCaseInsensitive() + { + var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().VaryByQuery("QueryA", "queryb").Build() }); + + foreach (var builder in builders) + { + using var host = builder.Build(); + + await host.StartAsync(); + + using (var server = host.GetTestServer()) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync("?querya=valuea&queryb=valueb"); + var subsequentResponse = await client.GetAsync("?QueryA=valuea&QueryB=valueb"); + + await AssertCachedResponseAsync(initialResponse, subsequentResponse); + } + } + } + + [Fact] + public async Task ServesCachedContent_IfVaryQueryKeyStar_Matches_QueryKeyCaseInsensitive() + { + var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().VaryByQuery("*").Build() }); + + foreach (var builder in builders) + { + using var host = builder.Build(); + + await host.StartAsync(); + + using (var server = host.GetTestServer()) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync("?querya=valuea&queryb=valueb"); + var subsequentResponse = await client.GetAsync("?QueryA=valuea&QueryB=valueb"); + + await AssertCachedResponseAsync(initialResponse, subsequentResponse); + } + } + } + + [Fact] + public async Task ServesCachedContent_IfVaryQueryKeyExplicit_Matches_OrderInsensitive() + { + var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().VaryByQuery("QueryB", "QueryA").Build() }); + + foreach (var builder in builders) + { + using var host = builder.Build(); + + await host.StartAsync(); + + using (var server = host.GetTestServer()) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync("?QueryA=ValueA&QueryB=ValueB"); + var subsequentResponse = await client.GetAsync("?QueryB=ValueB&QueryA=ValueA"); + + await AssertCachedResponseAsync(initialResponse, subsequentResponse); + } + } + } + + [Fact] + public async Task ServesCachedContent_IfVaryQueryKeyStar_Matches_OrderInsensitive() + { + var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().VaryByQuery("*").Build() }); + + foreach (var builder in builders) + { + using var host = builder.Build(); + + await host.StartAsync(); + + using (var server = host.GetTestServer()) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync("?QueryA=ValueA&QueryB=ValueB"); + var subsequentResponse = await client.GetAsync("?QueryB=ValueB&QueryA=ValueA"); + + await AssertCachedResponseAsync(initialResponse, subsequentResponse); + } + } + } + + [Fact] + public async Task ServesFreshContent_IfVaryQueryKey_Mismatches() + { + var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().VaryByQuery("query").Build() }); + + foreach (var builder in builders) + { + using var host = builder.Build(); + + await host.StartAsync(); + + using (var server = host.GetTestServer()) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync("?query=value"); + var subsequentResponse = await client.GetAsync("?query=value2"); + + await AssertFreshResponseAsync(initialResponse, subsequentResponse); + } + } + } + + [Fact] + public async Task ServesCachedContent_IfOtherVaryQueryKey_Mismatches() + { + var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().VaryByQuery("query").Build() }); + + foreach (var builder in builders) + { + using var host = builder.Build(); + + await host.StartAsync(); + + using (var server = host.GetTestServer()) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync("?other=value1"); + var subsequentResponse = await client.GetAsync("?other=value2"); + + await AssertCachedResponseAsync(initialResponse, subsequentResponse); + } + } + } + + [Fact] + public async Task ServesFreshContent_IfVaryQueryKeyExplicit_Mismatch_QueryKeyCaseSensitive() + { + var builders = TestUtils.CreateBuildersWithOutputCaching(contextAction: context => context.Features.Get().Policies.Add(new VaryByQueryPolicy("QueryA", "QueryB"))); + + foreach (var builder in builders) + { + using var host = builder.Build(); + + await host.StartAsync(); + + using (var server = host.GetTestServer()) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync("?querya=valuea&queryb=valueb"); + var subsequentResponse = await client.GetAsync("?querya=ValueA&queryb=ValueB"); + + await AssertFreshResponseAsync(initialResponse, subsequentResponse); + } + } + } + + [Fact] + public async Task ServesFreshContent_IfVaryQueryKeyStar_Mismatch_QueryKeyValueCaseSensitive() + { + var builders = TestUtils.CreateBuildersWithOutputCaching(contextAction: context => context.Features.Get().Policies.Add(new VaryByQueryPolicy("*"))); + + foreach (var builder in builders) + { + using var host = builder.Build(); + + await host.StartAsync(); + + using (var server = host.GetTestServer()) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync("?querya=valuea&queryb=valueb"); + var subsequentResponse = await client.GetAsync("?querya=ValueA&queryb=ValueB"); + + await AssertFreshResponseAsync(initialResponse, subsequentResponse); + } + } + } + + [Fact] + public async Task ServesCachedContent_IfRequestRequirements_NotMet() + { + var builders = TestUtils.CreateBuildersWithOutputCaching(); + + foreach (var builder in builders) + { + using var host = builder.Build(); + + await host.StartAsync(); + + using (var server = host.GetTestServer()) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue() + { + MaxAge = TimeSpan.FromSeconds(0) + }; + var subsequentResponse = await client.GetAsync(""); + + await AssertCachedResponseAsync(initialResponse, subsequentResponse); + } + } + } + + [Fact] + public async Task Serves504_IfOnlyIfCachedHeader_IsSpecified() + { + var builders = TestUtils.CreateBuildersWithOutputCaching(); + + foreach (var builder in builders) + { + using var host = builder.Build(); + + await host.StartAsync(); + + using (var server = host.GetTestServer()) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue() + { + OnlyIfCached = true + }; + var subsequentResponse = await client.GetAsync("/different"); + + initialResponse.EnsureSuccessStatusCode(); + Assert.Equal(System.Net.HttpStatusCode.GatewayTimeout, subsequentResponse.StatusCode); + } + } + } + + [Fact] + public async Task ServesFreshContent_IfSetCookie_IsSpecified() + { + var builders = TestUtils.CreateBuildersWithOutputCaching(contextAction: context => context.Response.Headers.SetCookie = "cookieName=cookieValue"); + + foreach (var builder in builders) + { + using var host = builder.Build(); + + await host.StartAsync(); + + using (var server = host.GetTestServer()) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + var subsequentResponse = await client.GetAsync(""); + + await AssertFreshResponseAsync(initialResponse, subsequentResponse); + } + } + } + + [Fact] + public async Task ServesCachedContent_IfSubsequentRequestContainsNoStore() + { + var builders = TestUtils.CreateBuildersWithOutputCaching(); + + foreach (var builder in builders) + { + using var host = builder.Build(); + + await host.StartAsync(); + + using (var server = host.GetTestServer()) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue() + { + NoStore = true + }; + var subsequentResponse = await client.GetAsync(""); + + await AssertCachedResponseAsync(initialResponse, subsequentResponse); + } + } + } + + [Fact] + public async Task ServesCachedContent_IfInitialRequestContainsNoStore() + { + var builders = TestUtils.CreateBuildersWithOutputCaching(); + + foreach (var builder in builders) + { + using var host = builder.Build(); + + await host.StartAsync(); + + using (var server = host.GetTestServer()) + { + var client = server.CreateClient(); + client.DefaultRequestHeaders.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue() + { + NoStore = true + }; + var initialResponse = await client.GetAsync(""); + var subsequentResponse = await client.GetAsync(""); + + await AssertCachedResponseAsync(initialResponse, subsequentResponse); + } + } + } + + [Fact] + public async Task ServesCachedContent_IfInitialResponseContainsNoStore() + { + var builders = TestUtils.CreateBuildersWithOutputCaching(contextAction: context => context.Response.Headers.CacheControl = CacheControlHeaderValue.NoStoreString); + + foreach (var builder in builders) + { + using var host = builder.Build(); + + await host.StartAsync(); + + using (var server = host.GetTestServer()) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + var subsequentResponse = await client.GetAsync(""); + + await AssertCachedResponseAsync(initialResponse, subsequentResponse); + } + } + } + + [Fact] + public async Task Serves304_IfIfModifiedSince_Satisfied() + { + var builders = TestUtils.CreateBuildersWithOutputCaching(contextAction: context => + { + // Ensure these headers are also returned on the subsequent response + context.Response.GetTypedHeaders().ETag = new EntityTagHeaderValue("\"E1\""); + context.Response.Headers.ContentLocation = "/"; + context.Response.Headers.Vary = HeaderNames.From; + }); + + foreach (var builder in builders) + { + using var host = builder.Build(); + + await host.StartAsync(); + + using (var server = host.GetTestServer()) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.IfModifiedSince = DateTimeOffset.MaxValue; + var subsequentResponse = await client.GetAsync(""); + + initialResponse.EnsureSuccessStatusCode(); + Assert.Equal(System.Net.HttpStatusCode.NotModified, subsequentResponse.StatusCode); + Assert304Headers(initialResponse, subsequentResponse); + } + } + } + + [Fact] + public async Task ServesCachedContent_IfIfModifiedSince_NotSatisfied() + { + var builders = TestUtils.CreateBuildersWithOutputCaching(); + + foreach (var builder in builders) + { + using var host = builder.Build(); + + await host.StartAsync(); + + using (var server = host.GetTestServer()) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.IfModifiedSince = DateTimeOffset.MinValue; + var subsequentResponse = await client.GetAsync(""); + + await AssertCachedResponseAsync(initialResponse, subsequentResponse); + } + } + } + + [Fact] + public async Task Serves304_IfIfNoneMatch_Satisfied() + { + var builders = TestUtils.CreateBuildersWithOutputCaching(contextAction: context => + { + context.Response.GetTypedHeaders().ETag = new EntityTagHeaderValue("\"E1\""); + context.Response.Headers.ContentLocation = "/"; + context.Response.Headers.Vary = HeaderNames.From; + }); + + foreach (var builder in builders) + { + using var host = builder.Build(); + + await host.StartAsync(); + + using (var server = host.GetTestServer()) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.IfNoneMatch.Add(new System.Net.Http.Headers.EntityTagHeaderValue("\"E1\"")); + var subsequentResponse = await client.GetAsync(""); + + initialResponse.EnsureSuccessStatusCode(); + Assert.Equal(System.Net.HttpStatusCode.NotModified, subsequentResponse.StatusCode); + Assert304Headers(initialResponse, subsequentResponse); + } + } + } + + [Fact] + public async Task ServesCachedContent_IfIfNoneMatch_NotSatisfied() + { + var builders = TestUtils.CreateBuildersWithOutputCaching(contextAction: context => context.Response.GetTypedHeaders().ETag = new EntityTagHeaderValue("\"E1\"")); + + foreach (var builder in builders) + { + using var host = builder.Build(); + + await host.StartAsync(); + + using (var server = host.GetTestServer()) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.IfNoneMatch.Add(new System.Net.Http.Headers.EntityTagHeaderValue("\"E2\"")); + var subsequentResponse = await client.GetAsync(""); + + await AssertCachedResponseAsync(initialResponse, subsequentResponse); + } + } + } + + [Fact] + public async Task ServesCachedContent_IfBodySize_IsCacheable() + { + var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions() + { + DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().Build(), + MaximumBodySize = 1000 + }); + + foreach (var builder in builders) + { + using var host = builder.Build(); + + await host.StartAsync(); + + using (var server = host.GetTestServer()) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + var subsequentResponse = await client.GetAsync(""); + + await AssertCachedResponseAsync(initialResponse, subsequentResponse); + } + } + } + + [Fact] + public async Task ServesFreshContent_IfBodySize_IsNotCacheable() + { + var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions() + { + MaximumBodySize = 1 + }); + + foreach (var builder in builders) + { + using var host = builder.Build(); + + await host.StartAsync(); + + using (var server = host.GetTestServer()) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + var subsequentResponse = await client.GetAsync("/different"); + + await AssertFreshResponseAsync(initialResponse, subsequentResponse); + } + } + } + + [Fact] + public async Task ServesFreshContent_CaseSensitivePaths_IsNotCacheable() + { + var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions() + { + UseCaseSensitivePaths = true + }); + + foreach (var builder in builders) + { + using var host = builder.Build(); + + await host.StartAsync(); + + using (var server = host.GetTestServer()) + { + var client = server.CreateClient(); + var initialResponse = await client.GetAsync("/path"); + var subsequentResponse = await client.GetAsync("/Path"); + + await AssertFreshResponseAsync(initialResponse, subsequentResponse); + } + } + } + + [Fact] + public async Task ServesCachedContent_WithoutReplacingCachedVaryBy_OnCacheMiss() + { + var builders = TestUtils.CreateBuildersWithOutputCaching(contextAction: context => context.Response.Headers.Vary = HeaderNames.From); + + foreach (var builder in builders) + { + using var host = builder.Build(); + + await host.StartAsync(); + + using (var server = host.GetTestServer()) + { + var client = server.CreateClient(); + client.DefaultRequestHeaders.From = "user@example.com"; + var initialResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.From = "user2@example.com"; + var otherResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.From = "user@example.com"; + var subsequentResponse = await client.GetAsync(""); + + await AssertCachedResponseAsync(initialResponse, subsequentResponse); + } + } + } + + [Fact] + public async Task ServesCachedContent_IfCachedVaryByNotUpdated_OnCacheMiss() + { + var builders = TestUtils.CreateBuildersWithOutputCaching(contextAction: context => context.Response.Headers.Vary = context.Request.Headers.Pragma); + + foreach (var builder in builders) + { + using var host = builder.Build(); + + await host.StartAsync(); + + using (var server = host.GetTestServer()) + { + var client = server.CreateClient(); + client.DefaultRequestHeaders.From = "user@example.com"; + client.DefaultRequestHeaders.Pragma.Clear(); + client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("From")); + client.DefaultRequestHeaders.MaxForwards = 1; + var initialResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.From = "user2@example.com"; + client.DefaultRequestHeaders.Pragma.Clear(); + client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("From")); + client.DefaultRequestHeaders.MaxForwards = 2; + var otherResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.From = "user@example.com"; + client.DefaultRequestHeaders.Pragma.Clear(); + client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("From")); + client.DefaultRequestHeaders.MaxForwards = 1; + var subsequentResponse = await client.GetAsync(""); + + await AssertCachedResponseAsync(initialResponse, subsequentResponse); + } + } + } + + [Fact] + public async Task ServesCachedContent_IfAvailable_UsingHead_WithContentLength() + { + var builders = TestUtils.CreateBuildersWithOutputCaching(); + + foreach (var builder in builders) + { + using var host = builder.Build(); + + await host.StartAsync(); + + using (var server = host.GetTestServer()) + { + var client = server.CreateClient(); + var initialResponse = await client.SendAsync(TestUtils.CreateRequest("HEAD", "?contentLength=10")); + var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest("HEAD", "?contentLength=10")); + + await AssertCachedResponseAsync(initialResponse, subsequentResponse); + } + } + } + + private static void Assert304Headers(HttpResponseMessage initialResponse, HttpResponseMessage subsequentResponse) + { + // https://tools.ietf.org/html/rfc7232#section-4.1 + // The server generating a 304 response MUST generate any of the + // following header fields that would have been sent in a 200 (OK) + // response to the same request: Cache-Control, Content-Location, Date, + // ETag, Expires, and Vary. + + Assert.Equal(initialResponse.Headers.CacheControl, subsequentResponse.Headers.CacheControl); + Assert.Equal(initialResponse.Content.Headers.ContentLocation, subsequentResponse.Content.Headers.ContentLocation); + Assert.Equal(initialResponse.Headers.Date, subsequentResponse.Headers.Date); + Assert.Equal(initialResponse.Headers.ETag, subsequentResponse.Headers.ETag); + Assert.Equal(initialResponse.Content.Headers.Expires, subsequentResponse.Content.Headers.Expires); + Assert.Equal(initialResponse.Headers.Vary, subsequentResponse.Headers.Vary); + } + + private static async Task AssertCachedResponseAsync(HttpResponseMessage initialResponse, HttpResponseMessage subsequentResponse) + { + initialResponse.EnsureSuccessStatusCode(); + subsequentResponse.EnsureSuccessStatusCode(); + + foreach (var header in initialResponse.Headers) + { + Assert.Equal(initialResponse.Headers.GetValues(header.Key), subsequentResponse.Headers.GetValues(header.Key)); + } + Assert.True(subsequentResponse.Headers.Contains(HeaderNames.Age)); + Assert.Equal(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync()); + } + + private static async Task AssertFreshResponseAsync(HttpResponseMessage initialResponse, HttpResponseMessage subsequentResponse) + { + initialResponse.EnsureSuccessStatusCode(); + subsequentResponse.EnsureSuccessStatusCode(); + + Assert.False(subsequentResponse.Headers.Contains(HeaderNames.Age)); + + if (initialResponse.RequestMessage.Method == HttpMethod.Head && + subsequentResponse.RequestMessage.Method == HttpMethod.Head) + { + Assert.True(initialResponse.Headers.Contains("X-Value")); + Assert.NotEqual(initialResponse.Headers.GetValues("X-Value"), subsequentResponse.Headers.GetValues("X-Value")); + } + else + { + Assert.NotEqual(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync()); + } + } +} diff --git a/src/Middleware/OutputCaching/test/ResponseCachingPolicyProviderTests.cs b/src/Middleware/OutputCaching/test/ResponseCachingPolicyProviderTests.cs deleted file mode 100644 index f75b3b482154..000000000000 --- a/src/Middleware/OutputCaching/test/ResponseCachingPolicyProviderTests.cs +++ /dev/null @@ -1,790 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -//using Microsoft.AspNetCore.Http; -//using Microsoft.Extensions.Logging.Testing; -//using Microsoft.Net.Http.Headers; - -//namespace Microsoft.AspNetCore.ResponseCaching.Tests; - -//public class ResponseCachingPolicyProviderTests -//{ -// public static TheoryData CacheableMethods -// { -// get -// { -// return new TheoryData -// { -// HttpMethods.Get, -// HttpMethods.Head -// }; -// } -// } - -// [Theory] -// [MemberData(nameof(CacheableMethods))] -// public void AttemptResponseCaching_CacheableMethods_Allowed(string method) -// { -// var sink = new TestSink(); -// var context = TestUtils.CreateTestContext(sink); -// context.HttpContext.Request.Method = method; - -// Assert.True(new ResponseCachingPolicyProvider().AttemptResponseCaching(context)); -// Assert.Empty(sink.Writes); -// } -// public static TheoryData NonCacheableMethods -// { -// get -// { -// return new TheoryData -// { -// HttpMethods.Post, -// HttpMethods.Put, -// HttpMethods.Delete, -// HttpMethods.Trace, -// HttpMethods.Connect, -// HttpMethods.Options, -// "", -// null -// }; -// } -// } - -// [Theory] -// [MemberData(nameof(NonCacheableMethods))] -// public void AttemptResponseCaching_UncacheableMethods_NotAllowed(string method) -// { -// var sink = new TestSink(); -// var context = TestUtils.CreateTestContext(sink); -// context.HttpContext.Request.Method = method; - -// Assert.False(new ResponseCachingPolicyProvider().AttemptResponseCaching(context)); -// TestUtils.AssertLoggedMessages( -// sink.Writes, -// LoggedMessage.RequestMethodNotCacheable); -// } - -// [Fact] -// public void AttemptResponseCaching_AuthorizationHeaders_NotAllowed() -// { -// var sink = new TestSink(); -// var context = TestUtils.CreateTestContext(sink); -// context.HttpContext.Request.Method = HttpMethods.Get; -// context.HttpContext.Request.Headers.Authorization = "Placeholder"; - -// Assert.False(new ResponseCachingPolicyProvider().AttemptResponseCaching(context)); -// TestUtils.AssertLoggedMessages( -// sink.Writes, -// LoggedMessage.RequestWithAuthorizationNotCacheable); -// } - -// [Fact] -// public void AllowCacheStorage_NoStore_Allowed() -// { -// var sink = new TestSink(); -// var context = TestUtils.CreateTestContext(sink); -// context.HttpContext.Request.Method = HttpMethods.Get; -// context.HttpContext.Request.Headers.CacheControl = new CacheControlHeaderValue() -// { -// NoStore = true -// }.ToString(); - -// Assert.True(new ResponseCachingPolicyProvider().AllowCacheLookup(context)); -// Assert.Empty(sink.Writes); -// } - -// [Fact] -// public void AllowCacheLookup_NoCache_NotAllowed() -// { -// var sink = new TestSink(); -// var context = TestUtils.CreateTestContext(sink); -// context.HttpContext.Request.Method = HttpMethods.Get; -// context.HttpContext.Request.Headers.CacheControl = new CacheControlHeaderValue() -// { -// NoCache = true -// }.ToString(); - -// Assert.False(new ResponseCachingPolicyProvider().AllowCacheLookup(context)); -// TestUtils.AssertLoggedMessages( -// sink.Writes, -// LoggedMessage.RequestWithNoCacheNotCacheable); -// } - -// [Fact] -// public void AllowCacheLookup_LegacyDirectives_NotAllowed() -// { -// var sink = new TestSink(); -// var context = TestUtils.CreateTestContext(sink); -// context.HttpContext.Request.Method = HttpMethods.Get; -// context.HttpContext.Request.Headers.Pragma = "no-cache"; - -// Assert.False(new ResponseCachingPolicyProvider().AllowCacheLookup(context)); -// TestUtils.AssertLoggedMessages( -// sink.Writes, -// LoggedMessage.RequestWithPragmaNoCacheNotCacheable); -// } - -// [Fact] -// public void AllowCacheLookup_LegacyDirectives_OverridenByCacheControl() -// { -// var sink = new TestSink(); -// var context = TestUtils.CreateTestContext(sink); -// context.HttpContext.Request.Method = HttpMethods.Get; -// context.HttpContext.Request.Headers.Pragma = "no-cache"; -// context.HttpContext.Request.Headers.CacheControl = "max-age=10"; - -// Assert.True(new ResponseCachingPolicyProvider().AllowCacheLookup(context)); -// Assert.Empty(sink.Writes); -// } - -// [Fact] -// public void AllowCacheStorage_NoStore_NotAllowed() -// { -// var sink = new TestSink(); -// var context = TestUtils.CreateTestContext(sink); -// context.HttpContext.Request.Method = HttpMethods.Get; -// context.HttpContext.Request.Headers.CacheControl = new CacheControlHeaderValue() -// { -// NoStore = true -// }.ToString(); - -// Assert.False(new ResponseCachingPolicyProvider().AllowCacheStorage(context)); -// Assert.Empty(sink.Writes); -// } - -// [Fact] -// public void IsResponseCacheable_NoPublic_NotAllowed() -// { -// var sink = new TestSink(); -// var context = TestUtils.CreateTestContext(sink); - -// Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); -// TestUtils.AssertLoggedMessages( -// sink.Writes, -// LoggedMessage.ResponseWithoutPublicNotCacheable); -// } - -// [Fact] -// public void IsResponseCacheable_Public_Allowed() -// { -// var sink = new TestSink(); -// var context = TestUtils.CreateTestContext(sink); -// context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() -// { -// Public = true -// }.ToString(); - -// Assert.True(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); -// Assert.Empty(sink.Writes); -// } - -// [Fact] -// public void IsResponseCacheable_NoCache_NotAllowed() -// { -// var sink = new TestSink(); -// var context = TestUtils.CreateTestContext(sink); -// context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() -// { -// Public = true, -// NoCache = true -// }.ToString(); - -// Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); -// TestUtils.AssertLoggedMessages( -// sink.Writes, -// LoggedMessage.ResponseWithNoCacheNotCacheable); -// } - -// [Fact] -// public void IsResponseCacheable_ResponseNoStore_NotAllowed() -// { -// var sink = new TestSink(); -// var context = TestUtils.CreateTestContext(sink); -// context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() -// { -// Public = true, -// NoStore = true -// }.ToString(); - -// Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); -// TestUtils.AssertLoggedMessages( -// sink.Writes, -// LoggedMessage.ResponseWithNoStoreNotCacheable); -// } - -// [Fact] -// public void IsResponseCacheable_SetCookieHeader_NotAllowed() -// { -// var sink = new TestSink(); -// var context = TestUtils.CreateTestContext(sink); -// context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() -// { -// Public = true -// }.ToString(); -// context.HttpContext.Response.Headers.SetCookie = "cookieName=cookieValue"; - -// Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); -// TestUtils.AssertLoggedMessages( -// sink.Writes, -// LoggedMessage.ResponseWithSetCookieNotCacheable); -// } - -// [Fact] -// public void IsResponseCacheable_VaryHeaderByStar_NotAllowed() -// { -// var sink = new TestSink(); -// var context = TestUtils.CreateTestContext(sink); -// context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() -// { -// Public = true -// }.ToString(); -// context.HttpContext.Response.Headers.Vary = "*"; - -// Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); -// TestUtils.AssertLoggedMessages( -// sink.Writes, -// LoggedMessage.ResponseWithVaryStarNotCacheable); -// } - -// [Fact] -// public void IsResponseCacheable_Private_NotAllowed() -// { -// var sink = new TestSink(); -// var context = TestUtils.CreateTestContext(sink); -// context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() -// { -// Public = true, -// Private = true -// }.ToString(); - -// Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); -// TestUtils.AssertLoggedMessages( -// sink.Writes, -// LoggedMessage.ResponseWithPrivateNotCacheable); -// } - -// [Theory] -// [InlineData(StatusCodes.Status200OK)] -// public void IsResponseCacheable_SuccessStatusCodes_Allowed(int statusCode) -// { -// var sink = new TestSink(); -// var context = TestUtils.CreateTestContext(sink); -// context.HttpContext.Response.StatusCode = statusCode; -// context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() -// { -// Public = true -// }.ToString(); - -// Assert.True(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); -// Assert.Empty(sink.Writes); -// } - -// [Theory] -// [InlineData(StatusCodes.Status100Continue)] -// [InlineData(StatusCodes.Status101SwitchingProtocols)] -// [InlineData(StatusCodes.Status102Processing)] -// [InlineData(StatusCodes.Status201Created)] -// [InlineData(StatusCodes.Status202Accepted)] -// [InlineData(StatusCodes.Status203NonAuthoritative)] -// [InlineData(StatusCodes.Status204NoContent)] -// [InlineData(StatusCodes.Status205ResetContent)] -// [InlineData(StatusCodes.Status206PartialContent)] -// [InlineData(StatusCodes.Status207MultiStatus)] -// [InlineData(StatusCodes.Status208AlreadyReported)] -// [InlineData(StatusCodes.Status226IMUsed)] -// [InlineData(StatusCodes.Status300MultipleChoices)] -// [InlineData(StatusCodes.Status301MovedPermanently)] -// [InlineData(StatusCodes.Status302Found)] -// [InlineData(StatusCodes.Status303SeeOther)] -// [InlineData(StatusCodes.Status304NotModified)] -// [InlineData(StatusCodes.Status305UseProxy)] -// [InlineData(StatusCodes.Status306SwitchProxy)] -// [InlineData(StatusCodes.Status307TemporaryRedirect)] -// [InlineData(StatusCodes.Status308PermanentRedirect)] -// [InlineData(StatusCodes.Status400BadRequest)] -// [InlineData(StatusCodes.Status401Unauthorized)] -// [InlineData(StatusCodes.Status402PaymentRequired)] -// [InlineData(StatusCodes.Status403Forbidden)] -// [InlineData(StatusCodes.Status404NotFound)] -// [InlineData(StatusCodes.Status405MethodNotAllowed)] -// [InlineData(StatusCodes.Status406NotAcceptable)] -// [InlineData(StatusCodes.Status407ProxyAuthenticationRequired)] -// [InlineData(StatusCodes.Status408RequestTimeout)] -// [InlineData(StatusCodes.Status409Conflict)] -// [InlineData(StatusCodes.Status410Gone)] -// [InlineData(StatusCodes.Status411LengthRequired)] -// [InlineData(StatusCodes.Status412PreconditionFailed)] -// [InlineData(StatusCodes.Status413RequestEntityTooLarge)] -// [InlineData(StatusCodes.Status414RequestUriTooLong)] -// [InlineData(StatusCodes.Status415UnsupportedMediaType)] -// [InlineData(StatusCodes.Status416RequestedRangeNotSatisfiable)] -// [InlineData(StatusCodes.Status417ExpectationFailed)] -// [InlineData(StatusCodes.Status418ImATeapot)] -// [InlineData(StatusCodes.Status419AuthenticationTimeout)] -// [InlineData(StatusCodes.Status421MisdirectedRequest)] -// [InlineData(StatusCodes.Status422UnprocessableEntity)] -// [InlineData(StatusCodes.Status423Locked)] -// [InlineData(StatusCodes.Status424FailedDependency)] -// [InlineData(StatusCodes.Status426UpgradeRequired)] -// [InlineData(StatusCodes.Status428PreconditionRequired)] -// [InlineData(StatusCodes.Status429TooManyRequests)] -// [InlineData(StatusCodes.Status431RequestHeaderFieldsTooLarge)] -// [InlineData(StatusCodes.Status451UnavailableForLegalReasons)] -// [InlineData(StatusCodes.Status500InternalServerError)] -// [InlineData(StatusCodes.Status501NotImplemented)] -// [InlineData(StatusCodes.Status502BadGateway)] -// [InlineData(StatusCodes.Status503ServiceUnavailable)] -// [InlineData(StatusCodes.Status504GatewayTimeout)] -// [InlineData(StatusCodes.Status505HttpVersionNotsupported)] -// [InlineData(StatusCodes.Status506VariantAlsoNegotiates)] -// [InlineData(StatusCodes.Status507InsufficientStorage)] -// [InlineData(StatusCodes.Status508LoopDetected)] -// [InlineData(StatusCodes.Status510NotExtended)] -// [InlineData(StatusCodes.Status511NetworkAuthenticationRequired)] -// public void IsResponseCacheable_NonSuccessStatusCodes_NotAllowed(int statusCode) -// { -// var sink = new TestSink(); -// var context = TestUtils.CreateTestContext(sink); -// context.HttpContext.Response.StatusCode = statusCode; -// context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() -// { -// Public = true -// }.ToString(); - -// Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); -// TestUtils.AssertLoggedMessages( -// sink.Writes, -// LoggedMessage.ResponseWithUnsuccessfulStatusCodeNotCacheable); -// } - -// [Fact] -// public void IsResponseCacheable_NoExpiryRequirements_IsAllowed() -// { -// var sink = new TestSink(); -// var context = TestUtils.CreateTestContext(sink); -// context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; -// context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() -// { -// Public = true -// }.ToString(); - -// var utcNow = DateTimeOffset.UtcNow; -// context.HttpContext.Response.Headers.Date = HeaderUtilities.FormatDate(utcNow); -// context.ResponseTime = DateTimeOffset.MaxValue; - -// Assert.True(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); -// Assert.Empty(sink.Writes); -// } - -// [Fact] -// public void IsResponseCacheable_AtExpiry_NotAllowed() -// { -// var sink = new TestSink(); -// var context = TestUtils.CreateTestContext(sink); -// context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; -// context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() -// { -// Public = true -// }.ToString(); -// var utcNow = DateTimeOffset.UtcNow; -// context.HttpContext.Response.Headers.Expires = HeaderUtilities.FormatDate(utcNow); - -// context.HttpContext.Response.Headers.Date = HeaderUtilities.FormatDate(utcNow); -// context.ResponseTime = utcNow; - -// Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); -// TestUtils.AssertLoggedMessages( -// sink.Writes, -// LoggedMessage.ExpirationExpiresExceeded); -// } - -// [Fact] -// public void IsResponseCacheable_MaxAgeOverridesExpiry_ToAllowed() -// { -// var utcNow = DateTimeOffset.UtcNow; -// var sink = new TestSink(); -// var context = TestUtils.CreateTestContext(sink); -// context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; -// context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() -// { -// Public = true, -// MaxAge = TimeSpan.FromSeconds(10) -// }.ToString(); -// context.HttpContext.Response.Headers.Expires = HeaderUtilities.FormatDate(utcNow); -// context.HttpContext.Response.Headers.Date = HeaderUtilities.FormatDate(utcNow); -// context.ResponseTime = utcNow + TimeSpan.FromSeconds(9); - -// Assert.True(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); -// Assert.Empty(sink.Writes); -// } - -// [Fact] -// public void IsResponseCacheable_MaxAgeOverridesExpiry_ToNotAllowed() -// { -// var utcNow = DateTimeOffset.UtcNow; -// var sink = new TestSink(); -// var context = TestUtils.CreateTestContext(sink); -// context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; -// context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() -// { -// Public = true, -// MaxAge = TimeSpan.FromSeconds(10) -// }.ToString(); -// context.HttpContext.Response.Headers.Expires = HeaderUtilities.FormatDate(utcNow); -// context.HttpContext.Response.Headers.Date = HeaderUtilities.FormatDate(utcNow); -// context.ResponseTime = utcNow + TimeSpan.FromSeconds(10); - -// Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); -// TestUtils.AssertLoggedMessages( -// sink.Writes, -// LoggedMessage.ExpirationMaxAgeExceeded); -// } - -// [Fact] -// public void IsResponseCacheable_SharedMaxAgeOverridesMaxAge_ToAllowed() -// { -// var utcNow = DateTimeOffset.UtcNow; -// var sink = new TestSink(); -// var context = TestUtils.CreateTestContext(sink); -// context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; -// context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() -// { -// Public = true, -// MaxAge = TimeSpan.FromSeconds(10), -// SharedMaxAge = TimeSpan.FromSeconds(15) -// }.ToString(); -// context.HttpContext.Response.Headers.Date = HeaderUtilities.FormatDate(utcNow); -// context.ResponseTime = utcNow + TimeSpan.FromSeconds(11); - -// Assert.True(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); -// Assert.Empty(sink.Writes); -// } - -// [Fact] -// public void IsResponseCacheable_SharedMaxAgeOverridesMaxAge_ToNotAllowed() -// { -// var utcNow = DateTimeOffset.UtcNow; -// var sink = new TestSink(); -// var context = TestUtils.CreateTestContext(sink); -// context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; -// context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() -// { -// Public = true, -// MaxAge = TimeSpan.FromSeconds(10), -// SharedMaxAge = TimeSpan.FromSeconds(5) -// }.ToString(); -// context.HttpContext.Response.Headers.Date = HeaderUtilities.FormatDate(utcNow); -// context.ResponseTime = utcNow + TimeSpan.FromSeconds(5); - -// Assert.False(new ResponseCachingPolicyProvider().IsResponseCacheable(context)); -// TestUtils.AssertLoggedMessages( -// sink.Writes, -// LoggedMessage.ExpirationSharedMaxAgeExceeded); -// } - -// [Fact] -// public void IsCachedEntryFresh_NoCachedCacheControl_FallsbackToEmptyCacheControl() -// { -// var utcNow = DateTimeOffset.UtcNow; -// var sink = new TestSink(); -// var context = TestUtils.CreateTestContext(sink); -// context.ResponseTime = DateTimeOffset.MaxValue; -// context.CachedEntryAge = TimeSpan.MaxValue; -// context.CachedResponseHeaders = new HeaderDictionary(); - -// Assert.True(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); -// Assert.Empty(sink.Writes); -// } - -// [Fact] -// public void IsCachedEntryFresh_NoExpiryRequirements_IsFresh() -// { -// var utcNow = DateTimeOffset.UtcNow; -// var sink = new TestSink(); -// var context = TestUtils.CreateTestContext(sink); -// context.ResponseTime = DateTimeOffset.MaxValue; -// context.CachedEntryAge = TimeSpan.MaxValue; -// context.CachedResponseHeaders = new HeaderDictionary(); -// context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() -// { -// Public = true -// }.ToString(); - -// Assert.True(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); -// Assert.Empty(sink.Writes); -// } - -// [Fact] -// public void IsCachedEntryFresh_AtExpiry_IsNotFresh() -// { -// var utcNow = DateTimeOffset.UtcNow; -// var sink = new TestSink(); -// var context = TestUtils.CreateTestContext(sink); -// context.ResponseTime = utcNow; -// context.CachedEntryAge = TimeSpan.Zero; -// context.CachedResponseHeaders = new HeaderDictionary(); -// context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() -// { -// Public = true -// }.ToString(); -// context.CachedResponseHeaders[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow); - -// Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); -// TestUtils.AssertLoggedMessages( -// sink.Writes, -// LoggedMessage.ExpirationExpiresExceeded); -// } - -// [Fact] -// public void IsCachedEntryFresh_MaxAgeOverridesExpiry_ToFresh() -// { -// var utcNow = DateTimeOffset.UtcNow; -// var sink = new TestSink(); -// var context = TestUtils.CreateTestContext(sink); -// context.CachedEntryAge = TimeSpan.FromSeconds(9); -// context.ResponseTime = utcNow + context.CachedEntryAge; -// context.CachedResponseHeaders = new HeaderDictionary(); -// context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() -// { -// Public = true, -// MaxAge = TimeSpan.FromSeconds(10) -// }.ToString(); -// context.HttpContext.Response.Headers.Expires = HeaderUtilities.FormatDate(utcNow); - -// Assert.True(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); -// Assert.Empty(sink.Writes); -// } - -// [Fact] -// public void IsCachedEntryFresh_MaxAgeOverridesExpiry_ToNotFresh() -// { -// var utcNow = DateTimeOffset.UtcNow; -// var sink = new TestSink(); -// var context = TestUtils.CreateTestContext(sink); -// context.CachedEntryAge = TimeSpan.FromSeconds(10); -// context.ResponseTime = utcNow + context.CachedEntryAge; -// context.CachedResponseHeaders = new HeaderDictionary(); -// context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() -// { -// Public = true, -// MaxAge = TimeSpan.FromSeconds(10) -// }.ToString(); -// context.HttpContext.Response.Headers.Expires = HeaderUtilities.FormatDate(utcNow); - -// Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); -// TestUtils.AssertLoggedMessages( -// sink.Writes, -// LoggedMessage.ExpirationMaxAgeExceeded); -// } - -// [Fact] -// public void IsCachedEntryFresh_SharedMaxAgeOverridesMaxAge_ToFresh() -// { -// var utcNow = DateTimeOffset.UtcNow; -// var sink = new TestSink(); -// var context = TestUtils.CreateTestContext(sink); -// context.CachedEntryAge = TimeSpan.FromSeconds(11); -// context.ResponseTime = utcNow + context.CachedEntryAge; -// context.CachedResponseHeaders = new HeaderDictionary(); -// context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() -// { -// Public = true, -// MaxAge = TimeSpan.FromSeconds(10), -// SharedMaxAge = TimeSpan.FromSeconds(15) -// }.ToString(); -// context.CachedResponseHeaders[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow); - -// Assert.True(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); -// Assert.Empty(sink.Writes); -// } - -// [Fact] -// public void IsCachedEntryFresh_SharedMaxAgeOverridesMaxAge_ToNotFresh() -// { -// var utcNow = DateTimeOffset.UtcNow; -// var sink = new TestSink(); -// var context = TestUtils.CreateTestContext(sink); -// context.CachedEntryAge = TimeSpan.FromSeconds(5); -// context.ResponseTime = utcNow + context.CachedEntryAge; -// context.CachedResponseHeaders = new HeaderDictionary(); -// context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() -// { -// Public = true, -// MaxAge = TimeSpan.FromSeconds(10), -// SharedMaxAge = TimeSpan.FromSeconds(5) -// }.ToString(); -// context.CachedResponseHeaders[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow); - -// Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); -// TestUtils.AssertLoggedMessages( -// sink.Writes, -// LoggedMessage.ExpirationSharedMaxAgeExceeded); -// } - -// [Fact] -// public void IsCachedEntryFresh_MinFreshReducesFreshness_ToNotFresh() -// { -// var sink = new TestSink(); -// var context = TestUtils.CreateTestContext(sink); -// context.HttpContext.Request.Headers.CacheControl = new CacheControlHeaderValue() -// { -// MinFresh = TimeSpan.FromSeconds(2) -// }.ToString(); -// context.CachedResponseHeaders = new HeaderDictionary(); -// context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() -// { -// MaxAge = TimeSpan.FromSeconds(10), -// SharedMaxAge = TimeSpan.FromSeconds(5) -// }.ToString(); -// context.CachedEntryAge = TimeSpan.FromSeconds(3); - -// Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); -// TestUtils.AssertLoggedMessages( -// sink.Writes, -// LoggedMessage.ExpirationMinFreshAdded, -// LoggedMessage.ExpirationSharedMaxAgeExceeded); -// } - -// [Fact] -// public void IsCachedEntryFresh_RequestMaxAgeRestrictAge_ToNotFresh() -// { -// var sink = new TestSink(); -// var context = TestUtils.CreateTestContext(sink); -// context.HttpContext.Request.Headers.CacheControl = new CacheControlHeaderValue() -// { -// MaxAge = TimeSpan.FromSeconds(5) -// }.ToString(); -// context.CachedResponseHeaders = new HeaderDictionary(); -// context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() -// { -// MaxAge = TimeSpan.FromSeconds(10), -// }.ToString(); -// context.CachedEntryAge = TimeSpan.FromSeconds(5); - -// Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); -// TestUtils.AssertLoggedMessages( -// sink.Writes, -// LoggedMessage.ExpirationMaxAgeExceeded); -// } - -// [Fact] -// public void IsCachedEntryFresh_MaxStaleOverridesFreshness_ToFresh() -// { -// var sink = new TestSink(); -// var context = TestUtils.CreateTestContext(sink); -// context.HttpContext.Request.Headers.CacheControl = new CacheControlHeaderValue() -// { -// MaxAge = TimeSpan.FromSeconds(5), -// MaxStale = true, // This value must be set to true in order to specify MaxStaleLimit -// MaxStaleLimit = TimeSpan.FromSeconds(2) -// }.ToString(); -// context.CachedResponseHeaders = new HeaderDictionary(); -// context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() -// { -// MaxAge = TimeSpan.FromSeconds(5), -// }.ToString(); -// context.CachedEntryAge = TimeSpan.FromSeconds(6); - -// Assert.True(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); -// TestUtils.AssertLoggedMessages( -// sink.Writes, -// LoggedMessage.ExpirationMaxStaleSatisfied); -// } - -// [Fact] -// public void IsCachedEntryFresh_MaxStaleInfiniteOverridesFreshness_ToFresh() -// { -// var sink = new TestSink(); -// var context = TestUtils.CreateTestContext(sink); -// context.HttpContext.Request.Headers.CacheControl = new CacheControlHeaderValue() -// { -// MaxAge = TimeSpan.FromSeconds(5), -// MaxStale = true // No value specified means a MaxStaleLimit of infinity -// }.ToString(); -// context.CachedResponseHeaders = new HeaderDictionary(); -// context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() -// { -// MaxAge = TimeSpan.FromSeconds(5), -// }.ToString(); -// context.CachedEntryAge = TimeSpan.FromSeconds(6); - -// Assert.True(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); -// TestUtils.AssertLoggedMessages( -// sink.Writes, -// LoggedMessage.ExpirationInfiniteMaxStaleSatisfied); -// } - -// [Fact] -// public void IsCachedEntryFresh_MaxStaleOverridesFreshness_ButStillNotFresh() -// { -// var sink = new TestSink(); -// var context = TestUtils.CreateTestContext(sink); -// context.HttpContext.Request.Headers.CacheControl = new CacheControlHeaderValue() -// { -// MaxAge = TimeSpan.FromSeconds(5), -// MaxStale = true, // This value must be set to true in order to specify MaxStaleLimit -// MaxStaleLimit = TimeSpan.FromSeconds(1) -// }.ToString(); -// context.CachedResponseHeaders = new HeaderDictionary(); -// context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() -// { -// MaxAge = TimeSpan.FromSeconds(5), -// }.ToString(); -// context.CachedEntryAge = TimeSpan.FromSeconds(6); - -// Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); -// TestUtils.AssertLoggedMessages( -// sink.Writes, -// LoggedMessage.ExpirationMaxAgeExceeded); -// } - -// [Fact] -// public void IsCachedEntryFresh_MustRevalidateOverridesRequestMaxStale_ToNotFresh() -// { -// var sink = new TestSink(); -// var context = TestUtils.CreateTestContext(sink); -// context.HttpContext.Request.Headers.CacheControl = new CacheControlHeaderValue() -// { -// MaxAge = TimeSpan.FromSeconds(5), -// MaxStale = true, // This value must be set to true in order to specify MaxStaleLimit -// MaxStaleLimit = TimeSpan.FromSeconds(2) -// }.ToString(); -// context.CachedResponseHeaders = new HeaderDictionary(); -// context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() -// { -// MaxAge = TimeSpan.FromSeconds(5), -// MustRevalidate = true -// }.ToString(); -// context.CachedEntryAge = TimeSpan.FromSeconds(6); - -// Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); -// TestUtils.AssertLoggedMessages( -// sink.Writes, -// LoggedMessage.ExpirationMustRevalidate); -// } - -// [Fact] -// public void IsCachedEntryFresh_ProxyRevalidateOverridesRequestMaxStale_ToNotFresh() -// { -// var sink = new TestSink(); -// var context = TestUtils.CreateTestContext(sink); -// context.HttpContext.Request.Headers.CacheControl = new CacheControlHeaderValue() -// { -// MaxAge = TimeSpan.FromSeconds(5), -// MaxStale = true, // This value must be set to true in order to specify MaxStaleLimit -// MaxStaleLimit = TimeSpan.FromSeconds(2) -// }.ToString(); -// context.CachedResponseHeaders = new HeaderDictionary(); -// context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() -// { -// MaxAge = TimeSpan.FromSeconds(5), -// MustRevalidate = true -// }.ToString(); -// context.CachedEntryAge = TimeSpan.FromSeconds(6); - -// Assert.False(new ResponseCachingPolicyProvider().IsCachedEntryFresh(context)); -// TestUtils.AssertLoggedMessages( -// sink.Writes, -// LoggedMessage.ExpirationMustRevalidate); -// } -//} diff --git a/src/Middleware/OutputCaching/test/ResponseCachingTests.cs b/src/Middleware/OutputCaching/test/ResponseCachingTests.cs deleted file mode 100644 index 0622854d79e8..000000000000 --- a/src/Middleware/OutputCaching/test/ResponseCachingTests.cs +++ /dev/null @@ -1,984 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -//using System.Net.Http; -//using Microsoft.AspNetCore.Http; -//using Microsoft.AspNetCore.TestHost; -//using Microsoft.Net.Http.Headers; - -//namespace Microsoft.AspNetCore.ResponseCaching.Tests; - -//public class ResponseCachingTests -//{ -// [Theory] -// [InlineData("GET")] -// [InlineData("HEAD")] -// public async Task ServesCachedContent_IfAvailable(string method) -// { -// var builders = TestUtils.CreateBuildersWithResponseCaching(); - -// foreach (var builder in builders) -// { -// using var host = builder.Build(); - -// await host.StartAsync(); - -// using (var server = host.GetTestServer()) -// { -// var client = server.CreateClient(); -// var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); -// var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); - -// await AssertCachedResponseAsync(initialResponse, subsequentResponse); -// } -// } -// } - -// [Theory] -// [InlineData("GET")] -// [InlineData("HEAD")] -// public async Task ServesFreshContent_IfNotAvailable(string method) -// { -// var builders = TestUtils.CreateBuildersWithResponseCaching(); - -// foreach (var builder in builders) -// { -// using var host = builder.Build(); - -// await host.StartAsync(); - -// using (var server = host.GetTestServer()) -// { -// var client = server.CreateClient(); -// var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); -// var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, "different")); - -// await AssertFreshResponseAsync(initialResponse, subsequentResponse); -// } -// } -// } - -// [Fact] -// public async Task ServesFreshContent_Post() -// { -// var builders = TestUtils.CreateBuildersWithResponseCaching(); - -// foreach (var builder in builders) -// { -// using var host = builder.Build(); - -// await host.StartAsync(); - -// using (var server = host.GetTestServer()) -// { -// var client = server.CreateClient(); -// var initialResponse = await client.PostAsync("", new StringContent(string.Empty)); -// var subsequentResponse = await client.PostAsync("", new StringContent(string.Empty)); - -// await AssertFreshResponseAsync(initialResponse, subsequentResponse); -// } -// } -// } - -// [Fact] -// public async Task ServesFreshContent_Head_Get() -// { -// var builders = TestUtils.CreateBuildersWithResponseCaching(); - -// foreach (var builder in builders) -// { -// using var host = builder.Build(); - -// await host.StartAsync(); - -// using (var server = host.GetTestServer()) -// { -// var client = server.CreateClient(); -// var subsequentResponse = await client.SendAsync(new HttpRequestMessage(HttpMethod.Head, "")); -// var initialResponse = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "")); - -// await AssertFreshResponseAsync(initialResponse, subsequentResponse); -// } -// } -// } - -// [Fact] -// public async Task ServesFreshContent_Get_Head() -// { -// var builders = TestUtils.CreateBuildersWithResponseCaching(); - -// foreach (var builder in builders) -// { -// using var host = builder.Build(); - -// await host.StartAsync(); - -// using (var server = host.GetTestServer()) -// { -// var client = server.CreateClient(); -// var initialResponse = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "")); -// var subsequentResponse = await client.SendAsync(new HttpRequestMessage(HttpMethod.Head, "")); - -// await AssertFreshResponseAsync(initialResponse, subsequentResponse); -// } -// } -// } - -// [Theory] -// [InlineData("GET")] -// [InlineData("HEAD")] -// public async Task ServesFreshContent_If_CacheControlNoCache(string method) -// { -// var builders = TestUtils.CreateBuildersWithResponseCaching(); - -// foreach (var builder in builders) -// { -// using var host = builder.Build(); - -// await host.StartAsync(); - -// using (var server = host.GetTestServer()) -// { -// var client = server.CreateClient(); - -// var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); - -// // verify the response is cached -// var cachedResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); -// await AssertCachedResponseAsync(initialResponse, cachedResponse); - -// // assert cached response no longer served -// client.DefaultRequestHeaders.CacheControl = -// new System.Net.Http.Headers.CacheControlHeaderValue { NoCache = true }; -// var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); - -// await AssertFreshResponseAsync(initialResponse, subsequentResponse); -// } -// } -// } - -// [Theory] -// [InlineData("GET")] -// [InlineData("HEAD")] -// public async Task ServesFreshContent_If_PragmaNoCache(string method) -// { -// var builders = TestUtils.CreateBuildersWithResponseCaching(); - -// foreach (var builder in builders) -// { -// using var host = builder.Build(); - -// await host.StartAsync(); - -// using (var server = host.GetTestServer()) -// { -// var client = server.CreateClient(); - -// var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); - -// // verify the response is cached -// var cachedResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); -// await AssertCachedResponseAsync(initialResponse, cachedResponse); - -// // assert cached response no longer served -// client.DefaultRequestHeaders.Pragma.Clear(); -// client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("no-cache")); -// var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); - -// await AssertFreshResponseAsync(initialResponse, subsequentResponse); -// } -// } -// } - -// [Theory] -// [InlineData("GET")] -// [InlineData("HEAD")] -// public async Task ServesCachedContent_If_PathCasingDiffers(string method) -// { -// var builders = TestUtils.CreateBuildersWithResponseCaching(); - -// foreach (var builder in builders) -// { -// using var host = builder.Build(); - -// await host.StartAsync(); - -// using (var server = host.GetTestServer()) -// { -// var client = server.CreateClient(); -// var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, "path")); -// var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, "PATH")); - -// await AssertCachedResponseAsync(initialResponse, subsequentResponse); -// } -// } -// } - -// [Theory] -// [InlineData("GET")] -// [InlineData("HEAD")] -// public async Task ServesFreshContent_If_ResponseExpired(string method) -// { -// var builders = TestUtils.CreateBuildersWithResponseCaching(); - -// foreach (var builder in builders) -// { -// using var host = builder.Build(); - -// await host.StartAsync(); - -// using (var server = host.GetTestServer()) -// { -// var client = server.CreateClient(); -// var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, "?Expires=0")); -// var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); - -// await AssertFreshResponseAsync(initialResponse, subsequentResponse); -// } -// } -// } - -// [Theory] -// [InlineData("GET")] -// [InlineData("HEAD")] -// public async Task ServesFreshContent_If_Authorization_HeaderExists(string method) -// { -// var builders = TestUtils.CreateBuildersWithResponseCaching(); - -// foreach (var builder in builders) -// { -// using var host = builder.Build(); - -// await host.StartAsync(); - -// using (var server = host.GetTestServer()) -// { -// var client = server.CreateClient(); -// client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("abc"); -// var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); -// var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); - -// await AssertFreshResponseAsync(initialResponse, subsequentResponse); -// } -// } -// } - -// [Fact] -// public async Task ServesCachedContent_IfVaryHeader_Matches() -// { -// var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Response.Headers.Vary = HeaderNames.From); - -// foreach (var builder in builders) -// { -// using var host = builder.Build(); - -// await host.StartAsync(); - -// using (var server = host.GetTestServer()) -// { -// var client = server.CreateClient(); -// client.DefaultRequestHeaders.From = "user@example.com"; -// var initialResponse = await client.GetAsync(""); -// var subsequentResponse = await client.GetAsync(""); - -// await AssertCachedResponseAsync(initialResponse, subsequentResponse); -// } -// } -// } - -// [Fact] -// public async Task ServesFreshContent_IfVaryHeader_Mismatches() -// { -// var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Response.Headers.Vary = HeaderNames.From); - -// foreach (var builder in builders) -// { -// using var host = builder.Build(); - -// await host.StartAsync(); - -// using (var server = host.GetTestServer()) -// { -// var client = server.CreateClient(); -// client.DefaultRequestHeaders.From = "user@example.com"; -// var initialResponse = await client.GetAsync(""); -// client.DefaultRequestHeaders.From = "user2@example.com"; -// var subsequentResponse = await client.GetAsync(""); - -// await AssertFreshResponseAsync(initialResponse, subsequentResponse); -// } -// } -// } - -// [Fact] -// public async Task ServesCachedContent_IfVaryQueryKeys_Matches() -// { -// var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Features.Get().VaryByQueryKeys = new[] { "query" }); - -// foreach (var builder in builders) -// { -// using var host = builder.Build(); - -// await host.StartAsync(); - -// using (var server = host.GetTestServer()) -// { -// var client = server.CreateClient(); -// var initialResponse = await client.GetAsync("?query=value"); -// var subsequentResponse = await client.GetAsync("?query=value"); - -// await AssertCachedResponseAsync(initialResponse, subsequentResponse); -// } -// } -// } - -// [Fact] -// public async Task ServesCachedContent_IfVaryQueryKeysExplicit_Matches_QueryKeyCaseInsensitive() -// { -// var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Features.Get().VaryByQueryKeys = new[] { "QueryA", "queryb" }); - -// foreach (var builder in builders) -// { -// using var host = builder.Build(); - -// await host.StartAsync(); - -// using (var server = host.GetTestServer()) -// { -// var client = server.CreateClient(); -// var initialResponse = await client.GetAsync("?querya=valuea&queryb=valueb"); -// var subsequentResponse = await client.GetAsync("?QueryA=valuea&QueryB=valueb"); - -// await AssertCachedResponseAsync(initialResponse, subsequentResponse); -// } -// } -// } - -// [Fact] -// public async Task ServesCachedContent_IfVaryQueryKeyStar_Matches_QueryKeyCaseInsensitive() -// { -// var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Features.Get().VaryByQueryKeys = new[] { "*" }); - -// foreach (var builder in builders) -// { -// using var host = builder.Build(); - -// await host.StartAsync(); - -// using (var server = host.GetTestServer()) -// { -// var client = server.CreateClient(); -// var initialResponse = await client.GetAsync("?querya=valuea&queryb=valueb"); -// var subsequentResponse = await client.GetAsync("?QueryA=valuea&QueryB=valueb"); - -// await AssertCachedResponseAsync(initialResponse, subsequentResponse); -// } -// } -// } - -// [Fact] -// public async Task ServesCachedContent_IfVaryQueryKeyExplicit_Matches_OrderInsensitive() -// { -// var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Features.Get().VaryByQueryKeys = new[] { "QueryB", "QueryA" }); - -// foreach (var builder in builders) -// { -// using var host = builder.Build(); - -// await host.StartAsync(); - -// using (var server = host.GetTestServer()) -// { -// var client = server.CreateClient(); -// var initialResponse = await client.GetAsync("?QueryA=ValueA&QueryB=ValueB"); -// var subsequentResponse = await client.GetAsync("?QueryB=ValueB&QueryA=ValueA"); - -// await AssertCachedResponseAsync(initialResponse, subsequentResponse); -// } -// } -// } - -// [Fact] -// public async Task ServesCachedContent_IfVaryQueryKeyStar_Matches_OrderInsensitive() -// { -// var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Features.Get().VaryByQueryKeys = new[] { "*" }); - -// foreach (var builder in builders) -// { -// using var host = builder.Build(); - -// await host.StartAsync(); - -// using (var server = host.GetTestServer()) -// { -// var client = server.CreateClient(); -// var initialResponse = await client.GetAsync("?QueryA=ValueA&QueryB=ValueB"); -// var subsequentResponse = await client.GetAsync("?QueryB=ValueB&QueryA=ValueA"); - -// await AssertCachedResponseAsync(initialResponse, subsequentResponse); -// } -// } -// } - -// [Fact] -// public async Task ServesFreshContent_IfVaryQueryKey_Mismatches() -// { -// var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Features.Get().VaryByQueryKeys = new[] { "query" }); - -// foreach (var builder in builders) -// { -// using var host = builder.Build(); - -// await host.StartAsync(); - -// using (var server = host.GetTestServer()) -// { -// var client = server.CreateClient(); -// var initialResponse = await client.GetAsync("?query=value"); -// var subsequentResponse = await client.GetAsync("?query=value2"); - -// await AssertFreshResponseAsync(initialResponse, subsequentResponse); -// } -// } -// } - -// [Fact] -// public async Task ServesFreshContent_IfVaryQueryKeyExplicit_Mismatch_QueryKeyCaseSensitive() -// { -// var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Features.Get().VaryByQueryKeys = new[] { "QueryA", "QueryB" }); - -// foreach (var builder in builders) -// { -// using var host = builder.Build(); - -// await host.StartAsync(); - -// using (var server = host.GetTestServer()) -// { -// var client = server.CreateClient(); -// var initialResponse = await client.GetAsync("?querya=valuea&queryb=valueb"); -// var subsequentResponse = await client.GetAsync("?querya=ValueA&queryb=ValueB"); - -// await AssertFreshResponseAsync(initialResponse, subsequentResponse); -// } -// } -// } - -// [Fact] -// public async Task ServesFreshContent_IfVaryQueryKeyStar_Mismatch_QueryKeyValueCaseSensitive() -// { -// var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Features.Get().VaryByQueryKeys = new[] { "*" }); - -// foreach (var builder in builders) -// { -// using var host = builder.Build(); - -// await host.StartAsync(); - -// using (var server = host.GetTestServer()) -// { -// var client = server.CreateClient(); -// var initialResponse = await client.GetAsync("?querya=valuea&queryb=valueb"); -// var subsequentResponse = await client.GetAsync("?querya=ValueA&queryb=ValueB"); - -// await AssertFreshResponseAsync(initialResponse, subsequentResponse); -// } -// } -// } - -// [Fact] -// public async Task ServesFreshContent_IfRequestRequirements_NotMet() -// { -// var builders = TestUtils.CreateBuildersWithResponseCaching(); - -// foreach (var builder in builders) -// { -// using var host = builder.Build(); - -// await host.StartAsync(); - -// using (var server = host.GetTestServer()) -// { -// var client = server.CreateClient(); -// var initialResponse = await client.GetAsync(""); -// client.DefaultRequestHeaders.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue() -// { -// MaxAge = TimeSpan.FromSeconds(0) -// }; -// var subsequentResponse = await client.GetAsync(""); - -// await AssertFreshResponseAsync(initialResponse, subsequentResponse); -// } -// } -// } - -// [Fact] -// public async Task Serves504_IfOnlyIfCachedHeader_IsSpecified() -// { -// var builders = TestUtils.CreateBuildersWithResponseCaching(); - -// foreach (var builder in builders) -// { -// using var host = builder.Build(); - -// await host.StartAsync(); - -// using (var server = host.GetTestServer()) -// { -// var client = server.CreateClient(); -// var initialResponse = await client.GetAsync(""); -// client.DefaultRequestHeaders.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue() -// { -// OnlyIfCached = true -// }; -// var subsequentResponse = await client.GetAsync("/different"); - -// initialResponse.EnsureSuccessStatusCode(); -// Assert.Equal(System.Net.HttpStatusCode.GatewayTimeout, subsequentResponse.StatusCode); -// } -// } -// } - -// [Fact] -// public async Task ServesFreshContent_IfSetCookie_IsSpecified() -// { -// var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Response.Headers.SetCookie = "cookieName=cookieValue"); - -// foreach (var builder in builders) -// { -// using var host = builder.Build(); - -// await host.StartAsync(); - -// using (var server = host.GetTestServer()) -// { -// var client = server.CreateClient(); -// var initialResponse = await client.GetAsync(""); -// var subsequentResponse = await client.GetAsync(""); - -// await AssertFreshResponseAsync(initialResponse, subsequentResponse); -// } -// } -// } - -// [Fact] -// public async Task ServesCachedContent_IfSubsequentRequestContainsNoStore() -// { -// var builders = TestUtils.CreateBuildersWithResponseCaching(); - -// foreach (var builder in builders) -// { -// using var host = builder.Build(); - -// await host.StartAsync(); - -// using (var server = host.GetTestServer()) -// { -// var client = server.CreateClient(); -// var initialResponse = await client.GetAsync(""); -// client.DefaultRequestHeaders.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue() -// { -// NoStore = true -// }; -// var subsequentResponse = await client.GetAsync(""); - -// await AssertCachedResponseAsync(initialResponse, subsequentResponse); -// } -// } -// } - -// [Fact] -// public async Task ServesFreshContent_IfInitialRequestContainsNoStore() -// { -// var builders = TestUtils.CreateBuildersWithResponseCaching(); - -// foreach (var builder in builders) -// { -// using var host = builder.Build(); - -// await host.StartAsync(); - -// using (var server = host.GetTestServer()) -// { -// var client = server.CreateClient(); -// client.DefaultRequestHeaders.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue() -// { -// NoStore = true -// }; -// var initialResponse = await client.GetAsync(""); -// var subsequentResponse = await client.GetAsync(""); - -// await AssertFreshResponseAsync(initialResponse, subsequentResponse); -// } -// } -// } - -// [Fact] -// public async Task ServesFreshContent_IfInitialResponseContainsNoStore() -// { -// var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Response.Headers.CacheControl = CacheControlHeaderValue.NoStoreString); - -// foreach (var builder in builders) -// { -// using var host = builder.Build(); - -// await host.StartAsync(); - -// using (var server = host.GetTestServer()) -// { -// var client = server.CreateClient(); -// var initialResponse = await client.GetAsync(""); -// var subsequentResponse = await client.GetAsync(""); - -// await AssertFreshResponseAsync(initialResponse, subsequentResponse); -// } -// } -// } - -// [Fact] -// public async Task Serves304_IfIfModifiedSince_Satisfied() -// { -// var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => -// { -// context.Response.GetTypedHeaders().ETag = new EntityTagHeaderValue("\"E1\""); -// context.Response.Headers.ContentLocation = "/"; -// context.Response.Headers.Vary = HeaderNames.From; -// }); - -// foreach (var builder in builders) -// { -// using var host = builder.Build(); - -// await host.StartAsync(); - -// using (var server = host.GetTestServer()) -// { -// var client = server.CreateClient(); -// var initialResponse = await client.GetAsync("?Expires=90"); -// client.DefaultRequestHeaders.IfModifiedSince = DateTimeOffset.MaxValue; -// var subsequentResponse = await client.GetAsync(""); - -// initialResponse.EnsureSuccessStatusCode(); -// Assert.Equal(System.Net.HttpStatusCode.NotModified, subsequentResponse.StatusCode); -// Assert304Headers(initialResponse, subsequentResponse); -// } -// } -// } - -// [Fact] -// public async Task ServesCachedContent_IfIfModifiedSince_NotSatisfied() -// { -// var builders = TestUtils.CreateBuildersWithResponseCaching(); - -// foreach (var builder in builders) -// { -// using var host = builder.Build(); - -// await host.StartAsync(); - -// using (var server = host.GetTestServer()) -// { -// var client = server.CreateClient(); -// var initialResponse = await client.GetAsync(""); -// client.DefaultRequestHeaders.IfModifiedSince = DateTimeOffset.MinValue; -// var subsequentResponse = await client.GetAsync(""); - -// await AssertCachedResponseAsync(initialResponse, subsequentResponse); -// } -// } -// } - -// [Fact] -// public async Task Serves304_IfIfNoneMatch_Satisfied() -// { -// var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => -// { -// context.Response.GetTypedHeaders().ETag = new EntityTagHeaderValue("\"E1\""); -// context.Response.Headers.ContentLocation = "/"; -// context.Response.Headers.Vary = HeaderNames.From; -// }); - -// foreach (var builder in builders) -// { -// using var host = builder.Build(); - -// await host.StartAsync(); - -// using (var server = host.GetTestServer()) -// { -// var client = server.CreateClient(); -// var initialResponse = await client.GetAsync("?Expires=90"); -// client.DefaultRequestHeaders.IfNoneMatch.Add(new System.Net.Http.Headers.EntityTagHeaderValue("\"E1\"")); -// var subsequentResponse = await client.GetAsync(""); - -// initialResponse.EnsureSuccessStatusCode(); -// Assert.Equal(System.Net.HttpStatusCode.NotModified, subsequentResponse.StatusCode); -// Assert304Headers(initialResponse, subsequentResponse); -// } -// } -// } - -// [Fact] -// public async Task ServesCachedContent_IfIfNoneMatch_NotSatisfied() -// { -// var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Response.GetTypedHeaders().ETag = new EntityTagHeaderValue("\"E1\"")); - -// foreach (var builder in builders) -// { -// using var host = builder.Build(); - -// await host.StartAsync(); - -// using (var server = host.GetTestServer()) -// { -// var client = server.CreateClient(); -// var initialResponse = await client.GetAsync(""); -// client.DefaultRequestHeaders.IfNoneMatch.Add(new System.Net.Http.Headers.EntityTagHeaderValue("\"E2\"")); -// var subsequentResponse = await client.GetAsync(""); - -// await AssertCachedResponseAsync(initialResponse, subsequentResponse); -// } -// } -// } - -// [Fact] -// public async Task ServesCachedContent_IfBodySize_IsCacheable() -// { -// var builders = TestUtils.CreateBuildersWithResponseCaching(options: new ResponseCachingOptions() -// { -// MaximumBodySize = 100 -// }); - -// foreach (var builder in builders) -// { -// using var host = builder.Build(); - -// await host.StartAsync(); - -// using (var server = host.GetTestServer()) -// { -// var client = server.CreateClient(); -// var initialResponse = await client.GetAsync(""); -// var subsequentResponse = await client.GetAsync(""); - -// await AssertCachedResponseAsync(initialResponse, subsequentResponse); -// } -// } -// } - -// [Fact] -// public async Task ServesFreshContent_IfBodySize_IsNotCacheable() -// { -// var builders = TestUtils.CreateBuildersWithResponseCaching(options: new ResponseCachingOptions() -// { -// MaximumBodySize = 1 -// }); - -// foreach (var builder in builders) -// { -// using var host = builder.Build(); - -// await host.StartAsync(); - -// using (var server = host.GetTestServer()) -// { -// var client = server.CreateClient(); -// var initialResponse = await client.GetAsync(""); -// var subsequentResponse = await client.GetAsync("/different"); - -// await AssertFreshResponseAsync(initialResponse, subsequentResponse); -// } -// } -// } - -// [Fact] -// public async Task ServesFreshContent_CaseSensitivePaths_IsNotCacheable() -// { -// var builders = TestUtils.CreateBuildersWithResponseCaching(options: new ResponseCachingOptions() -// { -// UseCaseSensitivePaths = true -// }); - -// foreach (var builder in builders) -// { -// using var host = builder.Build(); - -// await host.StartAsync(); - -// using (var server = host.GetTestServer()) -// { -// var client = server.CreateClient(); -// var initialResponse = await client.GetAsync("/path"); -// var subsequentResponse = await client.GetAsync("/Path"); - -// await AssertFreshResponseAsync(initialResponse, subsequentResponse); -// } -// } -// } - -// [Fact] -// public async Task ServesCachedContent_WithoutReplacingCachedVaryBy_OnCacheMiss() -// { -// var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Response.Headers.Vary = HeaderNames.From); - -// foreach (var builder in builders) -// { -// using var host = builder.Build(); - -// await host.StartAsync(); - -// using (var server = host.GetTestServer()) -// { -// var client = server.CreateClient(); -// client.DefaultRequestHeaders.From = "user@example.com"; -// var initialResponse = await client.GetAsync(""); -// client.DefaultRequestHeaders.From = "user2@example.com"; -// var otherResponse = await client.GetAsync(""); -// client.DefaultRequestHeaders.From = "user@example.com"; -// var subsequentResponse = await client.GetAsync(""); - -// await AssertCachedResponseAsync(initialResponse, subsequentResponse); -// } -// } -// } - -// [Fact] -// public async Task ServesFreshContent_IfCachedVaryByUpdated_OnCacheMiss() -// { -// var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Response.Headers.Vary = context.Request.Headers.Pragma); - -// foreach (var builder in builders) -// { -// using var host = builder.Build(); - -// await host.StartAsync(); - -// using (var server = host.GetTestServer()) -// { -// var client = server.CreateClient(); -// client.DefaultRequestHeaders.From = "user@example.com"; -// client.DefaultRequestHeaders.Pragma.Clear(); -// client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("From")); -// client.DefaultRequestHeaders.MaxForwards = 1; -// var initialResponse = await client.GetAsync(""); -// client.DefaultRequestHeaders.From = "user2@example.com"; -// client.DefaultRequestHeaders.Pragma.Clear(); -// client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("Max-Forwards")); -// client.DefaultRequestHeaders.MaxForwards = 2; -// var otherResponse = await client.GetAsync(""); -// client.DefaultRequestHeaders.From = "user@example.com"; -// client.DefaultRequestHeaders.Pragma.Clear(); -// client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("From")); -// client.DefaultRequestHeaders.MaxForwards = 1; -// var subsequentResponse = await client.GetAsync(""); - -// await AssertFreshResponseAsync(initialResponse, subsequentResponse); -// } -// } -// } - -// [Fact] -// public async Task ServesCachedContent_IfCachedVaryByNotUpdated_OnCacheMiss() -// { -// var builders = TestUtils.CreateBuildersWithResponseCaching(contextAction: context => context.Response.Headers.Vary = context.Request.Headers.Pragma); - -// foreach (var builder in builders) -// { -// using var host = builder.Build(); - -// await host.StartAsync(); - -// using (var server = host.GetTestServer()) -// { -// var client = server.CreateClient(); -// client.DefaultRequestHeaders.From = "user@example.com"; -// client.DefaultRequestHeaders.Pragma.Clear(); -// client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("From")); -// client.DefaultRequestHeaders.MaxForwards = 1; -// var initialResponse = await client.GetAsync(""); -// client.DefaultRequestHeaders.From = "user2@example.com"; -// client.DefaultRequestHeaders.Pragma.Clear(); -// client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("From")); -// client.DefaultRequestHeaders.MaxForwards = 2; -// var otherResponse = await client.GetAsync(""); -// client.DefaultRequestHeaders.From = "user@example.com"; -// client.DefaultRequestHeaders.Pragma.Clear(); -// client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("From")); -// client.DefaultRequestHeaders.MaxForwards = 1; -// var subsequentResponse = await client.GetAsync(""); - -// await AssertCachedResponseAsync(initialResponse, subsequentResponse); -// } -// } -// } - -// [Fact] -// public async Task ServesCachedContent_IfAvailable_UsingHead_WithContentLength() -// { -// var builders = TestUtils.CreateBuildersWithResponseCaching(); - -// foreach (var builder in builders) -// { -// using var host = builder.Build(); - -// await host.StartAsync(); - -// using (var server = host.GetTestServer()) -// { -// var client = server.CreateClient(); -// var initialResponse = await client.SendAsync(TestUtils.CreateRequest("HEAD", "?contentLength=10")); -// var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest("HEAD", "?contentLength=10")); - -// await AssertCachedResponseAsync(initialResponse, subsequentResponse); -// } -// } -// } - -// private static void Assert304Headers(HttpResponseMessage initialResponse, HttpResponseMessage subsequentResponse) -// { -// // https://tools.ietf.org/html/rfc7232#section-4.1 -// // The server generating a 304 response MUST generate any of the -// // following header fields that would have been sent in a 200 (OK) -// // response to the same request: Cache-Control, Content-Location, Date, -// // ETag, Expires, and Vary. - -// Assert.Equal(initialResponse.Headers.CacheControl, subsequentResponse.Headers.CacheControl); -// Assert.Equal(initialResponse.Content.Headers.ContentLocation, subsequentResponse.Content.Headers.ContentLocation); -// Assert.Equal(initialResponse.Headers.Date, subsequentResponse.Headers.Date); -// Assert.Equal(initialResponse.Headers.ETag, subsequentResponse.Headers.ETag); -// Assert.Equal(initialResponse.Content.Headers.Expires, subsequentResponse.Content.Headers.Expires); -// Assert.Equal(initialResponse.Headers.Vary, subsequentResponse.Headers.Vary); -// } - -// private static async Task AssertCachedResponseAsync(HttpResponseMessage initialResponse, HttpResponseMessage subsequentResponse) -// { -// initialResponse.EnsureSuccessStatusCode(); -// subsequentResponse.EnsureSuccessStatusCode(); - -// foreach (var header in initialResponse.Headers) -// { -// Assert.Equal(initialResponse.Headers.GetValues(header.Key), subsequentResponse.Headers.GetValues(header.Key)); -// } -// Assert.True(subsequentResponse.Headers.Contains(HeaderNames.Age)); -// Assert.Equal(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync()); -// } - -// private static async Task AssertFreshResponseAsync(HttpResponseMessage initialResponse, HttpResponseMessage subsequentResponse) -// { -// initialResponse.EnsureSuccessStatusCode(); -// subsequentResponse.EnsureSuccessStatusCode(); - -// Assert.False(subsequentResponse.Headers.Contains(HeaderNames.Age)); - -// if (initialResponse.RequestMessage.Method == HttpMethod.Head && -// subsequentResponse.RequestMessage.Method == HttpMethod.Head) -// { -// Assert.True(initialResponse.Headers.Contains("X-Value")); -// Assert.NotEqual(initialResponse.Headers.GetValues("X-Value"), subsequentResponse.Headers.GetValues("X-Value")); -// } -// else -// { -// Assert.NotEqual(await initialResponse.Content.ReadAsStringAsync(), await subsequentResponse.Content.ReadAsStringAsync()); -// } -// } -//} diff --git a/src/Middleware/OutputCaching/test/TestUtils.cs b/src/Middleware/OutputCaching/test/TestUtils.cs index 2d57d882cb03..0f24e2c531b2 100644 --- a/src/Middleware/OutputCaching/test/TestUtils.cs +++ b/src/Middleware/OutputCaching/test/TestUtils.cs @@ -32,35 +32,9 @@ static TestUtils() private static bool TestRequestDelegate(HttpContext context, string guid) { var headers = context.Response.GetTypedHeaders(); - - var expires = context.Request.Query["Expires"]; - if (!string.IsNullOrEmpty(expires)) - { - headers.Expires = DateTimeOffset.Now.AddSeconds(int.Parse(expires, CultureInfo.InvariantCulture)); - } - - if (headers.CacheControl == null) - { - headers.CacheControl = new CacheControlHeaderValue - { - Public = true, - MaxAge = string.IsNullOrEmpty(expires) ? TimeSpan.FromSeconds(10) : (TimeSpan?)null - }; - } - else - { - headers.CacheControl.Public = true; - headers.CacheControl.MaxAge = string.IsNullOrEmpty(expires) ? TimeSpan.FromSeconds(10) : (TimeSpan?)null; - } headers.Date = DateTimeOffset.UtcNow; headers.Headers["X-Value"] = guid; - var contentLength = context.Request.Query["ContentLength"]; - if (!string.IsNullOrEmpty(contentLength)) - { - headers.ContentLength = long.Parse(contentLength, CultureInfo.InvariantCulture); - } - if (context.Request.Method != "HEAD") { return true; @@ -119,23 +93,23 @@ internal static IEnumerable CreateBuildersWithOutputCaching( Action contextAction = null) { return CreateBuildersWithOutputCaching(configureDelegate, options, new RequestDelegate[] + { + context => { - context => - { - contextAction?.Invoke(context); - return TestRequestDelegateWrite(context); - }, - context => - { - contextAction?.Invoke(context); - return TestRequestDelegateWriteAsync(context); - }, - context => - { - contextAction?.Invoke(context); - return TestRequestDelegateSendFileAsync(context); - }, - }); + contextAction?.Invoke(context); + return TestRequestDelegateWrite(context); + }, + context => + { + contextAction?.Invoke(context); + return TestRequestDelegateWriteAsync(context); + }, + context => + { + contextAction?.Invoke(context); + return TestRequestDelegateSendFileAsync(context); + }, + }); } private static IEnumerable CreateBuildersWithOutputCaching( @@ -173,6 +147,13 @@ private static IEnumerable CreateBuildersWithOutputCaching( outputCachingOptions.MaximumBodySize = options.MaximumBodySize; outputCachingOptions.UseCaseSensitivePaths = options.UseCaseSensitivePaths; outputCachingOptions.SystemClock = options.SystemClock; + outputCachingOptions.DefaultPolicy = options.DefaultPolicy; + outputCachingOptions.DefaultExpirationTimeSpan = options.DefaultExpirationTimeSpan; + outputCachingOptions.SizeLimit = options.SizeLimit; + } + else + { + outputCachingOptions.DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().Build(); } }); }) @@ -228,6 +209,8 @@ internal static OutputCachingContext CreateTestContext() { return new OutputCachingContext(new DefaultHttpContext(), NullLogger.Instance) { + AllowCacheStorage = true, + IsResponseCacheable = true, ResponseTime = DateTimeOffset.UtcNow }; } @@ -236,6 +219,8 @@ internal static OutputCachingContext CreateTestContext(HttpContext httpContext) { return new OutputCachingContext(httpContext, NullLogger.Instance) { + AllowCacheStorage = true, + IsResponseCacheable = true, ResponseTime = DateTimeOffset.UtcNow }; } @@ -244,6 +229,8 @@ internal static OutputCachingContext CreateTestContext(ITestSink testSink) { return new OutputCachingContext(new DefaultHttpContext(), new TestLogger("OutputCachingTests", testSink, true)) { + AllowCacheStorage = true, + IsResponseCacheable = true, ResponseTime = DateTimeOffset.UtcNow }; } @@ -251,7 +238,7 @@ internal static OutputCachingContext CreateTestContext(ITestSink testSink) internal static void AssertLoggedMessages(IEnumerable messages, params LoggedMessage[] expectedMessages) { var messageList = messages.ToList(); - Assert.Equal(messageList.Count, expectedMessages.Length); + Assert.Equal(expectedMessages.Length, messageList.Count); for (var i = 0; i < messageList.Count; i++) { @@ -316,6 +303,7 @@ internal class LoggedMessage internal static LoggedMessage ResponseNotCached => new LoggedMessage(27, LogLevel.Information); internal static LoggedMessage ResponseContentLengthMismatchNotCached => new LoggedMessage(28, LogLevel.Warning); internal static LoggedMessage ExpirationInfiniteMaxStaleSatisfied => new LoggedMessage(29, LogLevel.Debug); + internal static LoggedMessage ExpirationExpiresExceededNoExpiration => new LoggedMessage(30, LogLevel.Debug); private LoggedMessage(int evenId, LogLevel logLevel) { From b7b7322495a810cd7ac3cdf0c86b1ae6d5e2935a Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Wed, 25 May 2022 11:32:17 -0700 Subject: [PATCH 29/48] Update API --- .../src/IPoliciesMetadata.cs | 6 +- .../src/PublicAPI.Unshipped.txt | 2 +- .../samples/OutputCachingSample/Startup.cs | 4 +- .../OutputCaching/src/OutputCacheAttribute.cs | 25 +++------ .../src/OutputCachingPolicyProvider.cs | 15 +---- .../src/Policies/CompositePolicy.cs | 2 +- .../src/Policies/DefaultOutputCachePolicy.cs | 2 +- .../src/Policies/EnableCachingPolicy.cs | 2 +- .../src/Policies/ExpirationPolicy.cs | 2 +- .../src/Policies/LockingPolicy.cs | 2 +- .../src/Policies/NoStorePolicy.cs | 2 +- .../src/Policies/OutputCachePolicyBuilder.cs | 32 +++++------ .../src/Policies/PoliciesMetadata.cs | 9 +-- .../src/Policies/PolicyExtensions.cs | 26 +++------ .../src/Policies/PredicatePolicy.cs | 2 +- .../src/Policies/ProfilePolicy.cs | 2 +- .../src/Policies/ResponseCachingPolicy.cs | 2 +- .../OutputCaching/src/Policies/TagsPolicy.cs | 2 +- .../src/Policies/VaryByHeaderPolicy.cs | 2 +- .../src/Policies/VaryByQueryPolicy.cs | 2 +- .../src/Policies/VaryByValuePolicy.cs | 2 +- .../src/Properties/AssemblyInfo.cs | 1 + .../OutputCaching/src/PublicAPI.Unshipped.txt | 56 ++++--------------- .../test/OutputCachingPolicyProviderTests.cs | 40 ++++++------- .../OutputCaching/test/OutputCachingTests.cs | 20 +++---- .../OutputCaching/test/TestUtils.cs | 2 +- src/Mvc/Mvc.Core/src/OutputCacheAttribute.cs | 20 +++---- src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt | 2 +- 28 files changed, 108 insertions(+), 178 deletions(-) diff --git a/src/Middleware/OutputCaching.Abstractions/src/IPoliciesMetadata.cs b/src/Middleware/OutputCaching.Abstractions/src/IPoliciesMetadata.cs index f20c9bba4db6..6ae242f879b5 100644 --- a/src/Middleware/OutputCaching.Abstractions/src/IPoliciesMetadata.cs +++ b/src/Middleware/OutputCaching.Abstractions/src/IPoliciesMetadata.cs @@ -4,12 +4,12 @@ namespace Microsoft.AspNetCore.OutputCaching; /// -/// Represents policies metadata for an endpoint. +/// Represents policy metadata for an endpoint. /// public interface IPoliciesMetadata { /// - /// Gets the policies. + /// Gets the policy. /// - IReadOnlyList Policies { get; } + IOutputCachingPolicy Policy { get; } } diff --git a/src/Middleware/OutputCaching.Abstractions/src/PublicAPI.Unshipped.txt b/src/Middleware/OutputCaching.Abstractions/src/PublicAPI.Unshipped.txt index f46498d57cdc..8e58f3f77bb3 100644 --- a/src/Middleware/OutputCaching.Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/OutputCaching.Abstractions/src/PublicAPI.Unshipped.txt @@ -67,4 +67,4 @@ Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicyProvider.OnRequestAsync(M Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicyProvider.OnServeFromCacheAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicyProvider.OnServeResponseAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.OutputCaching.IPoliciesMetadata -Microsoft.AspNetCore.OutputCaching.IPoliciesMetadata.Policies.get -> System.Collections.Generic.IReadOnlyList! \ No newline at end of file +Microsoft.AspNetCore.OutputCaching.IPoliciesMetadata.Policy.get -> Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy! diff --git a/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs b/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs index c3f90d6fe8f6..a5205c8de232 100644 --- a/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs +++ b/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs @@ -52,7 +52,7 @@ { await Task.Delay(1000); await context.Response.WriteAsync($"
{requests++}
"); -}).CacheOutput(p => p.Lock(false).Expires(TimeSpan.FromMilliseconds(1))); +}).CacheOutput(p => p.Lock(false).Expire(TimeSpan.FromMilliseconds(1))); // Cached because Response Caching policy and contains "Cache-Control: public" app.MapGet("/headers", async context => @@ -68,7 +68,7 @@ // If the client sends an If-None-Match header with the etag value, the server // returns 304 if the cache entry is fresh instead of the full response - var etag = $"\"{Guid.NewGuid().ToString("n")}\""; + var etag = $"\"{Guid.NewGuid():n}\""; context.Response.Headers.ETag = etag; await Gravatar.WriteGravatar(context); diff --git a/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs b/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs index 69fd87d1e73d..5c81a8ab9ad7 100644 --- a/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs @@ -15,7 +15,7 @@ public class OutputCacheAttribute : Attribute, IPoliciesMetadata private int? _duration; private bool? _noStore; - private List? _policies; + private IOutputCachingPolicy? _policy; /// /// Gets or sets the duration in seconds for which the response is cached. @@ -58,39 +58,32 @@ public bool NoStore public string? Profile { get; set; } /// - public IReadOnlyList Policies => _policies ??= GetPolicies(); + public IOutputCachingPolicy Policy => _policy ??= GetPolicy(); - private List GetPolicies() + private IOutputCachingPolicy GetPolicy() { - var policies = new List(5); - - policies.Add(EnableCachingPolicy.Instance); + var builder = new OutputCachePolicyBuilder().Enable(); if (_noStore != null && _noStore.Value) { - policies.Add(new NoStorePolicy()); + builder.NoStore(); } if (Profile != null) { - policies.Add(new ProfilePolicy(Profile)); + builder.Profile(Profile); } if (VaryByQueryKeys != null) { - policies.Add(new VaryByQueryPolicy(VaryByQueryKeys)); - } - - if (VaryByHeaders != null) - { - policies.Add(new VaryByHeaderPolicy(VaryByHeaders)); + builder.VaryByQuery(VaryByQueryKeys); } if (_duration != null) { - policies.Add(new ExpirationPolicy(TimeSpan.FromSeconds(_duration.Value))); + builder.Expire(TimeSpan.FromSeconds(_duration.Value)); } - return policies; + return builder.Build(); } } diff --git a/src/Middleware/OutputCaching/src/OutputCachingPolicyProvider.cs b/src/Middleware/OutputCaching/src/OutputCachingPolicyProvider.cs index 6bc1a09b1c92..5cd2dd159c82 100644 --- a/src/Middleware/OutputCaching/src/OutputCachingPolicyProvider.cs +++ b/src/Middleware/OutputCaching/src/OutputCachingPolicyProvider.cs @@ -33,10 +33,7 @@ public async Task OnRequestAsync(IOutputCachingContext context) throw new InvalidOperationException("Can't define output caching policies after headers have been sent to client."); } - foreach (var policy in policiesMetadata.Policies) - { - await policy.OnRequestAsync(context); - } + await policiesMetadata.Policy.OnRequestAsync(context); } } @@ -63,10 +60,7 @@ public async Task OnServeFromCacheAsync(IOutputCachingContext context) if (policiesMetadata != null) { - foreach (var policy in policiesMetadata.Policies) - { - await policy.OnServeFromCacheAsync(context); - } + await policiesMetadata.Policy.OnServeFromCacheAsync(context); } } @@ -93,10 +87,7 @@ public async Task OnServeResponseAsync(IOutputCachingContext context) if (policiesMetadata != null) { - foreach (var policy in policiesMetadata.Policies) - { - await policy.OnServeResponseAsync(context); - } + await policiesMetadata.Policy.OnServeResponseAsync(context); } } } diff --git a/src/Middleware/OutputCaching/src/Policies/CompositePolicy.cs b/src/Middleware/OutputCaching/src/Policies/CompositePolicy.cs index 2ba4ef40f2fa..ddb4bf3f6f74 100644 --- a/src/Middleware/OutputCaching/src/Policies/CompositePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/CompositePolicy.cs @@ -6,7 +6,7 @@ namespace Microsoft.AspNetCore.OutputCaching.Policies; /// /// A composite policy. /// -public sealed class CompositePolicy : IOutputCachingPolicy +internal sealed class CompositePolicy : IOutputCachingPolicy { private readonly IOutputCachingPolicy[] _policies; diff --git a/src/Middleware/OutputCaching/src/Policies/DefaultOutputCachePolicy.cs b/src/Middleware/OutputCaching/src/Policies/DefaultOutputCachePolicy.cs index a3765358a0a6..ee099f7eeea3 100644 --- a/src/Middleware/OutputCaching/src/Policies/DefaultOutputCachePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/DefaultOutputCachePolicy.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.OutputCaching; /// /// The default policy. /// -public sealed class DefaultOutputCachePolicy : IOutputCachingPolicy +internal sealed class DefaultOutputCachePolicy : IOutputCachingPolicy { /// Task IOutputCachingPolicy.OnRequestAsync(IOutputCachingContext context) diff --git a/src/Middleware/OutputCaching/src/Policies/EnableCachingPolicy.cs b/src/Middleware/OutputCaching/src/Policies/EnableCachingPolicy.cs index 2bb9bf6291a1..df373a8cd3b6 100644 --- a/src/Middleware/OutputCaching/src/Policies/EnableCachingPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/EnableCachingPolicy.cs @@ -6,7 +6,7 @@ namespace Microsoft.AspNetCore.OutputCaching; /// /// A policy that enables caching /// -public sealed class EnableCachingPolicy : IOutputCachingPolicy +internal sealed class EnableCachingPolicy : IOutputCachingPolicy { /// /// Default instance of . diff --git a/src/Middleware/OutputCaching/src/Policies/ExpirationPolicy.cs b/src/Middleware/OutputCaching/src/Policies/ExpirationPolicy.cs index 023ff05da3f4..a506c9a6048a 100644 --- a/src/Middleware/OutputCaching/src/Policies/ExpirationPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/ExpirationPolicy.cs @@ -6,7 +6,7 @@ namespace Microsoft.AspNetCore.OutputCaching; /// /// A policy that defines a custom expiration duration. /// -public sealed class ExpirationPolicy : IOutputCachingPolicy +internal sealed class ExpirationPolicy : IOutputCachingPolicy { private readonly TimeSpan _expiration; diff --git a/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs b/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs index 3c8022fc9b30..92dfd26c155e 100644 --- a/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs @@ -6,7 +6,7 @@ namespace Microsoft.AspNetCore.OutputCaching; /// /// A policy that changes the locking behavior. /// -public class LockingPolicy : IOutputCachingPolicy +internal class LockingPolicy : IOutputCachingPolicy { private readonly bool _lockResponse; diff --git a/src/Middleware/OutputCaching/src/Policies/NoStorePolicy.cs b/src/Middleware/OutputCaching/src/Policies/NoStorePolicy.cs index 11f8401111c7..8a0dfe655419 100644 --- a/src/Middleware/OutputCaching/src/Policies/NoStorePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/NoStorePolicy.cs @@ -6,7 +6,7 @@ namespace Microsoft.AspNetCore.OutputCaching; /// /// A policy that prevents the response from being cached. /// -public class NoStorePolicy : IOutputCachingPolicy +internal class NoStorePolicy : IOutputCachingPolicy { /// Task IOutputCachingPolicy.OnServeResponseAsync(IOutputCachingContext context) diff --git a/src/Middleware/OutputCaching/src/Policies/OutputCachePolicyBuilder.cs b/src/Middleware/OutputCaching/src/Policies/OutputCachePolicyBuilder.cs index f64d64cd411a..e6a3a9ba2362 100644 --- a/src/Middleware/OutputCaching/src/Policies/OutputCachePolicyBuilder.cs +++ b/src/Middleware/OutputCaching/src/Policies/OutputCachePolicyBuilder.cs @@ -18,25 +18,19 @@ public class OutputCachePolicyBuilder /// /// Gets an initialized with a instance. /// - public static OutputCachePolicyBuilder Default + public OutputCachePolicyBuilder Default() { - get - { - var builder = new OutputCachePolicyBuilder(); - builder.Policies.Add(new DefaultOutputCachePolicy()); - return builder; - } + Policies.Add(new DefaultOutputCachePolicy()); + return this; } /// - /// Gets an empty . + /// Adds a policy instance. /// - public static OutputCachePolicyBuilder Empty + public OutputCachePolicyBuilder Add(IOutputCachingPolicy policy) { - get - { - return new OutputCachePolicyBuilder(); - } + Policies.Add(policy); + return this; } /// @@ -52,7 +46,7 @@ public OutputCachePolicyBuilder Enable() /// Adds a requirement to the current policy. /// /// The predicate applied to the policy. - public OutputCachePolicyBuilder When(Func> predicate) + public OutputCachePolicyBuilder WithCondition(Func> predicate) { Requirements.Add(predicate); return this; @@ -62,7 +56,7 @@ public OutputCachePolicyBuilder When(Func> pre /// Adds a requirement to the current policy based on the request path. /// /// The base path to limit the policy to. - public OutputCachePolicyBuilder Path(PathString pathBase) + public OutputCachePolicyBuilder WithPath(PathString pathBase) { ArgumentNullException.ThrowIfNull(pathBase); @@ -78,7 +72,7 @@ public OutputCachePolicyBuilder Path(PathString pathBase) /// Adds a requirement to the current policy based on the request path. /// /// The base paths to limit the policy to. - public OutputCachePolicyBuilder Path(params PathString[] pathBases) + public OutputCachePolicyBuilder WithPath(params PathString[] pathBases) { ArgumentNullException.ThrowIfNull(pathBases); @@ -94,7 +88,7 @@ public OutputCachePolicyBuilder Path(params PathString[] pathBases) /// Adds a requirement to the current policy based on the request method. ///
/// The method to limit the policy to. - public OutputCachePolicyBuilder Method(string method) + public OutputCachePolicyBuilder WithMethod(string method) { ArgumentNullException.ThrowIfNull(method); @@ -111,7 +105,7 @@ public OutputCachePolicyBuilder Method(string method) /// Adds a requirement to the current policy based on the request method. ///
/// The methods to limit the policy to. - public OutputCachePolicyBuilder Method(params string[] methods) + public OutputCachePolicyBuilder WithMethod(params string[] methods) { ArgumentNullException.ThrowIfNull(methods); @@ -225,7 +219,7 @@ public OutputCachePolicyBuilder Tag(params string[] tags) /// Adds a policy to change the cached response expiration. ///
/// The expiration of the cached reponse. - public OutputCachePolicyBuilder Expires(TimeSpan expiration) + public OutputCachePolicyBuilder Expire(TimeSpan expiration) { Policies.Add(new ExpirationPolicy(expiration)); return this; diff --git a/src/Middleware/OutputCaching/src/Policies/PoliciesMetadata.cs b/src/Middleware/OutputCaching/src/Policies/PoliciesMetadata.cs index 047dacc8a25c..4990fd8138e6 100644 --- a/src/Middleware/OutputCaching/src/Policies/PoliciesMetadata.cs +++ b/src/Middleware/OutputCaching/src/Policies/PoliciesMetadata.cs @@ -5,9 +5,10 @@ namespace Microsoft.AspNetCore.OutputCaching.Policies; internal sealed class PoliciesMetadata : IPoliciesMetadata { - private readonly List _policies = new(); + public PoliciesMetadata(IOutputCachingPolicy policy) + { + Policy = policy; + } - public IReadOnlyList Policies => _policies; - - public void Add(IOutputCachingPolicy policy) => _policies.Add(policy); + public IOutputCachingPolicy Policy { get; } } diff --git a/src/Middleware/OutputCaching/src/Policies/PolicyExtensions.cs b/src/Middleware/OutputCaching/src/Policies/PolicyExtensions.cs index 3aea6a271d0f..30bf9e6055f0 100644 --- a/src/Middleware/OutputCaching/src/Policies/PolicyExtensions.cs +++ b/src/Middleware/OutputCaching/src/Policies/PolicyExtensions.cs @@ -17,10 +17,8 @@ public static TBuilder CacheOutput(this TBuilder builder) where TBuild { ArgumentNullException.ThrowIfNull(builder); - var policiesMetadata = new PoliciesMetadata(); - // Enable caching if this method is invoked on an endpoint, extra policies can disable it - policiesMetadata.Add(EnableCachingPolicy.Instance); + var policiesMetadata = new PoliciesMetadata(EnableCachingPolicy.Instance); builder.Add(endpointBuilder => { @@ -30,22 +28,14 @@ public static TBuilder CacheOutput(this TBuilder builder) where TBuild } /// - /// Marks an endpoint to be cached with the specified policies. + /// Marks an endpoint to be cached with the specified policy. /// - public static TBuilder CacheOutput(this TBuilder builder, params IOutputCachingPolicy[] items) where TBuilder : IEndpointConventionBuilder + public static TBuilder CacheOutput(this TBuilder builder, IOutputCachingPolicy policy) where TBuilder : IEndpointConventionBuilder { ArgumentNullException.ThrowIfNull(builder); - ArgumentNullException.ThrowIfNull(items); - - var policiesMetadata = new PoliciesMetadata(); // Enable caching if this method is invoked on an endpoint, extra policies can disable it - policiesMetadata.Add(EnableCachingPolicy.Instance); - - foreach (var item in items) - { - policiesMetadata.Add(item); - } + var policiesMetadata = new PoliciesMetadata(new OutputCachePolicyBuilder().Enable().Add(policy).Build()); builder.Add(endpointBuilder => { @@ -55,7 +45,7 @@ public static TBuilder CacheOutput(this TBuilder builder, params IOutp } /// - /// Marks an endpoint to be cached with the specified policy. + /// Marks an endpoint to be cached using the specified policy builder. /// public static TBuilder CacheOutput(this TBuilder builder, Action policy) where TBuilder : IEndpointConventionBuilder { @@ -64,12 +54,10 @@ public static TBuilder CacheOutput(this TBuilder builder, Action { diff --git a/src/Middleware/OutputCaching/src/Policies/PredicatePolicy.cs b/src/Middleware/OutputCaching/src/Policies/PredicatePolicy.cs index 171c1f66cfd0..88aa8317fd2e 100644 --- a/src/Middleware/OutputCaching/src/Policies/PredicatePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/PredicatePolicy.cs @@ -6,7 +6,7 @@ namespace Microsoft.AspNetCore.OutputCaching.Policies; /// /// A policy that adds a requirement to another policy. /// -public sealed class PredicatePolicy : IOutputCachingPolicy +internal sealed class PredicatePolicy : IOutputCachingPolicy { // TODO: Accept a non async predicate too? diff --git a/src/Middleware/OutputCaching/src/Policies/ProfilePolicy.cs b/src/Middleware/OutputCaching/src/Policies/ProfilePolicy.cs index 08f9abea8f6c..d213efb81462 100644 --- a/src/Middleware/OutputCaching/src/Policies/ProfilePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/ProfilePolicy.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.OutputCaching; /// /// A policy represented by a named profile. /// -public sealed class ProfilePolicy : IOutputCachingPolicy +internal sealed class ProfilePolicy : IOutputCachingPolicy { private readonly string _profileName; diff --git a/src/Middleware/OutputCaching/src/Policies/ResponseCachingPolicy.cs b/src/Middleware/OutputCaching/src/Policies/ResponseCachingPolicy.cs index eb6a21e74c6b..14725001d220 100644 --- a/src/Middleware/OutputCaching/src/Policies/ResponseCachingPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/ResponseCachingPolicy.cs @@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.OutputCaching; /// /// Provides the policy implemented by Response caching /// -public sealed class ResponseCachingPolicy : IOutputCachingPolicy +internal sealed class ResponseCachingPolicy : IOutputCachingPolicy { /// Task IOutputCachingPolicy.OnRequestAsync(IOutputCachingContext context) diff --git a/src/Middleware/OutputCaching/src/Policies/TagsPolicy.cs b/src/Middleware/OutputCaching/src/Policies/TagsPolicy.cs index 8b3db6076082..62b45682ed3b 100644 --- a/src/Middleware/OutputCaching/src/Policies/TagsPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/TagsPolicy.cs @@ -6,7 +6,7 @@ namespace Microsoft.AspNetCore.OutputCaching; /// /// A policy that defines custom tags on the cache entry. /// -public sealed class TagsPolicy : IOutputCachingPolicy +internal sealed class TagsPolicy : IOutputCachingPolicy { private readonly string[] _tags; diff --git a/src/Middleware/OutputCaching/src/Policies/VaryByHeaderPolicy.cs b/src/Middleware/OutputCaching/src/Policies/VaryByHeaderPolicy.cs index f80c56dc07d4..93b178030fac 100644 --- a/src/Middleware/OutputCaching/src/Policies/VaryByHeaderPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/VaryByHeaderPolicy.cs @@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.OutputCaching; /// /// When applied, the cached content will be different for every value of the provided headers. /// -public sealed class VaryByHeaderPolicy : IOutputCachingPolicy +internal sealed class VaryByHeaderPolicy : IOutputCachingPolicy { private StringValues _headers { get; set; } diff --git a/src/Middleware/OutputCaching/src/Policies/VaryByQueryPolicy.cs b/src/Middleware/OutputCaching/src/Policies/VaryByQueryPolicy.cs index a6b2086eeb2e..066f17071ce3 100644 --- a/src/Middleware/OutputCaching/src/Policies/VaryByQueryPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/VaryByQueryPolicy.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.OutputCaching; /// When applied, the cached content will be different for every value of the provided query string keys. /// It also disables the default behavior which is to vary on all query string keys. /// -public sealed class VaryByQueryPolicy : IOutputCachingPolicy +internal sealed class VaryByQueryPolicy : IOutputCachingPolicy { private StringValues _queryKeys { get; set; } diff --git a/src/Middleware/OutputCaching/src/Policies/VaryByValuePolicy.cs b/src/Middleware/OutputCaching/src/Policies/VaryByValuePolicy.cs index 88799213535f..9c2a4a490b2e 100644 --- a/src/Middleware/OutputCaching/src/Policies/VaryByValuePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/VaryByValuePolicy.cs @@ -6,7 +6,7 @@ namespace Microsoft.AspNetCore.OutputCaching; /// /// When applied, the cached content will be different for every provided value. /// -public sealed class VaryByValuePolicy : IOutputCachingPolicy +internal sealed class VaryByValuePolicy : IOutputCachingPolicy { private readonly Action? _varyBy; private readonly Func? _varyByAsync; diff --git a/src/Middleware/OutputCaching/src/Properties/AssemblyInfo.cs b/src/Middleware/OutputCaching/src/Properties/AssemblyInfo.cs index a185ed9b3c63..b2ae4af0f142 100644 --- a/src/Middleware/OutputCaching/src/Properties/AssemblyInfo.cs +++ b/src/Middleware/OutputCaching/src/Properties/AssemblyInfo.cs @@ -4,3 +4,4 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Microsoft.AspNetCore.OutputCaching.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] +[assembly: InternalsVisibleTo("OutputCachingSample, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt b/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt index 15930d558c34..206a093a9560 100644 --- a/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt @@ -1,21 +1,12 @@ #nullable enable Microsoft.AspNetCore.Builder.OutputCachingExtensions -Microsoft.AspNetCore.OutputCaching.DefaultOutputCachePolicy -Microsoft.AspNetCore.OutputCaching.DefaultOutputCachePolicy.DefaultOutputCachePolicy() -> void -Microsoft.AspNetCore.OutputCaching.EnableCachingPolicy -Microsoft.AspNetCore.OutputCaching.EnableCachingPolicy.EnableCachingPolicy() -> void -Microsoft.AspNetCore.OutputCaching.ExpirationPolicy -Microsoft.AspNetCore.OutputCaching.ExpirationPolicy.ExpirationPolicy(System.TimeSpan expiration) -> void -Microsoft.AspNetCore.OutputCaching.LockingPolicy -Microsoft.AspNetCore.OutputCaching.NoStorePolicy -Microsoft.AspNetCore.OutputCaching.NoStorePolicy.NoStorePolicy() -> void Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.Duration.get -> int Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.Duration.set -> void Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.NoStore.get -> bool Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.NoStore.set -> void Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.OutputCacheAttribute() -> void -Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.Policies.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.Policy.get -> Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy! Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.Profile.get -> string? Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.Profile.set -> void Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.VaryByHeaders.get -> string![]? @@ -23,17 +14,15 @@ Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.VaryByHeaders.set -> voi Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.VaryByQueryKeys.get -> string![]? Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.VaryByQueryKeys.set -> void Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Add(Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy! policy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Build() -> Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Clear() -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Default() -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Enable() -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! -Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Expires(System.TimeSpan expiration) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Expire(System.TimeSpan expiration) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Lock(bool lockResponse = true) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! -Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Method(params string![]! methods) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! -Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Method(string! method) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.NoStore() -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.OutputCachePolicyBuilder() -> void -Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Path(Microsoft.AspNetCore.Http.PathString pathBase) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! -Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Path(params Microsoft.AspNetCore.Http.PathString[]! pathBases) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Profile(string! profileName) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Tag(params string![]! tags) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByHeader(params string![]! headers) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! @@ -42,7 +31,11 @@ Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.F Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.Func!>! varyBy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.Func!>! varyBy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.Func! varyBy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! -Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.When(System.Func!>! predicate) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.WithCondition(System.Func!>! predicate) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.WithMethod(params string![]! methods) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.WithMethod(string! method) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.WithPath(Microsoft.AspNetCore.Http.PathString pathBase) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.WithPath(params Microsoft.AspNetCore.Http.PathString[]! pathBases) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachingFeature Microsoft.AspNetCore.OutputCaching.OutputCachingFeature.Context.get -> Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! Microsoft.AspNetCore.OutputCaching.OutputCachingFeature.OutputCachingFeature(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> void @@ -63,40 +56,11 @@ Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.SizeLimit.get -> long Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.SizeLimit.set -> void Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.UseCaseSensitivePaths.get -> bool Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.UseCaseSensitivePaths.set -> void -Microsoft.AspNetCore.OutputCaching.Policies.CompositePolicy -Microsoft.AspNetCore.OutputCaching.Policies.CompositePolicy.CompositePolicy(params Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy![]! policies) -> void Microsoft.AspNetCore.OutputCaching.Policies.PolicyExtensions -Microsoft.AspNetCore.OutputCaching.Policies.PredicatePolicy -Microsoft.AspNetCore.OutputCaching.Policies.PredicatePolicy.PredicatePolicy(System.Func!>! predicate, Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy! policy) -> void -Microsoft.AspNetCore.OutputCaching.ProfilePolicy -Microsoft.AspNetCore.OutputCaching.ProfilePolicy.ProfilePolicy(string! profileName) -> void -Microsoft.AspNetCore.OutputCaching.ResponseCachingPolicy -Microsoft.AspNetCore.OutputCaching.ResponseCachingPolicy.ResponseCachingPolicy() -> void -Microsoft.AspNetCore.OutputCaching.TagsPolicy -Microsoft.AspNetCore.OutputCaching.TagsPolicy.TagsPolicy(params string![]! tags) -> void -Microsoft.AspNetCore.OutputCaching.VaryByHeaderPolicy -Microsoft.AspNetCore.OutputCaching.VaryByHeaderPolicy.VaryByHeaderPolicy() -> void -Microsoft.AspNetCore.OutputCaching.VaryByHeaderPolicy.VaryByHeaderPolicy(params string![]! headers) -> void -Microsoft.AspNetCore.OutputCaching.VaryByHeaderPolicy.VaryByHeaderPolicy(string! header) -> void -Microsoft.AspNetCore.OutputCaching.VaryByQueryPolicy -Microsoft.AspNetCore.OutputCaching.VaryByQueryPolicy.VaryByQueryPolicy() -> void -Microsoft.AspNetCore.OutputCaching.VaryByQueryPolicy.VaryByQueryPolicy(params string![]! queryKeys) -> void -Microsoft.AspNetCore.OutputCaching.VaryByQueryPolicy.VaryByQueryPolicy(string! queryKey) -> void -Microsoft.AspNetCore.OutputCaching.VaryByValuePolicy -Microsoft.AspNetCore.OutputCaching.VaryByValuePolicy.VaryByValuePolicy() -> void -Microsoft.AspNetCore.OutputCaching.VaryByValuePolicy.VaryByValuePolicy(System.Func<(string!, string!)>! varyBy) -> void -Microsoft.AspNetCore.OutputCaching.VaryByValuePolicy.VaryByValuePolicy(System.Func!>! varyBy) -> void -Microsoft.AspNetCore.OutputCaching.VaryByValuePolicy.VaryByValuePolicy(System.Func!>! varyBy) -> void -Microsoft.AspNetCore.OutputCaching.VaryByValuePolicy.VaryByValuePolicy(System.Func! varyBy) -> void Microsoft.Extensions.DependencyInjection.OutputCachingServicesExtensions static Microsoft.AspNetCore.Builder.OutputCachingExtensions.UseOutputCaching(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! -static Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Default.get -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! -static Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Empty.get -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! static Microsoft.AspNetCore.OutputCaching.Policies.PolicyExtensions.CacheOutput(this TBuilder builder) -> TBuilder +static Microsoft.AspNetCore.OutputCaching.Policies.PolicyExtensions.CacheOutput(this TBuilder builder, Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy! policy) -> TBuilder static Microsoft.AspNetCore.OutputCaching.Policies.PolicyExtensions.CacheOutput(this TBuilder builder, System.Action! policy) -> TBuilder -static Microsoft.AspNetCore.OutputCaching.Policies.PolicyExtensions.CacheOutput(this TBuilder builder, params Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy![]! items) -> TBuilder static Microsoft.Extensions.DependencyInjection.OutputCachingServicesExtensions.AddOutputCaching(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.Extensions.DependencyInjection.OutputCachingServicesExtensions.AddOutputCaching(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static readonly Microsoft.AspNetCore.OutputCaching.EnableCachingPolicy.Instance -> Microsoft.AspNetCore.OutputCaching.EnableCachingPolicy! -static readonly Microsoft.AspNetCore.OutputCaching.LockingPolicy.Disabled -> Microsoft.AspNetCore.OutputCaching.LockingPolicy! -static readonly Microsoft.AspNetCore.OutputCaching.LockingPolicy.Enabled -> Microsoft.AspNetCore.OutputCaching.LockingPolicy! diff --git a/src/Middleware/OutputCaching/test/OutputCachingPolicyProviderTests.cs b/src/Middleware/OutputCaching/test/OutputCachingPolicyProviderTests.cs index 19672b6a2aa9..13add071d267 100644 --- a/src/Middleware/OutputCaching/test/OutputCachingPolicyProviderTests.cs +++ b/src/Middleware/OutputCaching/test/OutputCachingPolicyProviderTests.cs @@ -47,7 +47,7 @@ public async Task AttemptOutputCaching_CacheableMethods_Allowed(string method) { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - var options = new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().Build() }; + var options = new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().Build() }; context.HttpContext.Request.Method = method; await new OutputCachingPolicyProvider(Options.Create(options)).OnRequestAsync(context); @@ -62,7 +62,7 @@ public async Task AttemptOutputCaching_UncacheableMethods_NotAllowed(string meth { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - var options = new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().Build() }; + var options = new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().Build() }; context.HttpContext.Request.Method = method; await new OutputCachingPolicyProvider(Options.Create(options)).OnRequestAsync(context); @@ -81,7 +81,7 @@ public async Task AttemptResponseCaching_AuthorizationHeaders_NotAllowed() context.HttpContext.Request.Method = HttpMethods.Get; context.HttpContext.Request.Headers.Authorization = "Placeholder"; - var options = new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().Build() }; + var options = new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().Build() }; await new OutputCachingPolicyProvider(Options.Create(options)).OnRequestAsync(context); @@ -103,7 +103,7 @@ public async Task AllowCacheStorage_NoStore_Allowed() NoStore = true }.ToString(); - var options = new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().Build() }; + var options = new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().Build() }; await new OutputCachingPolicyProvider(Options.Create(options)).OnRequestAsync(context); Assert.True(context.AllowCacheStorage); @@ -119,7 +119,7 @@ public async Task AllowCacheLookup_LegacyDirectives_OverridenByCacheControl() context.HttpContext.Request.Headers.Pragma = "no-cache"; context.HttpContext.Request.Headers.CacheControl = "max-age=10"; - var options = new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().Build() }; + var options = new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().Build() }; await new OutputCachingPolicyProvider(Options.Create(options)).OnRequestAsync(context); Assert.True(context.AllowCacheLookup); @@ -132,7 +132,7 @@ public async Task IsResponseCacheable_NoPublic_Allowed() var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - var options = new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().Build() }; + var options = new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().Build() }; await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); Assert.True(context.IsResponseCacheable); @@ -149,7 +149,7 @@ public async Task IsResponseCacheable_Public_Allowed() Public = true }.ToString(); - var options = new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().Build() }; + var options = new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().Build() }; await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); Assert.True(context.IsResponseCacheable); @@ -167,7 +167,7 @@ public async Task IsResponseCacheable_NoCache_Allowed() NoCache = true }.ToString(); - var options = new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().Build() }; + var options = new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().Build() }; await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); Assert.True(context.IsResponseCacheable); @@ -185,7 +185,7 @@ public async Task IsResponseCacheable_ResponseNoStore_Allowed() NoStore = true }.ToString(); - var options = new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().Build() }; + var options = new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().Build() }; await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); Assert.True(context.IsResponseCacheable); @@ -203,7 +203,7 @@ public async Task IsResponseCacheable_SetCookieHeader_NotAllowed() }.ToString(); context.HttpContext.Response.Headers.SetCookie = "cookieName=cookieValue"; - var options = new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().Build() }; + var options = new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().Build() }; await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); Assert.False(context.IsResponseCacheable); @@ -223,7 +223,7 @@ public async Task IsResponseCacheable_VaryHeaderByStar_Allowed() }.ToString(); context.HttpContext.Response.Headers.Vary = "*"; - var options = new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().Build() }; + var options = new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().Build() }; await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); Assert.True(context.IsResponseCacheable); @@ -241,7 +241,7 @@ public async Task IsResponseCacheable_Private_Allowed() Private = true }.ToString(); - var options = new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().Build() }; + var options = new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().Build() }; await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); Assert.True(context.IsResponseCacheable); @@ -260,7 +260,7 @@ public async Task IsResponseCacheable_SuccessStatusCodes_Allowed(int statusCode) Public = true }.ToString(); - var options = new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().Build() }; + var options = new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().Build() }; await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); Assert.True(context.IsResponseCacheable); @@ -339,7 +339,7 @@ public async Task IsResponseCacheable_NonSuccessStatusCodes_NotAllowed(int statu Public = true }.ToString(); - var options = new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().Build() }; + var options = new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().Build() }; await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); Assert.False(context.IsResponseCacheable); @@ -363,7 +363,7 @@ public async Task IsResponseCacheable_NoExpiryRequirements_IsAllowed() context.HttpContext.Response.Headers.Date = HeaderUtilities.FormatDate(utcNow); context.ResponseTime = DateTimeOffset.MaxValue; - var options = new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().Build() }; + var options = new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().Build() }; await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); Assert.True(context.IsResponseCacheable); @@ -386,7 +386,7 @@ public async Task IsResponseCacheable_MaxAgeOverridesExpiry_ToAllowed() context.HttpContext.Response.Headers.Date = HeaderUtilities.FormatDate(utcNow); context.ResponseTime = utcNow + TimeSpan.FromSeconds(9); - var options = new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().Build() }; + var options = new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().Build() }; await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); Assert.True(context.IsResponseCacheable); @@ -409,7 +409,7 @@ public async Task IsResponseCacheable_SharedMaxAgeOverridesMaxAge_ToAllowed() context.HttpContext.Response.Headers.Date = HeaderUtilities.FormatDate(utcNow); context.ResponseTime = utcNow + TimeSpan.FromSeconds(11); - var options = new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().Build() }; + var options = new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().Build() }; await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); Assert.True(context.IsResponseCacheable); @@ -425,7 +425,7 @@ public async Task IsCachedEntryFresh_NoExpiryRequirements_IsFresh() context.ResponseTime = DateTimeOffset.MaxValue; context.CachedEntryAge = TimeSpan.MaxValue; - var options = new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().Build() }; + var options = new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().Build() }; await new OutputCachingPolicyProvider(Options.Create(options)).OnServeFromCacheAsync(context); Assert.True(context.IsCacheEntryFresh); @@ -441,7 +441,7 @@ public async Task IsCachedEntryFresh_AtExpiry_IsNotFresh() context.ResponseTime = utcNow; context.CachedEntryAge = TimeSpan.Zero; - var options = new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().Build() }; + var options = new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().Build() }; await new OutputCachingPolicyProvider(Options.Create(options)).OnServeFromCacheAsync(context); Assert.False(context.IsCacheEntryFresh); @@ -467,7 +467,7 @@ public async Task IsCachedEntryFresh_SharedMaxAgeOverridesMaxAge_ToFresh() }.ToString(); context.CachedResponseHeaders[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow); - var options = new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().Build() }; + var options = new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().Build() }; await new OutputCachingPolicyProvider(Options.Create(options)).OnServeFromCacheAsync(context); Assert.True(context.IsCacheEntryFresh); diff --git a/src/Middleware/OutputCaching/test/OutputCachingTests.cs b/src/Middleware/OutputCaching/test/OutputCachingTests.cs index 172dfd1d2e92..ab6ef268848b 100644 --- a/src/Middleware/OutputCaching/test/OutputCachingTests.cs +++ b/src/Middleware/OutputCaching/test/OutputCachingTests.cs @@ -221,7 +221,7 @@ public async Task ServesFreshContent_If_ResponseExpired(string method) { var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions { - DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().VaryByHeader(HeaderNames.From).Build(), + DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().VaryByHeader(HeaderNames.From).Build(), DefaultExpirationTimeSpan = TimeSpan.FromMicroseconds(100) }); @@ -294,7 +294,7 @@ public async Task ServesCachedContent_IfVaryHeader_Matches() [Fact] public async Task ServesFreshContent_IfVaryHeader_Mismatches() { - var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().VaryByHeader(HeaderNames.From).Build() }); + var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().VaryByHeader(HeaderNames.From).Build() }); foreach (var builder in builders) { @@ -318,7 +318,7 @@ public async Task ServesFreshContent_IfVaryHeader_Mismatches() [Fact] public async Task ServesCachedContent_IfVaryQueryKeys_Matches() { - var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().VaryByQuery("query").Build() }); + var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().VaryByQuery("query").Build() }); foreach (var builder in builders) { @@ -340,7 +340,7 @@ public async Task ServesCachedContent_IfVaryQueryKeys_Matches() [Fact] public async Task ServesCachedContent_IfVaryQueryKeysExplicit_Matches_QueryKeyCaseInsensitive() { - var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().VaryByQuery("QueryA", "queryb").Build() }); + var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().VaryByQuery("QueryA", "queryb").Build() }); foreach (var builder in builders) { @@ -362,7 +362,7 @@ public async Task ServesCachedContent_IfVaryQueryKeysExplicit_Matches_QueryKeyCa [Fact] public async Task ServesCachedContent_IfVaryQueryKeyStar_Matches_QueryKeyCaseInsensitive() { - var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().VaryByQuery("*").Build() }); + var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().VaryByQuery("*").Build() }); foreach (var builder in builders) { @@ -384,7 +384,7 @@ public async Task ServesCachedContent_IfVaryQueryKeyStar_Matches_QueryKeyCaseIns [Fact] public async Task ServesCachedContent_IfVaryQueryKeyExplicit_Matches_OrderInsensitive() { - var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().VaryByQuery("QueryB", "QueryA").Build() }); + var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().VaryByQuery("QueryB", "QueryA").Build() }); foreach (var builder in builders) { @@ -406,7 +406,7 @@ public async Task ServesCachedContent_IfVaryQueryKeyExplicit_Matches_OrderInsens [Fact] public async Task ServesCachedContent_IfVaryQueryKeyStar_Matches_OrderInsensitive() { - var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().VaryByQuery("*").Build() }); + var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().VaryByQuery("*").Build() }); foreach (var builder in builders) { @@ -428,7 +428,7 @@ public async Task ServesCachedContent_IfVaryQueryKeyStar_Matches_OrderInsensitiv [Fact] public async Task ServesFreshContent_IfVaryQueryKey_Mismatches() { - var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().VaryByQuery("query").Build() }); + var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().VaryByQuery("query").Build() }); foreach (var builder in builders) { @@ -450,7 +450,7 @@ public async Task ServesFreshContent_IfVaryQueryKey_Mismatches() [Fact] public async Task ServesCachedContent_IfOtherVaryQueryKey_Mismatches() { - var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions { DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().VaryByQuery("query").Build() }); + var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().VaryByQuery("query").Build() }); foreach (var builder in builders) { @@ -774,7 +774,7 @@ public async Task ServesCachedContent_IfBodySize_IsCacheable() { var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions() { - DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().Build(), + DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().Build(), MaximumBodySize = 1000 }); diff --git a/src/Middleware/OutputCaching/test/TestUtils.cs b/src/Middleware/OutputCaching/test/TestUtils.cs index 0f24e2c531b2..068c672444d9 100644 --- a/src/Middleware/OutputCaching/test/TestUtils.cs +++ b/src/Middleware/OutputCaching/test/TestUtils.cs @@ -153,7 +153,7 @@ private static IEnumerable CreateBuildersWithOutputCaching( } else { - outputCachingOptions.DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().Build(); + outputCachingOptions.DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().Build(); } }); }) diff --git a/src/Mvc/Mvc.Core/src/OutputCacheAttribute.cs b/src/Mvc/Mvc.Core/src/OutputCacheAttribute.cs index db515d6e6f61..0cbf0bcdb69b 100644 --- a/src/Mvc/Mvc.Core/src/OutputCacheAttribute.cs +++ b/src/Mvc/Mvc.Core/src/OutputCacheAttribute.cs @@ -18,7 +18,7 @@ public class OutputCacheAttribute : Attribute, IOrderedFilter, IPoliciesMetadata private int? _duration; private bool? _noStore; - private List? _policies; + private IOutputCachingPolicy? _policy; /// /// Gets or sets the duration in seconds for which the response is cached. @@ -56,34 +56,32 @@ public bool NoStore public int Order { get; set; } /// - public IReadOnlyList Policies => _policies ??= GetPolicies(); + public IOutputCachingPolicy Policy => _policy ??= GetPolicy(); - private List GetPolicies() + private IOutputCachingPolicy GetPolicy() { - var policies = new List(5); - - policies.Add(EnableCachingPolicy.Instance); + var builder = new OutputCachePolicyBuilder().Enable(); if (_noStore != null && _noStore.Value) { - policies.Add(new NoStorePolicy()); + builder.NoStore(); } if (Profile != null) { - policies.Add(new ProfilePolicy(Profile)); + builder.Profile(Profile); } if (VaryByQueryKeys != null) { - policies.Add(new VaryByQueryPolicy(VaryByQueryKeys)); + builder.VaryByQuery(VaryByQueryKeys); } if (_duration != null) { - policies.Add(new ExpirationPolicy(TimeSpan.FromSeconds(_duration.Value))); + builder.Expire(TimeSpan.FromSeconds(_duration.Value)); } - return policies; + return builder.Build(); } } diff --git a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt index dbf2f58a27f6..9e61bad2f36e 100644 --- a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt +++ b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt @@ -21,7 +21,7 @@ Microsoft.AspNetCore.Mvc.OutputCacheAttribute.NoStore.set -> void Microsoft.AspNetCore.Mvc.OutputCacheAttribute.Order.get -> int Microsoft.AspNetCore.Mvc.OutputCacheAttribute.Order.set -> void Microsoft.AspNetCore.Mvc.OutputCacheAttribute.OutputCacheAttribute() -> void -Microsoft.AspNetCore.Mvc.OutputCacheAttribute.Policies.get -> System.Collections.Generic.IReadOnlyList! +Microsoft.AspNetCore.Mvc.OutputCacheAttribute.Policy.get -> Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy! Microsoft.AspNetCore.Mvc.OutputCacheAttribute.Profile.get -> string? Microsoft.AspNetCore.Mvc.OutputCacheAttribute.Profile.set -> void Microsoft.AspNetCore.Mvc.OutputCacheAttribute.VaryByQueryKeys.get -> string![]? From f70712000a2eb02e59bbb9dc17259cb3f77d95fb Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Wed, 1 Jun 2022 08:49:05 -0700 Subject: [PATCH 30/48] Clean up api --- .../src/IOutputCachingContext.cs | 24 +- .../src/PublicAPI.Unshipped.txt | 4 - .../samples/OutputCachingSample/Startup.cs | 8 - .../OutputCaching/src/OutputCachingContext.cs | 68 ----- .../src/OutputCachingMiddleware.cs | 16 +- .../src/Policies/ResponseCachingPolicy.cs | 278 ------------------ 6 files changed, 6 insertions(+), 392 deletions(-) delete mode 100644 src/Middleware/OutputCaching/src/Policies/ResponseCachingPolicy.cs diff --git a/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingContext.cs b/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingContext.cs index 4e9561100008..af9d39aa275d 100644 --- a/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingContext.cs +++ b/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingContext.cs @@ -26,26 +26,6 @@ public interface IOutputCachingContext /// DateTimeOffset? ResponseTime { get; } - /// - /// Gets the response date. - /// - DateTimeOffset? ResponseDate { get; } - - /// - /// Gets the response expiration. - /// - DateTimeOffset? ResponseExpires { get; } - - /// - /// Gets the response shared max age. - /// - TimeSpan? ResponseSharedMaxAge { get; } - - /// - /// Gets the response max age. - /// - TimeSpan? ResponseMaxAge { get; } - /// /// Gets the cached response headers. /// @@ -67,9 +47,9 @@ public interface IOutputCachingContext ILogger Logger { get; } /// - /// Gets or sets the custom expiration timespan for the response + /// Gets or sets the amount of time the response should be cached for. /// - public TimeSpan? ResponseExpirationTimeSpan { get; set; } + TimeSpan? ResponseExpirationTimeSpan { get; set; } /// /// Determines whether the output caching logic should be configured for the incoming HTTP request. diff --git a/src/Middleware/OutputCaching.Abstractions/src/PublicAPI.Unshipped.txt b/src/Middleware/OutputCaching.Abstractions/src/PublicAPI.Unshipped.txt index 8e58f3f77bb3..39c18cb4ace9 100644 --- a/src/Middleware/OutputCaching.Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/OutputCaching.Abstractions/src/PublicAPI.Unshipped.txt @@ -47,12 +47,8 @@ Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.IsCacheEntryFresh.set - Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.IsResponseCacheable.get -> bool Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.IsResponseCacheable.set -> void Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.Logger.get -> Microsoft.Extensions.Logging.ILogger! -Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.ResponseDate.get -> System.DateTimeOffset? Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.ResponseExpirationTimeSpan.get -> System.TimeSpan? Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.ResponseExpirationTimeSpan.set -> void -Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.ResponseExpires.get -> System.DateTimeOffset? -Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.ResponseMaxAge.get -> System.TimeSpan? -Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.ResponseSharedMaxAge.get -> System.TimeSpan? Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.ResponseTime.get -> System.DateTimeOffset? Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.Tags.get -> System.Collections.Generic.HashSet! Microsoft.AspNetCore.OutputCaching.IOutputCachingFeature diff --git a/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs b/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs index a5205c8de232..f46fa3b29f0a 100644 --- a/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs +++ b/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs @@ -54,14 +54,6 @@ await context.Response.WriteAsync($"
{requests++}
"); }).CacheOutput(p => p.Lock(false).Expire(TimeSpan.FromMilliseconds(1))); -// Cached because Response Caching policy and contains "Cache-Control: public" -app.MapGet("/headers", async context => -{ - // From a browser this endpoint won't be cached because of max-age: 0 - context.Response.Headers.CacheControl = "public"; - await Gravatar.WriteGravatar(context); -}).CacheOutput(new ResponseCachingPolicy()); - // Etag app.MapGet("/etag", async (context) => { diff --git a/src/Middleware/OutputCaching/src/OutputCachingContext.cs b/src/Middleware/OutputCaching/src/OutputCachingContext.cs index d5dd89fb8938..bbe858895e39 100644 --- a/src/Middleware/OutputCaching/src/OutputCachingContext.cs +++ b/src/Middleware/OutputCaching/src/OutputCachingContext.cs @@ -5,21 +5,11 @@ using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; -using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.OutputCaching; internal class OutputCachingContext : IOutputCachingContext { - private DateTimeOffset? _responseDate; - private bool _parsedResponseDate; - private DateTimeOffset? _responseExpires; - private bool _parsedResponseExpires; - private TimeSpan? _responseSharedMaxAge; - private bool _parsedResponseSharedMaxAge; - private TimeSpan? _responseMaxAge; - private bool _parsedResponseMaxAge; - internal OutputCachingContext(HttpContext httpContext, ILogger logger) { HttpContext = httpContext; @@ -88,62 +78,4 @@ internal OutputCachingContext(HttpContext httpContext, ILogger logger) public IHeaderDictionary CachedResponseHeaders { get; set; } public TimeSpan? ResponseExpirationTimeSpan { get; set; } - - public DateTimeOffset? ResponseDate - { - get - { - if (!_parsedResponseDate) - { - _parsedResponseDate = true; - _responseDate = HeaderUtilities.TryParseDate(HttpContext.Response.Headers.Date.ToString(), out var date) ? date : null; - } - return _responseDate; - } - set - { - // Don't reparse the response date again if it's explicitly set - _parsedResponseDate = true; - _responseDate = value; - } - } - - public DateTimeOffset? ResponseExpires - { - get - { - if (!_parsedResponseExpires) - { - _parsedResponseExpires = true; - _responseExpires = HeaderUtilities.TryParseDate(HttpContext.Response.Headers.Expires.ToString(), out var expires) ? expires : null; - } - return _responseExpires; - } - } - - public TimeSpan? ResponseSharedMaxAge - { - get - { - if (!_parsedResponseSharedMaxAge) - { - _parsedResponseSharedMaxAge = true; - HeaderUtilities.TryParseSeconds(HttpContext.Response.Headers.CacheControl, CacheControlHeaderValue.SharedMaxAgeString, out _responseSharedMaxAge); - } - return _responseSharedMaxAge; - } - } - - public TimeSpan? ResponseMaxAge - { - get - { - if (!_parsedResponseMaxAge) - { - _parsedResponseMaxAge = true; - HeaderUtilities.TryParseSeconds(HttpContext.Response.Headers.CacheControl, CacheControlHeaderValue.MaxAgeString, out _responseMaxAge); - } - return _responseMaxAge; - } - } } diff --git a/src/Middleware/OutputCaching/src/OutputCachingMiddleware.cs b/src/Middleware/OutputCaching/src/OutputCachingMiddleware.cs index df1437298b85..509d39e1be71 100644 --- a/src/Middleware/OutputCaching/src/OutputCachingMiddleware.cs +++ b/src/Middleware/OutputCaching/src/OutputCachingMiddleware.cs @@ -317,23 +317,15 @@ internal void FinalizeCacheHeaders(OutputCachingContext context) var response = context.HttpContext.Response; var headers = response.Headers; - context.CachedResponseValidFor = context.ResponseSharedMaxAge ?? - context.ResponseMaxAge ?? - (context.ResponseExpires - context.ResponseTime!.Value) ?? - context.ResponseExpirationTimeSpan ?? _options.DefaultExpirationTimeSpan; + context.CachedResponseValidFor = context.ResponseExpirationTimeSpan ?? _options.DefaultExpirationTimeSpan; - // Ensure date header is set - if (!context.ResponseDate.HasValue) - { - context.ResponseDate = context.ResponseTime!.Value; - // Setting the date on the raw response headers. - headers.Date = HeaderUtilities.FormatDate(context.ResponseDate.Value); - } + // Setting the date on the raw response headers. + headers.Date = HeaderUtilities.FormatDate(context.ResponseTime!.Value); // Store the response on the state context.CachedResponse = new OutputCacheEntry { - Created = context.ResponseDate.Value, + Created = context.ResponseTime!.Value, StatusCode = response.StatusCode, Headers = new HeaderDictionary(), Tags = context.Tags.ToArray() diff --git a/src/Middleware/OutputCaching/src/Policies/ResponseCachingPolicy.cs b/src/Middleware/OutputCaching/src/Policies/ResponseCachingPolicy.cs deleted file mode 100644 index 14725001d220..000000000000 --- a/src/Middleware/OutputCaching/src/Policies/ResponseCachingPolicy.cs +++ /dev/null @@ -1,278 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Primitives; -using Microsoft.Net.Http.Headers; - -namespace Microsoft.AspNetCore.OutputCaching; - -/// -/// Provides the policy implemented by Response caching -/// -internal sealed class ResponseCachingPolicy : IOutputCachingPolicy -{ - /// - Task IOutputCachingPolicy.OnRequestAsync(IOutputCachingContext context) - { - context.AttemptOutputCaching = AttemptOutputCaching(context); - context.AllowCacheLookup = AllowCacheLookup(context); - context.AllowCacheStorage = AllowCacheStorage(context); - context.AllowLocking = true; - - // Vary by any query by default - context.CachedVaryByRules.QueryKeys = "*"; - - return Task.CompletedTask; - } - - /// - Task IOutputCachingPolicy.OnServeResponseAsync(IOutputCachingContext context) - { - context.IsResponseCacheable = IsResponseCacheable(context); - - return Task.CompletedTask; - } - - /// - Task IOutputCachingPolicy.OnServeFromCacheAsync(IOutputCachingContext context) - { - context.IsCacheEntryFresh = IsCachedEntryFresh(context); - - return Task.CompletedTask; - } - - internal static bool AttemptOutputCaching(IOutputCachingContext context) - { - var request = context.HttpContext.Request; - - // Verify the method - if (!HttpMethods.IsGet(request.Method) && !HttpMethods.IsHead(request.Method)) - { - context.Logger.RequestMethodNotCacheable(request.Method); - return false; - } - - // Verify existence of authorization headers - if (!StringValues.IsNullOrEmpty(request.Headers.Authorization)) - { - context.Logger.RequestWithAuthorizationNotCacheable(); - return false; - } - - return true; - } - - internal static bool IsResponseCacheable(IOutputCachingContext context) - { - var responseCacheControlHeader = context.HttpContext.Response.Headers.CacheControl; - - // Only cache pages explicitly marked with public - if (!HeaderUtilities.ContainsCacheDirective(responseCacheControlHeader, CacheControlHeaderValue.PublicString)) - { - context.Logger.ResponseWithoutPublicNotCacheable(); - return false; - } - - // Check response no-store - if (HeaderUtilities.ContainsCacheDirective(responseCacheControlHeader, CacheControlHeaderValue.NoStoreString)) - { - context.Logger.ResponseWithNoStoreNotCacheable(); - return false; - } - - // Check no-cache - if (HeaderUtilities.ContainsCacheDirective(responseCacheControlHeader, CacheControlHeaderValue.NoCacheString)) - { - context.Logger.ResponseWithNoCacheNotCacheable(); - return false; - } - - var response = context.HttpContext.Response; - - // Do not cache responses with Set-Cookie headers - if (!StringValues.IsNullOrEmpty(response.Headers.SetCookie)) - { - context.Logger.ResponseWithSetCookieNotCacheable(); - return false; - } - - // Do not cache responses varying by * - var varyHeader = response.Headers.Vary; - if (varyHeader.Count == 1 && string.Equals(varyHeader, "*", StringComparison.OrdinalIgnoreCase)) - { - context.Logger.ResponseWithVaryStarNotCacheable(); - return false; - } - - // Check private - if (HeaderUtilities.ContainsCacheDirective(responseCacheControlHeader, CacheControlHeaderValue.PrivateString)) - { - context.Logger.ResponseWithPrivateNotCacheable(); - return false; - } - - // Check response code - if (response.StatusCode != StatusCodes.Status200OK) - { - context.Logger.ResponseWithUnsuccessfulStatusCodeNotCacheable(response.StatusCode); - return false; - } - - // Check response freshness - if (!context.ResponseDate.HasValue) - { - if (!context.ResponseSharedMaxAge.HasValue && - !context.ResponseMaxAge.HasValue && - context.ResponseTime!.Value >= context.ResponseExpires) - { - context.Logger.ExpirationExpiresExceeded(context.ResponseTime.Value, context.ResponseExpires.Value); - return false; - } - } - else - { - var age = context.ResponseTime!.Value - context.ResponseDate.Value; - - // Validate shared max age - if (age >= context.ResponseSharedMaxAge) - { - context.Logger.ExpirationSharedMaxAgeExceeded(age, context.ResponseSharedMaxAge.Value); - return false; - } - else if (!context.ResponseSharedMaxAge.HasValue) - { - // Validate max age - if (age >= context.ResponseMaxAge) - { - context.Logger.ExpirationMaxAgeExceeded(age, context.ResponseMaxAge.Value); - return false; - } - else if (!context.ResponseMaxAge.HasValue) - { - // Validate expiration - if (context.ResponseTime.Value >= context.ResponseExpires) - { - context.Logger.ExpirationExpiresExceeded(context.ResponseTime.Value, context.ResponseExpires.Value); - return false; - } - } - } - } - - return true; - } - - internal static bool IsCachedEntryFresh(IOutputCachingContext context) - { - var age = context.CachedEntryAge!.Value; - var cachedCacheControlHeaders = context.CachedResponseHeaders.CacheControl; - var requestCacheControlHeaders = context.HttpContext.Request.Headers.CacheControl; - - // Add min-fresh requirements - if (HeaderUtilities.TryParseSeconds(requestCacheControlHeaders, CacheControlHeaderValue.MinFreshString, out var minFresh)) - { - age += minFresh.Value; - context.Logger.ExpirationMinFreshAdded(minFresh.Value); - } - - // Validate shared max age, this overrides any max age settings for shared caches - TimeSpan? cachedSharedMaxAge; - HeaderUtilities.TryParseSeconds(cachedCacheControlHeaders, CacheControlHeaderValue.SharedMaxAgeString, out cachedSharedMaxAge); - - if (age >= cachedSharedMaxAge) - { - // shared max age implies must revalidate - context.Logger.ExpirationSharedMaxAgeExceeded(age, cachedSharedMaxAge.Value); - return false; - } - else if (!cachedSharedMaxAge.HasValue) - { - TimeSpan? requestMaxAge; - HeaderUtilities.TryParseSeconds(requestCacheControlHeaders, CacheControlHeaderValue.MaxAgeString, out requestMaxAge); - - TimeSpan? cachedMaxAge; - HeaderUtilities.TryParseSeconds(cachedCacheControlHeaders, CacheControlHeaderValue.MaxAgeString, out cachedMaxAge); - - var lowestMaxAge = cachedMaxAge < requestMaxAge ? cachedMaxAge : requestMaxAge ?? cachedMaxAge; - // Validate max age - if (age >= lowestMaxAge) - { - // Must revalidate or proxy revalidate - if (HeaderUtilities.ContainsCacheDirective(cachedCacheControlHeaders, CacheControlHeaderValue.MustRevalidateString) - || HeaderUtilities.ContainsCacheDirective(cachedCacheControlHeaders, CacheControlHeaderValue.ProxyRevalidateString)) - { - context.Logger.ExpirationMustRevalidate(age, lowestMaxAge.Value); - return false; - } - - TimeSpan? requestMaxStale; - var maxStaleExist = HeaderUtilities.ContainsCacheDirective(requestCacheControlHeaders, CacheControlHeaderValue.MaxStaleString); - HeaderUtilities.TryParseSeconds(requestCacheControlHeaders, CacheControlHeaderValue.MaxStaleString, out requestMaxStale); - - // Request allows stale values with no age limit - if (maxStaleExist && !requestMaxStale.HasValue) - { - context.Logger.ExpirationInfiniteMaxStaleSatisfied(age, lowestMaxAge.Value); - return true; - } - - // Request allows stale values with age limit - if (requestMaxStale.HasValue && age - lowestMaxAge < requestMaxStale) - { - context.Logger.ExpirationMaxStaleSatisfied(age, lowestMaxAge.Value, requestMaxStale.Value); - return true; - } - - context.Logger.ExpirationMaxAgeExceeded(age, lowestMaxAge.Value); - return false; - } - else if (!cachedMaxAge.HasValue && !requestMaxAge.HasValue) - { - // Validate expiration - DateTimeOffset expires; - if (HeaderUtilities.TryParseDate(context.CachedResponseHeaders.Expires.ToString(), out expires) && - context.ResponseTime!.Value >= expires) - { - context.Logger.ExpirationExpiresExceeded(context.ResponseTime.Value, expires); - return false; - } - } - } - - return true; - } - - internal static bool AllowCacheLookup(IOutputCachingContext context) - { - var requestHeaders = context.HttpContext.Request.Headers; - var cacheControl = requestHeaders.CacheControl; - - // Verify request cache-control parameters - if (!StringValues.IsNullOrEmpty(cacheControl)) - { - if (HeaderUtilities.ContainsCacheDirective(cacheControl, CacheControlHeaderValue.NoCacheString)) - { - context.Logger.RequestWithNoCacheNotCacheable(); - return false; - } - } - else - { - // Support for legacy HTTP 1.0 cache directive - if (HeaderUtilities.ContainsCacheDirective(requestHeaders.Pragma, CacheControlHeaderValue.NoCacheString)) - { - context.Logger.RequestWithPragmaNoCacheNotCacheable(); - return false; - } - } - - return true; - } - - internal static bool AllowCacheStorage(IOutputCachingContext context) - { - // Check request no-store - return !HeaderUtilities.ContainsCacheDirective(context.HttpContext.Request.Headers.CacheControl, CacheControlHeaderValue.NoStoreString); - } -} From 52dfb2be7d6dee55a8121ab249f9e7f53fd30d2a Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Mon, 6 Jun 2022 11:52:48 -0700 Subject: [PATCH 31/48] Update unit tests --- .../OutputCaching/src/OutputCachingContext.cs | 42 +++++++++---------- .../test/OutputCachingMiddlewareTests.cs | 40 +++++++++++------- 2 files changed, 46 insertions(+), 36 deletions(-) diff --git a/src/Middleware/OutputCaching/src/OutputCachingContext.cs b/src/Middleware/OutputCaching/src/OutputCachingContext.cs index bbe858895e39..c2daee23ffc1 100644 --- a/src/Middleware/OutputCaching/src/OutputCachingContext.cs +++ b/src/Middleware/OutputCaching/src/OutputCachingContext.cs @@ -16,66 +16,66 @@ internal OutputCachingContext(HttpContext httpContext, ILogger logger) Logger = logger; } - /// - /// Determine whether the output caching logic should is configured for the incoming HTTP request. - /// + /// public bool EnableOutputCaching { get; set; } - /// - /// Determine whether the response caching logic should be attempted for the incoming HTTP request. - /// + /// public bool AttemptOutputCaching { get; set; } - /// - /// Determine whether a cache lookup is allowed for the incoming HTTP request. - /// + /// public bool AllowCacheLookup { get; set; } - /// - /// Determine whether storage of the response is allowed for the incoming HTTP request. - /// + /// public bool AllowCacheStorage { get; set; } - /// - /// Determine whether request should be locked. - /// + /// public bool AllowLocking { get; set; } - /// - /// Determine whether the response received by the middleware can be cached for future requests. - /// + /// public bool IsResponseCacheable { get; set; } - /// - /// Determine whether the response retrieved from the cache store is fresh and can be served. - /// + /// public bool IsCacheEntryFresh { get; set; } + /// public HttpContext HttpContext { get; } + /// public DateTimeOffset? ResponseTime { get; internal set; } + /// public TimeSpan? CachedEntryAge { get; internal set; } + /// public CachedVaryByRules CachedVaryByRules { get; set; } = new(); + /// public HashSet Tags { get; } = new(); + /// public ILogger Logger { get; } + /// internal string CacheKey { get; set; } + /// internal TimeSpan CachedResponseValidFor { get; set; } + /// internal IOutputCacheEntry CachedResponse { get; set; } + /// internal bool ResponseStarted { get; set; } + /// internal Stream OriginalResponseStream { get; set; } + /// internal OutputCachingStream OutputCachingStream { get; set; } + /// public IHeaderDictionary CachedResponseHeaders { get; set; } + /// public TimeSpan? ResponseExpirationTimeSpan { get; set; } } diff --git a/src/Middleware/OutputCaching/test/OutputCachingMiddlewareTests.cs b/src/Middleware/OutputCaching/test/OutputCachingMiddlewareTests.cs index be79352a27bb..a63187811f64 100644 --- a/src/Middleware/OutputCaching/test/OutputCachingMiddlewareTests.cs +++ b/src/Middleware/OutputCaching/test/OutputCachingMiddlewareTests.cs @@ -388,17 +388,20 @@ public void FinalizeCacheHeadersAsync_DefaultResponseValidity_Is60Seconds() } [Fact] - public void FinalizeCacheHeadersAsync_ResponseValidity_UseExpiryIfAvailable() + public void FinalizeCacheHeadersAsync_ResponseValidity_IgnoresExpiryIfAvailable() { + // The Expires header should not be used when set in the response + var clock = new TestClock { UtcNow = DateTimeOffset.MinValue }; - var sink = new TestSink(); - var middleware = TestUtils.CreateTestMiddleware(testSink: sink, options: new OutputCachingOptions + var options = new OutputCachingOptions { SystemClock = clock - }); + }; + var sink = new TestSink(); + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, options: options); var context = TestUtils.CreateTestContext(); context.ResponseTime = clock.UtcNow; @@ -406,22 +409,25 @@ public void FinalizeCacheHeadersAsync_ResponseValidity_UseExpiryIfAvailable() middleware.FinalizeCacheHeaders(context); - Assert.Equal(TimeSpan.FromSeconds(11), context.CachedResponseValidFor); + Assert.Equal(options.DefaultExpirationTimeSpan, context.CachedResponseValidFor); Assert.Empty(sink.Writes); } [Fact] public void FinalizeCacheHeadersAsync_ResponseValidity_UseMaxAgeIfAvailable() { + // The MaxAge header should not be used if set in the response + var clock = new TestClock { UtcNow = DateTimeOffset.UtcNow }; var sink = new TestSink(); - var middleware = TestUtils.CreateTestMiddleware(testSink: sink, options: new OutputCachingOptions + var options = new OutputCachingOptions { SystemClock = clock - }); + }; + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, options: options); var context = TestUtils.CreateTestContext(); context.ResponseTime = clock.UtcNow; @@ -434,7 +440,7 @@ public void FinalizeCacheHeadersAsync_ResponseValidity_UseMaxAgeIfAvailable() middleware.FinalizeCacheHeaders(context); - Assert.Equal(TimeSpan.FromSeconds(12), context.CachedResponseValidFor); + Assert.Equal(options.DefaultExpirationTimeSpan, context.CachedResponseValidFor); Assert.Empty(sink.Writes); } @@ -446,10 +452,11 @@ public void FinalizeCacheHeadersAsync_ResponseValidity_UseSharedMaxAgeIfAvailabl UtcNow = DateTimeOffset.UtcNow }; var sink = new TestSink(); - var middleware = TestUtils.CreateTestMiddleware(testSink: sink, options: new OutputCachingOptions + var options = new OutputCachingOptions { SystemClock = clock - }); + }; + var middleware = TestUtils.CreateTestMiddleware(testSink: sink, options: options); var context = TestUtils.CreateTestContext(); context.ResponseTime = clock.UtcNow; @@ -462,7 +469,7 @@ public void FinalizeCacheHeadersAsync_ResponseValidity_UseSharedMaxAgeIfAvailabl middleware.FinalizeCacheHeaders(context); - Assert.Equal(TimeSpan.FromSeconds(13), context.CachedResponseValidFor); + Assert.Equal(options.DefaultExpirationTimeSpan, context.CachedResponseValidFor); Assert.Empty(sink.Writes); } @@ -523,21 +530,24 @@ public void FinalizeCacheHeadersAsync_AddsDate_IfNoneSpecified() } [Fact] - public void FinalizeCacheHeadersAsync_DoNotAddDate_IfSpecified() + public void FinalizeCacheHeadersAsync_IgnoresDate_IfSpecified() { - var utcNow = DateTimeOffset.MinValue; + // The Date header should not be used when set in the response + + var utcNow = DateTimeOffset.UtcNow; + var responseTime = utcNow + TimeSpan.FromSeconds(10); var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink); var context = TestUtils.CreateTestContext(); context.HttpContext.Response.Headers.Date = HeaderUtilities.FormatDate(utcNow); - context.ResponseTime = utcNow + TimeSpan.FromSeconds(10); + context.ResponseTime = responseTime; Assert.Equal(HeaderUtilities.FormatDate(utcNow), context.HttpContext.Response.Headers.Date); middleware.FinalizeCacheHeaders(context); - Assert.Equal(HeaderUtilities.FormatDate(utcNow), context.HttpContext.Response.Headers.Date); + Assert.Equal(HeaderUtilities.FormatDate(responseTime), context.HttpContext.Response.Headers.Date); Assert.Empty(sink.Writes); } From ddfbdb3099ab85974a767da7636cf794f0037348 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Mon, 6 Jun 2022 14:56:01 -0700 Subject: [PATCH 32/48] Remove CachedResponseHeaders from public API --- .../src/IOutputCachingContext.cs | 5 -- .../OutputCaching/src/OutputCachingContext.cs | 19 +++---- .../src/OutputCachingMiddleware.cs | 9 ++-- .../test/OutputCachingMiddlewareTests.cs | 51 +++++++++---------- .../test/OutputCachingPolicyProviderTests.cs | 6 +-- 5 files changed, 40 insertions(+), 50 deletions(-) diff --git a/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingContext.cs b/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingContext.cs index af9d39aa275d..ca9ed4ca670e 100644 --- a/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingContext.cs +++ b/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingContext.cs @@ -26,11 +26,6 @@ public interface IOutputCachingContext ///
DateTimeOffset? ResponseTime { get; } - /// - /// Gets the cached response headers. - /// - IHeaderDictionary CachedResponseHeaders { get; } - /// /// Gets the instance. /// diff --git a/src/Middleware/OutputCaching/src/OutputCachingContext.cs b/src/Middleware/OutputCaching/src/OutputCachingContext.cs index c2daee23ffc1..ba57662883c5 100644 --- a/src/Middleware/OutputCaching/src/OutputCachingContext.cs +++ b/src/Middleware/OutputCaching/src/OutputCachingContext.cs @@ -56,26 +56,23 @@ internal OutputCachingContext(HttpContext httpContext, ILogger logger) public ILogger Logger { get; } /// + public TimeSpan? ResponseExpirationTimeSpan { get; set; } + internal string CacheKey { get; set; } - /// + /// + /// Gets or sets the amount of time the response is cached for. + /// + /// + /// It is computed from either or . + /// internal TimeSpan CachedResponseValidFor { get; set; } - /// internal IOutputCacheEntry CachedResponse { get; set; } - /// internal bool ResponseStarted { get; set; } - /// internal Stream OriginalResponseStream { get; set; } - /// internal OutputCachingStream OutputCachingStream { get; set; } - - /// - public IHeaderDictionary CachedResponseHeaders { get; set; } - - /// - public TimeSpan? ResponseExpirationTimeSpan { get; set; } } diff --git a/src/Middleware/OutputCaching/src/OutputCachingMiddleware.cs b/src/Middleware/OutputCaching/src/OutputCachingMiddleware.cs index 509d39e1be71..c41cf678bfe7 100644 --- a/src/Middleware/OutputCaching/src/OutputCachingMiddleware.cs +++ b/src/Middleware/OutputCaching/src/OutputCachingMiddleware.cs @@ -183,7 +183,6 @@ internal async Task TryServeCachedResponseAsync(OutputCachingContext conte } context.CachedResponse = cacheEntry; - context.CachedResponseHeaders = context.CachedResponse.Headers; context.ResponseTime = _options.SystemClock.UtcNow; var cachedEntryAge = context.ResponseTime.Value - context.CachedResponse.Created; context.CachedEntryAge = cachedEntryAge > TimeSpan.Zero ? cachedEntryAge : TimeSpan.Zero; @@ -192,17 +191,19 @@ internal async Task TryServeCachedResponseAsync(OutputCachingContext conte if (context.IsCacheEntryFresh) { + var cachedResponseHeaders = context.CachedResponse.Headers; + // Check conditional request rules if (ContentIsNotModified(context)) { _logger.NotModifiedServed(); context.HttpContext.Response.StatusCode = StatusCodes.Status304NotModified; - if (context.CachedResponseHeaders != null) + if (cachedResponseHeaders != null) { foreach (var key in HeadersToIncludeIn304) { - if (context.CachedResponseHeaders.TryGetValue(key, out var values)) + if (cachedResponseHeaders.TryGetValue(key, out var values)) { context.HttpContext.Response.Headers[key] = values; } @@ -447,7 +448,7 @@ internal static void UnshimResponseStream(OutputCachingContext context) internal static bool ContentIsNotModified(OutputCachingContext context) { - var cachedResponseHeaders = context.CachedResponseHeaders; + var cachedResponseHeaders = context.CachedResponse.Headers; var ifNoneMatchHeader = context.HttpContext.Request.Headers.IfNoneMatch; if (!StringValues.IsNullOrEmpty(ifNoneMatchHeader)) diff --git a/src/Middleware/OutputCaching/test/OutputCachingMiddlewareTests.cs b/src/Middleware/OutputCaching/test/OutputCachingMiddlewareTests.cs index a63187811f64..dc75a73e0529 100644 --- a/src/Middleware/OutputCaching/test/OutputCachingMiddlewareTests.cs +++ b/src/Middleware/OutputCaching/test/OutputCachingMiddlewareTests.cs @@ -130,7 +130,7 @@ public void ContentIsNotModified_NotConditionalRequest_False() { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponse = new OutputCacheEntry { Headers = new HeaderDictionary() }; Assert.False(OutputCachingMiddleware.ContentIsNotModified(context)); Assert.Empty(sink.Writes); @@ -142,22 +142,22 @@ public void ContentIsNotModified_IfModifiedSince_FallsbackToDateHeader() var utcNow = DateTimeOffset.UtcNow; var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponse = new OutputCacheEntry { Headers = new HeaderDictionary() }; context.HttpContext.Request.Headers.IfModifiedSince = HeaderUtilities.FormatDate(utcNow); // Verify modifications in the past succeeds - context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10)); + context.CachedResponse.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10)); Assert.True(OutputCachingMiddleware.ContentIsNotModified(context)); Assert.Single(sink.Writes); // Verify modifications at present succeeds - context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow); + context.CachedResponse.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow); Assert.True(OutputCachingMiddleware.ContentIsNotModified(context)); Assert.Equal(2, sink.Writes.Count); // Verify modifications in the future fails - context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); + context.CachedResponse.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); Assert.False(OutputCachingMiddleware.ContentIsNotModified(context)); // Verify logging @@ -173,25 +173,25 @@ public void ContentIsNotModified_IfModifiedSince_LastModifiedOverridesDateHeader var utcNow = DateTimeOffset.UtcNow; var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponse = new OutputCacheEntry { Headers = new HeaderDictionary() }; context.HttpContext.Request.Headers.IfModifiedSince = HeaderUtilities.FormatDate(utcNow); // Verify modifications in the past succeeds - context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); - context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10)); + context.CachedResponse.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); + context.CachedResponse.Headers[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10)); Assert.True(OutputCachingMiddleware.ContentIsNotModified(context)); Assert.Single(sink.Writes); // Verify modifications at present - context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); - context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow); + context.CachedResponse.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); + context.CachedResponse.Headers[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow); Assert.True(OutputCachingMiddleware.ContentIsNotModified(context)); Assert.Equal(2, sink.Writes.Count); // Verify modifications in the future fails - context.CachedResponseHeaders[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10)); - context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); + context.CachedResponse.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10)); + context.CachedResponse.Headers[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); Assert.False(OutputCachingMiddleware.ContentIsNotModified(context)); // Verify logging @@ -207,11 +207,11 @@ public void ContentIsNotModified_IfNoneMatch_Overrides_IfModifiedSince_ToTrue() var utcNow = DateTimeOffset.UtcNow; var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponse = new OutputCacheEntry { Headers = new HeaderDictionary() }; // This would fail the IfModifiedSince checks context.HttpContext.Request.Headers.IfModifiedSince = HeaderUtilities.FormatDate(utcNow); - context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); + context.CachedResponse.Headers[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); context.HttpContext.Request.Headers.IfNoneMatch = EntityTagHeaderValue.Any.ToString(); Assert.True(OutputCachingMiddleware.ContentIsNotModified(context)); @@ -226,11 +226,11 @@ public void ContentIsNotModified_IfNoneMatch_Overrides_IfModifiedSince_ToFalse() var utcNow = DateTimeOffset.UtcNow; var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponse = new OutputCacheEntry { Headers = new HeaderDictionary() }; // This would pass the IfModifiedSince checks context.HttpContext.Request.Headers.IfModifiedSince = HeaderUtilities.FormatDate(utcNow); - context.CachedResponseHeaders[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10)); + context.CachedResponse.Headers[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10)); context.HttpContext.Request.Headers.IfNoneMatch = "\"E1\""; Assert.False(OutputCachingMiddleware.ContentIsNotModified(context)); @@ -242,7 +242,7 @@ public void ContentIsNotModified_IfNoneMatch_AnyWithoutETagInResponse_False() { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.CachedResponseHeaders = new HeaderDictionary(); + context.CachedResponse = new OutputCacheEntry { Headers = new HeaderDictionary() }; context.HttpContext.Request.Headers.IfNoneMatch = "\"E1\""; Assert.False(OutputCachingMiddleware.ContentIsNotModified(context)); @@ -269,8 +269,8 @@ public void ContentIsNotModified_IfNoneMatch_ExplicitWithMatch_True(EntityTagHea { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.CachedResponseHeaders = new HeaderDictionary(); - context.CachedResponseHeaders[HeaderNames.ETag] = responseETag.ToString(); + context.CachedResponse = new OutputCacheEntry { Headers = new HeaderDictionary() }; + context.CachedResponse.Headers[HeaderNames.ETag] = responseETag.ToString(); context.HttpContext.Request.Headers.IfNoneMatch = requestETag.ToString(); Assert.True(OutputCachingMiddleware.ContentIsNotModified(context)); @@ -284,8 +284,8 @@ public void ContentIsNotModified_IfNoneMatch_ExplicitWithoutMatch_False() { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.CachedResponseHeaders = new HeaderDictionary(); - context.CachedResponseHeaders[HeaderNames.ETag] = "\"E2\""; + context.CachedResponse = new OutputCacheEntry { Headers = new HeaderDictionary() }; + context.CachedResponse.Headers[HeaderNames.ETag] = "\"E2\""; context.HttpContext.Request.Headers.IfNoneMatch = "\"E1\""; Assert.False(OutputCachingMiddleware.ContentIsNotModified(context)); @@ -297,8 +297,8 @@ public void ContentIsNotModified_IfNoneMatch_MatchesAtLeastOneValue_True() { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.CachedResponseHeaders = new HeaderDictionary(); - context.CachedResponseHeaders[HeaderNames.ETag] = "\"E2\""; + context.CachedResponse = new OutputCacheEntry { Headers = new HeaderDictionary() }; + context.CachedResponse.Headers[HeaderNames.ETag] = "\"E2\""; context.HttpContext.Request.Headers.IfNoneMatch = new string[] { "\"E0\", \"E1\"", "\"E1\", \"E2\"" }; Assert.True(OutputCachingMiddleware.ContentIsNotModified(context)); @@ -677,10 +677,7 @@ public async Task FinalizeCacheBody_Cache_IfContentLengthAbsent() await context.HttpContext.Response.WriteAsync(new string('0', 10)); - context.CachedResponse = new OutputCacheEntry() - { - Headers = new HeaderDictionary() - }; + context.CachedResponse = new OutputCacheEntry { Headers = new HeaderDictionary() }; context.CacheKey = "BaseKey"; context.CachedResponseValidFor = TimeSpan.FromSeconds(10); diff --git a/src/Middleware/OutputCaching/test/OutputCachingPolicyProviderTests.cs b/src/Middleware/OutputCaching/test/OutputCachingPolicyProviderTests.cs index 13add071d267..9d3646d4a38d 100644 --- a/src/Middleware/OutputCaching/test/OutputCachingPolicyProviderTests.cs +++ b/src/Middleware/OutputCaching/test/OutputCachingPolicyProviderTests.cs @@ -458,14 +458,14 @@ public async Task IsCachedEntryFresh_SharedMaxAgeOverridesMaxAge_ToFresh() var context = TestUtils.CreateTestContext(sink); context.CachedEntryAge = TimeSpan.FromSeconds(11); context.ResponseTime = utcNow + context.CachedEntryAge; - context.CachedResponseHeaders = new HeaderDictionary(); - context.CachedResponseHeaders[HeaderNames.CacheControl] = new CacheControlHeaderValue() + context.CachedResponse = new OutputCacheEntry { Headers = new HeaderDictionary() }; + context.CachedResponse.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { Public = true, MaxAge = TimeSpan.FromSeconds(10), SharedMaxAge = TimeSpan.FromSeconds(15) }.ToString(); - context.CachedResponseHeaders[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow); + context.CachedResponse.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow); var options = new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().Build() }; await new OutputCachingPolicyProvider(Options.Create(options)).OnServeFromCacheAsync(context); From b9ce0f7d8a09690e199320d99df47ca46bda487f Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Mon, 6 Jun 2022 15:34:57 -0700 Subject: [PATCH 33/48] Fix unshipped file --- .../OutputCaching.Abstractions/src/PublicAPI.Unshipped.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Middleware/OutputCaching.Abstractions/src/PublicAPI.Unshipped.txt b/src/Middleware/OutputCaching.Abstractions/src/PublicAPI.Unshipped.txt index 39c18cb4ace9..72dd9fd52b54 100644 --- a/src/Middleware/OutputCaching.Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/OutputCaching.Abstractions/src/PublicAPI.Unshipped.txt @@ -37,7 +37,6 @@ Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.AllowLocking.set -> voi Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.AttemptOutputCaching.get -> bool Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.AttemptOutputCaching.set -> void Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.CachedEntryAge.get -> System.TimeSpan? -Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.CachedResponseHeaders.get -> Microsoft.AspNetCore.Http.IHeaderDictionary! Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.CachedVaryByRules.get -> Microsoft.AspNetCore.OutputCaching.CachedVaryByRules! Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.EnableOutputCaching.get -> bool Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.EnableOutputCaching.set -> void From bc8795196fa08819d76bbbb56c0b18ee3c6411bd Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Tue, 7 Jun 2022 17:23:49 -0700 Subject: [PATCH 34/48] API review feedback --- .../src/IOutputCache.cs | 9 +- .../src/IOutputCachingContext.cs | 25 +--- .../src/IOutputCachingPolicyProvider.cs | 9 ++ .../src/PublicAPI.Unshipped.txt | 15 +-- .../samples/OutputCachingSample/Startup.cs | 18 ++- .../src/Memory/MemoryCachedResponse.cs | 2 +- .../src/Memory/MemoryOutputCacheStore.cs | 6 +- .../OutputCaching/src/OutputCacheAttribute.cs | 8 +- .../OutputCaching/src/OutputCacheEntry.cs | 2 +- .../OutputCaching/src/OutputCachingContext.cs | 30 ++--- .../src/OutputCachingMiddleware.cs | 54 ++++++--- .../OutputCaching/src/OutputCachingOptions.cs | 2 +- .../src/OutputCachingPolicyProvider.cs | 33 +++++- .../src/Policies/DefaultOutputCachePolicy.cs | 31 ++--- .../src/Policies/EnableCachingPolicy.cs | 12 +- .../src/Policies/LockingPolicy.cs | 2 +- .../src/Policies/NoStorePolicy.cs | 8 +- .../src/Policies/OutputCachePolicyBuilder.cs | 80 +++++++++++-- .../src/Policies/PolicyExtensions.cs | 12 +- .../OutputCaching/src/PublicAPI.Unshipped.txt | 12 +- .../test/OutputCachingMiddlewareTests.cs | 38 ++---- .../test/OutputCachingPolicyProviderTests.cs | 112 ++++++++---------- .../OutputCaching/test/OutputCachingTests.cs | 20 ++-- .../OutputCaching/test/TestUtils.cs | 46 +++---- src/Mvc/Mvc.Core/src/OutputCacheAttribute.cs | 8 +- src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt | 4 +- 26 files changed, 331 insertions(+), 267 deletions(-) diff --git a/src/Middleware/OutputCaching.Abstractions/src/IOutputCache.cs b/src/Middleware/OutputCaching.Abstractions/src/IOutputCache.cs index 57ea7f192451..be14e6853da6 100644 --- a/src/Middleware/OutputCaching.Abstractions/src/IOutputCache.cs +++ b/src/Middleware/OutputCaching.Abstractions/src/IOutputCache.cs @@ -12,15 +12,17 @@ public interface IOutputCacheStore /// Evicts cached responses by tag. /// /// The tag to evict. - ValueTask EvictByTagAsync(string tag); + /// Indicates that the operation should be cancelled. + ValueTask EvictByTagAsync(string tag, CancellationToken token); /// /// Gets the cached response for the given key, if it exists. /// If no cached response exists for the given key, null is returned. /// /// The cache key to look up. + /// Indicates that the operation should be cancelled. /// The response cache entry if it exists; otherwise null. - ValueTask GetAsync(string key); + ValueTask GetAsync(string key, CancellationToken token); /// /// Stores the given response in the response cache. @@ -28,5 +30,6 @@ public interface IOutputCacheStore /// The cache key to store the response under. /// The response cache entry to store. /// The amount of time the entry will be kept in the cache before expiring, relative to now. - ValueTask SetAsync(string key, IOutputCacheEntry entry, TimeSpan validFor); + /// Indicates that the operation should be cancelled. + ValueTask SetAsync(string key, IOutputCacheEntry entry, TimeSpan validFor, CancellationToken token); } diff --git a/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingContext.cs b/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingContext.cs index ca9ed4ca670e..25652ca249f3 100644 --- a/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingContext.cs +++ b/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingContext.cs @@ -11,11 +11,6 @@ namespace Microsoft.AspNetCore.OutputCaching; /// public interface IOutputCachingContext { - /// - /// Gets the cached entry age. - /// - TimeSpan? CachedEntryAge { get; } - /// /// Gets the . /// @@ -41,6 +36,11 @@ public interface IOutputCachingContext /// ILogger Logger { get; } + /// + /// Gets the instance. + /// + IOutputCacheStore Store { get; } + /// /// Gets or sets the amount of time the response should be cached for. /// @@ -51,11 +51,6 @@ public interface IOutputCachingContext /// bool EnableOutputCaching { get; set; } - /// - /// Determines whether the output caching logic should be attempted for the incoming HTTP request. - /// - bool AttemptOutputCaching { get; set; } - /// /// Determines whether a cache lookup is allowed for the incoming HTTP request. /// @@ -70,14 +65,4 @@ public interface IOutputCachingContext /// Determines whether the request should be locked. /// bool AllowLocking { get; set; } - - /// - /// Determines whether the response received by the middleware can be cached for future requests. - /// - bool IsResponseCacheable { get; set; } - - /// - /// Determines whether the response retrieved from the cache store is fresh and can be served. - /// - bool IsCacheEntryFresh { get; set; } } diff --git a/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingPolicyProvider.cs b/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingPolicyProvider.cs index 23cde9db7023..64d43307a8ed 100644 --- a/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingPolicyProvider.cs +++ b/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingPolicyProvider.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.AspNetCore.Http; + namespace Microsoft.AspNetCore.OutputCaching; /// @@ -8,6 +10,13 @@ namespace Microsoft.AspNetCore.OutputCaching; /// public interface IOutputCachingPolicyProvider { + /// + /// Returns wether the current request has any configured policy. + /// + /// The current instance. + /// True if the current request has a policy, false otherwise. + bool HasPolicies(HttpContext httpContext); + /// /// Determines whether the response caching logic should be attempted for the incoming HTTP request. /// diff --git a/src/Middleware/OutputCaching.Abstractions/src/PublicAPI.Unshipped.txt b/src/Middleware/OutputCaching.Abstractions/src/PublicAPI.Unshipped.txt index 72dd9fd52b54..475d28924b79 100644 --- a/src/Middleware/OutputCaching.Abstractions/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/OutputCaching.Abstractions/src/PublicAPI.Unshipped.txt @@ -24,9 +24,9 @@ Microsoft.AspNetCore.OutputCaching.IOutputCacheEntry.StatusCode.set -> void Microsoft.AspNetCore.OutputCaching.IOutputCacheEntry.Tags.get -> string![]? Microsoft.AspNetCore.OutputCaching.IOutputCacheEntry.Tags.set -> void Microsoft.AspNetCore.OutputCaching.IOutputCacheStore -Microsoft.AspNetCore.OutputCaching.IOutputCacheStore.EvictByTagAsync(string! tag) -> System.Threading.Tasks.ValueTask -Microsoft.AspNetCore.OutputCaching.IOutputCacheStore.GetAsync(string! key) -> System.Threading.Tasks.ValueTask -Microsoft.AspNetCore.OutputCaching.IOutputCacheStore.SetAsync(string! key, Microsoft.AspNetCore.OutputCaching.IOutputCacheEntry! entry, System.TimeSpan validFor) -> System.Threading.Tasks.ValueTask +Microsoft.AspNetCore.OutputCaching.IOutputCacheStore.EvictByTagAsync(string! tag, System.Threading.CancellationToken token) -> System.Threading.Tasks.ValueTask +Microsoft.AspNetCore.OutputCaching.IOutputCacheStore.GetAsync(string! key, System.Threading.CancellationToken token) -> System.Threading.Tasks.ValueTask +Microsoft.AspNetCore.OutputCaching.IOutputCacheStore.SetAsync(string! key, Microsoft.AspNetCore.OutputCaching.IOutputCacheEntry! entry, System.TimeSpan validFor, System.Threading.CancellationToken token) -> System.Threading.Tasks.ValueTask Microsoft.AspNetCore.OutputCaching.IOutputCachingContext Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.AllowCacheLookup.get -> bool Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.AllowCacheLookup.set -> void @@ -34,21 +34,15 @@ Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.AllowCacheStorage.get - Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.AllowCacheStorage.set -> void Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.AllowLocking.get -> bool Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.AllowLocking.set -> void -Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.AttemptOutputCaching.get -> bool -Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.AttemptOutputCaching.set -> void -Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.CachedEntryAge.get -> System.TimeSpan? Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.CachedVaryByRules.get -> Microsoft.AspNetCore.OutputCaching.CachedVaryByRules! Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.EnableOutputCaching.get -> bool Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.EnableOutputCaching.set -> void Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.HttpContext.get -> Microsoft.AspNetCore.Http.HttpContext! -Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.IsCacheEntryFresh.get -> bool -Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.IsCacheEntryFresh.set -> void -Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.IsResponseCacheable.get -> bool -Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.IsResponseCacheable.set -> void Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.Logger.get -> Microsoft.Extensions.Logging.ILogger! Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.ResponseExpirationTimeSpan.get -> System.TimeSpan? Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.ResponseExpirationTimeSpan.set -> void Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.ResponseTime.get -> System.DateTimeOffset? +Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.Store.get -> Microsoft.AspNetCore.OutputCaching.IOutputCacheStore! Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.Tags.get -> System.Collections.Generic.HashSet! Microsoft.AspNetCore.OutputCaching.IOutputCachingFeature Microsoft.AspNetCore.OutputCaching.IOutputCachingFeature.Context.get -> Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! @@ -58,6 +52,7 @@ Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy.OnRequestAsync(Microsoft Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy.OnServeFromCacheAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy.OnServeResponseAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicyProvider +Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicyProvider.HasPolicies(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> bool Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicyProvider.OnRequestAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicyProvider.OnServeFromCacheAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicyProvider.OnServeResponseAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! diff --git a/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs b/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs index f46fa3b29f0a..d71243c3b9ca 100644 --- a/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs +++ b/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs @@ -10,9 +10,9 @@ builder.Services.AddOutputCaching(options => { // Enable caching on all requests - // options.DefaultPolicy = OutputCachePolicyBuilder.Default.Enable().Build(); + // options.BasePolicy = new OutputCachePolicyBuilder().Enable(); - options.Policies["NoCache"] = new OutputCachePolicyBuilder().NoStore().Build(); + options.Policies["NoCache"] = new OutputCachePolicyBuilder().WithPath("/wwwroot").Expire(TimeSpan.FromDays(1)); }); var app = builder.Build(); @@ -25,19 +25,22 @@ app.MapGet("/nocache", Gravatar.WriteGravatar).CacheOutput(x => x.NoStore()); -app.MapGet("/profile", Gravatar.WriteGravatar).CacheOutput(x => x.Profile("NoCache")); +app.MapGet("/profile", Gravatar.WriteGravatar).CacheOutput(x => x.Policy("NoCache")); -app.MapGet("/attribute", [OutputCache(Profile = "NoCache")] (c) => Gravatar.WriteGravatar(c)); +var myPolicy = new OutputCachePolicyBuilder().Expire(TimeSpan.FromDays(1)).Build(); +app.MapGet("/custom", Gravatar.WriteGravatar).CacheOutput(myPolicy); + +app.MapGet("/attribute", (RequestDelegate)([OutputCache(PolicyName = "NoCache")] (c) => Gravatar.WriteGravatar(c))); var blog = app.MapGroup("blog").CacheOutput(x => x.Tag("blog")); blog.MapGet("/", Gravatar.WriteGravatar); -blog.MapGet("/post/{id}", Gravatar.WriteGravatar); +blog.MapGet("/post/{id}", Gravatar.WriteGravatar).CacheOutput(x => x.Tag("byid")); // check we get the two tags. Or if we need to get the last one app.MapPost("/purge/{tag}", async (IOutputCacheStore cache, string tag) => { // POST such that the endpoint is not cached itself - await cache.EvictByTagAsync(tag); + await cache.EvictByTagAsync(tag, default); }); // Cached entries will vary by culture, but any other additional query is ignored and returns the same cached content @@ -64,6 +67,9 @@ context.Response.Headers.ETag = etag; await Gravatar.WriteGravatar(context); + + var cacheContext = context.Features.Get()?.Context; + }).CacheOutput(); // When the request header If-Modified-Since is provided, return 304 if the cached entry is older diff --git a/src/Middleware/OutputCaching/src/Memory/MemoryCachedResponse.cs b/src/Middleware/OutputCaching/src/Memory/MemoryCachedResponse.cs index a50e4c1fdfb2..d76c305b2615 100644 --- a/src/Middleware/OutputCaching/src/Memory/MemoryCachedResponse.cs +++ b/src/Middleware/OutputCaching/src/Memory/MemoryCachedResponse.cs @@ -5,7 +5,7 @@ namespace Microsoft.AspNetCore.OutputCaching.Memory; -internal class MemoryCachedResponse +internal sealed class MemoryCachedResponse { public DateTimeOffset Created { get; set; } diff --git a/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs b/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs index 186140fcfac8..fbfa75430a5a 100644 --- a/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs +++ b/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs @@ -18,7 +18,7 @@ internal MemoryOutputCacheStore(IMemoryCache cache) _cache = cache; } - public ValueTask EvictByTagAsync(string tag) + public ValueTask EvictByTagAsync(string tag, CancellationToken token) { if (_taggedEntries.TryGetValue(tag, out var keys)) { @@ -31,7 +31,7 @@ public ValueTask EvictByTagAsync(string tag) return ValueTask.CompletedTask; } - public ValueTask GetAsync(string key) + public ValueTask GetAsync(string key, CancellationToken token) { var entry = _cache.Get(key); @@ -52,7 +52,7 @@ public ValueTask EvictByTagAsync(string tag) return ValueTask.FromResult(default(IOutputCacheEntry)); } - public ValueTask SetAsync(string key, IOutputCacheEntry cachedResponse, TimeSpan validFor) + public ValueTask SetAsync(string key, IOutputCacheEntry cachedResponse, TimeSpan validFor, CancellationToken token) { if (cachedResponse.Tags != null) { diff --git a/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs b/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs index 5c81a8ab9ad7..b72bee23d0d7 100644 --- a/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs @@ -53,9 +53,9 @@ public bool NoStore public string[]? VaryByHeaders { get; set; } /// - /// Gets or sets the value of the cache profile name. + /// Gets or sets the value of the cache policy name. /// - public string? Profile { get; set; } + public string? PolicyName { get; set; } /// public IOutputCachingPolicy Policy => _policy ??= GetPolicy(); @@ -69,9 +69,9 @@ private IOutputCachingPolicy GetPolicy() builder.NoStore(); } - if (Profile != null) + if (PolicyName != null) { - builder.Profile(Profile); + builder.Policy(PolicyName); } if (VaryByQueryKeys != null) diff --git a/src/Middleware/OutputCaching/src/OutputCacheEntry.cs b/src/Middleware/OutputCaching/src/OutputCacheEntry.cs index 18e6e6fcb069..deb59a0b8824 100644 --- a/src/Middleware/OutputCaching/src/OutputCacheEntry.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheEntry.cs @@ -6,7 +6,7 @@ namespace Microsoft.AspNetCore.OutputCaching; /// -internal class OutputCacheEntry : IOutputCacheEntry +internal sealed class OutputCacheEntry : IOutputCacheEntry { /// public DateTimeOffset Created { get; set; } diff --git a/src/Middleware/OutputCaching/src/OutputCachingContext.cs b/src/Middleware/OutputCaching/src/OutputCachingContext.cs index ba57662883c5..03eb836995ba 100644 --- a/src/Middleware/OutputCaching/src/OutputCachingContext.cs +++ b/src/Middleware/OutputCaching/src/OutputCachingContext.cs @@ -8,20 +8,18 @@ namespace Microsoft.AspNetCore.OutputCaching; -internal class OutputCachingContext : IOutputCachingContext +internal sealed class OutputCachingContext : IOutputCachingContext { - internal OutputCachingContext(HttpContext httpContext, ILogger logger) + internal OutputCachingContext(HttpContext httpContext, IOutputCacheStore store, ILogger logger) { HttpContext = httpContext; Logger = logger; + Store = store; } /// public bool EnableOutputCaching { get; set; } - /// - public bool AttemptOutputCaching { get; set; } - /// public bool AllowCacheLookup { get; set; } @@ -31,21 +29,12 @@ internal OutputCachingContext(HttpContext httpContext, ILogger logger) /// public bool AllowLocking { get; set; } - /// - public bool IsResponseCacheable { get; set; } - - /// - public bool IsCacheEntryFresh { get; set; } - /// public HttpContext HttpContext { get; } /// public DateTimeOffset? ResponseTime { get; internal set; } - /// - public TimeSpan? CachedEntryAge { get; internal set; } - /// public CachedVaryByRules CachedVaryByRules { get; set; } = new(); @@ -55,19 +44,20 @@ internal OutputCachingContext(HttpContext httpContext, ILogger logger) /// public ILogger Logger { get; } + /// + public IOutputCacheStore Store { get; } + /// public TimeSpan? ResponseExpirationTimeSpan { get; set; } internal string CacheKey { get; set; } - /// - /// Gets or sets the amount of time the response is cached for. - /// - /// - /// It is computed from either or . - /// internal TimeSpan CachedResponseValidFor { get; set; } + internal bool IsCacheEntryFresh { get; set; } + + internal TimeSpan CachedEntryAge { get; set; } + internal IOutputCacheEntry CachedResponse { get; set; } internal bool ResponseStarted { get; set; } diff --git a/src/Middleware/OutputCaching/src/OutputCachingMiddleware.cs b/src/Middleware/OutputCaching/src/OutputCachingMiddleware.cs index c41cf678bfe7..3ed17b8a517a 100644 --- a/src/Middleware/OutputCaching/src/OutputCachingMiddleware.cs +++ b/src/Middleware/OutputCaching/src/OutputCachingMiddleware.cs @@ -24,7 +24,7 @@ public class OutputCachingMiddleware private readonly OutputCachingOptions _options; private readonly ILogger _logger; private readonly IOutputCachingPolicyProvider _policyProvider; - private readonly IOutputCacheStore _cache; + private readonly IOutputCacheStore _store; private readonly IOutputCachingKeyProvider _keyProvider; private readonly WorkDispatcher _outputCacheEntryDispatcher; private readonly WorkDispatcher _requestDispatcher; @@ -73,7 +73,7 @@ internal OutputCachingMiddleware( _options = options.Value; _logger = loggerFactory.CreateLogger(); _policyProvider = policyProvider; - _cache = cache; + _store = cache; _keyProvider = keyProvider; _outputCacheEntryDispatcher = new(); _requestDispatcher = new(); @@ -86,7 +86,14 @@ internal OutputCachingMiddleware( /// A that completes when the middleware has completed processing. public async Task Invoke(HttpContext httpContext) { - var context = new OutputCachingContext(httpContext, _logger); + // Skip the middleware if there is no policy for the current request + if (!_policyProvider.HasPolicies(httpContext)) + { + await _next(httpContext); + return; + } + + var context = new OutputCachingContext(httpContext, _store, _logger); // Add IOutputCachingFeature AddOutputCachingFeature(context); @@ -96,7 +103,7 @@ public async Task Invoke(HttpContext httpContext) await _policyProvider.OnRequestAsync(context); // Should we attempt any caching logic? - if (context.EnableOutputCaching && context.AttemptOutputCaching) + if (context.EnableOutputCaching) { // Can this request be served from cache? if (context.AllowCacheLookup) @@ -184,11 +191,20 @@ internal async Task TryServeCachedResponseAsync(OutputCachingContext conte context.CachedResponse = cacheEntry; context.ResponseTime = _options.SystemClock.UtcNow; - var cachedEntryAge = context.ResponseTime.Value - context.CachedResponse.Created; - context.CachedEntryAge = cachedEntryAge > TimeSpan.Zero ? cachedEntryAge : TimeSpan.Zero; + var cacheEntryAge = context.ResponseTime.Value - context.CachedResponse.Created; + context.CachedEntryAge = cacheEntryAge > TimeSpan.Zero ? cacheEntryAge : TimeSpan.Zero; await _policyProvider.OnServeFromCacheAsync(context); + context.IsCacheEntryFresh = true; + + // Validate expiration + if (context.CachedEntryAge <= TimeSpan.Zero) + { + context.Logger.ExpirationExpiresExceeded(context.ResponseTime!.Value); + context.IsCacheEntryFresh = false; + } + if (context.IsCacheEntryFresh) { var cachedResponseHeaders = context.CachedResponse.Headers; @@ -223,7 +239,7 @@ internal async Task TryServeCachedResponseAsync(OutputCachingContext conte // Note: int64 division truncates result and errors may be up to 1 second. This reduction in // accuracy of age calculation is considered appropriate since it is small compared to clock // skews and the "Age" header is an estimate of the real age of cached content. - response.Headers.Age = HeaderUtilities.FormatNonNegativeInt64(context.CachedEntryAge.Value.Ticks / TimeSpan.TicksPerSecond); + response.Headers.Age = HeaderUtilities.FormatNonNegativeInt64(context.CachedEntryAge.Ticks / TimeSpan.TicksPerSecond); // Copy the cached response body var body = context.CachedResponse.Body; @@ -246,25 +262,25 @@ internal async Task TryServeCachedResponseAsync(OutputCachingContext conte return false; } - internal async Task TryServeFromCacheAsync(OutputCachingContext context) + internal async Task TryServeFromCacheAsync(OutputCachingContext cacheContext) { - CreateCacheKey(context); + CreateCacheKey(cacheContext); // Locking cache lookups by default // TODO: should it be part of the cache implementations or can we assume all caches would benefit from it? // It makes sense for caches that use IO (disk, network) or need to deserialize the state but could also be a global option - var cacheEntry = await _outputCacheEntryDispatcher.ScheduleAsync(context.CacheKey, _cache, static async (key, cache) => await cache.GetAsync(key)); + var cacheEntry = await _outputCacheEntryDispatcher.ScheduleAsync(cacheContext.CacheKey, cacheContext, static async (key, cacheContext) => await cacheContext.Store.GetAsync(key, cacheContext.HttpContext.RequestAborted)); - if (await TryServeCachedResponseAsync(context, cacheEntry)) + if (await TryServeCachedResponseAsync(cacheContext, cacheEntry)) { return true; } - if (HeaderUtilities.ContainsCacheDirective(context.HttpContext.Request.Headers.CacheControl, CacheControlHeaderValue.OnlyIfCachedString)) + if (HeaderUtilities.ContainsCacheDirective(cacheContext.HttpContext.Request.Headers.CacheControl, CacheControlHeaderValue.OnlyIfCachedString)) { _logger.GatewayTimeoutServed(); - context.HttpContext.Response.StatusCode = StatusCodes.Status504GatewayTimeout; + cacheContext.HttpContext.Response.StatusCode = StatusCodes.Status504GatewayTimeout; return true; } @@ -312,7 +328,7 @@ internal void CreateCacheKey(OutputCachingContext context) /// internal void FinalizeCacheHeaders(OutputCachingContext context) { - if (context.IsResponseCacheable) + if (context.AllowCacheStorage) { // Create the cache entry now var response = context.HttpContext.Response; @@ -351,8 +367,14 @@ internal void FinalizeCacheHeaders(OutputCachingContext context) /// internal async ValueTask FinalizeCacheBodyAsync(OutputCachingContext context) { - if (context.IsResponseCacheable && context.OutputCachingStream.BufferingEnabled) + if (context.AllowCacheStorage && context.OutputCachingStream.BufferingEnabled) { + // If AllowCacheLookup is false, the cache key was not created + if (context.CacheKey == null) + { + CreateCacheKey(context); + } + var contentLength = context.HttpContext.Response.ContentLength; var cachedResponseBody = context.OutputCachingStream.GetCachedResponseBody(); if (!contentLength.HasValue || contentLength == cachedResponseBody.Length @@ -374,7 +396,7 @@ internal async ValueTask FinalizeCacheBodyAsync(OutputCachingContext context) throw new InvalidOperationException("Cache key must be defined"); } - await _cache.SetAsync(context.CacheKey, context.CachedResponse, context.CachedResponseValidFor); + await _store.SetAsync(context.CacheKey, context.CachedResponse, context.CachedResponseValidFor, context.HttpContext.RequestAborted); } else { diff --git a/src/Middleware/OutputCaching/src/OutputCachingOptions.cs b/src/Middleware/OutputCaching/src/OutputCachingOptions.cs index cf839b14cd8a..a12fc8dac356 100644 --- a/src/Middleware/OutputCaching/src/OutputCachingOptions.cs +++ b/src/Middleware/OutputCaching/src/OutputCachingOptions.cs @@ -36,7 +36,7 @@ public class OutputCachingOptions /// /// Gets the policy applied to all requests. /// - public IOutputCachingPolicy? DefaultPolicy { get; set; } = new DefaultOutputCachePolicy(); + public IOutputCachingPolicy? BasePolicy { get; set; } /// /// Gets a Dictionary of policy names, which are pre-defined settings for diff --git a/src/Middleware/OutputCaching/src/OutputCachingPolicyProvider.cs b/src/Middleware/OutputCaching/src/OutputCachingPolicyProvider.cs index 5cd2dd159c82..c38e8b8a8546 100644 --- a/src/Middleware/OutputCaching/src/OutputCachingPolicyProvider.cs +++ b/src/Middleware/OutputCaching/src/OutputCachingPolicyProvider.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Linq; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Options; @@ -15,11 +16,31 @@ public OutputCachingPolicyProvider(IOptions options) _options = options.Value; } + public bool HasPolicies(HttpContext httpContext) + { + if (_options.BasePolicy != null) + { + return true; + } + + if (httpContext.Features.Get()?.Policies.Any() ?? false) + { + return true; + } + + if (httpContext.GetEndpoint()?.Metadata.GetMetadata()?.Policy != null) + { + return true; + } + + return false; + } + public async Task OnRequestAsync(IOutputCachingContext context) { - if (_options.DefaultPolicy != null) + if (_options.BasePolicy != null) { - await _options.DefaultPolicy.OnRequestAsync(context); + await _options.BasePolicy.OnRequestAsync(context); } var policiesMetadata = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata(); @@ -39,9 +60,9 @@ public async Task OnRequestAsync(IOutputCachingContext context) public async Task OnServeFromCacheAsync(IOutputCachingContext context) { - if (_options.DefaultPolicy != null) + if (_options.BasePolicy != null) { - await _options.DefaultPolicy.OnServeFromCacheAsync(context); + await _options.BasePolicy.OnServeFromCacheAsync(context); } // Apply response policies defined on the feature, e.g. from action attributes @@ -66,9 +87,9 @@ public async Task OnServeFromCacheAsync(IOutputCachingContext context) public async Task OnServeResponseAsync(IOutputCachingContext context) { - if (_options.DefaultPolicy != null) + if (_options.BasePolicy != null) { - await _options.DefaultPolicy.OnServeResponseAsync(context); + await _options.BasePolicy.OnServeResponseAsync(context); } // Apply response policies defined on the feature, e.g. from action attributes diff --git a/src/Middleware/OutputCaching/src/Policies/DefaultOutputCachePolicy.cs b/src/Middleware/OutputCaching/src/Policies/DefaultOutputCachePolicy.cs index ee099f7eeea3..8bf45a9de935 100644 --- a/src/Middleware/OutputCaching/src/Policies/DefaultOutputCachePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/DefaultOutputCachePolicy.cs @@ -7,18 +7,23 @@ namespace Microsoft.AspNetCore.OutputCaching; /// -/// The default policy. +/// A policy which caches un-authenticated, GET and HEAD, 200 responses. /// internal sealed class DefaultOutputCachePolicy : IOutputCachingPolicy { + public static readonly DefaultOutputCachePolicy Instance = new(); + + private DefaultOutputCachePolicy() + { + } + /// Task IOutputCachingPolicy.OnRequestAsync(IOutputCachingContext context) { - context.AttemptOutputCaching = AttemptOutputCaching(context); - context.AllowCacheLookup = true; - context.AllowCacheStorage = true; + var attemptOutputCaching = AttemptOutputCaching(context); + context.AllowCacheLookup = attemptOutputCaching; + context.AllowCacheStorage = attemptOutputCaching; context.AllowLocking = true; - context.IsResponseCacheable = true; // Vary by any query by default context.CachedVaryByRules.QueryKeys = "*"; @@ -29,15 +34,6 @@ Task IOutputCachingPolicy.OnRequestAsync(IOutputCachingContext context) /// Task IOutputCachingPolicy.OnServeFromCacheAsync(IOutputCachingContext context) { - context.IsCacheEntryFresh = true; - - // Validate expiration - if (context.CachedEntryAge <= TimeSpan.Zero) - { - context.Logger.ExpirationExpiresExceeded(context.ResponseTime!.Value); - context.IsCacheEntryFresh = false; - } - return Task.CompletedTask; } @@ -50,7 +46,7 @@ Task IOutputCachingPolicy.OnServeResponseAsync(IOutputCachingContext context) if (!StringValues.IsNullOrEmpty(response.Headers.SetCookie)) { context.Logger.ResponseWithSetCookieNotCacheable(); - context.IsResponseCacheable = false; + context.AllowCacheStorage = false; return Task.CompletedTask; } @@ -58,17 +54,16 @@ Task IOutputCachingPolicy.OnServeResponseAsync(IOutputCachingContext context) if (response.StatusCode != StatusCodes.Status200OK) { context.Logger.ResponseWithUnsuccessfulStatusCodeNotCacheable(response.StatusCode); - context.IsResponseCacheable = false; + context.AllowCacheStorage = false; return Task.CompletedTask; } - context.IsResponseCacheable = true; return Task.CompletedTask; } private static bool AttemptOutputCaching(IOutputCachingContext context) { - // TODO: Should it come from options such that it can be changed without a custom default policy? + // Check if the current request fulfisls the requirements to be cached var request = context.HttpContext.Request; diff --git a/src/Middleware/OutputCaching/src/Policies/EnableCachingPolicy.cs b/src/Middleware/OutputCaching/src/Policies/EnableCachingPolicy.cs index df373a8cd3b6..16e2371cc2ba 100644 --- a/src/Middleware/OutputCaching/src/Policies/EnableCachingPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/EnableCachingPolicy.cs @@ -8,15 +8,17 @@ namespace Microsoft.AspNetCore.OutputCaching; /// internal sealed class EnableCachingPolicy : IOutputCachingPolicy { - /// - /// Default instance of . - /// - public static readonly EnableCachingPolicy Instance = new(); + public static readonly EnableCachingPolicy Enabled = new(); + public static readonly EnableCachingPolicy Disabled = new(); + + private EnableCachingPolicy() + { + } /// Task IOutputCachingPolicy.OnRequestAsync(IOutputCachingContext context) { - context.EnableOutputCaching = true; + context.EnableOutputCaching = this == Enabled; return Task.CompletedTask; } diff --git a/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs b/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs index 92dfd26c155e..17b941a0f050 100644 --- a/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs @@ -6,7 +6,7 @@ namespace Microsoft.AspNetCore.OutputCaching; /// /// A policy that changes the locking behavior. /// -internal class LockingPolicy : IOutputCachingPolicy +internal sealed class LockingPolicy : IOutputCachingPolicy { private readonly bool _lockResponse; diff --git a/src/Middleware/OutputCaching/src/Policies/NoStorePolicy.cs b/src/Middleware/OutputCaching/src/Policies/NoStorePolicy.cs index 8a0dfe655419..3bb35eeff4c0 100644 --- a/src/Middleware/OutputCaching/src/Policies/NoStorePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/NoStorePolicy.cs @@ -8,10 +8,16 @@ namespace Microsoft.AspNetCore.OutputCaching; /// internal class NoStorePolicy : IOutputCachingPolicy { + public static NoStorePolicy Instance = new(); + + private NoStorePolicy() + { + } + /// Task IOutputCachingPolicy.OnServeResponseAsync(IOutputCachingContext context) { - context.IsResponseCacheable = false; + context.AllowCacheStorage = false; return Task.CompletedTask; } diff --git a/src/Middleware/OutputCaching/src/Policies/OutputCachePolicyBuilder.cs b/src/Middleware/OutputCaching/src/Policies/OutputCachePolicyBuilder.cs index e6a3a9ba2362..d55a5340afef 100644 --- a/src/Middleware/OutputCaching/src/Policies/OutputCachePolicyBuilder.cs +++ b/src/Middleware/OutputCaching/src/Policies/OutputCachePolicyBuilder.cs @@ -10,18 +10,22 @@ namespace Microsoft.AspNetCore.OutputCaching; /// /// Provides helper methods to create custom policies. /// -public class OutputCachePolicyBuilder +public sealed class OutputCachePolicyBuilder : IOutputCachingPolicy { + private IOutputCachingPolicy? _builtPolicy; private List Policies { get; } = new(); private List>> Requirements { get; } = new(); /// - /// Gets an initialized with a instance. + /// Creates a new instance of with a policy which allows unauthenticated GET, HEAD, 200 responses without cookies to be cached. /// - public OutputCachePolicyBuilder Default() + /// + /// The default policy doesn't cache any request by default. To enable caching use or invoke CacheOutput() on an endpoint. + /// + public OutputCachePolicyBuilder() { - Policies.Add(new DefaultOutputCachePolicy()); - return this; + _builtPolicy = null; + Policies.Add(DefaultOutputCachePolicy.Instance); } /// @@ -29,6 +33,7 @@ public OutputCachePolicyBuilder Default() /// public OutputCachePolicyBuilder Add(IOutputCachingPolicy policy) { + _builtPolicy = null; Policies.Add(policy); return this; } @@ -38,7 +43,21 @@ public OutputCachePolicyBuilder Add(IOutputCachingPolicy policy) /// public OutputCachePolicyBuilder Enable() { - Policies.Add(new EnableCachingPolicy()); + _builtPolicy = null; + Policies.Add(EnableCachingPolicy.Enabled); + return this; + } + + /// + /// Disables caching. + /// + /// + /// This is the default. + /// + public OutputCachePolicyBuilder Disable() + { + _builtPolicy = null; + Policies.Add(EnableCachingPolicy.Disabled); return this; } @@ -48,6 +67,9 @@ public OutputCachePolicyBuilder Enable() /// The predicate applied to the policy. public OutputCachePolicyBuilder WithCondition(Func> predicate) { + ArgumentNullException.ThrowIfNull(predicate); + + _builtPolicy = null; Requirements.Add(predicate); return this; } @@ -60,6 +82,7 @@ public OutputCachePolicyBuilder WithPath(PathString pathBase) { ArgumentNullException.ThrowIfNull(pathBase); + _builtPolicy = null; Requirements.Add(context => { var match = context.HttpContext.Request.Path.StartsWithSegments(pathBase); @@ -76,6 +99,7 @@ public OutputCachePolicyBuilder WithPath(params PathString[] pathBases) { ArgumentNullException.ThrowIfNull(pathBases); + _builtPolicy = null; Requirements.Add(context => { var match = pathBases.Any(x => context.HttpContext.Request.Path.StartsWithSegments(x)); @@ -92,6 +116,7 @@ public OutputCachePolicyBuilder WithMethod(string method) { ArgumentNullException.ThrowIfNull(method); + _builtPolicy = null; Requirements.Add(context => { var upperMethod = method.ToUpperInvariant(); @@ -109,6 +134,7 @@ public OutputCachePolicyBuilder WithMethod(params string[] methods) { ArgumentNullException.ThrowIfNull(methods); + _builtPolicy = null; Requirements.Add(context => { var upperMethods = methods.Select(m => m.ToUpperInvariant()).ToArray(); @@ -122,10 +148,14 @@ public OutputCachePolicyBuilder WithMethod(params string[] methods) /// Adds a policy to vary the cached responses by query strings. /// /// The query keys to vary the cached responses by. + /// + /// By default all query keys vary the cache entries. However when specific query keys are specified only these are then taken into account. + /// public OutputCachePolicyBuilder VaryByQuery(params string[] queryKeys) { ArgumentNullException.ThrowIfNull(queryKeys); + _builtPolicy = null; Policies.Add(new VaryByQueryPolicy(queryKeys)); return this; } @@ -138,6 +168,7 @@ public OutputCachePolicyBuilder VaryByHeader(params string[] headers) { ArgumentNullException.ThrowIfNull(headers); + _builtPolicy = null; Policies.Add(new VaryByHeaderPolicy(headers)); return this; } @@ -150,6 +181,7 @@ public OutputCachePolicyBuilder VaryByValue(Func> varyBy) { ArgumentNullException.ThrowIfNull(varyBy); + _builtPolicy = null; Policies.Add(new VaryByValuePolicy(varyBy)); return this; } @@ -162,6 +194,7 @@ public OutputCachePolicyBuilder VaryByValue(Func> varyBy) { ArgumentNullException.ThrowIfNull(varyBy); + _builtPolicy = null; Policies.Add(new VaryByValuePolicy(varyBy)); return this; } @@ -174,6 +207,7 @@ public OutputCachePolicyBuilder VaryByValue(Func varyBy) { ArgumentNullException.ThrowIfNull(varyBy); + _builtPolicy = null; Policies.Add(new VaryByValuePolicy(varyBy)); return this; } @@ -186,6 +220,7 @@ public OutputCachePolicyBuilder VaryByValue(Func<(string, string)> varyBy) { ArgumentNullException.ThrowIfNull(varyBy); + _builtPolicy = null; Policies.Add(new VaryByValuePolicy(varyBy)); return this; } @@ -194,12 +229,12 @@ public OutputCachePolicyBuilder VaryByValue(Func<(string, string)> varyBy) /// Adds a named policy. /// /// The name of the policy to add. - public OutputCachePolicyBuilder Profile(string profileName) + public OutputCachePolicyBuilder Policy(string profileName) { ArgumentNullException.ThrowIfNull(profileName); + _builtPolicy = null; Policies.Add(new ProfilePolicy(profileName)); - return this; } @@ -211,6 +246,7 @@ public OutputCachePolicyBuilder Tag(params string[] tags) { ArgumentNullException.ThrowIfNull(tags); + _builtPolicy = null; Policies.Add(new TagsPolicy(tags)); return this; } @@ -221,6 +257,7 @@ public OutputCachePolicyBuilder Tag(params string[] tags) /// The expiration of the cached reponse. public OutputCachePolicyBuilder Expire(TimeSpan expiration) { + _builtPolicy = null; Policies.Add(new ExpirationPolicy(expiration)); return this; } @@ -231,6 +268,7 @@ public OutputCachePolicyBuilder Expire(TimeSpan expiration) /// Whether the request should be locked. public OutputCachePolicyBuilder Lock(bool lockResponse = true) { + _builtPolicy = null; Policies.Add(lockResponse ? LockingPolicy.Enabled : LockingPolicy.Disabled); return this; } @@ -240,6 +278,7 @@ public OutputCachePolicyBuilder Lock(bool lockResponse = true) /// public OutputCachePolicyBuilder Clear() { + _builtPolicy = null; Requirements.Clear(); Policies.Clear(); return this; @@ -250,7 +289,8 @@ public OutputCachePolicyBuilder Clear() /// public OutputCachePolicyBuilder NoStore() { - Policies.Add(new NoStorePolicy()); + _builtPolicy = null; + Policies.Add(NoStorePolicy.Instance); return this; } @@ -263,6 +303,11 @@ public OutputCachePolicyBuilder NoStore() /// public IOutputCachingPolicy Build() { + if (_builtPolicy != null) + { + return _builtPolicy; + } + var policies = new CompositePolicy(Policies.ToArray()); if (Requirements.Any()) @@ -281,6 +326,21 @@ public IOutputCachingPolicy Build() }, policies); } - return policies; + return _builtPolicy = policies; + } + + Task IOutputCachingPolicy.OnRequestAsync(IOutputCachingContext context) + { + return Build().OnRequestAsync(context); + } + + Task IOutputCachingPolicy.OnServeFromCacheAsync(IOutputCachingContext context) + { + return Build().OnServeFromCacheAsync(context); + } + + Task IOutputCachingPolicy.OnServeResponseAsync(IOutputCachingContext context) + { + return Build().OnServeResponseAsync(context); } } diff --git a/src/Middleware/OutputCaching/src/Policies/PolicyExtensions.cs b/src/Middleware/OutputCaching/src/Policies/PolicyExtensions.cs index 30bf9e6055f0..8c9614d30fe9 100644 --- a/src/Middleware/OutputCaching/src/Policies/PolicyExtensions.cs +++ b/src/Middleware/OutputCaching/src/Policies/PolicyExtensions.cs @@ -10,15 +10,17 @@ namespace Microsoft.AspNetCore.OutputCaching.Policies; /// public static class PolicyExtensions { + private static readonly IOutputCachingPolicy _defaultEnabledPolicy = new OutputCachePolicyBuilder().Enable(); + /// - /// Marks an endpoint to be cached. + /// Marks an endpoint to be cached with the default policy. /// public static TBuilder CacheOutput(this TBuilder builder) where TBuilder : IEndpointConventionBuilder { ArgumentNullException.ThrowIfNull(builder); // Enable caching if this method is invoked on an endpoint, extra policies can disable it - var policiesMetadata = new PoliciesMetadata(EnableCachingPolicy.Instance); + var policiesMetadata = new PoliciesMetadata(_defaultEnabledPolicy); builder.Add(endpointBuilder => { @@ -51,11 +53,9 @@ public static TBuilder CacheOutput(this TBuilder builder, Action bool Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.NoStore.set -> void Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.OutputCacheAttribute() -> void Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.Policy.get -> Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy! -Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.Profile.get -> string? -Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.Profile.set -> void +Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.PolicyName.get -> string? +Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.PolicyName.set -> void Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.VaryByHeaders.get -> string![]? Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.VaryByHeaders.set -> void Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.VaryByQueryKeys.get -> string![]? @@ -17,13 +17,13 @@ Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Add(Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy! policy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Build() -> Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Clear() -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! -Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Default() -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Disable() -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Enable() -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Expire(System.TimeSpan expiration) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Lock(bool lockResponse = true) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.NoStore() -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.OutputCachePolicyBuilder() -> void -Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Profile(string! profileName) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Policy(string! profileName) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Tag(params string![]! tags) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByHeader(params string![]! headers) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByQuery(params string![]! queryKeys) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! @@ -44,10 +44,10 @@ Microsoft.AspNetCore.OutputCaching.OutputCachingMiddleware Microsoft.AspNetCore.OutputCaching.OutputCachingMiddleware.Invoke(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.OutputCaching.OutputCachingMiddleware.OutputCachingMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Options.IOptions! options, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, Microsoft.AspNetCore.OutputCaching.IOutputCacheStore! outputCache, Microsoft.Extensions.ObjectPool.ObjectPoolProvider! poolProvider) -> void Microsoft.AspNetCore.OutputCaching.OutputCachingOptions +Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.BasePolicy.get -> Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy? +Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.BasePolicy.set -> void Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.DefaultExpirationTimeSpan.get -> System.TimeSpan Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.DefaultExpirationTimeSpan.set -> void -Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.DefaultPolicy.get -> Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy? -Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.DefaultPolicy.set -> void Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.MaximumBodySize.get -> long Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.MaximumBodySize.set -> void Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.OutputCachingOptions() -> void diff --git a/src/Middleware/OutputCaching/test/OutputCachingMiddlewareTests.cs b/src/Middleware/OutputCaching/test/OutputCachingMiddlewareTests.cs index dc75a73e0529..58620f3b5b53 100644 --- a/src/Middleware/OutputCaching/test/OutputCachingMiddlewareTests.cs +++ b/src/Middleware/OutputCaching/test/OutputCachingMiddlewareTests.cs @@ -63,7 +63,8 @@ await cache.SetAsync( Headers = new HeaderDictionary(), Body = new CachedResponseBody(new List(0), 0) }, - TimeSpan.Zero); + TimeSpan.Zero, + default); Assert.True(await middleware.TryServeFromCacheAsync(context)); Assert.Equal(1, cache.GetCount); @@ -91,7 +92,8 @@ await cache.SetAsync( }, Body = new CachedResponseBody(new List(0), 0) }, - TimeSpan.Zero); + TimeSpan.Zero, + default); Assert.True(await middleware.TryServeFromCacheAsync(context)); Assert.Equal("NewValue", context.HttpContext.Response.Headers["MyHeader"]); @@ -116,7 +118,8 @@ await cache.SetAsync( { Body = new CachedResponseBody(new List(0), 0) }, - TimeSpan.Zero); + TimeSpan.Zero, + default); Assert.True(await middleware.TryServeFromCacheAsync(context)); Assert.Equal(1, cache.GetCount); @@ -351,29 +354,6 @@ public void StartResponseAsync_IfAllowResponseCaptureIsTrue_SetsResponseTimeOnly Assert.Equal(initialTime, context.ResponseTime); } - [Fact] - public void FinalizeCacheHeadersAsync_DoesntUpdateAllowCacheStorage_IfResponseCacheable() - { - // Contrary to ResponseCaching which reacts to server headers. - - var sink = new TestSink(); - var middleware = TestUtils.CreateTestMiddleware(testSink: sink); - var context = TestUtils.CreateTestContext(); - context.AllowCacheStorage = false; - - context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() - { - Public = true - }.ToString(); - - Assert.False(context.AllowCacheStorage); - - middleware.FinalizeCacheHeaders(context); - - Assert.False(context.AllowCacheStorage); - Assert.Empty(sink.Writes); - } - [Fact] public void FinalizeCacheHeadersAsync_DefaultResponseValidity_Is60Seconds() { @@ -699,7 +679,7 @@ public async Task FinalizeCacheBody_DoNotCache_IfIsResponseCacheableFalse() middleware.ShimResponseStream(context); await context.HttpContext.Response.WriteAsync(new string('0', 10)); - context.IsResponseCacheable = false; + context.AllowCacheStorage = false; context.CacheKey = "BaseKey"; await middleware.FinalizeCacheBodyAsync(context); @@ -791,7 +771,7 @@ public override void OnStarting(Func callback, object state) { } [InlineData(true, false, true)] [InlineData(true, true, false)] [InlineData(true, true, true)] - public async Task Invoke_AddsOutputCachingFeature_Always(bool allowResponseCaching, bool allowCacheLookup, bool allowCacheStorage) + public async Task Invoke_AddsOutputCachingFeature_Always(bool enableOutputCaching, bool allowCacheLookup, bool allowCacheStorage) { var responseCachingFeatureAdded = false; var middleware = TestUtils.CreateTestMiddleware(next: httpContext => @@ -801,7 +781,7 @@ public async Task Invoke_AddsOutputCachingFeature_Always(bool allowResponseCachi }, policyProvider: new TestOutputCachingPolicyProvider { - AttemptOutputCachingValue = allowResponseCaching, + EnableOutputCaching = enableOutputCaching, AllowCacheLookupValue = allowCacheLookup, AllowCacheStorageValue = allowCacheStorage }); diff --git a/src/Middleware/OutputCaching/test/OutputCachingPolicyProviderTests.cs b/src/Middleware/OutputCaching/test/OutputCachingPolicyProviderTests.cs index 9d3646d4a38d..2deaa6ef3f70 100644 --- a/src/Middleware/OutputCaching/test/OutputCachingPolicyProviderTests.cs +++ b/src/Middleware/OutputCaching/test/OutputCachingPolicyProviderTests.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Runtime.CompilerServices; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.Options; @@ -47,12 +46,13 @@ public async Task AttemptOutputCaching_CacheableMethods_Allowed(string method) { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - var options = new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().Build() }; + var options = new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().Build() }; context.HttpContext.Request.Method = method; await new OutputCachingPolicyProvider(Options.Create(options)).OnRequestAsync(context); - Assert.True(context.AttemptOutputCaching); + Assert.True(context.AllowCacheStorage); + Assert.True(context.AllowCacheLookup); Assert.Empty(sink.Writes); } @@ -62,12 +62,13 @@ public async Task AttemptOutputCaching_UncacheableMethods_NotAllowed(string meth { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - var options = new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().Build() }; + var options = new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().Build() }; context.HttpContext.Request.Method = method; await new OutputCachingPolicyProvider(Options.Create(options)).OnRequestAsync(context); - Assert.False(context.AttemptOutputCaching); + Assert.False(context.AllowCacheLookup); + Assert.False(context.AllowCacheStorage); TestUtils.AssertLoggedMessages( sink.Writes, LoggedMessage.RequestMethodNotCacheable); @@ -81,11 +82,12 @@ public async Task AttemptResponseCaching_AuthorizationHeaders_NotAllowed() context.HttpContext.Request.Method = HttpMethods.Get; context.HttpContext.Request.Headers.Authorization = "Placeholder"; - var options = new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().Build() }; + var options = new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().Build() }; await new OutputCachingPolicyProvider(Options.Create(options)).OnRequestAsync(context); - Assert.False(context.AttemptOutputCaching); + Assert.False(context.AllowCacheStorage); + Assert.False(context.AllowCacheLookup); TestUtils.AssertLoggedMessages( sink.Writes, @@ -103,7 +105,7 @@ public async Task AllowCacheStorage_NoStore_Allowed() NoStore = true }.ToString(); - var options = new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().Build() }; + var options = new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().Build() }; await new OutputCachingPolicyProvider(Options.Create(options)).OnRequestAsync(context); Assert.True(context.AllowCacheStorage); @@ -119,7 +121,7 @@ public async Task AllowCacheLookup_LegacyDirectives_OverridenByCacheControl() context.HttpContext.Request.Headers.Pragma = "no-cache"; context.HttpContext.Request.Headers.CacheControl = "max-age=10"; - var options = new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().Build() }; + var options = new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().Build() }; await new OutputCachingPolicyProvider(Options.Create(options)).OnRequestAsync(context); Assert.True(context.AllowCacheLookup); @@ -132,10 +134,11 @@ public async Task IsResponseCacheable_NoPublic_Allowed() var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - var options = new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().Build() }; + var options = new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().Build() }; await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); - Assert.True(context.IsResponseCacheable); + Assert.True(context.AllowCacheStorage); + Assert.True(context.AllowCacheLookup); Assert.Empty(sink.Writes); } @@ -149,10 +152,11 @@ public async Task IsResponseCacheable_Public_Allowed() Public = true }.ToString(); - var options = new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().Build() }; + var options = new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().Build() }; await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); - Assert.True(context.IsResponseCacheable); + Assert.True(context.AllowCacheStorage); + Assert.True(context.AllowCacheLookup); Assert.Empty(sink.Writes); } @@ -163,14 +167,14 @@ public async Task IsResponseCacheable_NoCache_Allowed() var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() { - Public = true, NoCache = true }.ToString(); - var options = new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().Build() }; + var options = new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().Build() }; await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); - Assert.True(context.IsResponseCacheable); + Assert.True(context.AllowCacheStorage); + Assert.True(context.AllowCacheLookup); Assert.Empty(sink.Writes); } @@ -181,14 +185,14 @@ public async Task IsResponseCacheable_ResponseNoStore_Allowed() var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() { - Public = true, NoStore = true }.ToString(); - var options = new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().Build() }; + var options = new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().Build() }; await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); - Assert.True(context.IsResponseCacheable); + Assert.True(context.AllowCacheStorage); + Assert.True(context.AllowCacheLookup); Assert.Empty(sink.Writes); } @@ -197,16 +201,13 @@ public async Task IsResponseCacheable_SetCookieHeader_NotAllowed() { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() - { - Public = true - }.ToString(); context.HttpContext.Response.Headers.SetCookie = "cookieName=cookieValue"; - var options = new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().Build() }; + var options = new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().Build() }; await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); - Assert.False(context.IsResponseCacheable); + Assert.False(context.AllowCacheStorage); + Assert.True(context.AllowCacheLookup); TestUtils.AssertLoggedMessages( sink.Writes, LoggedMessage.ResponseWithSetCookieNotCacheable); @@ -217,16 +218,13 @@ public async Task IsResponseCacheable_VaryHeaderByStar_Allowed() { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() - { - Public = true - }.ToString(); context.HttpContext.Response.Headers.Vary = "*"; - var options = new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().Build() }; + var options = new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().Build() }; await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); - Assert.True(context.IsResponseCacheable); + Assert.True(context.AllowCacheStorage); + Assert.True(context.AllowCacheLookup); Assert.Empty(sink.Writes); } @@ -237,14 +235,14 @@ public async Task IsResponseCacheable_Private_Allowed() var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() { - Public = true, Private = true }.ToString(); - var options = new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().Build() }; + var options = new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().Build() }; await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); - Assert.True(context.IsResponseCacheable); + Assert.True(context.AllowCacheStorage); + Assert.True(context.AllowCacheLookup); Assert.Empty(sink.Writes); } @@ -255,15 +253,12 @@ public async Task IsResponseCacheable_SuccessStatusCodes_Allowed(int statusCode) var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.StatusCode = statusCode; - context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() - { - Public = true - }.ToString(); - var options = new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().Build() }; + var options = new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().Build() }; await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); - Assert.True(context.IsResponseCacheable); + Assert.True(context.AllowCacheStorage); + Assert.True(context.AllowCacheLookup); Assert.Empty(sink.Writes); } @@ -334,15 +329,12 @@ public async Task IsResponseCacheable_NonSuccessStatusCodes_NotAllowed(int statu var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.StatusCode = statusCode; - context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() - { - Public = true - }.ToString(); - var options = new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().Build() }; + var options = new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().Build() }; await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); - Assert.False(context.IsResponseCacheable); + Assert.True(context.AllowCacheLookup); + Assert.False(context.AllowCacheStorage); TestUtils.AssertLoggedMessages( sink.Writes, LoggedMessage.ResponseWithUnsuccessfulStatusCodeNotCacheable); @@ -354,19 +346,16 @@ public async Task IsResponseCacheable_NoExpiryRequirements_IsAllowed() var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; - context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() - { - Public = true - }.ToString(); var utcNow = DateTimeOffset.UtcNow; context.HttpContext.Response.Headers.Date = HeaderUtilities.FormatDate(utcNow); context.ResponseTime = DateTimeOffset.MaxValue; - var options = new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().Build() }; + var options = new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().Build() }; await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); - Assert.True(context.IsResponseCacheable); + Assert.True(context.AllowCacheStorage); + Assert.True(context.AllowCacheLookup); Assert.Empty(sink.Writes); } @@ -379,17 +368,17 @@ public async Task IsResponseCacheable_MaxAgeOverridesExpiry_ToAllowed() context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() { - Public = true, MaxAge = TimeSpan.FromSeconds(10) }.ToString(); context.HttpContext.Response.Headers.Expires = HeaderUtilities.FormatDate(utcNow); context.HttpContext.Response.Headers.Date = HeaderUtilities.FormatDate(utcNow); context.ResponseTime = utcNow + TimeSpan.FromSeconds(9); - var options = new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().Build() }; + var options = new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().Build() }; await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); - Assert.True(context.IsResponseCacheable); + Assert.True(context.AllowCacheStorage); + Assert.True(context.AllowCacheLookup); Assert.Empty(sink.Writes); } @@ -402,17 +391,17 @@ public async Task IsResponseCacheable_SharedMaxAgeOverridesMaxAge_ToAllowed() context.HttpContext.Response.StatusCode = StatusCodes.Status200OK; context.HttpContext.Response.Headers.CacheControl = new CacheControlHeaderValue() { - Public = true, MaxAge = TimeSpan.FromSeconds(10), SharedMaxAge = TimeSpan.FromSeconds(15) }.ToString(); context.HttpContext.Response.Headers.Date = HeaderUtilities.FormatDate(utcNow); context.ResponseTime = utcNow + TimeSpan.FromSeconds(11); - var options = new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().Build() }; + var options = new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().Build() }; await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); - Assert.True(context.IsResponseCacheable); + Assert.True(context.AllowCacheStorage); + Assert.True(context.AllowCacheLookup); Assert.Empty(sink.Writes); } @@ -425,7 +414,7 @@ public async Task IsCachedEntryFresh_NoExpiryRequirements_IsFresh() context.ResponseTime = DateTimeOffset.MaxValue; context.CachedEntryAge = TimeSpan.MaxValue; - var options = new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().Build() }; + var options = new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().Build() }; await new OutputCachingPolicyProvider(Options.Create(options)).OnServeFromCacheAsync(context); Assert.True(context.IsCacheEntryFresh); @@ -441,7 +430,7 @@ public async Task IsCachedEntryFresh_AtExpiry_IsNotFresh() context.ResponseTime = utcNow; context.CachedEntryAge = TimeSpan.Zero; - var options = new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().Build() }; + var options = new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().Build() }; await new OutputCachingPolicyProvider(Options.Create(options)).OnServeFromCacheAsync(context); Assert.False(context.IsCacheEntryFresh); @@ -461,13 +450,12 @@ public async Task IsCachedEntryFresh_SharedMaxAgeOverridesMaxAge_ToFresh() context.CachedResponse = new OutputCacheEntry { Headers = new HeaderDictionary() }; context.CachedResponse.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() { - Public = true, MaxAge = TimeSpan.FromSeconds(10), SharedMaxAge = TimeSpan.FromSeconds(15) }.ToString(); context.CachedResponse.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow); - var options = new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().Build() }; + var options = new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().Build() }; await new OutputCachingPolicyProvider(Options.Create(options)).OnServeFromCacheAsync(context); Assert.True(context.IsCacheEntryFresh); diff --git a/src/Middleware/OutputCaching/test/OutputCachingTests.cs b/src/Middleware/OutputCaching/test/OutputCachingTests.cs index ab6ef268848b..d4aac8b6dbc0 100644 --- a/src/Middleware/OutputCaching/test/OutputCachingTests.cs +++ b/src/Middleware/OutputCaching/test/OutputCachingTests.cs @@ -221,7 +221,7 @@ public async Task ServesFreshContent_If_ResponseExpired(string method) { var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions { - DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().VaryByHeader(HeaderNames.From).Build(), + BasePolicy = new OutputCachePolicyBuilder().Enable().VaryByHeader(HeaderNames.From).Build(), DefaultExpirationTimeSpan = TimeSpan.FromMicroseconds(100) }); @@ -294,7 +294,7 @@ public async Task ServesCachedContent_IfVaryHeader_Matches() [Fact] public async Task ServesFreshContent_IfVaryHeader_Mismatches() { - var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().VaryByHeader(HeaderNames.From).Build() }); + var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().VaryByHeader(HeaderNames.From).Build() }); foreach (var builder in builders) { @@ -318,7 +318,7 @@ public async Task ServesFreshContent_IfVaryHeader_Mismatches() [Fact] public async Task ServesCachedContent_IfVaryQueryKeys_Matches() { - var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().VaryByQuery("query").Build() }); + var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().VaryByQuery("query").Build() }); foreach (var builder in builders) { @@ -340,7 +340,7 @@ public async Task ServesCachedContent_IfVaryQueryKeys_Matches() [Fact] public async Task ServesCachedContent_IfVaryQueryKeysExplicit_Matches_QueryKeyCaseInsensitive() { - var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().VaryByQuery("QueryA", "queryb").Build() }); + var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().VaryByQuery("QueryA", "queryb").Build() }); foreach (var builder in builders) { @@ -362,7 +362,7 @@ public async Task ServesCachedContent_IfVaryQueryKeysExplicit_Matches_QueryKeyCa [Fact] public async Task ServesCachedContent_IfVaryQueryKeyStar_Matches_QueryKeyCaseInsensitive() { - var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().VaryByQuery("*").Build() }); + var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().VaryByQuery("*").Build() }); foreach (var builder in builders) { @@ -384,7 +384,7 @@ public async Task ServesCachedContent_IfVaryQueryKeyStar_Matches_QueryKeyCaseIns [Fact] public async Task ServesCachedContent_IfVaryQueryKeyExplicit_Matches_OrderInsensitive() { - var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().VaryByQuery("QueryB", "QueryA").Build() }); + var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().VaryByQuery("QueryB", "QueryA").Build() }); foreach (var builder in builders) { @@ -406,7 +406,7 @@ public async Task ServesCachedContent_IfVaryQueryKeyExplicit_Matches_OrderInsens [Fact] public async Task ServesCachedContent_IfVaryQueryKeyStar_Matches_OrderInsensitive() { - var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().VaryByQuery("*").Build() }); + var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().VaryByQuery("*").Build() }); foreach (var builder in builders) { @@ -428,7 +428,7 @@ public async Task ServesCachedContent_IfVaryQueryKeyStar_Matches_OrderInsensitiv [Fact] public async Task ServesFreshContent_IfVaryQueryKey_Mismatches() { - var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().VaryByQuery("query").Build() }); + var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().VaryByQuery("query").Build() }); foreach (var builder in builders) { @@ -450,7 +450,7 @@ public async Task ServesFreshContent_IfVaryQueryKey_Mismatches() [Fact] public async Task ServesCachedContent_IfOtherVaryQueryKey_Mismatches() { - var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions { DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().VaryByQuery("query").Build() }); + var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().VaryByQuery("query").Build() }); foreach (var builder in builders) { @@ -774,7 +774,7 @@ public async Task ServesCachedContent_IfBodySize_IsCacheable() { var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions() { - DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().Build(), + BasePolicy = new OutputCachePolicyBuilder().Enable().Build(), MaximumBodySize = 1000 }); diff --git a/src/Middleware/OutputCaching/test/TestUtils.cs b/src/Middleware/OutputCaching/test/TestUtils.cs index 068c672444d9..9e8f037a2021 100644 --- a/src/Middleware/OutputCaching/test/TestUtils.cs +++ b/src/Middleware/OutputCaching/test/TestUtils.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Globalization; using System.Net.Http; using System.Text; using Microsoft.AspNetCore.Builder; @@ -16,8 +15,6 @@ using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.ObjectPool; using Microsoft.Extensions.Options; -using Microsoft.Extensions.Primitives; -using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.OutputCaching.Tests; @@ -147,13 +144,13 @@ private static IEnumerable CreateBuildersWithOutputCaching( outputCachingOptions.MaximumBodySize = options.MaximumBodySize; outputCachingOptions.UseCaseSensitivePaths = options.UseCaseSensitivePaths; outputCachingOptions.SystemClock = options.SystemClock; - outputCachingOptions.DefaultPolicy = options.DefaultPolicy; + outputCachingOptions.BasePolicy = options.BasePolicy; outputCachingOptions.DefaultExpirationTimeSpan = options.DefaultExpirationTimeSpan; outputCachingOptions.SizeLimit = options.SizeLimit; } else { - outputCachingOptions.DefaultPolicy = new OutputCachePolicyBuilder().Default().Enable().Build(); + outputCachingOptions.BasePolicy = new OutputCachePolicyBuilder().Enable().Build(); } }); }) @@ -207,30 +204,33 @@ internal static OutputCachingMiddleware CreateTestMiddleware( internal static OutputCachingContext CreateTestContext() { - return new OutputCachingContext(new DefaultHttpContext(), NullLogger.Instance) + return new OutputCachingContext(new DefaultHttpContext(), new TestOutputCache(), NullLogger.Instance) { + EnableOutputCaching = true, AllowCacheStorage = true, - IsResponseCacheable = true, + AllowCacheLookup = true, ResponseTime = DateTimeOffset.UtcNow }; } internal static OutputCachingContext CreateTestContext(HttpContext httpContext) { - return new OutputCachingContext(httpContext, NullLogger.Instance) + return new OutputCachingContext(httpContext, new TestOutputCache(), NullLogger.Instance) { + EnableOutputCaching = true, AllowCacheStorage = true, - IsResponseCacheable = true, + AllowCacheLookup = true, ResponseTime = DateTimeOffset.UtcNow }; } internal static OutputCachingContext CreateTestContext(ITestSink testSink) { - return new OutputCachingContext(new DefaultHttpContext(), new TestLogger("OutputCachingTests", testSink, true)) + return new OutputCachingContext(new DefaultHttpContext(), new TestOutputCache(), new TestLogger("OutputCachingTests", testSink, true)) { + EnableOutputCaching = true, AllowCacheStorage = true, - IsResponseCacheable = true, + AllowCacheLookup = true, ResponseTime = DateTimeOffset.UtcNow }; } @@ -317,17 +317,20 @@ private LoggedMessage(int evenId, LogLevel logLevel) internal class TestOutputCachingPolicyProvider : IOutputCachingPolicyProvider { - public bool AllowCacheLookupValue { get; set; } = false; - public bool AllowCacheStorageValue { get; set; } = false; - public bool AttemptOutputCachingValue { get; set; } = false; + public bool AllowCacheLookupValue { get; set; } + public bool AllowCacheStorageValue { get; set; } public bool EnableOutputCaching { get; set; } = true; - public bool IsCachedEntryFreshValue { get; set; } = true; - public bool IsResponseCacheableValue { get; set; } = true; + + public bool HasPolicies(HttpContext httpContext) + { + return true; + } public Task OnRequestAsync(IOutputCachingContext context) { context.EnableOutputCaching = EnableOutputCaching; - context.AttemptOutputCaching = AttemptOutputCachingValue; + context.AllowCacheLookup = AllowCacheLookupValue; + context.AllowCacheStorage = AllowCacheStorageValue; return Task.CompletedTask; } @@ -335,14 +338,13 @@ public Task OnRequestAsync(IOutputCachingContext context) public Task OnServeFromCacheAsync(IOutputCachingContext context) { context.AllowCacheLookup = AllowCacheLookupValue; - context.IsCacheEntryFresh = IsCachedEntryFreshValue; + context.AllowCacheStorage = AllowCacheStorageValue; return Task.CompletedTask; } public Task OnServeResponseAsync(IOutputCachingContext context) { - context.IsResponseCacheable = IsResponseCacheableValue; context.AllowCacheStorage = AllowCacheStorageValue; return Task.CompletedTask; @@ -370,12 +372,12 @@ internal class TestOutputCache : IOutputCacheStore public int GetCount { get; private set; } public int SetCount { get; private set; } - public ValueTask EvictByTagAsync(string tag) + public ValueTask EvictByTagAsync(string tag, CancellationToken token) { throw new NotImplementedException(); } - public ValueTask GetAsync(string key) + public ValueTask GetAsync(string key, CancellationToken token) { GetCount++; try @@ -388,7 +390,7 @@ public ValueTask GetAsync(string key) } } - public ValueTask SetAsync(string key, IOutputCacheEntry entry, TimeSpan validFor) + public ValueTask SetAsync(string key, IOutputCacheEntry entry, TimeSpan validFor, CancellationToken token) { SetCount++; _storage[key] = entry; diff --git a/src/Mvc/Mvc.Core/src/OutputCacheAttribute.cs b/src/Mvc/Mvc.Core/src/OutputCacheAttribute.cs index 0cbf0bcdb69b..20f41fd76729 100644 --- a/src/Mvc/Mvc.Core/src/OutputCacheAttribute.cs +++ b/src/Mvc/Mvc.Core/src/OutputCacheAttribute.cs @@ -48,9 +48,9 @@ public bool NoStore public string[]? VaryByQueryKeys { get; set; } /// - /// Gets or sets the value of the cache profile name. + /// Gets or sets the value of the cache policy name. /// - public string? Profile { get; set; } + public string? PolicyName { get; set; } /// public int Order { get; set; } @@ -67,9 +67,9 @@ private IOutputCachingPolicy GetPolicy() builder.NoStore(); } - if (Profile != null) + if (PolicyName != null) { - builder.Profile(Profile); + builder.Policy(PolicyName); } if (VaryByQueryKeys != null) diff --git a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt index 9e61bad2f36e..d50a40462ecb 100644 --- a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt +++ b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt @@ -22,8 +22,8 @@ Microsoft.AspNetCore.Mvc.OutputCacheAttribute.Order.get -> int Microsoft.AspNetCore.Mvc.OutputCacheAttribute.Order.set -> void Microsoft.AspNetCore.Mvc.OutputCacheAttribute.OutputCacheAttribute() -> void Microsoft.AspNetCore.Mvc.OutputCacheAttribute.Policy.get -> Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy! -Microsoft.AspNetCore.Mvc.OutputCacheAttribute.Profile.get -> string? -Microsoft.AspNetCore.Mvc.OutputCacheAttribute.Profile.set -> void +Microsoft.AspNetCore.Mvc.OutputCacheAttribute.PolicyName.get -> string? +Microsoft.AspNetCore.Mvc.OutputCacheAttribute.PolicyName.set -> void Microsoft.AspNetCore.Mvc.OutputCacheAttribute.VaryByQueryKeys.get -> string![]? Microsoft.AspNetCore.Mvc.OutputCacheAttribute.VaryByQueryKeys.set -> void virtual Microsoft.AspNetCore.Mvc.Infrastructure.ConfigureCompatibilityOptions.PostConfigure(string? name, TOptions! options) -> void From 2239c8a881d6f75c0853c853b4614b126d087d61 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Wed, 8 Jun 2022 09:31:26 -0700 Subject: [PATCH 35/48] Fix unit tests --- .../src/OutputCachingMiddleware.cs | 10 ++-- .../test/OutputCachingMiddlewareTests.cs | 27 ++++----- .../test/OutputCachingPolicyProviderTests.cs | 57 ------------------- .../OutputCaching/test/TestUtils.cs | 12 ++-- 4 files changed, 26 insertions(+), 80 deletions(-) diff --git a/src/Middleware/OutputCaching/src/OutputCachingMiddleware.cs b/src/Middleware/OutputCaching/src/OutputCachingMiddleware.cs index 3ed17b8a517a..89eab4d28918 100644 --- a/src/Middleware/OutputCaching/src/OutputCachingMiddleware.cs +++ b/src/Middleware/OutputCaching/src/OutputCachingMiddleware.cs @@ -290,6 +290,11 @@ internal async Task TryServeFromCacheAsync(OutputCachingContext cacheConte internal void CreateCacheKey(OutputCachingContext context) { + if (!string.IsNullOrEmpty(context.CacheKey)) + { + return; + } + var varyHeaders = context.CachedVaryByRules.Headers; var varyQueryKeys = context.CachedVaryByRules.QueryKeys; var varyByCustomKeys = context.CachedVaryByRules.VaryByCustom; @@ -370,10 +375,7 @@ internal async ValueTask FinalizeCacheBodyAsync(OutputCachingContext context) if (context.AllowCacheStorage && context.OutputCachingStream.BufferingEnabled) { // If AllowCacheLookup is false, the cache key was not created - if (context.CacheKey == null) - { - CreateCacheKey(context); - } + CreateCacheKey(context); var contentLength = context.HttpContext.Response.ContentLength; var cachedResponseBody = context.OutputCachingStream.GetCachedResponseBody(); diff --git a/src/Middleware/OutputCaching/test/OutputCachingMiddlewareTests.cs b/src/Middleware/OutputCaching/test/OutputCachingMiddlewareTests.cs index 58620f3b5b53..674c5c5e2666 100644 --- a/src/Middleware/OutputCaching/test/OutputCachingMiddlewareTests.cs +++ b/src/Middleware/OutputCaching/test/OutputCachingMiddlewareTests.cs @@ -20,7 +20,7 @@ public async Task TryServeFromCacheAsync_OnlyIfCached_Serves504() var cache = new TestOutputCache(); var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider("BaseKey")); - var context = TestUtils.CreateTestContext(); + var context = TestUtils.CreateTestContext(cache); context.HttpContext.Request.Headers.CacheControl = new CacheControlHeaderValue() { OnlyIfCached = true @@ -39,7 +39,7 @@ public async Task TryServeFromCacheAsync_CachedResponseNotFound_Fails() var cache = new TestOutputCache(); var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider("BaseKey")); - var context = TestUtils.CreateTestContext(); + var context = TestUtils.CreateTestContext(cache); Assert.False(await middleware.TryServeFromCacheAsync(context)); Assert.Equal(1, cache.GetCount); @@ -54,7 +54,7 @@ public async Task TryServeFromCacheAsync_CachedResponseFound_Succeeds() var cache = new TestOutputCache(); var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider("BaseKey")); - var context = TestUtils.CreateTestContext(); + var context = TestUtils.CreateTestContext(cache); await cache.SetAsync( "BaseKey", @@ -79,11 +79,12 @@ public async Task TryServeFromCacheAsync_CachedResponseFound_OverwritesExistingH var cache = new TestOutputCache(); var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider("BaseKey")); - var context = TestUtils.CreateTestContext(); + var context = TestUtils.CreateTestContext(cache); + context.CacheKey = "BaseKey"; context.HttpContext.Response.Headers["MyHeader"] = "OldValue"; await cache.SetAsync( - "BaseKey", + context.CacheKey, new OutputCacheEntry() { Headers = new HeaderDictionary() @@ -109,7 +110,7 @@ public async Task TryServeFromCacheAsync_CachedResponseFound_Serves304IfPossible var cache = new TestOutputCache(); var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider("BaseKey")); - var context = TestUtils.CreateTestContext(); + var context = TestUtils.CreateTestContext(cache); context.HttpContext.Request.Headers.IfNoneMatch = "*"; await cache.SetAsync( @@ -478,7 +479,7 @@ public void FinalizeCacheHeadersAsync_UpdateCachedVaryByRules_NullOrEmptyRules(S var cache = new TestOutputCache(); var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); - var context = TestUtils.CreateTestContext(); + var context = TestUtils.CreateTestContext(cache); context.HttpContext.Response.Headers.Vary = vary; context.HttpContext.Features.Set(new OutputCachingFeature(context)); @@ -566,7 +567,7 @@ public async Task FinalizeCacheBody_Cache_IfContentLengthMatches() var cache = new TestOutputCache(); var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); - var context = TestUtils.CreateTestContext(); + var context = TestUtils.CreateTestContext(cache); middleware.ShimResponseStream(context); context.HttpContext.Response.ContentLength = 20; @@ -593,7 +594,7 @@ public async Task FinalizeCacheBody_DoNotCache_IfContentLengthMismatches(string var cache = new TestOutputCache(); var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); - var context = TestUtils.CreateTestContext(); + var context = TestUtils.CreateTestContext(cache); middleware.ShimResponseStream(context); context.HttpContext.Response.ContentLength = 9; @@ -621,7 +622,7 @@ public async Task FinalizeCacheBody_RequestHead_Cache_IfContentLengthPresent_And var cache = new TestOutputCache(); var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); - var context = TestUtils.CreateTestContext(); + var context = TestUtils.CreateTestContext(cache); middleware.ShimResponseStream(context); context.HttpContext.Response.ContentLength = 10; @@ -651,7 +652,7 @@ public async Task FinalizeCacheBody_Cache_IfContentLengthAbsent() var cache = new TestOutputCache(); var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); - var context = TestUtils.CreateTestContext(); + var context = TestUtils.CreateTestContext(cache); middleware.ShimResponseStream(context); @@ -675,7 +676,7 @@ public async Task FinalizeCacheBody_DoNotCache_IfIsResponseCacheableFalse() var cache = new TestOutputCache(); var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); - var context = TestUtils.CreateTestContext(); + var context = TestUtils.CreateTestContext(cache); middleware.ShimResponseStream(context); await context.HttpContext.Response.WriteAsync(new string('0', 10)); @@ -696,7 +697,7 @@ public async Task FinalizeCacheBody_DoNotCache_IfBufferingDisabled() var cache = new TestOutputCache(); var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache); - var context = TestUtils.CreateTestContext(); + var context = TestUtils.CreateTestContext(cache); middleware.ShimResponseStream(context); await context.HttpContext.Response.WriteAsync(new string('0', 10)); diff --git a/src/Middleware/OutputCaching/test/OutputCachingPolicyProviderTests.cs b/src/Middleware/OutputCaching/test/OutputCachingPolicyProviderTests.cs index 2deaa6ef3f70..8dc3a6b9f74f 100644 --- a/src/Middleware/OutputCaching/test/OutputCachingPolicyProviderTests.cs +++ b/src/Middleware/OutputCaching/test/OutputCachingPolicyProviderTests.cs @@ -404,61 +404,4 @@ public async Task IsResponseCacheable_SharedMaxAgeOverridesMaxAge_ToAllowed() Assert.True(context.AllowCacheLookup); Assert.Empty(sink.Writes); } - - [Fact] - public async Task IsCachedEntryFresh_NoExpiryRequirements_IsFresh() - { - var utcNow = DateTimeOffset.UtcNow; - var sink = new TestSink(); - var context = TestUtils.CreateTestContext(sink); - context.ResponseTime = DateTimeOffset.MaxValue; - context.CachedEntryAge = TimeSpan.MaxValue; - - var options = new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().Build() }; - await new OutputCachingPolicyProvider(Options.Create(options)).OnServeFromCacheAsync(context); - - Assert.True(context.IsCacheEntryFresh); - Assert.Empty(sink.Writes); - } - - [Fact] - public async Task IsCachedEntryFresh_AtExpiry_IsNotFresh() - { - var utcNow = DateTimeOffset.UtcNow; - var sink = new TestSink(); - var context = TestUtils.CreateTestContext(sink); - context.ResponseTime = utcNow; - context.CachedEntryAge = TimeSpan.Zero; - - var options = new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().Build() }; - await new OutputCachingPolicyProvider(Options.Create(options)).OnServeFromCacheAsync(context); - - Assert.False(context.IsCacheEntryFresh); - TestUtils.AssertLoggedMessages( - sink.Writes, - LoggedMessage.ExpirationExpiresExceededNoExpiration); - } - - [Fact] - public async Task IsCachedEntryFresh_SharedMaxAgeOverridesMaxAge_ToFresh() - { - var utcNow = DateTimeOffset.UtcNow; - var sink = new TestSink(); - var context = TestUtils.CreateTestContext(sink); - context.CachedEntryAge = TimeSpan.FromSeconds(11); - context.ResponseTime = utcNow + context.CachedEntryAge; - context.CachedResponse = new OutputCacheEntry { Headers = new HeaderDictionary() }; - context.CachedResponse.Headers[HeaderNames.CacheControl] = new CacheControlHeaderValue() - { - MaxAge = TimeSpan.FromSeconds(10), - SharedMaxAge = TimeSpan.FromSeconds(15) - }.ToString(); - context.CachedResponse.Headers[HeaderNames.Expires] = HeaderUtilities.FormatDate(utcNow); - - var options = new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().Build() }; - await new OutputCachingPolicyProvider(Options.Create(options)).OnServeFromCacheAsync(context); - - Assert.True(context.IsCacheEntryFresh); - Assert.Empty(sink.Writes); - } } diff --git a/src/Middleware/OutputCaching/test/TestUtils.cs b/src/Middleware/OutputCaching/test/TestUtils.cs index 9e8f037a2021..94592f819182 100644 --- a/src/Middleware/OutputCaching/test/TestUtils.cs +++ b/src/Middleware/OutputCaching/test/TestUtils.cs @@ -202,9 +202,9 @@ internal static OutputCachingMiddleware CreateTestMiddleware( keyProvider); } - internal static OutputCachingContext CreateTestContext() + internal static OutputCachingContext CreateTestContext(IOutputCacheStore cache = null) { - return new OutputCachingContext(new DefaultHttpContext(), new TestOutputCache(), NullLogger.Instance) + return new OutputCachingContext(new DefaultHttpContext(), cache ?? new TestOutputCache(), NullLogger.Instance) { EnableOutputCaching = true, AllowCacheStorage = true, @@ -213,9 +213,9 @@ internal static OutputCachingContext CreateTestContext() }; } - internal static OutputCachingContext CreateTestContext(HttpContext httpContext) + internal static OutputCachingContext CreateTestContext(HttpContext httpContext, IOutputCacheStore cache = null) { - return new OutputCachingContext(httpContext, new TestOutputCache(), NullLogger.Instance) + return new OutputCachingContext(httpContext, cache ?? new TestOutputCache(), NullLogger.Instance) { EnableOutputCaching = true, AllowCacheStorage = true, @@ -224,9 +224,9 @@ internal static OutputCachingContext CreateTestContext(HttpContext httpContext) }; } - internal static OutputCachingContext CreateTestContext(ITestSink testSink) + internal static OutputCachingContext CreateTestContext(ITestSink testSink, IOutputCacheStore cache = null) { - return new OutputCachingContext(new DefaultHttpContext(), new TestOutputCache(), new TestLogger("OutputCachingTests", testSink, true)) + return new OutputCachingContext(new DefaultHttpContext(), cache ?? new TestOutputCache(), new TestLogger("OutputCachingTests", testSink, true)) { EnableOutputCaching = true, AllowCacheStorage = true, From 8d6563eddcbb1dc997326bfc1d5f11b85685acc9 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Mon, 13 Jun 2022 18:36:47 -0700 Subject: [PATCH 36/48] Feedback --- AspNetCore.sln | 40 +---- eng/ProjectReferences.props | 1 - eng/SharedFramework.Local.props | 1 - eng/TrimmableProjects.props | 1 - src/Analyzers/Analyzers.slnf | 1 - src/Components/Components.slnf | 1 - src/Framework/Framework.slnf | 1 - src/Framework/test/TestData.cs | 2 - src/Identity/Identity.slnf | 1 - src/Middleware/Middleware.slnf | 1 - .../src/IOutputCacheEntry.cs | 37 ---- .../src/IOutputCachingContext.cs | 68 ------- ...pNetCore.OutputCaching.Abstractions.csproj | 18 -- .../src/PublicAPI.Shipped.txt | 1 - .../src/PublicAPI.Unshipped.txt | 60 ------- .../OutputCaching/OutputCaching.slnf | 1 - .../samples/OutputCachingSample/Startup.cs | 13 +- .../OutputCaching/src/CacheEntryHelpers.cs | 2 +- .../src/CachedResponseBody.cs | 0 .../src/CachedVaryByRules.cs | 13 +- .../src/IOutputCacheStore.cs} | 4 +- .../src/IOutputCachingFeature.cs | 3 +- .../src/IOutputCachingPolicy.cs | 12 +- .../src/IOutputCachingPolicyProvider.cs | 12 +- .../src/IPoliciesMetadata.cs | 0 .../src/Memory/MemoryOutputCacheStore.cs | 8 +- .../Microsoft.AspNetCore.OutputCaching.csproj | 16 +- .../OutputCaching/src/OutputCacheEntry.cs | 22 ++- .../OutputCaching/src/OutputCachingContext.cs | 59 +++++-- .../OutputCaching/src/OutputCachingFeature.cs | 13 +- .../src/OutputCachingMiddleware.cs | 16 +- .../OutputCaching/src/OutputCachingOptions.cs | 45 ++++- .../src/OutputCachingOptionsSetup.cs | 21 +++ .../src/OutputCachingPolicyProvider.cs | 31 ++-- .../src/OutputCachingServicesExtensions.cs | 2 + .../src/Policies/CompositePolicy.cs | 6 +- .../src/Policies/DefaultOutputCachePolicy.cs | 9 +- .../src/Policies/EnableCachingPolicy.cs | 6 +- .../src/Policies/ExpirationPolicy.cs | 6 +- .../src/Policies/LockingPolicy.cs | 6 +- .../src/Policies/NoLookupPolicy.cs | 36 ++++ .../src/Policies/NoStorePolicy.cs | 6 +- .../src/Policies/OutputCachePolicyBuilder.cs | 166 +++++++++++------- .../src/Policies/PoliciesCollection.cs | 93 ++++++++++ .../src/Policies/PolicyExtensions.cs | 6 +- .../src/Policies/PredicatePolicy.cs | 47 ++--- .../src/Policies/ProfilePolicy.cs | 15 +- .../OutputCaching/src/Policies/TagsPolicy.cs | 6 +- .../OutputCaching/src/Policies/TypedPolicy.cs | 52 ++++++ .../src/Policies/VaryByHeaderPolicy.cs | 6 +- .../src/Policies/VaryByQueryPolicy.cs | 6 +- .../src/Policies/VaryByValuePolicy.cs | 10 +- .../OutputCaching/src/PublicAPI.Unshipped.txt | 96 ++++++++-- .../OutputCaching/src/Resources.Designer.cs | 72 ++++++++ .../OutputCaching/src/Resources.resx | 123 +++++++++++++ .../test/OutputCachingPolicyProviderTests.cs | 53 ++++-- .../OutputCaching/test/OutputCachingTests.cs | 59 +++++-- .../OutputCaching/test/TestUtils.cs | 32 ++-- src/Mvc/Mvc.Core/src/OutputCacheAttribute.cs | 2 +- src/Mvc/Mvc.slnf | 1 - src/submodules/googletest | 2 +- 61 files changed, 938 insertions(+), 511 deletions(-) delete mode 100644 src/Middleware/OutputCaching.Abstractions/src/IOutputCacheEntry.cs delete mode 100644 src/Middleware/OutputCaching.Abstractions/src/IOutputCachingContext.cs delete mode 100644 src/Middleware/OutputCaching.Abstractions/src/Microsoft.AspNetCore.OutputCaching.Abstractions.csproj delete mode 100644 src/Middleware/OutputCaching.Abstractions/src/PublicAPI.Shipped.txt delete mode 100644 src/Middleware/OutputCaching.Abstractions/src/PublicAPI.Unshipped.txt rename src/Middleware/{OutputCaching.Abstractions => OutputCaching}/src/CachedResponseBody.cs (100%) rename src/Middleware/{OutputCaching.Abstractions => OutputCaching}/src/CachedVaryByRules.cs (69%) rename src/Middleware/{OutputCaching.Abstractions/src/IOutputCache.cs => OutputCaching/src/IOutputCacheStore.cs} (88%) rename src/Middleware/{OutputCaching.Abstractions => OutputCaching}/src/IOutputCachingFeature.cs (90%) rename src/Middleware/{OutputCaching.Abstractions => OutputCaching}/src/IOutputCachingPolicy.cs (64%) rename src/Middleware/{OutputCaching.Abstractions => OutputCaching}/src/IOutputCachingPolicyProvider.cs (72%) rename src/Middleware/{OutputCaching.Abstractions => OutputCaching}/src/IPoliciesMetadata.cs (100%) create mode 100644 src/Middleware/OutputCaching/src/OutputCachingOptionsSetup.cs create mode 100644 src/Middleware/OutputCaching/src/Policies/NoLookupPolicy.cs create mode 100644 src/Middleware/OutputCaching/src/Policies/PoliciesCollection.cs create mode 100644 src/Middleware/OutputCaching/src/Policies/TypedPolicy.cs create mode 100644 src/Middleware/OutputCaching/src/Resources.Designer.cs create mode 100644 src/Middleware/OutputCaching/src/Resources.resx diff --git a/AspNetCore.sln b/AspNetCore.sln index bc5804ed66c2..b251c88c38c8 100644 --- a/AspNetCore.sln +++ b/AspNetCore.sln @@ -1656,15 +1656,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Compon EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "OutputCaching", "OutputCaching", "{AA5ABFBC-177C-421E-B743-005E0FD1248B}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "OutputCaching.Abstractions", "OutputCaching.Abstractions", "{B034044C-C1AF-4077-B413-35F0A9CF8042}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.OutputCaching.Abstractions", "src\Middleware\OutputCaching.Abstractions\src\Microsoft.AspNetCore.OutputCaching.Abstractions.csproj", "{7ABE16D3-E0A6-43B8-8E7A-A8C5A4857384}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.OutputCaching", "src\Middleware\OutputCaching\src\Microsoft.AspNetCore.OutputCaching.csproj", "{5D5A3B60-A014-447C-9126-B1FA6C821C8D}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{B5AC1D8B-9D43-4261-AE0F-6B7574656F2C}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OutputCachingSample", "src\Middleware\OutputCaching\samples\OutputCachingSample\OutputCachingSample.csproj", "{C3FFA4E4-0E7E-4866-A15F-034245BFD800}" +EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "RequestDecompression", "RequestDecompression", "{5465F96F-33D5-454E-9C40-494E58AEEE5D}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.RequestDecompression.Tests", "src\Middleware\RequestDecompression\test\Microsoft.AspNetCore.RequestDecompression.Tests.csproj", "{97996D39-7722-4AFC-A41A-AD61CA7A413D}" @@ -9998,22 +9995,6 @@ Global {DC349A25-0DBF-4468-99E1-B95C22D3A7EF}.Release|x64.Build.0 = Release|Any CPU {DC349A25-0DBF-4468-99E1-B95C22D3A7EF}.Release|x86.ActiveCfg = Release|Any CPU {DC349A25-0DBF-4468-99E1-B95C22D3A7EF}.Release|x86.Build.0 = Release|Any CPU - {7ABE16D3-E0A6-43B8-8E7A-A8C5A4857384}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7ABE16D3-E0A6-43B8-8E7A-A8C5A4857384}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7ABE16D3-E0A6-43B8-8E7A-A8C5A4857384}.Debug|arm64.ActiveCfg = Debug|Any CPU - {7ABE16D3-E0A6-43B8-8E7A-A8C5A4857384}.Debug|arm64.Build.0 = Debug|Any CPU - {7ABE16D3-E0A6-43B8-8E7A-A8C5A4857384}.Debug|x64.ActiveCfg = Debug|Any CPU - {7ABE16D3-E0A6-43B8-8E7A-A8C5A4857384}.Debug|x64.Build.0 = Debug|Any CPU - {7ABE16D3-E0A6-43B8-8E7A-A8C5A4857384}.Debug|x86.ActiveCfg = Debug|Any CPU - {7ABE16D3-E0A6-43B8-8E7A-A8C5A4857384}.Debug|x86.Build.0 = Debug|Any CPU - {7ABE16D3-E0A6-43B8-8E7A-A8C5A4857384}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7ABE16D3-E0A6-43B8-8E7A-A8C5A4857384}.Release|Any CPU.Build.0 = Release|Any CPU - {7ABE16D3-E0A6-43B8-8E7A-A8C5A4857384}.Release|arm64.ActiveCfg = Release|Any CPU - {7ABE16D3-E0A6-43B8-8E7A-A8C5A4857384}.Release|arm64.Build.0 = Release|Any CPU - {7ABE16D3-E0A6-43B8-8E7A-A8C5A4857384}.Release|x64.ActiveCfg = Release|Any CPU - {7ABE16D3-E0A6-43B8-8E7A-A8C5A4857384}.Release|x64.Build.0 = Release|Any CPU - {7ABE16D3-E0A6-43B8-8E7A-A8C5A4857384}.Release|x86.ActiveCfg = Release|Any CPU - {7ABE16D3-E0A6-43B8-8E7A-A8C5A4857384}.Release|x86.Build.0 = Release|Any CPU {5D5A3B60-A014-447C-9126-B1FA6C821C8D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5D5A3B60-A014-447C-9126-B1FA6C821C8D}.Debug|Any CPU.Build.0 = Debug|Any CPU {5D5A3B60-A014-447C-9126-B1FA6C821C8D}.Debug|arm64.ActiveCfg = Debug|Any CPU @@ -10030,22 +10011,6 @@ Global {5D5A3B60-A014-447C-9126-B1FA6C821C8D}.Release|x64.Build.0 = Release|Any CPU {5D5A3B60-A014-447C-9126-B1FA6C821C8D}.Release|x86.ActiveCfg = Release|Any CPU {5D5A3B60-A014-447C-9126-B1FA6C821C8D}.Release|x86.Build.0 = Release|Any CPU - {6EBAD7A2-0FA8-4314-89F5-54009E86EE2F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6EBAD7A2-0FA8-4314-89F5-54009E86EE2F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6EBAD7A2-0FA8-4314-89F5-54009E86EE2F}.Debug|arm64.ActiveCfg = Debug|Any CPU - {6EBAD7A2-0FA8-4314-89F5-54009E86EE2F}.Debug|arm64.Build.0 = Debug|Any CPU - {6EBAD7A2-0FA8-4314-89F5-54009E86EE2F}.Debug|x64.ActiveCfg = Debug|Any CPU - {6EBAD7A2-0FA8-4314-89F5-54009E86EE2F}.Debug|x64.Build.0 = Debug|Any CPU - {6EBAD7A2-0FA8-4314-89F5-54009E86EE2F}.Debug|x86.ActiveCfg = Debug|Any CPU - {6EBAD7A2-0FA8-4314-89F5-54009E86EE2F}.Debug|x86.Build.0 = Debug|Any CPU - {6EBAD7A2-0FA8-4314-89F5-54009E86EE2F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6EBAD7A2-0FA8-4314-89F5-54009E86EE2F}.Release|Any CPU.Build.0 = Release|Any CPU - {6EBAD7A2-0FA8-4314-89F5-54009E86EE2F}.Release|arm64.ActiveCfg = Release|Any CPU - {6EBAD7A2-0FA8-4314-89F5-54009E86EE2F}.Release|arm64.Build.0 = Release|Any CPU - {6EBAD7A2-0FA8-4314-89F5-54009E86EE2F}.Release|x64.ActiveCfg = Release|Any CPU - {6EBAD7A2-0FA8-4314-89F5-54009E86EE2F}.Release|x64.Build.0 = Release|Any CPU - {6EBAD7A2-0FA8-4314-89F5-54009E86EE2F}.Release|x86.ActiveCfg = Release|Any CPU - {6EBAD7A2-0FA8-4314-89F5-54009E86EE2F}.Release|x86.Build.0 = Release|Any CPU {C3FFA4E4-0E7E-4866-A15F-034245BFD800}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {C3FFA4E4-0E7E-4866-A15F-034245BFD800}.Debug|Any CPU.Build.0 = Debug|Any CPU {C3FFA4E4-0E7E-4866-A15F-034245BFD800}.Debug|arm64.ActiveCfg = Debug|Any CPU @@ -11298,11 +11263,8 @@ Global {825BCF97-67A9-4834-B3A8-C3DC97A90E41} = {CC45FA2D-128B-485D-BA6D-DFD9735CB3C3} {DC349A25-0DBF-4468-99E1-B95C22D3A7EF} = {CC45FA2D-128B-485D-BA6D-DFD9735CB3C3} {AA5ABFBC-177C-421E-B743-005E0FD1248B} = {E5963C9F-20A6-4385-B364-814D2581FADF} - {B034044C-C1AF-4077-B413-35F0A9CF8042} = {E5963C9F-20A6-4385-B364-814D2581FADF} - {7ABE16D3-E0A6-43B8-8E7A-A8C5A4857384} = {B034044C-C1AF-4077-B413-35F0A9CF8042} {5D5A3B60-A014-447C-9126-B1FA6C821C8D} = {AA5ABFBC-177C-421E-B743-005E0FD1248B} {B5AC1D8B-9D43-4261-AE0F-6B7574656F2C} = {AA5ABFBC-177C-421E-B743-005E0FD1248B} - {6EBAD7A2-0FA8-4314-89F5-54009E86EE2F} = {AA5ABFBC-177C-421E-B743-005E0FD1248B} {C3FFA4E4-0E7E-4866-A15F-034245BFD800} = {B5AC1D8B-9D43-4261-AE0F-6B7574656F2C} {5465F96F-33D5-454E-9C40-494E58AEEE5D} = {E5963C9F-20A6-4385-B364-814D2581FADF} {97996D39-7722-4AFC-A41A-AD61CA7A413D} = {5465F96F-33D5-454E-9C40-494E58AEEE5D} diff --git a/eng/ProjectReferences.props b/eng/ProjectReferences.props index b77eb6c811ff..f0d8504957ac 100644 --- a/eng/ProjectReferences.props +++ b/eng/ProjectReferences.props @@ -89,7 +89,6 @@ - diff --git a/eng/SharedFramework.Local.props b/eng/SharedFramework.Local.props index 89eec28671bb..df28fe453b25 100644 --- a/eng/SharedFramework.Local.props +++ b/eng/SharedFramework.Local.props @@ -77,7 +77,6 @@ - diff --git a/eng/TrimmableProjects.props b/eng/TrimmableProjects.props index fd635215fc40..b4c71948fe7f 100644 --- a/eng/TrimmableProjects.props +++ b/eng/TrimmableProjects.props @@ -66,7 +66,6 @@ - diff --git a/src/Analyzers/Analyzers.slnf b/src/Analyzers/Analyzers.slnf index 5f65fd7ed785..bfb010ea2d1c 100644 --- a/src/Analyzers/Analyzers.slnf +++ b/src/Analyzers/Analyzers.slnf @@ -42,7 +42,6 @@ "src\\Mvc\\Mvc.ViewFeatures\\src\\Microsoft.AspNetCore.Mvc.ViewFeatures.csproj", "src\\Razor\\Razor\\src\\Microsoft.AspNetCore.Razor.csproj", "src\\Razor\\Razor.Runtime\\src\\Microsoft.AspNetCore.Razor.Runtime.csproj", - "src\\Middleware\\OutputCaching.Abstractions\\src\\Microsoft.AspNetCore.OutputCaching.Abstractions.csproj", "src\\Middleware\\ResponseCaching.Abstractions\\src\\Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj", "src\\Http\\Routing\\src\\Microsoft.AspNetCore.Routing.csproj", "src\\Http\\Routing.Abstractions\\src\\Microsoft.AspNetCore.Routing.Abstractions.csproj", diff --git a/src/Components/Components.slnf b/src/Components/Components.slnf index acf537d4ad4e..d867f6dc47cf 100644 --- a/src/Components/Components.slnf +++ b/src/Components/Components.slnf @@ -91,7 +91,6 @@ "src\\Middleware\\HttpOverrides\\src\\Microsoft.AspNetCore.HttpOverrides.csproj", "src\\Middleware\\HttpsPolicy\\src\\Microsoft.AspNetCore.HttpsPolicy.csproj", "src\\Middleware\\Localization\\src\\Microsoft.AspNetCore.Localization.csproj", - "src\\Middleware\\OutputCaching.Abstractions\\src\\Microsoft.AspNetCore.OutputCaching.Abstractions.csproj", "src\\Middleware\\ResponseCaching.Abstractions\\src\\Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj", "src\\Middleware\\ResponseCompression\\src\\Microsoft.AspNetCore.ResponseCompression.csproj", "src\\Middleware\\StaticFiles\\src\\Microsoft.AspNetCore.StaticFiles.csproj", diff --git a/src/Framework/Framework.slnf b/src/Framework/Framework.slnf index 847e9c2db095..d3f3abdc9f19 100644 --- a/src/Framework/Framework.slnf +++ b/src/Framework/Framework.slnf @@ -56,7 +56,6 @@ "src\\Middleware\\HttpsPolicy\\src\\Microsoft.AspNetCore.HttpsPolicy.csproj", "src\\Middleware\\Localization.Routing\\src\\Microsoft.AspNetCore.Localization.Routing.csproj", "src\\Middleware\\Localization\\src\\Microsoft.AspNetCore.Localization.csproj", - "src\\Middleware\\OutputCaching.Abstractions\\src\\Microsoft.AspNetCore.OutputCaching.Abstractions.csproj", "src\\Middleware\\OutputCaching\\src\\Microsoft.AspNetCore.OutputCaching.csproj", "src\\Middleware\\ResponseCaching.Abstractions\\src\\Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj", "src\\Middleware\\ResponseCaching\\src\\Microsoft.AspNetCore.ResponseCaching.csproj", diff --git a/src/Framework/test/TestData.cs b/src/Framework/test/TestData.cs index e451120d0d64..28aaa88dcc7b 100644 --- a/src/Framework/test/TestData.cs +++ b/src/Framework/test/TestData.cs @@ -74,7 +74,6 @@ static TestData() "Microsoft.AspNetCore.Mvc.TagHelpers", "Microsoft.AspNetCore.Mvc.ViewFeatures", "Microsoft.AspNetCore.OutputCaching", - "Microsoft.AspNetCore.OutputCaching.Abstractions", "Microsoft.AspNetCore.Razor", "Microsoft.AspNetCore.Razor.Runtime", "Microsoft.AspNetCore.RequestDecompression", @@ -212,7 +211,6 @@ static TestData() { "Microsoft.AspNetCore.Mvc.TagHelpers", "7.0.0.0" }, { "Microsoft.AspNetCore.Mvc.ViewFeatures", "7.0.0.0" }, { "Microsoft.AspNetCore.OutputCaching", "7.0.0.0" }, - { "Microsoft.AspNetCore.OutputCaching.Abstractions", "7.0.0.0" }, { "Microsoft.AspNetCore.Razor", "7.0.0.0" }, { "Microsoft.AspNetCore.Razor.Runtime", "7.0.0.0" }, { "Microsoft.AspNetCore.RequestDecompression", "7.0.0.0" }, diff --git a/src/Identity/Identity.slnf b/src/Identity/Identity.slnf index 8f46593f2c67..cd63efdba11c 100644 --- a/src/Identity/Identity.slnf +++ b/src/Identity/Identity.slnf @@ -69,7 +69,6 @@ "src\\Mvc\\Mvc.ViewFeatures\\src\\Microsoft.AspNetCore.Mvc.ViewFeatures.csproj", "src\\Razor\\Razor\\src\\Microsoft.AspNetCore.Razor.csproj", "src\\Razor\\Razor.Runtime\\src\\Microsoft.AspNetCore.Razor.Runtime.csproj", - "src\\Middleware\\OutputCaching.Abstractions\\src\\Microsoft.AspNetCore.OutputCaching.Abstractions.csproj", "src\\Middleware\\ResponseCaching.Abstractions\\src\\Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj", "src\\Http\\Routing\\src\\Microsoft.AspNetCore.Routing.csproj", "src\\Http\\Routing.Abstractions\\src\\Microsoft.AspNetCore.Routing.Abstractions.csproj", diff --git a/src/Middleware/Middleware.slnf b/src/Middleware/Middleware.slnf index 8b556d064daa..c75854876fbf 100644 --- a/src/Middleware/Middleware.slnf +++ b/src/Middleware/Middleware.slnf @@ -76,7 +76,6 @@ "src\\Middleware\\MiddlewareAnalysis\\samples\\MiddlewareAnalysisSample\\MiddlewareAnalysisSample.csproj", "src\\Middleware\\MiddlewareAnalysis\\src\\Microsoft.AspNetCore.MiddlewareAnalysis.csproj", "src\\Middleware\\MiddlewareAnalysis\\test\\Microsoft.AspNetCore.MiddlewareAnalysis.Tests.csproj", - "src\\Middleware\\OutputCaching.Abstractions\\src\\Microsoft.AspNetCore.OutputCaching.Abstractions.csproj", "src\\Middleware\\OutputCaching\\samples\\OutputCachingSample\\OutputCachingSample.csproj", "src\\Middleware\\OutputCaching\\src\\Microsoft.AspNetCore.OutputCaching.csproj", "src\\Middleware\\OutputCaching\\test\\Microsoft.AspNetCore.OutputCaching.Tests.csproj", diff --git a/src/Middleware/OutputCaching.Abstractions/src/IOutputCacheEntry.cs b/src/Middleware/OutputCaching.Abstractions/src/IOutputCacheEntry.cs deleted file mode 100644 index 7a70495e1d48..000000000000 --- a/src/Middleware/OutputCaching.Abstractions/src/IOutputCacheEntry.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.AspNetCore.Http; - -namespace Microsoft.AspNetCore.OutputCaching; - -/// -/// Represents a cached response entry. -/// -public interface IOutputCacheEntry -{ - /// - /// Gets the created date and time of the cache entry. - /// - public DateTimeOffset Created { get; set; } - - /// - /// Gets the status code of the cache entry. - /// - public int StatusCode { get; set; } - - /// - /// Gets the headers of the cache entry. - /// - public IHeaderDictionary Headers { get; set; } - - /// - /// Gets the body of the cache entry. - /// - public CachedResponseBody Body { get; set; } - - /// - /// Gets the tags of the cache entry. - /// - public string[]? Tags { get; set; } -} diff --git a/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingContext.cs b/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingContext.cs deleted file mode 100644 index 25652ca249f3..000000000000 --- a/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingContext.cs +++ /dev/null @@ -1,68 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; - -namespace Microsoft.AspNetCore.OutputCaching; - -/// -/// Represent the current caching context for the request. -/// -public interface IOutputCachingContext -{ - /// - /// Gets the . - /// - HttpContext HttpContext { get; } - - /// - /// Gets the response time. - /// - DateTimeOffset? ResponseTime { get; } - - /// - /// Gets the instance. - /// - CachedVaryByRules CachedVaryByRules { get; } - - /// - /// Gets the tags of the cached response. - /// - HashSet Tags { get; } - - /// - /// Gets the logger. - /// - ILogger Logger { get; } - - /// - /// Gets the instance. - /// - IOutputCacheStore Store { get; } - - /// - /// Gets or sets the amount of time the response should be cached for. - /// - TimeSpan? ResponseExpirationTimeSpan { get; set; } - - /// - /// Determines whether the output caching logic should be configured for the incoming HTTP request. - /// - bool EnableOutputCaching { get; set; } - - /// - /// Determines whether a cache lookup is allowed for the incoming HTTP request. - /// - bool AllowCacheLookup { get; set; } - - /// - /// Determines whether storage of the response is allowed for the incoming HTTP request. - /// - bool AllowCacheStorage { get; set; } - - /// - /// Determines whether the request should be locked. - /// - bool AllowLocking { get; set; } -} diff --git a/src/Middleware/OutputCaching.Abstractions/src/Microsoft.AspNetCore.OutputCaching.Abstractions.csproj b/src/Middleware/OutputCaching.Abstractions/src/Microsoft.AspNetCore.OutputCaching.Abstractions.csproj deleted file mode 100644 index 545b77910f09..000000000000 --- a/src/Middleware/OutputCaching.Abstractions/src/Microsoft.AspNetCore.OutputCaching.Abstractions.csproj +++ /dev/null @@ -1,18 +0,0 @@ - - - - ASP.NET Core output caching middleware abstractions and feature interface definitions. - $(DefaultNetCoreTargetFramework) - true - true - aspnetcore;cache;caching - false - true - - - - - - - - diff --git a/src/Middleware/OutputCaching.Abstractions/src/PublicAPI.Shipped.txt b/src/Middleware/OutputCaching.Abstractions/src/PublicAPI.Shipped.txt deleted file mode 100644 index 91b0e1a43b98..000000000000 --- a/src/Middleware/OutputCaching.Abstractions/src/PublicAPI.Shipped.txt +++ /dev/null @@ -1 +0,0 @@ -#nullable enable \ No newline at end of file diff --git a/src/Middleware/OutputCaching.Abstractions/src/PublicAPI.Unshipped.txt b/src/Middleware/OutputCaching.Abstractions/src/PublicAPI.Unshipped.txt deleted file mode 100644 index 475d28924b79..000000000000 --- a/src/Middleware/OutputCaching.Abstractions/src/PublicAPI.Unshipped.txt +++ /dev/null @@ -1,60 +0,0 @@ -Microsoft.AspNetCore.OutputCaching.CachedResponseBody -Microsoft.AspNetCore.OutputCaching.CachedResponseBody.CachedResponseBody(System.Collections.Generic.List! segments, long length) -> void -Microsoft.AspNetCore.OutputCaching.CachedResponseBody.CopyToAsync(System.IO.Pipelines.PipeWriter! destination, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.OutputCaching.CachedResponseBody.Length.get -> long -Microsoft.AspNetCore.OutputCaching.CachedResponseBody.Segments.get -> System.Collections.Generic.List! -Microsoft.AspNetCore.OutputCaching.CachedVaryByRules -Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.CachedVaryByRules() -> void -Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.Headers.get -> Microsoft.Extensions.Primitives.StringValues -Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.Headers.set -> void -Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.QueryKeys.get -> Microsoft.Extensions.Primitives.StringValues -Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.QueryKeys.set -> void -Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.VaryByCustom.get -> System.Collections.Generic.IDictionary! -Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.VaryByPrefix.get -> Microsoft.Extensions.Primitives.StringValues -Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.VaryByPrefix.set -> void -Microsoft.AspNetCore.OutputCaching.IOutputCacheEntry -Microsoft.AspNetCore.OutputCaching.IOutputCacheEntry.Body.get -> Microsoft.AspNetCore.OutputCaching.CachedResponseBody! -Microsoft.AspNetCore.OutputCaching.IOutputCacheEntry.Body.set -> void -Microsoft.AspNetCore.OutputCaching.IOutputCacheEntry.Created.get -> System.DateTimeOffset -Microsoft.AspNetCore.OutputCaching.IOutputCacheEntry.Created.set -> void -Microsoft.AspNetCore.OutputCaching.IOutputCacheEntry.Headers.get -> Microsoft.AspNetCore.Http.IHeaderDictionary! -Microsoft.AspNetCore.OutputCaching.IOutputCacheEntry.Headers.set -> void -Microsoft.AspNetCore.OutputCaching.IOutputCacheEntry.StatusCode.get -> int -Microsoft.AspNetCore.OutputCaching.IOutputCacheEntry.StatusCode.set -> void -Microsoft.AspNetCore.OutputCaching.IOutputCacheEntry.Tags.get -> string![]? -Microsoft.AspNetCore.OutputCaching.IOutputCacheEntry.Tags.set -> void -Microsoft.AspNetCore.OutputCaching.IOutputCacheStore -Microsoft.AspNetCore.OutputCaching.IOutputCacheStore.EvictByTagAsync(string! tag, System.Threading.CancellationToken token) -> System.Threading.Tasks.ValueTask -Microsoft.AspNetCore.OutputCaching.IOutputCacheStore.GetAsync(string! key, System.Threading.CancellationToken token) -> System.Threading.Tasks.ValueTask -Microsoft.AspNetCore.OutputCaching.IOutputCacheStore.SetAsync(string! key, Microsoft.AspNetCore.OutputCaching.IOutputCacheEntry! entry, System.TimeSpan validFor, System.Threading.CancellationToken token) -> System.Threading.Tasks.ValueTask -Microsoft.AspNetCore.OutputCaching.IOutputCachingContext -Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.AllowCacheLookup.get -> bool -Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.AllowCacheLookup.set -> void -Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.AllowCacheStorage.get -> bool -Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.AllowCacheStorage.set -> void -Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.AllowLocking.get -> bool -Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.AllowLocking.set -> void -Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.CachedVaryByRules.get -> Microsoft.AspNetCore.OutputCaching.CachedVaryByRules! -Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.EnableOutputCaching.get -> bool -Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.EnableOutputCaching.set -> void -Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.HttpContext.get -> Microsoft.AspNetCore.Http.HttpContext! -Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.Logger.get -> Microsoft.Extensions.Logging.ILogger! -Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.ResponseExpirationTimeSpan.get -> System.TimeSpan? -Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.ResponseExpirationTimeSpan.set -> void -Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.ResponseTime.get -> System.DateTimeOffset? -Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.Store.get -> Microsoft.AspNetCore.OutputCaching.IOutputCacheStore! -Microsoft.AspNetCore.OutputCaching.IOutputCachingContext.Tags.get -> System.Collections.Generic.HashSet! -Microsoft.AspNetCore.OutputCaching.IOutputCachingFeature -Microsoft.AspNetCore.OutputCaching.IOutputCachingFeature.Context.get -> Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! -Microsoft.AspNetCore.OutputCaching.IOutputCachingFeature.Policies.get -> System.Collections.Generic.List! -Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy -Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy.OnRequestAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy.OnServeFromCacheAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy.OnServeResponseAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicyProvider -Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicyProvider.HasPolicies(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> bool -Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicyProvider.OnRequestAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicyProvider.OnServeFromCacheAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicyProvider.OnServeResponseAsync(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.OutputCaching.IPoliciesMetadata -Microsoft.AspNetCore.OutputCaching.IPoliciesMetadata.Policy.get -> Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy! diff --git a/src/Middleware/OutputCaching/OutputCaching.slnf b/src/Middleware/OutputCaching/OutputCaching.slnf index 983dc206f0bd..7d85ca7205c3 100644 --- a/src/Middleware/OutputCaching/OutputCaching.slnf +++ b/src/Middleware/OutputCaching/OutputCaching.slnf @@ -2,7 +2,6 @@ "solution": { "path": "..\\..\\..\\AspNetCore.sln", "projects": [ - "src\\Middleware\\OutputCaching.Abstractions\\src\\Microsoft.AspNetCore.OutputCaching.Abstractions.csproj", "src\\Middleware\\OutputCaching\\samples\\OutputCachingSample\\OutputCachingSample.csproj", "src\\Middleware\\OutputCaching\\src\\Microsoft.AspNetCore.OutputCaching.csproj", "src\\Middleware\\OutputCaching\\test\\Microsoft.AspNetCore.OutputCaching.Tests.csproj" diff --git a/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs b/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs index d71243c3b9ca..7bd8a0aaad1b 100644 --- a/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs +++ b/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs @@ -9,10 +9,14 @@ builder.Services.AddOutputCaching(options => { - // Enable caching on all requests - // options.BasePolicy = new OutputCachePolicyBuilder().Enable(); + // Define policies for all requests which are not configured per endpoint or per request + options.BasePolicies.AddPolicy(b => b.WithPathBase("/js").Expire(TimeSpan.FromDays(1))); + options.BasePolicies.AddPolicy(b => b.WithPathBase("/admin").NoStore()); + + options.AddPolicy("NoCache", b => b.WithPathBase("/wwwroot").Expire(TimeSpan.FromDays(1))); + + options.AddPolicy("Disable", b => b.Add()); - options.Policies["NoCache"] = new OutputCachePolicyBuilder().WithPath("/wwwroot").Expire(TimeSpan.FromDays(1)); }); var app = builder.Build(); @@ -27,9 +31,6 @@ app.MapGet("/profile", Gravatar.WriteGravatar).CacheOutput(x => x.Policy("NoCache")); -var myPolicy = new OutputCachePolicyBuilder().Expire(TimeSpan.FromDays(1)).Build(); -app.MapGet("/custom", Gravatar.WriteGravatar).CacheOutput(myPolicy); - app.MapGet("/attribute", (RequestDelegate)([OutputCache(PolicyName = "NoCache")] (c) => Gravatar.WriteGravatar(c))); var blog = app.MapGroup("blog").CacheOutput(x => x.Tag("blog")); diff --git a/src/Middleware/OutputCaching/src/CacheEntryHelpers.cs b/src/Middleware/OutputCaching/src/CacheEntryHelpers.cs index bb6fcda43a18..e7fdcf1bcbf3 100644 --- a/src/Middleware/OutputCaching/src/CacheEntryHelpers.cs +++ b/src/Middleware/OutputCaching/src/CacheEntryHelpers.cs @@ -7,7 +7,7 @@ namespace Microsoft.AspNetCore.OutputCaching; internal static class CacheEntryHelpers { - internal static long EstimateCachedResponseSize(IOutputCacheEntry cachedResponse) + internal static long EstimateCachedResponseSize(OutputCacheEntry cachedResponse) { if (cachedResponse == null) { diff --git a/src/Middleware/OutputCaching.Abstractions/src/CachedResponseBody.cs b/src/Middleware/OutputCaching/src/CachedResponseBody.cs similarity index 100% rename from src/Middleware/OutputCaching.Abstractions/src/CachedResponseBody.cs rename to src/Middleware/OutputCaching/src/CachedResponseBody.cs diff --git a/src/Middleware/OutputCaching.Abstractions/src/CachedVaryByRules.cs b/src/Middleware/OutputCaching/src/CachedVaryByRules.cs similarity index 69% rename from src/Middleware/OutputCaching.Abstractions/src/CachedVaryByRules.cs rename to src/Middleware/OutputCaching/src/CachedVaryByRules.cs index df0083dc047d..d83fc9c903c9 100644 --- a/src/Middleware/OutputCaching.Abstractions/src/CachedVaryByRules.cs +++ b/src/Middleware/OutputCaching/src/CachedVaryByRules.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Collections.Immutable; using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.OutputCaching; @@ -11,10 +10,18 @@ namespace Microsoft.AspNetCore.OutputCaching; /// public class CachedVaryByRules { + internal Dictionary? VaryByCustom; + /// - /// Gets the custom values to vary by. + /// Defines a custom key-pair value to vary the cache by. /// - public IDictionary VaryByCustom { get; } = ImmutableDictionary.Empty; + /// The key. + /// The value. + public void SetVaryByCustom(string key, string value) + { + VaryByCustom ??= new(); + VaryByCustom[key] = value; + } /// /// Gets or sets the list of headers to vary by. diff --git a/src/Middleware/OutputCaching.Abstractions/src/IOutputCache.cs b/src/Middleware/OutputCaching/src/IOutputCacheStore.cs similarity index 88% rename from src/Middleware/OutputCaching.Abstractions/src/IOutputCache.cs rename to src/Middleware/OutputCaching/src/IOutputCacheStore.cs index be14e6853da6..afb5d390d093 100644 --- a/src/Middleware/OutputCaching.Abstractions/src/IOutputCache.cs +++ b/src/Middleware/OutputCaching/src/IOutputCacheStore.cs @@ -22,7 +22,7 @@ public interface IOutputCacheStore /// The cache key to look up. /// Indicates that the operation should be cancelled. /// The response cache entry if it exists; otherwise null. - ValueTask GetAsync(string key, CancellationToken token); + ValueTask GetAsync(string key, CancellationToken token); /// /// Stores the given response in the response cache. @@ -31,5 +31,5 @@ public interface IOutputCacheStore /// The response cache entry to store. /// The amount of time the entry will be kept in the cache before expiring, relative to now. /// Indicates that the operation should be cancelled. - ValueTask SetAsync(string key, IOutputCacheEntry entry, TimeSpan validFor, CancellationToken token); + ValueTask SetAsync(string key, OutputCacheEntry entry, TimeSpan validFor, CancellationToken token); } diff --git a/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingFeature.cs b/src/Middleware/OutputCaching/src/IOutputCachingFeature.cs similarity index 90% rename from src/Middleware/OutputCaching.Abstractions/src/IOutputCachingFeature.cs rename to src/Middleware/OutputCaching/src/IOutputCachingFeature.cs index 8a6fd2957579..fdeac3df4bc4 100644 --- a/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingFeature.cs +++ b/src/Middleware/OutputCaching/src/IOutputCachingFeature.cs @@ -11,8 +11,9 @@ public interface IOutputCachingFeature /// /// Gets the caching context. /// - IOutputCachingContext Context { get; } + OutputCachingContext Context { get; } + // Remove /// /// Gets the policies. /// diff --git a/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingPolicy.cs b/src/Middleware/OutputCaching/src/IOutputCachingPolicy.cs similarity index 64% rename from src/Middleware/OutputCaching.Abstractions/src/IOutputCachingPolicy.cs rename to src/Middleware/OutputCaching/src/IOutputCachingPolicy.cs index 87b1d15b3d06..5315714df392 100644 --- a/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingPolicy.cs +++ b/src/Middleware/OutputCaching/src/IOutputCachingPolicy.cs @@ -9,22 +9,22 @@ namespace Microsoft.AspNetCore.OutputCaching; public interface IOutputCachingPolicy { /// - /// Updates the before the cache middleware is invoked. + /// Updates the before the cache middleware is invoked. /// At that point the cache middleware can still be enabled or disabled for the request. /// /// The current request's caching context. - Task OnRequestAsync(IOutputCachingContext context); + Task OnRequestAsync(OutputCachingContext context); /// - /// Updates the before the cached response is used. + /// Updates the before the cached response is used. /// At that point the freshness of the cached response can be updated. /// /// The current request's caching context. - Task OnServeFromCacheAsync(IOutputCachingContext context); + Task OnServeFromCacheAsync(OutputCachingContext context); /// - /// Updates the before the response is served and can be cached. + /// Updates the before the response is served and can be cached. /// At that point cacheability of the response can be updated. /// - Task OnServeResponseAsync(IOutputCachingContext context); + Task OnServeResponseAsync(OutputCachingContext context); } diff --git a/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingPolicyProvider.cs b/src/Middleware/OutputCaching/src/IOutputCachingPolicyProvider.cs similarity index 72% rename from src/Middleware/OutputCaching.Abstractions/src/IOutputCachingPolicyProvider.cs rename to src/Middleware/OutputCaching/src/IOutputCachingPolicyProvider.cs index 64d43307a8ed..8dc984a842bd 100644 --- a/src/Middleware/OutputCaching.Abstractions/src/IOutputCachingPolicyProvider.cs +++ b/src/Middleware/OutputCaching/src/IOutputCachingPolicyProvider.cs @@ -20,18 +20,18 @@ public interface IOutputCachingPolicyProvider /// /// Determines whether the response caching logic should be attempted for the incoming HTTP request. /// - /// The . - Task OnRequestAsync(IOutputCachingContext context); + /// The . + Task OnRequestAsync(OutputCachingContext context); /// /// Determines whether the response retrieved from the response cache is fresh and can be served. /// - /// The . - Task OnServeFromCacheAsync(IOutputCachingContext context); + /// The . + Task OnServeFromCacheAsync(OutputCachingContext context); /// /// Determines whether the response can be cached for future requests. /// - /// The . - Task OnServeResponseAsync(IOutputCachingContext context); + /// The . + Task OnServeResponseAsync(OutputCachingContext context); } diff --git a/src/Middleware/OutputCaching.Abstractions/src/IPoliciesMetadata.cs b/src/Middleware/OutputCaching/src/IPoliciesMetadata.cs similarity index 100% rename from src/Middleware/OutputCaching.Abstractions/src/IPoliciesMetadata.cs rename to src/Middleware/OutputCaching/src/IPoliciesMetadata.cs diff --git a/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs b/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs index fbfa75430a5a..9d58d6b55178 100644 --- a/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs +++ b/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs @@ -31,7 +31,7 @@ public ValueTask EvictByTagAsync(string tag, CancellationToken token) return ValueTask.CompletedTask; } - public ValueTask GetAsync(string key, CancellationToken token) + public ValueTask GetAsync(string key, CancellationToken token) { var entry = _cache.Get(key); @@ -46,13 +46,13 @@ public ValueTask EvictByTagAsync(string tag, CancellationToken token) Tags = memoryCachedResponse.Tags }; - return ValueTask.FromResult(outputCacheEntry); + return ValueTask.FromResult(outputCacheEntry); } - return ValueTask.FromResult(default(IOutputCacheEntry)); + return ValueTask.FromResult(default(OutputCacheEntry)); } - public ValueTask SetAsync(string key, IOutputCacheEntry cachedResponse, TimeSpan validFor, CancellationToken token) + public ValueTask SetAsync(string key, OutputCacheEntry cachedResponse, TimeSpan validFor, CancellationToken token) { if (cachedResponse.Tags != null) { diff --git a/src/Middleware/OutputCaching/src/Microsoft.AspNetCore.OutputCaching.csproj b/src/Middleware/OutputCaching/src/Microsoft.AspNetCore.OutputCaching.csproj index eab2febfb3ad..0b8ad8e44ffb 100644 --- a/src/Middleware/OutputCaching/src/Microsoft.AspNetCore.OutputCaching.csproj +++ b/src/Middleware/OutputCaching/src/Microsoft.AspNetCore.OutputCaching.csproj @@ -12,7 +12,6 @@ - @@ -23,4 +22,19 @@ + + + True + True + Resources.resx + + + + + + ResXFileCodeGenerator + Resources.Designer.cs + + + diff --git a/src/Middleware/OutputCaching/src/OutputCacheEntry.cs b/src/Middleware/OutputCaching/src/OutputCacheEntry.cs index deb59a0b8824..e466a98e7cdd 100644 --- a/src/Middleware/OutputCaching/src/OutputCacheEntry.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheEntry.cs @@ -6,20 +6,30 @@ namespace Microsoft.AspNetCore.OutputCaching; /// -internal sealed class OutputCacheEntry : IOutputCacheEntry +public sealed class OutputCacheEntry { - /// + /// + /// Gets the created date and time of the cache entry. + /// public DateTimeOffset Created { get; set; } - /// + /// + /// Gets the status code of the cache entry. + /// public int StatusCode { get; set; } - /// + /// + /// Gets the headers of the cache entry. + /// public IHeaderDictionary Headers { get; set; } = default!; - /// + /// + /// Gets the body of the cache entry. + /// public CachedResponseBody Body { get; set; } = default!; - /// + /// + /// Gets the tags of the cache entry. + /// public string[]? Tags { get; set; } } diff --git a/src/Middleware/OutputCaching/src/OutputCachingContext.cs b/src/Middleware/OutputCaching/src/OutputCachingContext.cs index 03eb836995ba..96fc67b72d91 100644 --- a/src/Middleware/OutputCaching/src/OutputCachingContext.cs +++ b/src/Middleware/OutputCaching/src/OutputCachingContext.cs @@ -8,46 +8,77 @@ namespace Microsoft.AspNetCore.OutputCaching; -internal sealed class OutputCachingContext : IOutputCachingContext +/// +/// Represent the current caching context for the request. +/// +public sealed class OutputCachingContext { - internal OutputCachingContext(HttpContext httpContext, IOutputCacheStore store, ILogger logger) + internal OutputCachingContext(HttpContext httpContext, IOutputCacheStore store, OutputCachingOptions options, ILogger logger) { HttpContext = httpContext; Logger = logger; Store = store; + Options = options; } - /// + /// + /// Determines whether the output caching logic should be configured for the incoming HTTP request. + /// public bool EnableOutputCaching { get; set; } - /// + /// + /// Determines whether a cache lookup is allowed for the incoming HTTP request. + /// public bool AllowCacheLookup { get; set; } - /// + /// + /// Determines whether storage of the response is allowed for the incoming HTTP request. + /// public bool AllowCacheStorage { get; set; } - /// + /// + /// Determines whether the request should be locked. + /// public bool AllowLocking { get; set; } - /// + /// + /// Gets the . + /// public HttpContext HttpContext { get; } - /// + /// + /// Gets the response time. + /// public DateTimeOffset? ResponseTime { get; internal set; } - /// + /// + /// Gets the instance. + /// public CachedVaryByRules CachedVaryByRules { get; set; } = new(); - /// + /// + /// Gets the tags of the cached response. + /// public HashSet Tags { get; } = new(); - /// + /// + /// Gets the logger. + /// public ILogger Logger { get; } - /// + /// + /// Gets the options. + /// + public OutputCachingOptions Options { get; } + + /// + /// Gets the instance. + /// public IOutputCacheStore Store { get; } - /// + /// + /// Gets or sets the amount of time the response should be cached for. + /// public TimeSpan? ResponseExpirationTimeSpan { get; set; } internal string CacheKey { get; set; } @@ -58,7 +89,7 @@ internal OutputCachingContext(HttpContext httpContext, IOutputCacheStore store, internal TimeSpan CachedEntryAge { get; set; } - internal IOutputCacheEntry CachedResponse { get; set; } + internal OutputCacheEntry CachedResponse { get; set; } internal bool ResponseStarted { get; set; } diff --git a/src/Middleware/OutputCaching/src/OutputCachingFeature.cs b/src/Middleware/OutputCaching/src/OutputCachingFeature.cs index f979a23b64b4..62ead74846fa 100644 --- a/src/Middleware/OutputCaching/src/OutputCachingFeature.cs +++ b/src/Middleware/OutputCaching/src/OutputCachingFeature.cs @@ -3,21 +3,14 @@ namespace Microsoft.AspNetCore.OutputCaching; -/// Default implementation for -public class OutputCachingFeature : IOutputCachingFeature +internal class OutputCachingFeature : IOutputCachingFeature { - /// - /// Creates a new instance. - /// - /// - public OutputCachingFeature(IOutputCachingContext context) + public OutputCachingFeature(OutputCachingContext context) { Context = context; } - /// - public IOutputCachingContext Context { get; } + public OutputCachingContext Context { get; } - /// public List Policies { get; } = new(); } diff --git a/src/Middleware/OutputCaching/src/OutputCachingMiddleware.cs b/src/Middleware/OutputCaching/src/OutputCachingMiddleware.cs index 89eab4d28918..02710cdf5550 100644 --- a/src/Middleware/OutputCaching/src/OutputCachingMiddleware.cs +++ b/src/Middleware/OutputCaching/src/OutputCachingMiddleware.cs @@ -26,8 +26,8 @@ public class OutputCachingMiddleware private readonly IOutputCachingPolicyProvider _policyProvider; private readonly IOutputCacheStore _store; private readonly IOutputCachingKeyProvider _keyProvider; - private readonly WorkDispatcher _outputCacheEntryDispatcher; - private readonly WorkDispatcher _requestDispatcher; + private readonly WorkDispatcher _outputCacheEntryDispatcher; + private readonly WorkDispatcher _requestDispatcher; /// /// Creates a new . @@ -93,7 +93,7 @@ public async Task Invoke(HttpContext httpContext) return; } - var context = new OutputCachingContext(httpContext, _store, _logger); + var context = new OutputCachingContext(httpContext, _store, _options, _logger); // Add IOutputCachingFeature AddOutputCachingFeature(context); @@ -141,7 +141,7 @@ public async Task Invoke(HttpContext httpContext) await ExecuteResponseAsync(); } - async Task ExecuteResponseAsync() + async Task ExecuteResponseAsync() { // Hook up to listen to the response stream ShimResponseStream(context); @@ -182,7 +182,7 @@ public async Task Invoke(HttpContext httpContext) } } - internal async Task TryServeCachedResponseAsync(OutputCachingContext context, IOutputCacheEntry? cacheEntry) + internal async Task TryServeCachedResponseAsync(OutputCachingContext context, OutputCacheEntry? cacheEntry) { if (cacheEntry == null) { @@ -301,7 +301,7 @@ internal void CreateCacheKey(OutputCachingContext context) var varyByPrefix = context.CachedVaryByRules.VaryByPrefix; // Check if any vary rules exist - if (!StringValues.IsNullOrEmpty(varyHeaders) || !StringValues.IsNullOrEmpty(varyQueryKeys) || !StringValues.IsNullOrEmpty(varyByPrefix) || varyByCustomKeys.Count > 0) + if (!StringValues.IsNullOrEmpty(varyHeaders) || !StringValues.IsNullOrEmpty(varyQueryKeys) || !StringValues.IsNullOrEmpty(varyByPrefix) || varyByCustomKeys?.Count > 0) { // Normalize order and casing of vary by rules var normalizedVaryHeaders = GetOrderCasingNormalizedStringValues(varyHeaders); @@ -549,11 +549,11 @@ internal static StringValues GetOrderCasingNormalizedStringValues(StringValues s } } - internal static StringValues GetOrderCasingNormalizedDictionary(IDictionary dictionary) + internal static StringValues GetOrderCasingNormalizedDictionary(IDictionary? dictionary) { const char KeySubDelimiter = '\x1f'; - if (dictionary.Count == 0) + if (dictionary == null || dictionary.Count == 0) { return StringValues.Empty; } diff --git a/src/Middleware/OutputCaching/src/OutputCachingOptions.cs b/src/Middleware/OutputCaching/src/OutputCachingOptions.cs index a12fc8dac356..5e50a4eb5d0c 100644 --- a/src/Middleware/OutputCaching/src/OutputCachingOptions.cs +++ b/src/Middleware/OutputCaching/src/OutputCachingOptions.cs @@ -2,6 +2,10 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using Microsoft.AspNetCore.OutputCaching.Policies; +using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.OutputCaching; @@ -10,6 +14,14 @@ namespace Microsoft.AspNetCore.OutputCaching; /// public class OutputCachingOptions { + /// + /// Creates a new instance. + /// + public OutputCachingOptions() + { + BasePolicies = new(this); + } + /// /// The size limit for the output cache middleware in bytes. The default is set to 100 MB. /// When this limit is exceeded, no new responses will be cached until older entries are @@ -36,17 +48,42 @@ public class OutputCachingOptions /// /// Gets the policy applied to all requests. /// - public IOutputCachingPolicy? BasePolicy { get; set; } + public PoliciesCollection BasePolicies { get; internal set; } /// - /// Gets a Dictionary of policy names, which are pre-defined settings for - /// output caching. + /// Gets the application . /// - public IDictionary Policies { get; } = new Dictionary(StringComparer.OrdinalIgnoreCase); + public IServiceProvider ApplicationServices { get; set; } = default!; + + internal IDictionary? NamedPolicies { get; set; } /// /// For testing purposes only. /// [EditorBrowsable(EditorBrowsableState.Never)] internal ISystemClock SystemClock { get; set; } = new SystemClock(); + + /// + /// Defines a which can be referenced by name. + /// + /// The name of the policy. + /// The policy to add + public void AddPolicy(string name, IOutputCachingPolicy policy) + { + NamedPolicies ??= new Dictionary(StringComparer.OrdinalIgnoreCase); + NamedPolicies[name] = policy; + } + + /// + /// Defines a which can be referenced by name. + /// + /// The name of the policy. + /// an action on . + public void AddPolicy(string name, Action build) + { + var builder = new OutputCachePolicyBuilder(this); + build(builder); + NamedPolicies ??= new Dictionary(StringComparer.OrdinalIgnoreCase); + NamedPolicies[name] = builder.Build(); + } } diff --git a/src/Middleware/OutputCaching/src/OutputCachingOptionsSetup.cs b/src/Middleware/OutputCaching/src/OutputCachingOptionsSetup.cs new file mode 100644 index 000000000000..2e57df7abce3 --- /dev/null +++ b/src/Middleware/OutputCaching/src/OutputCachingOptionsSetup.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.OutputCaching; + +internal sealed class OutputCachingOptionsSetup : IConfigureOptions +{ + private readonly IServiceProvider _services; + + public OutputCachingOptionsSetup(IServiceProvider services) + { + _services = services; + } + + public void Configure(OutputCachingOptions options) + { + options.ApplicationServices = _services; + } +} diff --git a/src/Middleware/OutputCaching/src/OutputCachingPolicyProvider.cs b/src/Middleware/OutputCaching/src/OutputCachingPolicyProvider.cs index c38e8b8a8546..a0f2d2dd9024 100644 --- a/src/Middleware/OutputCaching/src/OutputCachingPolicyProvider.cs +++ b/src/Middleware/OutputCaching/src/OutputCachingPolicyProvider.cs @@ -16,13 +16,15 @@ public OutputCachingPolicyProvider(IOptions options) _options = options.Value; } + // Not in interface public bool HasPolicies(HttpContext httpContext) { - if (_options.BasePolicy != null) + if (_options.BasePolicies != null) { return true; } + // Remove check if (httpContext.Features.Get()?.Policies.Any() ?? false) { return true; @@ -36,11 +38,14 @@ public bool HasPolicies(HttpContext httpContext) return false; } - public async Task OnRequestAsync(IOutputCachingContext context) + public async Task OnRequestAsync(OutputCachingContext context) { - if (_options.BasePolicy != null) + if (_options.BasePolicies != null) { - await _options.BasePolicy.OnRequestAsync(context); + foreach (var policy in _options.BasePolicies) + { + await policy.OnRequestAsync(context); + } } var policiesMetadata = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata(); @@ -58,11 +63,14 @@ public async Task OnRequestAsync(IOutputCachingContext context) } } - public async Task OnServeFromCacheAsync(IOutputCachingContext context) + public async Task OnServeFromCacheAsync(OutputCachingContext context) { - if (_options.BasePolicy != null) + if (_options.BasePolicies != null) { - await _options.BasePolicy.OnServeFromCacheAsync(context); + foreach (var policy in _options.BasePolicies) + { + await policy.OnServeFromCacheAsync(context); + } } // Apply response policies defined on the feature, e.g. from action attributes @@ -85,11 +93,14 @@ public async Task OnServeFromCacheAsync(IOutputCachingContext context) } } - public async Task OnServeResponseAsync(IOutputCachingContext context) + public async Task OnServeResponseAsync(OutputCachingContext context) { - if (_options.BasePolicy != null) + if (_options.BasePolicies != null) { - await _options.BasePolicy.OnServeResponseAsync(context); + foreach (var policy in _options.BasePolicies) + { + await policy.OnServeResponseAsync(context); + } } // Apply response policies defined on the feature, e.g. from action attributes diff --git a/src/Middleware/OutputCaching/src/OutputCachingServicesExtensions.cs b/src/Middleware/OutputCaching/src/OutputCachingServicesExtensions.cs index 064b07d731f5..3787b839aedf 100644 --- a/src/Middleware/OutputCaching/src/OutputCachingServicesExtensions.cs +++ b/src/Middleware/OutputCaching/src/OutputCachingServicesExtensions.cs @@ -24,6 +24,8 @@ public static IServiceCollection AddOutputCaching(this IServiceCollection servic { ArgumentNullException.ThrowIfNull(services); + services.AddTransient, OutputCachingOptionsSetup>(); + services.TryAddSingleton(); services.TryAddSingleton(sp => diff --git a/src/Middleware/OutputCaching/src/Policies/CompositePolicy.cs b/src/Middleware/OutputCaching/src/Policies/CompositePolicy.cs index ddb4bf3f6f74..cf4e6c2fdd63 100644 --- a/src/Middleware/OutputCaching/src/Policies/CompositePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/CompositePolicy.cs @@ -20,7 +20,7 @@ public CompositePolicy(params IOutputCachingPolicy[] policies!!) } /// - async Task IOutputCachingPolicy.OnRequestAsync(IOutputCachingContext context) + async Task IOutputCachingPolicy.OnRequestAsync(OutputCachingContext context) { foreach (var policy in _policies) { @@ -29,7 +29,7 @@ async Task IOutputCachingPolicy.OnRequestAsync(IOutputCachingContext context) } /// - async Task IOutputCachingPolicy.OnServeFromCacheAsync(IOutputCachingContext context) + async Task IOutputCachingPolicy.OnServeFromCacheAsync(OutputCachingContext context) { foreach (var policy in _policies) { @@ -38,7 +38,7 @@ async Task IOutputCachingPolicy.OnServeFromCacheAsync(IOutputCachingContext cont } /// - async Task IOutputCachingPolicy.OnServeResponseAsync(IOutputCachingContext context) + async Task IOutputCachingPolicy.OnServeResponseAsync(OutputCachingContext context) { foreach (var policy in _policies) { diff --git a/src/Middleware/OutputCaching/src/Policies/DefaultOutputCachePolicy.cs b/src/Middleware/OutputCaching/src/Policies/DefaultOutputCachePolicy.cs index 8bf45a9de935..8e84509e7a5b 100644 --- a/src/Middleware/OutputCaching/src/Policies/DefaultOutputCachePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/DefaultOutputCachePolicy.cs @@ -18,9 +18,10 @@ private DefaultOutputCachePolicy() } /// - Task IOutputCachingPolicy.OnRequestAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnRequestAsync(OutputCachingContext context) { var attemptOutputCaching = AttemptOutputCaching(context); + context.EnableOutputCaching = true; context.AllowCacheLookup = attemptOutputCaching; context.AllowCacheStorage = attemptOutputCaching; context.AllowLocking = true; @@ -32,13 +33,13 @@ Task IOutputCachingPolicy.OnRequestAsync(IOutputCachingContext context) } /// - Task IOutputCachingPolicy.OnServeFromCacheAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnServeFromCacheAsync(OutputCachingContext context) { return Task.CompletedTask; } /// - Task IOutputCachingPolicy.OnServeResponseAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnServeResponseAsync(OutputCachingContext context) { var response = context.HttpContext.Response; @@ -61,7 +62,7 @@ Task IOutputCachingPolicy.OnServeResponseAsync(IOutputCachingContext context) return Task.CompletedTask; } - private static bool AttemptOutputCaching(IOutputCachingContext context) + private static bool AttemptOutputCaching(OutputCachingContext context) { // Check if the current request fulfisls the requirements to be cached diff --git a/src/Middleware/OutputCaching/src/Policies/EnableCachingPolicy.cs b/src/Middleware/OutputCaching/src/Policies/EnableCachingPolicy.cs index 16e2371cc2ba..39ce03fc0c5d 100644 --- a/src/Middleware/OutputCaching/src/Policies/EnableCachingPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/EnableCachingPolicy.cs @@ -16,7 +16,7 @@ private EnableCachingPolicy() } /// - Task IOutputCachingPolicy.OnRequestAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnRequestAsync(OutputCachingContext context) { context.EnableOutputCaching = this == Enabled; @@ -24,13 +24,13 @@ Task IOutputCachingPolicy.OnRequestAsync(IOutputCachingContext context) } /// - Task IOutputCachingPolicy.OnServeResponseAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnServeResponseAsync(OutputCachingContext context) { return Task.CompletedTask; } /// - Task IOutputCachingPolicy.OnServeFromCacheAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnServeFromCacheAsync(OutputCachingContext context) { return Task.CompletedTask; } diff --git a/src/Middleware/OutputCaching/src/Policies/ExpirationPolicy.cs b/src/Middleware/OutputCaching/src/Policies/ExpirationPolicy.cs index a506c9a6048a..2f9c3e3f74df 100644 --- a/src/Middleware/OutputCaching/src/Policies/ExpirationPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/ExpirationPolicy.cs @@ -20,7 +20,7 @@ public ExpirationPolicy(TimeSpan expiration) } /// - Task IOutputCachingPolicy.OnRequestAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnRequestAsync(OutputCachingContext context) { context.ResponseExpirationTimeSpan = _expiration; @@ -28,13 +28,13 @@ Task IOutputCachingPolicy.OnRequestAsync(IOutputCachingContext context) } /// - Task IOutputCachingPolicy.OnServeFromCacheAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnServeFromCacheAsync(OutputCachingContext context) { return Task.CompletedTask; } /// - Task IOutputCachingPolicy.OnServeResponseAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnServeResponseAsync(OutputCachingContext context) { return Task.CompletedTask; } diff --git a/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs b/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs index 17b941a0f050..39639f0d7260 100644 --- a/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs @@ -26,7 +26,7 @@ private LockingPolicy(bool lockResponse) public static readonly LockingPolicy Disabled = new(false); /// - Task IOutputCachingPolicy.OnRequestAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnRequestAsync(OutputCachingContext context) { context.AllowLocking = _lockResponse; @@ -34,13 +34,13 @@ Task IOutputCachingPolicy.OnRequestAsync(IOutputCachingContext context) } /// - Task IOutputCachingPolicy.OnServeFromCacheAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnServeFromCacheAsync(OutputCachingContext context) { return Task.CompletedTask; } /// - Task IOutputCachingPolicy.OnServeResponseAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnServeResponseAsync(OutputCachingContext context) { return Task.CompletedTask; } diff --git a/src/Middleware/OutputCaching/src/Policies/NoLookupPolicy.cs b/src/Middleware/OutputCaching/src/Policies/NoLookupPolicy.cs new file mode 100644 index 000000000000..40558f402ecd --- /dev/null +++ b/src/Middleware/OutputCaching/src/Policies/NoLookupPolicy.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.OutputCaching; + +/// +/// A policy that prevents the response from being served from cached. +/// +internal class NoLookupPolicy : IOutputCachingPolicy +{ + public static NoLookupPolicy Instance = new(); + + private NoLookupPolicy() + { + } + + /// + Task IOutputCachingPolicy.OnServeResponseAsync(OutputCachingContext context) + { + context.AllowCacheLookup = false; + + return Task.CompletedTask; + } + + /// + Task IOutputCachingPolicy.OnServeFromCacheAsync(OutputCachingContext context) + { + return Task.CompletedTask; + } + + /// + Task IOutputCachingPolicy.OnRequestAsync(OutputCachingContext context) + { + return Task.CompletedTask; + } +} diff --git a/src/Middleware/OutputCaching/src/Policies/NoStorePolicy.cs b/src/Middleware/OutputCaching/src/Policies/NoStorePolicy.cs index 3bb35eeff4c0..540f966a6840 100644 --- a/src/Middleware/OutputCaching/src/Policies/NoStorePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/NoStorePolicy.cs @@ -15,7 +15,7 @@ private NoStorePolicy() } /// - Task IOutputCachingPolicy.OnServeResponseAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnServeResponseAsync(OutputCachingContext context) { context.AllowCacheStorage = false; @@ -23,13 +23,13 @@ Task IOutputCachingPolicy.OnServeResponseAsync(IOutputCachingContext context) } /// - Task IOutputCachingPolicy.OnServeFromCacheAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnServeFromCacheAsync(OutputCachingContext context) { return Task.CompletedTask; } /// - Task IOutputCachingPolicy.OnRequestAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnRequestAsync(OutputCachingContext context) { return Task.CompletedTask; } diff --git a/src/Middleware/OutputCaching/src/Policies/OutputCachePolicyBuilder.cs b/src/Middleware/OutputCaching/src/Policies/OutputCachePolicyBuilder.cs index d55a5340afef..0c1cff546cf8 100644 --- a/src/Middleware/OutputCaching/src/Policies/OutputCachePolicyBuilder.cs +++ b/src/Middleware/OutputCaching/src/Policies/OutputCachePolicyBuilder.cs @@ -1,9 +1,12 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Linq; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.OutputCaching.Policies; +using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.OutputCaching; @@ -12,20 +15,18 @@ namespace Microsoft.AspNetCore.OutputCaching; /// public sealed class OutputCachePolicyBuilder : IOutputCachingPolicy { + private const DynamicallyAccessedMemberTypes ActivatorAccessibility = DynamicallyAccessedMemberTypes.PublicConstructors; + private IOutputCachingPolicy? _builtPolicy; - private List Policies { get; } = new(); - private List>> Requirements { get; } = new(); + private readonly List _policies = new(); + private readonly OutputCachingOptions _options; + private List>>? _requirements; - /// - /// Creates a new instance of with a policy which allows unauthenticated GET, HEAD, 200 responses without cookies to be cached. - /// - /// - /// The default policy doesn't cache any request by default. To enable caching use or invoke CacheOutput() on an endpoint. - /// - public OutputCachePolicyBuilder() + internal OutputCachePolicyBuilder(OutputCachingOptions options) { _builtPolicy = null; - Policies.Add(DefaultOutputCachePolicy.Instance); + _policies.Add(DefaultOutputCachePolicy.Instance); + _options = options; } /// @@ -34,30 +35,7 @@ public OutputCachePolicyBuilder() public OutputCachePolicyBuilder Add(IOutputCachingPolicy policy) { _builtPolicy = null; - Policies.Add(policy); - return this; - } - - /// - /// Enables caching. - /// - public OutputCachePolicyBuilder Enable() - { - _builtPolicy = null; - Policies.Add(EnableCachingPolicy.Enabled); - return this; - } - - /// - /// Disables caching. - /// - /// - /// This is the default. - /// - public OutputCachePolicyBuilder Disable() - { - _builtPolicy = null; - Policies.Add(EnableCachingPolicy.Disabled); + _policies.Add(policy); return this; } @@ -65,12 +43,13 @@ public OutputCachePolicyBuilder Disable() /// Adds a requirement to the current policy. /// /// The predicate applied to the policy. - public OutputCachePolicyBuilder WithCondition(Func> predicate) + public OutputCachePolicyBuilder WithCondition(Func> predicate) { ArgumentNullException.ThrowIfNull(predicate); _builtPolicy = null; - Requirements.Add(predicate); + _requirements ??= new(); + _requirements.Add(predicate); return this; } @@ -78,12 +57,13 @@ public OutputCachePolicyBuilder WithCondition(Func /// The base path to limit the policy to. - public OutputCachePolicyBuilder WithPath(PathString pathBase) + public OutputCachePolicyBuilder WithPathBase(PathString pathBase) { ArgumentNullException.ThrowIfNull(pathBase); _builtPolicy = null; - Requirements.Add(context => + _requirements ??= new(); + _requirements.Add(context => { var match = context.HttpContext.Request.Path.StartsWithSegments(pathBase); return Task.FromResult(match); @@ -95,12 +75,13 @@ public OutputCachePolicyBuilder WithPath(PathString pathBase) /// Adds a requirement to the current policy based on the request path. /// /// The base paths to limit the policy to. - public OutputCachePolicyBuilder WithPath(params PathString[] pathBases) + public OutputCachePolicyBuilder WithPathBase(params PathString[] pathBases) { ArgumentNullException.ThrowIfNull(pathBases); _builtPolicy = null; - Requirements.Add(context => + _requirements ??= new(); + _requirements.Add(context => { var match = pathBases.Any(x => context.HttpContext.Request.Path.StartsWithSegments(x)); return Task.FromResult(match); @@ -117,7 +98,8 @@ public OutputCachePolicyBuilder WithMethod(string method) ArgumentNullException.ThrowIfNull(method); _builtPolicy = null; - Requirements.Add(context => + _requirements ??= new(); + _requirements.Add(context => { var upperMethod = method.ToUpperInvariant(); var match = context.HttpContext.Request.Method.ToUpperInvariant() == upperMethod; @@ -135,7 +117,8 @@ public OutputCachePolicyBuilder WithMethod(params string[] methods) ArgumentNullException.ThrowIfNull(methods); _builtPolicy = null; - Requirements.Add(context => + _requirements ??= new(); + _requirements.Add(context => { var upperMethods = methods.Select(m => m.ToUpperInvariant()).ToArray(); var match = methods.Any(m => context.HttpContext.Request.Method.ToUpperInvariant() == m); @@ -156,7 +139,7 @@ public OutputCachePolicyBuilder VaryByQuery(params string[] queryKeys) ArgumentNullException.ThrowIfNull(queryKeys); _builtPolicy = null; - Policies.Add(new VaryByQueryPolicy(queryKeys)); + _policies.Add(new VaryByQueryPolicy(queryKeys)); return this; } @@ -169,7 +152,7 @@ public OutputCachePolicyBuilder VaryByHeader(params string[] headers) ArgumentNullException.ThrowIfNull(headers); _builtPolicy = null; - Policies.Add(new VaryByHeaderPolicy(headers)); + _policies.Add(new VaryByHeaderPolicy(headers)); return this; } @@ -182,7 +165,7 @@ public OutputCachePolicyBuilder VaryByValue(Func> varyBy) ArgumentNullException.ThrowIfNull(varyBy); _builtPolicy = null; - Policies.Add(new VaryByValuePolicy(varyBy)); + _policies.Add(new VaryByValuePolicy(varyBy)); return this; } @@ -195,7 +178,7 @@ public OutputCachePolicyBuilder VaryByValue(Func> varyBy) ArgumentNullException.ThrowIfNull(varyBy); _builtPolicy = null; - Policies.Add(new VaryByValuePolicy(varyBy)); + _policies.Add(new VaryByValuePolicy(varyBy)); return this; } @@ -208,7 +191,7 @@ public OutputCachePolicyBuilder VaryByValue(Func varyBy) ArgumentNullException.ThrowIfNull(varyBy); _builtPolicy = null; - Policies.Add(new VaryByValuePolicy(varyBy)); + _policies.Add(new VaryByValuePolicy(varyBy)); return this; } @@ -221,7 +204,7 @@ public OutputCachePolicyBuilder VaryByValue(Func<(string, string)> varyBy) ArgumentNullException.ThrowIfNull(varyBy); _builtPolicy = null; - Policies.Add(new VaryByValuePolicy(varyBy)); + _policies.Add(new VaryByValuePolicy(varyBy)); return this; } @@ -234,7 +217,7 @@ public OutputCachePolicyBuilder Policy(string profileName) ArgumentNullException.ThrowIfNull(profileName); _builtPolicy = null; - Policies.Add(new ProfilePolicy(profileName)); + _policies.Add(new ProfilePolicy(profileName)); return this; } @@ -247,7 +230,7 @@ public OutputCachePolicyBuilder Tag(params string[] tags) ArgumentNullException.ThrowIfNull(tags); _builtPolicy = null; - Policies.Add(new TagsPolicy(tags)); + _policies.Add(new TagsPolicy(tags)); return this; } @@ -258,7 +241,7 @@ public OutputCachePolicyBuilder Tag(params string[] tags) public OutputCachePolicyBuilder Expire(TimeSpan expiration) { _builtPolicy = null; - Policies.Add(new ExpirationPolicy(expiration)); + _policies.Add(new ExpirationPolicy(expiration)); return this; } @@ -269,7 +252,7 @@ public OutputCachePolicyBuilder Expire(TimeSpan expiration) public OutputCachePolicyBuilder Lock(bool lockResponse = true) { _builtPolicy = null; - Policies.Add(lockResponse ? LockingPolicy.Enabled : LockingPolicy.Disabled); + _policies.Add(lockResponse ? LockingPolicy.Enabled : LockingPolicy.Disabled); return this; } @@ -279,18 +262,51 @@ public OutputCachePolicyBuilder Lock(bool lockResponse = true) public OutputCachePolicyBuilder Clear() { _builtPolicy = null; - Requirements.Clear(); - Policies.Clear(); + if (_requirements != null) + { + _requirements.Clear(); + } + _policies.Clear(); return this; } /// - /// Adds a policy to prevent the response from being cached. + /// Adds a policy to prevent the response from being cached automatically. /// + /// + /// The cache key will still be computed for lookups. + /// public OutputCachePolicyBuilder NoStore() { _builtPolicy = null; - Policies.Add(NoStorePolicy.Instance); + _policies.Add(NoStorePolicy.Instance); + return this; + } + + /// + /// Adds a policy to prevent the response from being served from cached automatically. + /// + /// + /// The cache key will still be computed for storage. + /// + public OutputCachePolicyBuilder NoLookup() + { + _builtPolicy = null; + _policies.Add(NoLookupPolicy.Instance); + return this; + } + + /// + /// Clears the policies and adds one preventing any caching logic to happen. + /// + /// + /// The cache key will never be computed. + /// + public OutputCachePolicyBuilder NoCache() + { + _builtPolicy = null; + _policies.Clear(); + _policies.Add(EnableCachingPolicy.Disabled); return this; } @@ -308,13 +324,14 @@ public IOutputCachingPolicy Build() return _builtPolicy; } - var policies = new CompositePolicy(Policies.ToArray()); + var policies = new CompositePolicy(_policies.ToArray()); - if (Requirements.Any()) + // If the policy was built with requirements, wrap it + if (_requirements != null && _requirements.Any()) { return new PredicatePolicy(async c => { - foreach (var r in Requirements) + foreach (var r in _requirements) { if (!await r(c)) { @@ -329,17 +346,42 @@ public IOutputCachingPolicy Build() return _builtPolicy = policies; } - Task IOutputCachingPolicy.OnRequestAsync(IOutputCachingContext context) + + /// + /// Defines a which can be referenced by name. + /// + /// The name of the policy. + /// The type of policy to add + public void Add([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type policyType) + { + if (ActivatorUtilities.GetServiceOrCreateInstance(_options.ApplicationServices, policyType) is not IOutputCachingPolicy policy) + { + throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Resources.Policy_InvalidType)); + } + + Add(policy); + } + + /// + /// Defines a which can be referenced by name. + /// + /// The policy type. + public void Add<[DynamicallyAccessedMembers(ActivatorAccessibility)] T>() where T : IOutputCachingPolicy + { + Add(typeof(T)); + } + + Task IOutputCachingPolicy.OnRequestAsync(OutputCachingContext context) { return Build().OnRequestAsync(context); } - Task IOutputCachingPolicy.OnServeFromCacheAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnServeFromCacheAsync(OutputCachingContext context) { return Build().OnServeFromCacheAsync(context); } - Task IOutputCachingPolicy.OnServeResponseAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnServeResponseAsync(OutputCachingContext context) { return Build().OnServeResponseAsync(context); } diff --git a/src/Middleware/OutputCaching/src/Policies/PoliciesCollection.cs b/src/Middleware/OutputCaching/src/Policies/PoliciesCollection.cs new file mode 100644 index 000000000000..d62854ea400c --- /dev/null +++ b/src/Middleware/OutputCaching/src/Policies/PoliciesCollection.cs @@ -0,0 +1,93 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.OutputCaching.Policies; + +/// +/// A collection of policies. +/// +public sealed class PoliciesCollection : IReadOnlyCollection +{ + private const DynamicallyAccessedMemberTypes ActivatorAccessibility = DynamicallyAccessedMemberTypes.PublicConstructors; + + private readonly OutputCachingOptions _options; + private List? _policies; + + internal PoliciesCollection(OutputCachingOptions options) + { + _options = options; + } + + /// + public int Count => _policies == null ? 0 : _policies.Count; + + /// + /// Adds an instance. + /// + /// The policy + public void AddPolicy(IOutputCachingPolicy policy) + { + ArgumentNullException.ThrowIfNull(policy); + + _policies ??= new(); + _policies.Add(policy); + } + + /// + /// Adds a dynamically resolved instance. + /// + /// The type of policy to add. + public void AddPolicy([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type policyType) + { + if (ActivatorUtilities.GetServiceOrCreateInstance(_options.ApplicationServices, policyType) is not IOutputCachingPolicy policy) + { + throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Resources.Policy_InvalidType)); + } + + AddPolicy(policy); + } + + /// + /// Adds a dynamically resolved instance. + /// + /// The type of policy to add. + public void AddPolicy<[DynamicallyAccessedMembers(ActivatorAccessibility)] T>(string name) where T : IOutputCachingPolicy + { + AddPolicy(typeof(T)); + } + + /// + /// Adds instance. + /// + public void AddPolicy(Action build) + { + var builder = new OutputCachePolicyBuilder(_options); + build(builder); + AddPolicy(builder.Build()); + } + + /// + /// Clears the collection. + /// + public void Clear() + { + _policies = null; + } + + /// + public IEnumerator GetEnumerator() + { + return _policies == null ? Enumerable.Empty().GetEnumerator() : _policies.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } +} diff --git a/src/Middleware/OutputCaching/src/Policies/PolicyExtensions.cs b/src/Middleware/OutputCaching/src/Policies/PolicyExtensions.cs index 8c9614d30fe9..3da963fe28a7 100644 --- a/src/Middleware/OutputCaching/src/Policies/PolicyExtensions.cs +++ b/src/Middleware/OutputCaching/src/Policies/PolicyExtensions.cs @@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.OutputCaching.Policies; /// public static class PolicyExtensions { - private static readonly IOutputCachingPolicy _defaultEnabledPolicy = new OutputCachePolicyBuilder().Enable(); + private static readonly IOutputCachingPolicy _defaultEnabledPolicy = new OutputCachePolicyBuilder(); /// /// Marks an endpoint to be cached with the default policy. @@ -37,7 +37,7 @@ public static TBuilder CacheOutput(this TBuilder builder, IOutputCachi ArgumentNullException.ThrowIfNull(builder); // Enable caching if this method is invoked on an endpoint, extra policies can disable it - var policiesMetadata = new PoliciesMetadata(new OutputCachePolicyBuilder().Enable().Add(policy).Build()); + var policiesMetadata = new PoliciesMetadata(new OutputCachePolicyBuilder().Add(policy).Build()); builder.Add(endpointBuilder => { @@ -53,7 +53,7 @@ public static TBuilder CacheOutput(this TBuilder builder, Action> _predicate; + private readonly Func> _predicate; private readonly IOutputCachingPolicy _policy; /// @@ -18,18 +18,37 @@ internal sealed class PredicatePolicy : IOutputCachingPolicy /// /// The predicate. /// The policy. - public PredicatePolicy(Func> predicate, IOutputCachingPolicy policy) + public PredicatePolicy(Func> predicate, IOutputCachingPolicy policy) { _predicate = predicate; _policy = policy; } /// - Task IOutputCachingPolicy.OnRequestAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnRequestAsync(OutputCachingContext context) { + return ExecuteAwaited(static (policy, context) => policy.OnRequestAsync(context), _policy, context); + } + + /// + Task IOutputCachingPolicy.OnServeFromCacheAsync(OutputCachingContext context) + { + return ExecuteAwaited(static (policy, context) => policy.OnServeFromCacheAsync(context), _policy, context); + } + + /// + Task IOutputCachingPolicy.OnServeResponseAsync(OutputCachingContext context) + { + return ExecuteAwaited(static (policy, context) => policy.OnServeResponseAsync(context), _policy, context); + } + + private Task ExecuteAwaited(Func action, IOutputCachingPolicy policy, OutputCachingContext context) + { + ArgumentNullException.ThrowIfNull(action); + if (_predicate == null) { - return _policy.OnRequestAsync(context); + return action(policy, context); } var task = _predicate(context); @@ -38,32 +57,20 @@ Task IOutputCachingPolicy.OnRequestAsync(IOutputCachingContext context) { if (task.Result) { - return _policy.OnRequestAsync(context); + return action(policy, context); } return Task.CompletedTask; } - return Awaited(task, _policy, context); + return Awaited(task); - async static Task Awaited(Task task, IOutputCachingPolicy policy, IOutputCachingContext context) + async Task Awaited(Task task) { if (await task) { - await policy.OnRequestAsync(context); + await action(policy, context); } } } - - /// - Task IOutputCachingPolicy.OnServeFromCacheAsync(IOutputCachingContext context) - { - return Task.CompletedTask; - } - - /// - Task IOutputCachingPolicy.OnServeResponseAsync(IOutputCachingContext context) - { - return Task.CompletedTask; - } } diff --git a/src/Middleware/OutputCaching/src/Policies/ProfilePolicy.cs b/src/Middleware/OutputCaching/src/Policies/ProfilePolicy.cs index d213efb81462..9b3aa635fb5b 100644 --- a/src/Middleware/OutputCaching/src/Policies/ProfilePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/ProfilePolicy.cs @@ -1,9 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; - namespace Microsoft.AspNetCore.OutputCaching; /// @@ -23,7 +20,7 @@ public ProfilePolicy(string profileName) } /// - Task IOutputCachingPolicy.OnServeResponseAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnServeResponseAsync(OutputCachingContext context) { var policy = GetProfilePolicy(context); @@ -36,7 +33,7 @@ Task IOutputCachingPolicy.OnServeResponseAsync(IOutputCachingContext context) } /// - Task IOutputCachingPolicy.OnServeFromCacheAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnServeFromCacheAsync(OutputCachingContext context) { var policy = GetProfilePolicy(context); @@ -49,7 +46,7 @@ Task IOutputCachingPolicy.OnServeFromCacheAsync(IOutputCachingContext context) } /// - Task IOutputCachingPolicy.OnRequestAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnRequestAsync(OutputCachingContext context) { var policy = GetProfilePolicy(context); @@ -61,11 +58,11 @@ Task IOutputCachingPolicy.OnRequestAsync(IOutputCachingContext context) return policy.OnRequestAsync(context); ; } - internal IOutputCachingPolicy? GetProfilePolicy(IOutputCachingContext context) + internal IOutputCachingPolicy? GetProfilePolicy(OutputCachingContext context) { - var options = context.HttpContext.RequestServices.GetRequiredService>(); + var policies = context.Options.NamedPolicies; - return options.Value.Policies.TryGetValue(_profileName, out var cacheProfile) + return policies != null && policies.TryGetValue(_profileName, out var cacheProfile) ? cacheProfile : null; } diff --git a/src/Middleware/OutputCaching/src/Policies/TagsPolicy.cs b/src/Middleware/OutputCaching/src/Policies/TagsPolicy.cs index 62b45682ed3b..3771ca3cd561 100644 --- a/src/Middleware/OutputCaching/src/Policies/TagsPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/TagsPolicy.cs @@ -20,7 +20,7 @@ public TagsPolicy(params string[] tags) } /// - Task IOutputCachingPolicy.OnRequestAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnRequestAsync(OutputCachingContext context) { foreach (var tag in _tags) { @@ -31,13 +31,13 @@ Task IOutputCachingPolicy.OnRequestAsync(IOutputCachingContext context) } /// - Task IOutputCachingPolicy.OnServeFromCacheAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnServeFromCacheAsync(OutputCachingContext context) { return Task.CompletedTask; } /// - Task IOutputCachingPolicy.OnServeResponseAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnServeResponseAsync(OutputCachingContext context) { return Task.CompletedTask; } diff --git a/src/Middleware/OutputCaching/src/Policies/TypedPolicy.cs b/src/Middleware/OutputCaching/src/Policies/TypedPolicy.cs new file mode 100644 index 000000000000..3bc4a46b2f0d --- /dev/null +++ b/src/Middleware/OutputCaching/src/Policies/TypedPolicy.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.OutputCaching.Policies; + +/// +/// A type base policy. +/// +internal sealed class TypedPolicy : IOutputCachingPolicy +{ + private IOutputCachingPolicy? _instance; + + [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] + private readonly Type _policyType; + + /// + /// Creates a new instance of + /// + /// The type of policy. + public TypedPolicy([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type policyType) + { + ArgumentNullException.ThrowIfNull(policyType); + + _policyType = policyType; + } + + private IOutputCachingPolicy? CreatePolicy(OutputCachingContext context) + { + return _instance ??= ActivatorUtilities.CreateInstance(context.Options.ApplicationServices, _policyType) as IOutputCachingPolicy; + } + + /// + Task IOutputCachingPolicy.OnRequestAsync(OutputCachingContext context) + { + return CreatePolicy(context)?.OnRequestAsync(context) ?? Task.CompletedTask; + } + + /// + Task IOutputCachingPolicy.OnServeFromCacheAsync(OutputCachingContext context) + { + return CreatePolicy(context)?.OnServeFromCacheAsync(context) ?? Task.CompletedTask; + } + + /// + Task IOutputCachingPolicy.OnServeResponseAsync(OutputCachingContext context) + { + return CreatePolicy(context)?.OnServeResponseAsync(context) ?? Task.CompletedTask; + } +} diff --git a/src/Middleware/OutputCaching/src/Policies/VaryByHeaderPolicy.cs b/src/Middleware/OutputCaching/src/Policies/VaryByHeaderPolicy.cs index 93b178030fac..db6ee7e990cd 100644 --- a/src/Middleware/OutputCaching/src/Policies/VaryByHeaderPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/VaryByHeaderPolicy.cs @@ -36,7 +36,7 @@ public VaryByHeaderPolicy(params string[] headers) } /// - Task IOutputCachingPolicy.OnRequestAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnRequestAsync(OutputCachingContext context) { // No vary by header? if (_headers.Count == 0) @@ -51,13 +51,13 @@ Task IOutputCachingPolicy.OnRequestAsync(IOutputCachingContext context) } /// - Task IOutputCachingPolicy.OnServeFromCacheAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnServeFromCacheAsync(OutputCachingContext context) { return Task.CompletedTask; } /// - Task IOutputCachingPolicy.OnServeResponseAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnServeResponseAsync(OutputCachingContext context) { return Task.CompletedTask; } diff --git a/src/Middleware/OutputCaching/src/Policies/VaryByQueryPolicy.cs b/src/Middleware/OutputCaching/src/Policies/VaryByQueryPolicy.cs index 066f17071ce3..33af9a1e5623 100644 --- a/src/Middleware/OutputCaching/src/Policies/VaryByQueryPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/VaryByQueryPolicy.cs @@ -37,7 +37,7 @@ public VaryByQueryPolicy(params string[] queryKeys) } /// - Task IOutputCachingPolicy.OnRequestAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnRequestAsync(OutputCachingContext context) { // No vary by query? if (_queryKeys.Count == 0) @@ -59,13 +59,13 @@ Task IOutputCachingPolicy.OnRequestAsync(IOutputCachingContext context) } /// - Task IOutputCachingPolicy.OnServeFromCacheAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnServeFromCacheAsync(OutputCachingContext context) { return Task.CompletedTask; } /// - Task IOutputCachingPolicy.OnServeResponseAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnServeResponseAsync(OutputCachingContext context) { return Task.CompletedTask; } diff --git a/src/Middleware/OutputCaching/src/Policies/VaryByValuePolicy.cs b/src/Middleware/OutputCaching/src/Policies/VaryByValuePolicy.cs index 9c2a4a490b2e..b064602e7b29 100644 --- a/src/Middleware/OutputCaching/src/Policies/VaryByValuePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/VaryByValuePolicy.cs @@ -42,7 +42,7 @@ public VaryByValuePolicy(Func<(string, string)> varyBy) _varyBy = (c) => { var result = varyBy(); - c.VaryByCustom.TryAdd(result.Item1, result.Item2); + c.VaryByCustom?.TryAdd(result.Item1, result.Item2); }; } @@ -54,12 +54,12 @@ public VaryByValuePolicy(Func> varyBy) _varyBy = async (c) => { var result = await varyBy(); - c.VaryByCustom.TryAdd(result.Item1, result.Item2); + c.VaryByCustom?.TryAdd(result.Item1, result.Item2); }; } /// - Task IOutputCachingPolicy.OnRequestAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnRequestAsync(OutputCachingContext context) { _varyBy?.Invoke(context.CachedVaryByRules); @@ -67,13 +67,13 @@ Task IOutputCachingPolicy.OnRequestAsync(IOutputCachingContext context) } /// - Task IOutputCachingPolicy.OnServeFromCacheAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnServeFromCacheAsync(OutputCachingContext context) { return Task.CompletedTask; } /// - Task IOutputCachingPolicy.OnServeResponseAsync(IOutputCachingContext context) + Task IOutputCachingPolicy.OnServeResponseAsync(OutputCachingContext context) { return Task.CompletedTask; } diff --git a/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt b/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt index be2540eb18bc..f6d20f019aee 100644 --- a/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt @@ -1,5 +1,37 @@ #nullable enable Microsoft.AspNetCore.Builder.OutputCachingExtensions +Microsoft.AspNetCore.OutputCaching.CachedResponseBody +Microsoft.AspNetCore.OutputCaching.CachedResponseBody.CachedResponseBody(System.Collections.Generic.List! segments, long length) -> void +Microsoft.AspNetCore.OutputCaching.CachedResponseBody.CopyToAsync(System.IO.Pipelines.PipeWriter! destination, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.CachedResponseBody.Length.get -> long +Microsoft.AspNetCore.OutputCaching.CachedResponseBody.Segments.get -> System.Collections.Generic.List! +Microsoft.AspNetCore.OutputCaching.CachedVaryByRules +Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.CachedVaryByRules() -> void +Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.Headers.get -> Microsoft.Extensions.Primitives.StringValues +Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.Headers.set -> void +Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.QueryKeys.get -> Microsoft.Extensions.Primitives.StringValues +Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.QueryKeys.set -> void +Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.SetVaryByCustom(string! key, string! value) -> void +Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.VaryByPrefix.get -> Microsoft.Extensions.Primitives.StringValues +Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.VaryByPrefix.set -> void +Microsoft.AspNetCore.OutputCaching.IOutputCacheStore +Microsoft.AspNetCore.OutputCaching.IOutputCacheStore.EvictByTagAsync(string! tag, System.Threading.CancellationToken token) -> System.Threading.Tasks.ValueTask +Microsoft.AspNetCore.OutputCaching.IOutputCacheStore.GetAsync(string! key, System.Threading.CancellationToken token) -> System.Threading.Tasks.ValueTask +Microsoft.AspNetCore.OutputCaching.IOutputCacheStore.SetAsync(string! key, Microsoft.AspNetCore.OutputCaching.OutputCacheEntry! entry, System.TimeSpan validFor, System.Threading.CancellationToken token) -> System.Threading.Tasks.ValueTask +Microsoft.AspNetCore.OutputCaching.IOutputCachingFeature +Microsoft.AspNetCore.OutputCaching.IOutputCachingFeature.Context.get -> Microsoft.AspNetCore.OutputCaching.OutputCachingContext! +Microsoft.AspNetCore.OutputCaching.IOutputCachingFeature.Policies.get -> System.Collections.Generic.List! +Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy +Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy.OnRequestAsync(Microsoft.AspNetCore.OutputCaching.OutputCachingContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy.OnServeFromCacheAsync(Microsoft.AspNetCore.OutputCaching.OutputCachingContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy.OnServeResponseAsync(Microsoft.AspNetCore.OutputCaching.OutputCachingContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicyProvider +Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicyProvider.HasPolicies(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> bool +Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicyProvider.OnRequestAsync(Microsoft.AspNetCore.OutputCaching.OutputCachingContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicyProvider.OnServeFromCacheAsync(Microsoft.AspNetCore.OutputCaching.OutputCachingContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicyProvider.OnServeResponseAsync(Microsoft.AspNetCore.OutputCaching.OutputCachingContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.IPoliciesMetadata +Microsoft.AspNetCore.OutputCaching.IPoliciesMetadata.Policy.get -> Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy! Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.Duration.get -> int Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.Duration.set -> void @@ -13,16 +45,29 @@ Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.VaryByHeaders.get -> str Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.VaryByHeaders.set -> void Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.VaryByQueryKeys.get -> string![]? Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.VaryByQueryKeys.set -> void +Microsoft.AspNetCore.OutputCaching.OutputCacheEntry +Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.Body.get -> Microsoft.AspNetCore.OutputCaching.CachedResponseBody! +Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.Body.set -> void +Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.Created.get -> System.DateTimeOffset +Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.Created.set -> void +Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.Headers.get -> Microsoft.AspNetCore.Http.IHeaderDictionary! +Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.Headers.set -> void +Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.OutputCacheEntry() -> void +Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.StatusCode.get -> int +Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.StatusCode.set -> void +Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.Tags.get -> string![]? +Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.Tags.set -> void Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Add(Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy! policy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Add(string! name, System.Type! policyType) -> void +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Add(string! name) -> void Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Build() -> Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Clear() -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! -Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Disable() -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! -Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Enable() -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Expire(System.TimeSpan expiration) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Lock(bool lockResponse = true) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.NoCache() -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.NoLookup() -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.NoStore() -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! -Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.OutputCachePolicyBuilder() -> void Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Policy(string! profileName) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Tag(params string![]! tags) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByHeader(params string![]! headers) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! @@ -31,31 +76,49 @@ Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.F Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.Func!>! varyBy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.Func!>! varyBy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.Func! varyBy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! -Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.WithCondition(System.Func!>! predicate) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.WithCondition(System.Func!>! predicate) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.WithMethod(params string![]! methods) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.WithMethod(string! method) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! -Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.WithPath(Microsoft.AspNetCore.Http.PathString pathBase) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! -Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.WithPath(params Microsoft.AspNetCore.Http.PathString[]! pathBases) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! -Microsoft.AspNetCore.OutputCaching.OutputCachingFeature -Microsoft.AspNetCore.OutputCaching.OutputCachingFeature.Context.get -> Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! -Microsoft.AspNetCore.OutputCaching.OutputCachingFeature.OutputCachingFeature(Microsoft.AspNetCore.OutputCaching.IOutputCachingContext! context) -> void -Microsoft.AspNetCore.OutputCaching.OutputCachingFeature.Policies.get -> System.Collections.Generic.List! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.WithPathBase(Microsoft.AspNetCore.Http.PathString pathBase) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.WithPathBase(params Microsoft.AspNetCore.Http.PathString[]! pathBases) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachingContext +Microsoft.AspNetCore.OutputCaching.OutputCachingContext.AllowCacheLookup.get -> bool +Microsoft.AspNetCore.OutputCaching.OutputCachingContext.AllowCacheLookup.set -> void +Microsoft.AspNetCore.OutputCaching.OutputCachingContext.AllowCacheStorage.get -> bool +Microsoft.AspNetCore.OutputCaching.OutputCachingContext.AllowCacheStorage.set -> void +Microsoft.AspNetCore.OutputCaching.OutputCachingContext.AllowLocking.get -> bool +Microsoft.AspNetCore.OutputCaching.OutputCachingContext.AllowLocking.set -> void +Microsoft.AspNetCore.OutputCaching.OutputCachingContext.EnableOutputCaching.get -> bool +Microsoft.AspNetCore.OutputCaching.OutputCachingContext.EnableOutputCaching.set -> void +Microsoft.AspNetCore.OutputCaching.OutputCachingContext.ResponseExpirationTimeSpan.get -> System.TimeSpan? +Microsoft.AspNetCore.OutputCaching.OutputCachingContext.ResponseExpirationTimeSpan.set -> void +Microsoft.AspNetCore.OutputCaching.OutputCachingContext.ResponseTime.get -> System.DateTimeOffset? Microsoft.AspNetCore.OutputCaching.OutputCachingMiddleware Microsoft.AspNetCore.OutputCaching.OutputCachingMiddleware.Invoke(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.OutputCaching.OutputCachingMiddleware.OutputCachingMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Options.IOptions! options, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, Microsoft.AspNetCore.OutputCaching.IOutputCacheStore! outputCache, Microsoft.Extensions.ObjectPool.ObjectPoolProvider! poolProvider) -> void Microsoft.AspNetCore.OutputCaching.OutputCachingOptions -Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.BasePolicy.get -> Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy? -Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.BasePolicy.set -> void +Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.AddPolicy(string! name, Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy! policy) -> void +Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.AddPolicy(string! name, System.Action! build) -> void +Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.ApplicationServices.get -> System.IServiceProvider! +Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.ApplicationServices.set -> void +Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.BasePolicies.get -> Microsoft.AspNetCore.OutputCaching.Policies.PoliciesCollection! Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.DefaultExpirationTimeSpan.get -> System.TimeSpan Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.DefaultExpirationTimeSpan.set -> void Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.MaximumBodySize.get -> long Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.MaximumBodySize.set -> void Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.OutputCachingOptions() -> void -Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.Policies.get -> System.Collections.Generic.IDictionary! Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.SizeLimit.get -> long Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.SizeLimit.set -> void Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.UseCaseSensitivePaths.get -> bool Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.UseCaseSensitivePaths.set -> void +Microsoft.AspNetCore.OutputCaching.Policies.PoliciesCollection +Microsoft.AspNetCore.OutputCaching.Policies.PoliciesCollection.AddPolicy(Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy! policy) -> void +Microsoft.AspNetCore.OutputCaching.Policies.PoliciesCollection.AddPolicy(System.Action! build) -> void +Microsoft.AspNetCore.OutputCaching.Policies.PoliciesCollection.AddPolicy(System.Type! policyType) -> void +Microsoft.AspNetCore.OutputCaching.Policies.PoliciesCollection.AddPolicy(string! name) -> void +Microsoft.AspNetCore.OutputCaching.Policies.PoliciesCollection.Clear() -> void +Microsoft.AspNetCore.OutputCaching.Policies.PoliciesCollection.Count.get -> int +Microsoft.AspNetCore.OutputCaching.Policies.PoliciesCollection.GetEnumerator() -> System.Collections.Generic.IEnumerator! Microsoft.AspNetCore.OutputCaching.Policies.PolicyExtensions Microsoft.Extensions.DependencyInjection.OutputCachingServicesExtensions static Microsoft.AspNetCore.Builder.OutputCachingExtensions.UseOutputCaching(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! @@ -64,3 +127,10 @@ static Microsoft.AspNetCore.OutputCaching.Policies.PolicyExtensions.CacheOutput< static Microsoft.AspNetCore.OutputCaching.Policies.PolicyExtensions.CacheOutput(this TBuilder builder, System.Action! policy) -> TBuilder static Microsoft.Extensions.DependencyInjection.OutputCachingServicesExtensions.AddOutputCaching(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.Extensions.DependencyInjection.OutputCachingServicesExtensions.AddOutputCaching(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +~Microsoft.AspNetCore.OutputCaching.OutputCachingContext.CachedVaryByRules.get -> Microsoft.AspNetCore.OutputCaching.CachedVaryByRules +~Microsoft.AspNetCore.OutputCaching.OutputCachingContext.CachedVaryByRules.set -> void +~Microsoft.AspNetCore.OutputCaching.OutputCachingContext.HttpContext.get -> Microsoft.AspNetCore.Http.HttpContext +~Microsoft.AspNetCore.OutputCaching.OutputCachingContext.Logger.get -> Microsoft.Extensions.Logging.ILogger +~Microsoft.AspNetCore.OutputCaching.OutputCachingContext.Options.get -> Microsoft.AspNetCore.OutputCaching.OutputCachingOptions +~Microsoft.AspNetCore.OutputCaching.OutputCachingContext.Store.get -> Microsoft.AspNetCore.OutputCaching.IOutputCacheStore +~Microsoft.AspNetCore.OutputCaching.OutputCachingContext.Tags.get -> System.Collections.Generic.HashSet diff --git a/src/Middleware/OutputCaching/src/Resources.Designer.cs b/src/Middleware/OutputCaching/src/Resources.Designer.cs new file mode 100644 index 000000000000..da8d0c09ec09 --- /dev/null +++ b/src/Middleware/OutputCaching/src/Resources.Designer.cs @@ -0,0 +1,72 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Microsoft.AspNetCore.OutputCaching { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Resources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Resources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNetCore.OutputCaching.Resources", typeof(Resources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to The type '{0}' is not a valid output policy.. + /// + internal static string Policy_InvalidType { + get { + return ResourceManager.GetString("Policy_InvalidType", resourceCulture); + } + } + } +} diff --git a/src/Middleware/OutputCaching/src/Resources.resx b/src/Middleware/OutputCaching/src/Resources.resx new file mode 100644 index 000000000000..3a19868a7302 --- /dev/null +++ b/src/Middleware/OutputCaching/src/Resources.resx @@ -0,0 +1,123 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The type '{0}' is not a valid output policy. + + \ No newline at end of file diff --git a/src/Middleware/OutputCaching/test/OutputCachingPolicyProviderTests.cs b/src/Middleware/OutputCaching/test/OutputCachingPolicyProviderTests.cs index 8dc3a6b9f74f..d5009290133b 100644 --- a/src/Middleware/OutputCaching/test/OutputCachingPolicyProviderTests.cs +++ b/src/Middleware/OutputCaching/test/OutputCachingPolicyProviderTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.OutputCaching.Policies; using Microsoft.Extensions.Logging.Testing; using Microsoft.Extensions.Options; using Microsoft.Net.Http.Headers; @@ -46,7 +47,8 @@ public async Task AttemptOutputCaching_CacheableMethods_Allowed(string method) { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - var options = new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().Build() }; + var options = new OutputCachingOptions(); + options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); context.HttpContext.Request.Method = method; await new OutputCachingPolicyProvider(Options.Create(options)).OnRequestAsync(context); @@ -62,7 +64,8 @@ public async Task AttemptOutputCaching_UncacheableMethods_NotAllowed(string meth { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - var options = new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().Build() }; + var options = new OutputCachingOptions(); + options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); context.HttpContext.Request.Method = method; await new OutputCachingPolicyProvider(Options.Create(options)).OnRequestAsync(context); @@ -82,7 +85,8 @@ public async Task AttemptResponseCaching_AuthorizationHeaders_NotAllowed() context.HttpContext.Request.Method = HttpMethods.Get; context.HttpContext.Request.Headers.Authorization = "Placeholder"; - var options = new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().Build() }; + var options = new OutputCachingOptions(); + options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); await new OutputCachingPolicyProvider(Options.Create(options)).OnRequestAsync(context); @@ -105,7 +109,8 @@ public async Task AllowCacheStorage_NoStore_Allowed() NoStore = true }.ToString(); - var options = new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().Build() }; + var options = new OutputCachingOptions(); + options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); await new OutputCachingPolicyProvider(Options.Create(options)).OnRequestAsync(context); Assert.True(context.AllowCacheStorage); @@ -121,7 +126,8 @@ public async Task AllowCacheLookup_LegacyDirectives_OverridenByCacheControl() context.HttpContext.Request.Headers.Pragma = "no-cache"; context.HttpContext.Request.Headers.CacheControl = "max-age=10"; - var options = new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().Build() }; + var options = new OutputCachingOptions(); + options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); await new OutputCachingPolicyProvider(Options.Create(options)).OnRequestAsync(context); Assert.True(context.AllowCacheLookup); @@ -134,7 +140,8 @@ public async Task IsResponseCacheable_NoPublic_Allowed() var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - var options = new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().Build() }; + var options = new OutputCachingOptions(); + options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); Assert.True(context.AllowCacheStorage); @@ -152,7 +159,8 @@ public async Task IsResponseCacheable_Public_Allowed() Public = true }.ToString(); - var options = new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().Build() }; + var options = new OutputCachingOptions(); + options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); Assert.True(context.AllowCacheStorage); @@ -170,7 +178,8 @@ public async Task IsResponseCacheable_NoCache_Allowed() NoCache = true }.ToString(); - var options = new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().Build() }; + var options = new OutputCachingOptions(); + options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); Assert.True(context.AllowCacheStorage); @@ -188,7 +197,8 @@ public async Task IsResponseCacheable_ResponseNoStore_Allowed() NoStore = true }.ToString(); - var options = new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().Build() }; + var options = new OutputCachingOptions(); + options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); Assert.True(context.AllowCacheStorage); @@ -203,7 +213,8 @@ public async Task IsResponseCacheable_SetCookieHeader_NotAllowed() var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.Headers.SetCookie = "cookieName=cookieValue"; - var options = new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().Build() }; + var options = new OutputCachingOptions(); + options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); Assert.False(context.AllowCacheStorage); @@ -219,8 +230,8 @@ public async Task IsResponseCacheable_VaryHeaderByStar_Allowed() var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.Headers.Vary = "*"; - - var options = new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().Build() }; + var options = new OutputCachingOptions(); + options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); Assert.True(context.AllowCacheStorage); @@ -238,7 +249,8 @@ public async Task IsResponseCacheable_Private_Allowed() Private = true }.ToString(); - var options = new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().Build() }; + var options = new OutputCachingOptions(); + options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); Assert.True(context.AllowCacheStorage); @@ -254,7 +266,8 @@ public async Task IsResponseCacheable_SuccessStatusCodes_Allowed(int statusCode) var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.StatusCode = statusCode; - var options = new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().Build() }; + var options = new OutputCachingOptions(); + options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); Assert.True(context.AllowCacheStorage); @@ -330,7 +343,8 @@ public async Task IsResponseCacheable_NonSuccessStatusCodes_NotAllowed(int statu var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.StatusCode = statusCode; - var options = new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().Build() }; + var options = new OutputCachingOptions(); + options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); Assert.True(context.AllowCacheLookup); @@ -351,7 +365,8 @@ public async Task IsResponseCacheable_NoExpiryRequirements_IsAllowed() context.HttpContext.Response.Headers.Date = HeaderUtilities.FormatDate(utcNow); context.ResponseTime = DateTimeOffset.MaxValue; - var options = new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().Build() }; + var options = new OutputCachingOptions(); + options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); Assert.True(context.AllowCacheStorage); @@ -374,7 +389,8 @@ public async Task IsResponseCacheable_MaxAgeOverridesExpiry_ToAllowed() context.HttpContext.Response.Headers.Date = HeaderUtilities.FormatDate(utcNow); context.ResponseTime = utcNow + TimeSpan.FromSeconds(9); - var options = new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().Build() }; + var options = new OutputCachingOptions(); + options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); Assert.True(context.AllowCacheStorage); @@ -397,7 +413,8 @@ public async Task IsResponseCacheable_SharedMaxAgeOverridesMaxAge_ToAllowed() context.HttpContext.Response.Headers.Date = HeaderUtilities.FormatDate(utcNow); context.ResponseTime = utcNow + TimeSpan.FromSeconds(11); - var options = new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().Build() }; + var options = new OutputCachingOptions(); + options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); Assert.True(context.AllowCacheStorage); diff --git a/src/Middleware/OutputCaching/test/OutputCachingTests.cs b/src/Middleware/OutputCaching/test/OutputCachingTests.cs index d4aac8b6dbc0..da2667680280 100644 --- a/src/Middleware/OutputCaching/test/OutputCachingTests.cs +++ b/src/Middleware/OutputCaching/test/OutputCachingTests.cs @@ -219,11 +219,11 @@ public async Task ServesCachedContent_If_PathCasingDiffers(string method) [InlineData("HEAD")] public async Task ServesFreshContent_If_ResponseExpired(string method) { - var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions - { - BasePolicy = new OutputCachePolicyBuilder().Enable().VaryByHeader(HeaderNames.From).Build(), - DefaultExpirationTimeSpan = TimeSpan.FromMicroseconds(100) - }); + var options = new OutputCachingOptions(); + options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().VaryByHeader(HeaderNames.From).Build()); + options.DefaultExpirationTimeSpan = TimeSpan.FromMicroseconds(100); + + var builders = TestUtils.CreateBuildersWithOutputCaching(options: options); foreach (var builder in builders) { @@ -294,7 +294,10 @@ public async Task ServesCachedContent_IfVaryHeader_Matches() [Fact] public async Task ServesFreshContent_IfVaryHeader_Mismatches() { - var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().VaryByHeader(HeaderNames.From).Build() }); + var options = new OutputCachingOptions(); + options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().VaryByHeader(HeaderNames.From).Build()); + + var builders = TestUtils.CreateBuildersWithOutputCaching(options: options); foreach (var builder in builders) { @@ -318,7 +321,10 @@ public async Task ServesFreshContent_IfVaryHeader_Mismatches() [Fact] public async Task ServesCachedContent_IfVaryQueryKeys_Matches() { - var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().VaryByQuery("query").Build() }); + var options = new OutputCachingOptions(); + options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().VaryByQuery("query").Build()); + + var builders = TestUtils.CreateBuildersWithOutputCaching(options: options); foreach (var builder in builders) { @@ -340,7 +346,10 @@ public async Task ServesCachedContent_IfVaryQueryKeys_Matches() [Fact] public async Task ServesCachedContent_IfVaryQueryKeysExplicit_Matches_QueryKeyCaseInsensitive() { - var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().VaryByQuery("QueryA", "queryb").Build() }); + var options = new OutputCachingOptions(); + options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().VaryByQuery("QueryA", "queryb").Build()); + + var builders = TestUtils.CreateBuildersWithOutputCaching(options: options); foreach (var builder in builders) { @@ -362,7 +371,10 @@ public async Task ServesCachedContent_IfVaryQueryKeysExplicit_Matches_QueryKeyCa [Fact] public async Task ServesCachedContent_IfVaryQueryKeyStar_Matches_QueryKeyCaseInsensitive() { - var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().VaryByQuery("*").Build() }); + var options = new OutputCachingOptions(); + options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().VaryByQuery("*").Build()); + + var builders = TestUtils.CreateBuildersWithOutputCaching(options: options); foreach (var builder in builders) { @@ -384,7 +396,10 @@ public async Task ServesCachedContent_IfVaryQueryKeyStar_Matches_QueryKeyCaseIns [Fact] public async Task ServesCachedContent_IfVaryQueryKeyExplicit_Matches_OrderInsensitive() { - var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().VaryByQuery("QueryB", "QueryA").Build() }); + var options = new OutputCachingOptions(); + options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().VaryByQuery("QueryB", "QueryA").Build()); + + var builders = TestUtils.CreateBuildersWithOutputCaching(options: options); foreach (var builder in builders) { @@ -406,7 +421,10 @@ public async Task ServesCachedContent_IfVaryQueryKeyExplicit_Matches_OrderInsens [Fact] public async Task ServesCachedContent_IfVaryQueryKeyStar_Matches_OrderInsensitive() { - var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().VaryByQuery("*").Build() }); + var options = new OutputCachingOptions(); + options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().VaryByQuery("*").Build()); + + var builders = TestUtils.CreateBuildersWithOutputCaching(options: options); foreach (var builder in builders) { @@ -428,7 +446,9 @@ public async Task ServesCachedContent_IfVaryQueryKeyStar_Matches_OrderInsensitiv [Fact] public async Task ServesFreshContent_IfVaryQueryKey_Mismatches() { - var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().VaryByQuery("query").Build() }); + var options = new OutputCachingOptions(); + options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().VaryByQuery("query").Build()); + var builders = TestUtils.CreateBuildersWithOutputCaching(options: options); foreach (var builder in builders) { @@ -450,7 +470,10 @@ public async Task ServesFreshContent_IfVaryQueryKey_Mismatches() [Fact] public async Task ServesCachedContent_IfOtherVaryQueryKey_Mismatches() { - var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions { BasePolicy = new OutputCachePolicyBuilder().Enable().VaryByQuery("query").Build() }); + var options = new OutputCachingOptions(); + options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().VaryByQuery("query").Build()); + + var builders = TestUtils.CreateBuildersWithOutputCaching(options: options); foreach (var builder in builders) { @@ -772,11 +795,11 @@ public async Task ServesCachedContent_IfIfNoneMatch_NotSatisfied() [Fact] public async Task ServesCachedContent_IfBodySize_IsCacheable() { - var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions() - { - BasePolicy = new OutputCachePolicyBuilder().Enable().Build(), - MaximumBodySize = 1000 - }); + var options = new OutputCachingOptions(); + options.MaximumBodySize = 1000; + options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); + + var builders = TestUtils.CreateBuildersWithOutputCaching(options: options); foreach (var builder in builders) { diff --git a/src/Middleware/OutputCaching/test/TestUtils.cs b/src/Middleware/OutputCaching/test/TestUtils.cs index 94592f819182..b2cd880b4632 100644 --- a/src/Middleware/OutputCaching/test/TestUtils.cs +++ b/src/Middleware/OutputCaching/test/TestUtils.cs @@ -144,13 +144,13 @@ private static IEnumerable CreateBuildersWithOutputCaching( outputCachingOptions.MaximumBodySize = options.MaximumBodySize; outputCachingOptions.UseCaseSensitivePaths = options.UseCaseSensitivePaths; outputCachingOptions.SystemClock = options.SystemClock; - outputCachingOptions.BasePolicy = options.BasePolicy; + outputCachingOptions.BasePolicies = options.BasePolicies; outputCachingOptions.DefaultExpirationTimeSpan = options.DefaultExpirationTimeSpan; outputCachingOptions.SizeLimit = options.SizeLimit; } else { - outputCachingOptions.BasePolicy = new OutputCachePolicyBuilder().Enable().Build(); + outputCachingOptions.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); } }); }) @@ -202,9 +202,9 @@ internal static OutputCachingMiddleware CreateTestMiddleware( keyProvider); } - internal static OutputCachingContext CreateTestContext(IOutputCacheStore cache = null) + internal static OutputCachingContext CreateTestContext(IOutputCacheStore cache = null, OutputCachingOptions options = null) { - return new OutputCachingContext(new DefaultHttpContext(), cache ?? new TestOutputCache(), NullLogger.Instance) + return new OutputCachingContext(new DefaultHttpContext(), cache ?? new TestOutputCache(), options ?? Options.Create(new OutputCachingOptions()).Value, NullLogger.Instance) { EnableOutputCaching = true, AllowCacheStorage = true, @@ -213,9 +213,9 @@ internal static OutputCachingContext CreateTestContext(IOutputCacheStore cache = }; } - internal static OutputCachingContext CreateTestContext(HttpContext httpContext, IOutputCacheStore cache = null) + internal static OutputCachingContext CreateTestContext(HttpContext httpContext, IOutputCacheStore cache = null, OutputCachingOptions options = null) { - return new OutputCachingContext(httpContext, cache ?? new TestOutputCache(), NullLogger.Instance) + return new OutputCachingContext(httpContext, cache ?? new TestOutputCache(), options ?? Options.Create(new OutputCachingOptions()).Value, NullLogger.Instance) { EnableOutputCaching = true, AllowCacheStorage = true, @@ -224,9 +224,9 @@ internal static OutputCachingContext CreateTestContext(HttpContext httpContext, }; } - internal static OutputCachingContext CreateTestContext(ITestSink testSink, IOutputCacheStore cache = null) + internal static OutputCachingContext CreateTestContext(ITestSink testSink, IOutputCacheStore cache = null, OutputCachingOptions options = null) { - return new OutputCachingContext(new DefaultHttpContext(), cache ?? new TestOutputCache(), new TestLogger("OutputCachingTests", testSink, true)) + return new OutputCachingContext(new DefaultHttpContext(), cache ?? new TestOutputCache(), options ?? Options.Create(new OutputCachingOptions()).Value, new TestLogger("OutputCachingTests", testSink, true)) { EnableOutputCaching = true, AllowCacheStorage = true, @@ -326,7 +326,7 @@ public bool HasPolicies(HttpContext httpContext) return true; } - public Task OnRequestAsync(IOutputCachingContext context) + public Task OnRequestAsync(OutputCachingContext context) { context.EnableOutputCaching = EnableOutputCaching; context.AllowCacheLookup = AllowCacheLookupValue; @@ -335,7 +335,7 @@ public Task OnRequestAsync(IOutputCachingContext context) return Task.CompletedTask; } - public Task OnServeFromCacheAsync(IOutputCachingContext context) + public Task OnServeFromCacheAsync(OutputCachingContext context) { context.AllowCacheLookup = AllowCacheLookupValue; context.AllowCacheStorage = AllowCacheStorageValue; @@ -343,7 +343,7 @@ public Task OnServeFromCacheAsync(IOutputCachingContext context) return Task.CompletedTask; } - public Task OnServeResponseAsync(IOutputCachingContext context) + public Task OnServeResponseAsync(OutputCachingContext context) { context.AllowCacheStorage = AllowCacheStorageValue; @@ -368,7 +368,7 @@ public string CreateStorageKey(OutputCachingContext context) internal class TestOutputCache : IOutputCacheStore { - private readonly IDictionary _storage = new Dictionary(); + private readonly IDictionary _storage = new Dictionary(); public int GetCount { get; private set; } public int SetCount { get; private set; } @@ -377,20 +377,20 @@ public ValueTask EvictByTagAsync(string tag, CancellationToken token) throw new NotImplementedException(); } - public ValueTask GetAsync(string key, CancellationToken token) + public ValueTask GetAsync(string key, CancellationToken token) { GetCount++; try { - return new ValueTask(_storage[key]); + return new ValueTask(_storage[key]); } catch { - return new ValueTask(default(IOutputCacheEntry)); + return new ValueTask(default(OutputCacheEntry)); } } - public ValueTask SetAsync(string key, IOutputCacheEntry entry, TimeSpan validFor, CancellationToken token) + public ValueTask SetAsync(string key, OutputCacheEntry entry, TimeSpan validFor, CancellationToken token) { SetCount++; _storage[key] = entry; diff --git a/src/Mvc/Mvc.Core/src/OutputCacheAttribute.cs b/src/Mvc/Mvc.Core/src/OutputCacheAttribute.cs index 20f41fd76729..0a40bbe0fc59 100644 --- a/src/Mvc/Mvc.Core/src/OutputCacheAttribute.cs +++ b/src/Mvc/Mvc.Core/src/OutputCacheAttribute.cs @@ -60,7 +60,7 @@ public bool NoStore private IOutputCachingPolicy GetPolicy() { - var builder = new OutputCachePolicyBuilder().Enable(); + var builder = new OutputCachePolicyBuilder(); if (_noStore != null && _noStore.Value) { diff --git a/src/Mvc/Mvc.slnf b/src/Mvc/Mvc.slnf index 8a04dc070284..2599ea39e358 100644 --- a/src/Mvc/Mvc.slnf +++ b/src/Mvc/Mvc.slnf @@ -46,7 +46,6 @@ "src\\Middleware\\HttpOverrides\\src\\Microsoft.AspNetCore.HttpOverrides.csproj", "src\\Middleware\\Localization.Routing\\src\\Microsoft.AspNetCore.Localization.Routing.csproj", "src\\Middleware\\Localization\\src\\Microsoft.AspNetCore.Localization.csproj", - "src\\Middleware\\OutputCaching.Abstractions\\src\\Microsoft.AspNetCore.OutputCaching.Abstractions.csproj", "src\\Middleware\\OutputCaching\\src\\Microsoft.AspNetCore.OutputCaching.csproj", "src\\Middleware\\ResponseCaching.Abstractions\\src\\Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj", "src\\Middleware\\ResponseCaching\\src\\Microsoft.AspNetCore.ResponseCaching.csproj", diff --git a/src/submodules/googletest b/src/submodules/googletest index 0320f517fd92..5126f7166109 160000 --- a/src/submodules/googletest +++ b/src/submodules/googletest @@ -1 +1 @@ -Subproject commit 0320f517fd920866d918e564105d68fd4362040a +Subproject commit 5126f7166109666a9c0534021fb1a3038659494c From 3c15e2a11dd357b8a456e09c33d5fdfc5fa0842b Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Tue, 14 Jun 2022 09:24:09 -0700 Subject: [PATCH 37/48] Refactor resolved policies --- .../OutputCaching/src/OutputCacheAttribute.cs | 2 +- .../OutputCaching/src/OutputCachingOptions.cs | 5 +---- .../src/Policies/OutputCachePolicyBuilder.cs | 22 ++++++------------- .../src/Policies/PoliciesCollection.cs | 2 +- .../OutputCaching/src/PublicAPI.Unshipped.txt | 5 +++-- 5 files changed, 13 insertions(+), 23 deletions(-) diff --git a/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs b/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs index b72bee23d0d7..862d7865ff51 100644 --- a/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs @@ -62,7 +62,7 @@ public bool NoStore private IOutputCachingPolicy GetPolicy() { - var builder = new OutputCachePolicyBuilder().Enable(); + var builder = new OutputCachePolicyBuilder(); if (_noStore != null && _noStore.Value) { diff --git a/src/Middleware/OutputCaching/src/OutputCachingOptions.cs b/src/Middleware/OutputCaching/src/OutputCachingOptions.cs index 5e50a4eb5d0c..9a25acfa8804 100644 --- a/src/Middleware/OutputCaching/src/OutputCachingOptions.cs +++ b/src/Middleware/OutputCaching/src/OutputCachingOptions.cs @@ -2,10 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.ComponentModel; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; using Microsoft.AspNetCore.OutputCaching.Policies; -using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.OutputCaching; @@ -81,7 +78,7 @@ public void AddPolicy(string name, IOutputCachingPolicy policy) /// an action on . public void AddPolicy(string name, Action build) { - var builder = new OutputCachePolicyBuilder(this); + var builder = new OutputCachePolicyBuilder(); build(builder); NamedPolicies ??= new Dictionary(StringComparer.OrdinalIgnoreCase); NamedPolicies[name] = builder.Build(); diff --git a/src/Middleware/OutputCaching/src/Policies/OutputCachePolicyBuilder.cs b/src/Middleware/OutputCaching/src/Policies/OutputCachePolicyBuilder.cs index 0c1cff546cf8..d20d30fce972 100644 --- a/src/Middleware/OutputCaching/src/Policies/OutputCachePolicyBuilder.cs +++ b/src/Middleware/OutputCaching/src/Policies/OutputCachePolicyBuilder.cs @@ -2,11 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; -using System.Globalization; using System.Linq; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.OutputCaching.Policies; -using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.OutputCaching; @@ -19,14 +17,15 @@ public sealed class OutputCachePolicyBuilder : IOutputCachingPolicy private IOutputCachingPolicy? _builtPolicy; private readonly List _policies = new(); - private readonly OutputCachingOptions _options; private List>>? _requirements; - internal OutputCachePolicyBuilder(OutputCachingOptions options) + /// + /// Creates a new instance. + /// + public OutputCachePolicyBuilder() { _builtPolicy = null; _policies.Add(DefaultOutputCachePolicy.Instance); - _options = options; } /// @@ -346,24 +345,17 @@ public IOutputCachingPolicy Build() return _builtPolicy = policies; } - /// - /// Defines a which can be referenced by name. + /// Adds a dynamically resolved policy. /// - /// The name of the policy. /// The type of policy to add public void Add([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type policyType) { - if (ActivatorUtilities.GetServiceOrCreateInstance(_options.ApplicationServices, policyType) is not IOutputCachingPolicy policy) - { - throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Resources.Policy_InvalidType)); - } - - Add(policy); + Add(new TypedPolicy(policyType)); } /// - /// Defines a which can be referenced by name. + /// Adds a dynamically resolved policy. /// /// The policy type. public void Add<[DynamicallyAccessedMembers(ActivatorAccessibility)] T>() where T : IOutputCachingPolicy diff --git a/src/Middleware/OutputCaching/src/Policies/PoliciesCollection.cs b/src/Middleware/OutputCaching/src/Policies/PoliciesCollection.cs index d62854ea400c..8b175f48f6dd 100644 --- a/src/Middleware/OutputCaching/src/Policies/PoliciesCollection.cs +++ b/src/Middleware/OutputCaching/src/Policies/PoliciesCollection.cs @@ -67,7 +67,7 @@ public void AddPolicy([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes /// public void AddPolicy(Action build) { - var builder = new OutputCachePolicyBuilder(_options); + var builder = new OutputCachePolicyBuilder(); build(builder); AddPolicy(builder.Build()); } diff --git a/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt b/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt index f6d20f019aee..6bc96960f0e2 100644 --- a/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt @@ -59,8 +59,8 @@ Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.Tags.get -> string![]? Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.Tags.set -> void Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Add(Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy! policy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! -Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Add(string! name, System.Type! policyType) -> void -Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Add(string! name) -> void +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Add(System.Type! policyType) -> void +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Add() -> void Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Build() -> Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Clear() -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Expire(System.TimeSpan expiration) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! @@ -68,6 +68,7 @@ Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Lock(bool lockRespon Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.NoCache() -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.NoLookup() -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.NoStore() -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.OutputCachePolicyBuilder() -> void Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Policy(string! profileName) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Tag(params string![]! tags) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByHeader(params string![]! headers) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! From 499e8a203634183962c47cd722bb74bf641a2365 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Tue, 14 Jun 2022 09:27:33 -0700 Subject: [PATCH 38/48] Clean sample --- .../OutputCaching/samples/OutputCachingSample/Startup.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs b/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs index 7bd8a0aaad1b..0edbb8ef3229 100644 --- a/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs +++ b/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs @@ -14,9 +14,6 @@ options.BasePolicies.AddPolicy(b => b.WithPathBase("/admin").NoStore()); options.AddPolicy("NoCache", b => b.WithPathBase("/wwwroot").Expire(TimeSpan.FromDays(1))); - - options.AddPolicy("Disable", b => b.Add()); - }); var app = builder.Build(); @@ -31,7 +28,7 @@ app.MapGet("/profile", Gravatar.WriteGravatar).CacheOutput(x => x.Policy("NoCache")); -app.MapGet("/attribute", (RequestDelegate)([OutputCache(PolicyName = "NoCache")] (c) => Gravatar.WriteGravatar(c))); +app.MapGet("/attribute", [OutputCache(PolicyName = "NoCache")] () => Gravatar.WriteGravatar); var blog = app.MapGroup("blog").CacheOutput(x => x.Tag("blog")); blog.MapGet("/", Gravatar.WriteGravatar); From 4136f9d168cac71bf4254e02aa883646f929aaeb Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Tue, 14 Jun 2022 13:21:41 -0700 Subject: [PATCH 39/48] Provide HttpContext in VaryByValue --- .../samples/OutputCachingSample/Startup.cs | 2 +- .../OutputCachePolicyBuilder.cs | 17 +++------- .../src/Policies/VaryByValuePolicy.cs | 34 ++++++++++--------- .../OutputCaching/src/PublicAPI.Unshipped.txt | 9 +++-- src/Mvc/Mvc.Core/src/OutputCacheAttribute.cs | 2 +- 5 files changed, 29 insertions(+), 35 deletions(-) rename src/Middleware/OutputCaching/src/{Policies => }/OutputCachePolicyBuilder.cs (94%) diff --git a/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs b/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs index 0edbb8ef3229..f0894b598ff2 100644 --- a/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs +++ b/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs @@ -44,7 +44,7 @@ // Cached entries will vary by culture, but any other additional query is ignored and returns the same cached content app.MapGet("/query", Gravatar.WriteGravatar).CacheOutput(p => p.VaryByQuery("culture")); -app.MapGet("/vary", Gravatar.WriteGravatar).CacheOutput(c => c.VaryByValue(() => ("time", (DateTime.Now.Second % 2).ToString(CultureInfo.InvariantCulture)))); +app.MapGet("/vary", Gravatar.WriteGravatar).CacheOutput(c => c.VaryByValue((context) => ("time", (DateTime.Now.Second % 2).ToString(CultureInfo.InvariantCulture)))); long requests = 0; diff --git a/src/Middleware/OutputCaching/src/Policies/OutputCachePolicyBuilder.cs b/src/Middleware/OutputCaching/src/OutputCachePolicyBuilder.cs similarity index 94% rename from src/Middleware/OutputCaching/src/Policies/OutputCachePolicyBuilder.cs rename to src/Middleware/OutputCaching/src/OutputCachePolicyBuilder.cs index d20d30fce972..f6d1b7640598 100644 --- a/src/Middleware/OutputCaching/src/Policies/OutputCachePolicyBuilder.cs +++ b/src/Middleware/OutputCaching/src/OutputCachePolicyBuilder.cs @@ -159,7 +159,7 @@ public OutputCachePolicyBuilder VaryByHeader(params string[] headers) /// Adds a policy to vary the cached responses by custom values. /// /// The value to vary the cached responses by. - public OutputCachePolicyBuilder VaryByValue(Func> varyBy) + public OutputCachePolicyBuilder VaryByValue(Func> varyBy) { ArgumentNullException.ThrowIfNull(varyBy); @@ -172,7 +172,7 @@ public OutputCachePolicyBuilder VaryByValue(Func> varyBy) /// Adds a policy to vary the cached responses by custom key/value. /// /// The key/value to vary the cached responses by. - public OutputCachePolicyBuilder VaryByValue(Func> varyBy) + public OutputCachePolicyBuilder VaryByValue(Func> varyBy) { ArgumentNullException.ThrowIfNull(varyBy); @@ -185,7 +185,7 @@ public OutputCachePolicyBuilder VaryByValue(Func> varyBy) /// Adds a policy to vary the cached responses by custom values. /// /// The value to vary the cached responses by. - public OutputCachePolicyBuilder VaryByValue(Func varyBy) + public OutputCachePolicyBuilder VaryByValue(Func varyBy) { ArgumentNullException.ThrowIfNull(varyBy); @@ -198,7 +198,7 @@ public OutputCachePolicyBuilder VaryByValue(Func varyBy) /// Adds a policy to vary the cached responses by custom key/value. /// /// The key/value to vary the cached responses by. - public OutputCachePolicyBuilder VaryByValue(Func<(string, string)> varyBy) + public OutputCachePolicyBuilder VaryByValue(Func varyBy) { ArgumentNullException.ThrowIfNull(varyBy); @@ -309,14 +309,7 @@ public OutputCachePolicyBuilder NoCache() return this; } - /// - /// Builds a new from the definitions - /// in this instance. - /// - /// - /// A new built from the definitions in this instance. - /// - public IOutputCachingPolicy Build() + internal IOutputCachingPolicy Build() { if (_builtPolicy != null) { diff --git a/src/Middleware/OutputCaching/src/Policies/VaryByValuePolicy.cs b/src/Middleware/OutputCaching/src/Policies/VaryByValuePolicy.cs index b064602e7b29..6d04c9c3113b 100644 --- a/src/Middleware/OutputCaching/src/Policies/VaryByValuePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/VaryByValuePolicy.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.AspNetCore.Http; + namespace Microsoft.AspNetCore.OutputCaching; /// @@ -8,8 +10,8 @@ namespace Microsoft.AspNetCore.OutputCaching; /// internal sealed class VaryByValuePolicy : IOutputCachingPolicy { - private readonly Action? _varyBy; - private readonly Func? _varyByAsync; + private readonly Action? _varyBy; + private readonly Func? _varyByAsync; /// /// Creates a policy that doesn't vary the cached content based on values. @@ -21,49 +23,49 @@ public VaryByValuePolicy() /// /// Creates a policy that vary the cached content based on the specified value. /// - public VaryByValuePolicy(Func varyBy) + public VaryByValuePolicy(Func varyBy) { - _varyBy = (c) => c.VaryByPrefix += varyBy(); + _varyBy = (context, rules) => rules.VaryByPrefix += varyBy(context); } /// /// Creates a policy that vary the cached content based on the specified value. /// - public VaryByValuePolicy(Func> varyBy) + public VaryByValuePolicy(Func> varyBy) { - _varyByAsync = async (c) => c.VaryByPrefix += await varyBy(); + _varyByAsync = async (context, rules) => rules.VaryByPrefix += await varyBy(context); } /// /// Creates a policy that vary the cached content based on the specified value. /// - public VaryByValuePolicy(Func<(string, string)> varyBy) + public VaryByValuePolicy(Func varyBy) { - _varyBy = (c) => + _varyBy = (context, rules) => { - var result = varyBy(); - c.VaryByCustom?.TryAdd(result.Item1, result.Item2); + var result = varyBy(context); + rules.VaryByCustom?.TryAdd(result.Item1, result.Item2); }; } /// /// Creates a policy that vary the cached content based on the specified value. /// - public VaryByValuePolicy(Func> varyBy) + public VaryByValuePolicy(Func> varyBy) { - _varyBy = async (c) => + _varyBy = async (context, rules) => { - var result = await varyBy(); - c.VaryByCustom?.TryAdd(result.Item1, result.Item2); + var result = await varyBy(context); + rules.VaryByCustom?.TryAdd(result.Item1, result.Item2); }; } /// Task IOutputCachingPolicy.OnRequestAsync(OutputCachingContext context) { - _varyBy?.Invoke(context.CachedVaryByRules); + _varyBy?.Invoke(context.HttpContext, context.CachedVaryByRules); - return _varyByAsync?.Invoke(context.CachedVaryByRules) ?? Task.CompletedTask; + return _varyByAsync?.Invoke(context.HttpContext, context.CachedVaryByRules) ?? Task.CompletedTask; } /// diff --git a/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt b/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt index 6bc96960f0e2..c1ddfc47e3a5 100644 --- a/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt @@ -61,7 +61,6 @@ Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Add(Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy! policy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Add(System.Type! policyType) -> void Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Add() -> void -Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Build() -> Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Clear() -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Expire(System.TimeSpan expiration) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Lock(bool lockResponse = true) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! @@ -73,10 +72,10 @@ Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Policy(string! profi Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Tag(params string![]! tags) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByHeader(params string![]! headers) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByQuery(params string![]! queryKeys) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! -Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.Func<(string!, string!)>! varyBy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! -Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.Func!>! varyBy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! -Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.Func!>! varyBy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! -Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.Func! varyBy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.Func! varyBy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.Func!>! varyBy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.Func!>! varyBy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.Func! varyBy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.WithCondition(System.Func!>! predicate) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.WithMethod(params string![]! methods) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.WithMethod(string! method) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! diff --git a/src/Mvc/Mvc.Core/src/OutputCacheAttribute.cs b/src/Mvc/Mvc.Core/src/OutputCacheAttribute.cs index 0a40bbe0fc59..d9c7549bd5a6 100644 --- a/src/Mvc/Mvc.Core/src/OutputCacheAttribute.cs +++ b/src/Mvc/Mvc.Core/src/OutputCacheAttribute.cs @@ -82,6 +82,6 @@ private IOutputCachingPolicy GetPolicy() builder.Expire(TimeSpan.FromSeconds(_duration.Value)); } - return builder.Build(); + return builder; } } From 8d54293cd122e36a9d0d92a71c03ef8aaa2988b3 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Wed, 15 Jun 2022 09:40:39 -0700 Subject: [PATCH 40/48] Reduce public API --- .../samples/OutputCachingSample/Startup.cs | 15 +- ...chingFeature.cs => IOutputCacheFeature.cs} | 8 +- ...CachingPolicy.cs => IOutputCachePolicy.cs} | 14 +- .../IOutputCachingKeyProvider.cs | 4 +- .../src/IOutputCachingPolicyProvider.cs | 37 ----- .../OutputCaching/src/IPoliciesMetadata.cs | 15 -- .../src/{Interfaces => }/ISystemClock.cs | 0 ...sponse.cs => MemoryOutputCacheResponse.cs} | 2 +- .../src/Memory/MemoryOutputCacheStore.cs | 4 +- .../OutputCaching/src/OutputCacheAttribute.cs | 43 ++++-- ...achingContext.cs => OutputCacheContext.cs} | 25 +--- ...Extensions.cs => OutputCacheExtensions.cs} | 10 +- .../OutputCaching/src/OutputCacheFeature.cs | 18 +++ ...yProvider.cs => OutputCacheKeyProvider.cs} | 10 +- ...Middleware.cs => OutputCacheMiddleware.cs} | 63 ++++---- ...achingOptions.cs => OutputCacheOptions.cs} | 28 ++-- ...onsSetup.cs => OutputCacheOptionsSetup.cs} | 6 +- .../src/OutputCachePolicyBuilder.cs | 129 +++-------------- ...ovider.cs => OutputCachePolicyProvider.cs} | 36 +++-- ...ns.cs => OutputCacheServicesExtensions.cs} | 10 +- .../OutputCaching/src/OutputCachingFeature.cs | 16 --- .../src/Policies/CompositePolicy.cs | 12 +- .../src/Policies/DefaultOutputCachePolicy.cs | 10 +- ...eCachingPolicy.cs => EnableCachePolicy.cs} | 14 +- .../src/Policies/ExpirationPolicy.cs | 8 +- .../src/Policies/LockingPolicy.cs | 8 +- .../src/Policies/NoLookupPolicy.cs | 8 +- .../src/Policies/NoStorePolicy.cs | 8 +- .../src/Policies/PoliciesCollection.cs | 48 +------ .../src/Policies/PoliciesMetadata.cs | 14 -- .../src/Policies/PolicyExtensions.cs | 16 +-- .../src/Policies/PredicatePolicy.cs | 16 +-- .../src/Policies/ProfilePolicy.cs | 10 +- .../OutputCaching/src/Policies/TagsPolicy.cs | 8 +- .../OutputCaching/src/Policies/TypedPolicy.cs | 14 +- .../src/Policies/VaryByHeaderPolicy.cs | 8 +- .../src/Policies/VaryByQueryPolicy.cs | 8 +- .../src/Policies/VaryByValuePolicy.cs | 8 +- .../OutputCaching/src/PublicAPI.Unshipped.txt | 136 ++++++++---------- ...tCachingStream.cs => OutputCacheStream.cs} | 4 +- .../test/OutputCachingKeyProviderTests.cs | 4 +- .../test/OutputCachingMiddlewareTests.cs | 80 ++++------- .../test/OutputCachingPolicyProviderTests.cs | 68 ++++----- .../OutputCaching/test/OutputCachingTests.cs | 28 ++-- .../OutputCaching/test/TestUtils.cs | 79 +++------- .../Mvc.Core/src/Filters/OutputCacheFilter.cs | 7 +- src/Mvc/Mvc.Core/src/OutputCacheAttribute.cs | 44 ++++-- src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt | 5 +- 48 files changed, 445 insertions(+), 721 deletions(-) rename src/Middleware/OutputCaching/src/{IOutputCachingFeature.cs => IOutputCacheFeature.cs} (72%) rename src/Middleware/OutputCaching/src/{IOutputCachingPolicy.cs => IOutputCachePolicy.cs} (61%) rename src/Middleware/OutputCaching/src/{Interfaces => }/IOutputCachingKeyProvider.cs (72%) delete mode 100644 src/Middleware/OutputCaching/src/IOutputCachingPolicyProvider.cs delete mode 100644 src/Middleware/OutputCaching/src/IPoliciesMetadata.cs rename src/Middleware/OutputCaching/src/{Interfaces => }/ISystemClock.cs (100%) rename src/Middleware/OutputCaching/src/Memory/{MemoryCachedResponse.cs => MemoryOutputCacheResponse.cs} (91%) rename src/Middleware/OutputCaching/src/{OutputCachingContext.cs => OutputCacheContext.cs} (79%) rename src/Middleware/OutputCaching/src/{OutputCachingExtensions.cs => OutputCacheExtensions.cs} (53%) create mode 100644 src/Middleware/OutputCaching/src/OutputCacheFeature.cs rename src/Middleware/OutputCaching/src/{OutputCachingKeyProvider.cs => OutputCacheKeyProvider.cs} (94%) rename src/Middleware/OutputCaching/src/{OutputCachingMiddleware.cs => OutputCacheMiddleware.cs} (90%) rename src/Middleware/OutputCaching/src/{OutputCachingOptions.cs => OutputCacheOptions.cs} (73%) rename src/Middleware/OutputCaching/src/{OutputCachingOptionsSetup.cs => OutputCacheOptionsSetup.cs} (64%) rename src/Middleware/OutputCaching/src/{OutputCachingPolicyProvider.cs => OutputCachePolicyProvider.cs} (69%) rename src/Middleware/OutputCaching/src/{OutputCachingServicesExtensions.cs => OutputCacheServicesExtensions.cs} (86%) delete mode 100644 src/Middleware/OutputCaching/src/OutputCachingFeature.cs rename src/Middleware/OutputCaching/src/Policies/{EnableCachingPolicy.cs => EnableCachePolicy.cs} (53%) delete mode 100644 src/Middleware/OutputCaching/src/Policies/PoliciesMetadata.cs rename src/Middleware/OutputCaching/src/Streams/{OutputCachingStream.cs => OutputCacheStream.cs} (96%) diff --git a/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs b/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs index f0894b598ff2..7b2fb9dd982b 100644 --- a/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs +++ b/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs @@ -3,28 +3,27 @@ using System.Globalization; using Microsoft.AspNetCore.OutputCaching; -using Microsoft.AspNetCore.OutputCaching.Policies; var builder = WebApplication.CreateBuilder(args); builder.Services.AddOutputCaching(options => { // Define policies for all requests which are not configured per endpoint or per request - options.BasePolicies.AddPolicy(b => b.WithPathBase("/js").Expire(TimeSpan.FromDays(1))); - options.BasePolicies.AddPolicy(b => b.WithPathBase("/admin").NoStore()); + options.BasePolicies.AddPolicy(b => b.With(c => c.HttpContext.Request.Path.StartsWithSegments("/js")).Expire(TimeSpan.FromDays(1)).AddPolicy()); + options.BasePolicies.AddPolicy(b => b.With(c => c.HttpContext.Request.Path.StartsWithSegments("/js")).NoCache()); - options.AddPolicy("NoCache", b => b.WithPathBase("/wwwroot").Expire(TimeSpan.FromDays(1))); + options.AddPolicy("NoCache", b => b.NoCache()); }); var app = builder.Build(); -app.UseOutputCaching(); +app.UseOutputCache(); app.MapGet("/", Gravatar.WriteGravatar); app.MapGet("/cached", Gravatar.WriteGravatar).CacheOutput(); -app.MapGet("/nocache", Gravatar.WriteGravatar).CacheOutput(x => x.NoStore()); +app.MapGet("/nocache", Gravatar.WriteGravatar).CacheOutput(x => x.NoCache()); app.MapGet("/profile", Gravatar.WriteGravatar).CacheOutput(x => x.Policy("NoCache")); @@ -53,7 +52,7 @@ { await Task.Delay(1000); await context.Response.WriteAsync($"
{requests++}
"); -}).CacheOutput(p => p.Lock(false).Expire(TimeSpan.FromMilliseconds(1))); +}).CacheOutput(p => p.AllowLocking(false).Expire(TimeSpan.FromMilliseconds(1))); // Etag app.MapGet("/etag", async (context) => @@ -66,7 +65,7 @@ await Gravatar.WriteGravatar(context); - var cacheContext = context.Features.Get()?.Context; + var cacheContext = context.Features.Get()?.Context; }).CacheOutput(); diff --git a/src/Middleware/OutputCaching/src/IOutputCachingFeature.cs b/src/Middleware/OutputCaching/src/IOutputCacheFeature.cs similarity index 72% rename from src/Middleware/OutputCaching/src/IOutputCachingFeature.cs rename to src/Middleware/OutputCaching/src/IOutputCacheFeature.cs index fdeac3df4bc4..4722a86077ca 100644 --- a/src/Middleware/OutputCaching/src/IOutputCachingFeature.cs +++ b/src/Middleware/OutputCaching/src/IOutputCacheFeature.cs @@ -1,21 +1,23 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.AspNetCore.OutputCaching.Policies; + namespace Microsoft.AspNetCore.OutputCaching; /// /// A feature for configuring additional output cache options on the HTTP response. /// -public interface IOutputCachingFeature +public interface IOutputCacheFeature { /// /// Gets the caching context. /// - OutputCachingContext Context { get; } + OutputCacheContext Context { get; } // Remove /// /// Gets the policies. /// - List Policies { get; } + PoliciesCollection Policies { get; } } diff --git a/src/Middleware/OutputCaching/src/IOutputCachingPolicy.cs b/src/Middleware/OutputCaching/src/IOutputCachePolicy.cs similarity index 61% rename from src/Middleware/OutputCaching/src/IOutputCachingPolicy.cs rename to src/Middleware/OutputCaching/src/IOutputCachePolicy.cs index 5315714df392..17ebe8f20aac 100644 --- a/src/Middleware/OutputCaching/src/IOutputCachingPolicy.cs +++ b/src/Middleware/OutputCaching/src/IOutputCachePolicy.cs @@ -6,25 +6,25 @@ namespace Microsoft.AspNetCore.OutputCaching; /// /// An implementation of this interface can update how the current request is cached. /// -public interface IOutputCachingPolicy +public interface IOutputCachePolicy { /// - /// Updates the before the cache middleware is invoked. + /// Updates the before the cache middleware is invoked. /// At that point the cache middleware can still be enabled or disabled for the request. /// /// The current request's caching context. - Task OnRequestAsync(OutputCachingContext context); + Task OnRequestAsync(OutputCacheContext context); /// - /// Updates the before the cached response is used. + /// Updates the before the cached response is used. /// At that point the freshness of the cached response can be updated. /// /// The current request's caching context. - Task OnServeFromCacheAsync(OutputCachingContext context); + Task OnServeFromCacheAsync(OutputCacheContext context); /// - /// Updates the before the response is served and can be cached. + /// Updates the before the response is served and can be cached. /// At that point cacheability of the response can be updated. /// - Task OnServeResponseAsync(OutputCachingContext context); + Task OnServeResponseAsync(OutputCacheContext context); } diff --git a/src/Middleware/OutputCaching/src/Interfaces/IOutputCachingKeyProvider.cs b/src/Middleware/OutputCaching/src/IOutputCachingKeyProvider.cs similarity index 72% rename from src/Middleware/OutputCaching/src/Interfaces/IOutputCachingKeyProvider.cs rename to src/Middleware/OutputCaching/src/IOutputCachingKeyProvider.cs index 9d16e44b794e..c74d1217787b 100644 --- a/src/Middleware/OutputCaching/src/Interfaces/IOutputCachingKeyProvider.cs +++ b/src/Middleware/OutputCaching/src/IOutputCachingKeyProvider.cs @@ -8,7 +8,7 @@ internal interface IOutputCachingKeyProvider /// /// Create a key for storing cached responses. /// - /// The . + /// The . /// The created key. - string CreateStorageKey(OutputCachingContext context); + string CreateStorageKey(OutputCacheContext context); } diff --git a/src/Middleware/OutputCaching/src/IOutputCachingPolicyProvider.cs b/src/Middleware/OutputCaching/src/IOutputCachingPolicyProvider.cs deleted file mode 100644 index 8dc984a842bd..000000000000 --- a/src/Middleware/OutputCaching/src/IOutputCachingPolicyProvider.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.AspNetCore.Http; - -namespace Microsoft.AspNetCore.OutputCaching; - -/// -/// An implementation of this interface can update how the current request is cached. -/// -public interface IOutputCachingPolicyProvider -{ - /// - /// Returns wether the current request has any configured policy. - /// - /// The current instance. - /// True if the current request has a policy, false otherwise. - bool HasPolicies(HttpContext httpContext); - - /// - /// Determines whether the response caching logic should be attempted for the incoming HTTP request. - /// - /// The . - Task OnRequestAsync(OutputCachingContext context); - - /// - /// Determines whether the response retrieved from the response cache is fresh and can be served. - /// - /// The . - Task OnServeFromCacheAsync(OutputCachingContext context); - - /// - /// Determines whether the response can be cached for future requests. - /// - /// The . - Task OnServeResponseAsync(OutputCachingContext context); -} diff --git a/src/Middleware/OutputCaching/src/IPoliciesMetadata.cs b/src/Middleware/OutputCaching/src/IPoliciesMetadata.cs deleted file mode 100644 index 6ae242f879b5..000000000000 --- a/src/Middleware/OutputCaching/src/IPoliciesMetadata.cs +++ /dev/null @@ -1,15 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.AspNetCore.OutputCaching; - -/// -/// Represents policy metadata for an endpoint. -/// -public interface IPoliciesMetadata -{ - /// - /// Gets the policy. - /// - IOutputCachingPolicy Policy { get; } -} diff --git a/src/Middleware/OutputCaching/src/Interfaces/ISystemClock.cs b/src/Middleware/OutputCaching/src/ISystemClock.cs similarity index 100% rename from src/Middleware/OutputCaching/src/Interfaces/ISystemClock.cs rename to src/Middleware/OutputCaching/src/ISystemClock.cs diff --git a/src/Middleware/OutputCaching/src/Memory/MemoryCachedResponse.cs b/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheResponse.cs similarity index 91% rename from src/Middleware/OutputCaching/src/Memory/MemoryCachedResponse.cs rename to src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheResponse.cs index d76c305b2615..09cbfca30bc7 100644 --- a/src/Middleware/OutputCaching/src/Memory/MemoryCachedResponse.cs +++ b/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheResponse.cs @@ -5,7 +5,7 @@ namespace Microsoft.AspNetCore.OutputCaching.Memory; -internal sealed class MemoryCachedResponse +internal sealed class MemoryOutputCacheResponse { public DateTimeOffset Created { get; set; } diff --git a/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs b/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs index 9d58d6b55178..43549d6f054b 100644 --- a/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs +++ b/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs @@ -35,7 +35,7 @@ public ValueTask EvictByTagAsync(string tag, CancellationToken token) { var entry = _cache.Get(key); - if (entry is MemoryCachedResponse memoryCachedResponse) + if (entry is MemoryOutputCacheResponse memoryCachedResponse) { var outputCacheEntry = new OutputCacheEntry { @@ -73,7 +73,7 @@ public ValueTask SetAsync(string key, OutputCacheEntry cachedResponse, TimeSpan _cache.Set( key, - new MemoryCachedResponse + new MemoryOutputCacheResponse { Created = cachedResponse.Created, StatusCode = cachedResponse.StatusCode, diff --git a/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs b/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs index 862d7865ff51..2ac31b487b19 100644 --- a/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs @@ -7,15 +7,15 @@ namespace Microsoft.AspNetCore.OutputCaching; /// Specifies the parameters necessary for setting appropriate headers in output caching. ///
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] -public class OutputCacheAttribute : Attribute, IPoliciesMetadata +public class OutputCacheAttribute : Attribute, IOutputCachePolicy { // A nullable-int cannot be used as an Attribute parameter. // Hence this nullable-int is present to back the Duration property. // The same goes for nullable-ResponseCacheLocation and nullable-bool. private int? _duration; - private bool? _noStore; + private bool? _noCache; - private IOutputCachingPolicy? _policy; + private IOutputCachePolicy? _policy; /// /// Gets or sets the duration in seconds for which the response is cached. @@ -27,13 +27,13 @@ public int Duration } /// - /// Gets or sets the value which determines whether the data should be stored or not. + /// Gets or sets the value which determines whether the reponse should be cached or not. /// When set to , the response won't be cached. /// - public bool NoStore + public bool NoCache { - get => _noStore ?? false; - set => _noStore = value; + get => _noCache ?? false; + set => _noCache = value; } /// @@ -57,16 +57,33 @@ public bool NoStore /// public string? PolicyName { get; set; } - /// - public IOutputCachingPolicy Policy => _policy ??= GetPolicy(); + Task IOutputCachePolicy.OnRequestAsync(OutputCacheContext context) + { + return GetPolicy().OnRequestAsync(context); + } + + Task IOutputCachePolicy.OnServeFromCacheAsync(OutputCacheContext context) + { + return GetPolicy().OnServeFromCacheAsync(context); + } - private IOutputCachingPolicy GetPolicy() + Task IOutputCachePolicy.OnServeResponseAsync(OutputCacheContext context) { + return GetPolicy().OnServeResponseAsync(context); + } + + private IOutputCachePolicy GetPolicy() + { + if (_policy != null) + { + return _policy; + } + var builder = new OutputCachePolicyBuilder(); - if (_noStore != null && _noStore.Value) + if (_noCache != null && _noCache.Value) { - builder.NoStore(); + builder.NoCache(); } if (PolicyName != null) @@ -84,6 +101,6 @@ private IOutputCachingPolicy GetPolicy() builder.Expire(TimeSpan.FromSeconds(_duration.Value)); } - return builder.Build(); + return _policy = builder.Build(); } } diff --git a/src/Middleware/OutputCaching/src/OutputCachingContext.cs b/src/Middleware/OutputCaching/src/OutputCacheContext.cs similarity index 79% rename from src/Middleware/OutputCaching/src/OutputCachingContext.cs rename to src/Middleware/OutputCaching/src/OutputCacheContext.cs index 96fc67b72d91..8c8406f8d897 100644 --- a/src/Middleware/OutputCaching/src/OutputCachingContext.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheContext.cs @@ -11,9 +11,9 @@ namespace Microsoft.AspNetCore.OutputCaching; /// /// Represent the current caching context for the request. /// -public sealed class OutputCachingContext +public sealed class OutputCacheContext { - internal OutputCachingContext(HttpContext httpContext, IOutputCacheStore store, OutputCachingOptions options, ILogger logger) + internal OutputCacheContext(HttpContext httpContext, IOutputCacheStore store, OutputCacheOptions options, ILogger logger) { HttpContext = httpContext; Logger = logger; @@ -61,21 +61,6 @@ internal OutputCachingContext(HttpContext httpContext, IOutputCacheStore store, /// public HashSet Tags { get; } = new(); - /// - /// Gets the logger. - /// - public ILogger Logger { get; } - - /// - /// Gets the options. - /// - public OutputCachingOptions Options { get; } - - /// - /// Gets the instance. - /// - public IOutputCacheStore Store { get; } - /// /// Gets or sets the amount of time the response should be cached for. /// @@ -95,5 +80,9 @@ internal OutputCachingContext(HttpContext httpContext, IOutputCacheStore store, internal Stream OriginalResponseStream { get; set; } - internal OutputCachingStream OutputCachingStream { get; set; } + internal OutputCacheStream OutputCachingStream { get; set; } + internal ILogger Logger { get; } + internal OutputCacheOptions Options { get; } + internal IOutputCacheStore Store { get; } + } diff --git a/src/Middleware/OutputCaching/src/OutputCachingExtensions.cs b/src/Middleware/OutputCaching/src/OutputCacheExtensions.cs similarity index 53% rename from src/Middleware/OutputCaching/src/OutputCachingExtensions.cs rename to src/Middleware/OutputCaching/src/OutputCacheExtensions.cs index 2b2c336b9f91..fba63af35ec4 100644 --- a/src/Middleware/OutputCaching/src/OutputCachingExtensions.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheExtensions.cs @@ -6,18 +6,18 @@ namespace Microsoft.AspNetCore.Builder; /// -/// Extension methods for adding the to an application. +/// Extension methods for adding the to an application. /// -public static class OutputCachingExtensions +public static class OutputCacheExtensions { /// - /// Adds the for caching HTTP responses. + /// Adds the for caching HTTP responses. /// /// The . - public static IApplicationBuilder UseOutputCaching(this IApplicationBuilder app) + public static IApplicationBuilder UseOutputCache(this IApplicationBuilder app) { ArgumentNullException.ThrowIfNull(app); - return app.UseMiddleware(); + return app.UseMiddleware(); } } diff --git a/src/Middleware/OutputCaching/src/OutputCacheFeature.cs b/src/Middleware/OutputCaching/src/OutputCacheFeature.cs new file mode 100644 index 000000000000..b774e928feee --- /dev/null +++ b/src/Middleware/OutputCaching/src/OutputCacheFeature.cs @@ -0,0 +1,18 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.OutputCaching.Policies; + +namespace Microsoft.AspNetCore.OutputCaching; + +internal class OutputCacheFeature : IOutputCacheFeature +{ + public OutputCacheFeature(OutputCacheContext context) + { + Context = context; + } + + public OutputCacheContext Context { get; } + + public PoliciesCollection Policies { get; } = new(); +} diff --git a/src/Middleware/OutputCaching/src/OutputCachingKeyProvider.cs b/src/Middleware/OutputCaching/src/OutputCacheKeyProvider.cs similarity index 94% rename from src/Middleware/OutputCaching/src/OutputCachingKeyProvider.cs rename to src/Middleware/OutputCaching/src/OutputCacheKeyProvider.cs index 40a21e7e26a7..04705fd80fed 100644 --- a/src/Middleware/OutputCaching/src/OutputCachingKeyProvider.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheKeyProvider.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.OutputCaching; -internal sealed class OutputCachingKeyProvider : IOutputCachingKeyProvider +internal sealed class OutputCacheKeyProvider : IOutputCachingKeyProvider { // Use the record separator for delimiting components of the cache key to avoid possible collisions private const char KeyDelimiter = '\x1e'; @@ -17,9 +17,9 @@ internal sealed class OutputCachingKeyProvider : IOutputCachingKeyProvider private const char KeySubDelimiter = '\x1f'; private readonly ObjectPool _builderPool; - private readonly OutputCachingOptions _options; + private readonly OutputCacheOptions _options; - internal OutputCachingKeyProvider(ObjectPoolProvider poolProvider, IOptions options) + internal OutputCacheKeyProvider(ObjectPoolProvider poolProvider, IOptions options) { ArgumentNullException.ThrowIfNull(poolProvider); ArgumentNullException.ThrowIfNull(options); @@ -29,14 +29,14 @@ internal OutputCachingKeyProvider(ObjectPoolProvider poolProvider, IOptionsSCHEMEHOST:PORT/PATHBASE/PATHHHeaderName=HeaderValueQQueryName=QueryValue1QueryValue2 - public string CreateStorageKey(OutputCachingContext context) + public string CreateStorageKey(OutputCacheContext context) { ArgumentNullException.ThrowIfNull(_builderPool); var varyByRules = context.CachedVaryByRules; if (varyByRules == null) { - throw new InvalidOperationException($"{nameof(CachedVaryByRules)} must not be null on the {nameof(OutputCachingContext)}"); + throw new InvalidOperationException($"{nameof(CachedVaryByRules)} must not be null on the {nameof(OutputCacheContext)}"); } var request = context.HttpContext.Request; diff --git a/src/Middleware/OutputCaching/src/OutputCachingMiddleware.cs b/src/Middleware/OutputCaching/src/OutputCacheMiddleware.cs similarity index 90% rename from src/Middleware/OutputCaching/src/OutputCachingMiddleware.cs rename to src/Middleware/OutputCaching/src/OutputCacheMiddleware.cs index 02710cdf5550..f9c00606e1f9 100644 --- a/src/Middleware/OutputCaching/src/OutputCachingMiddleware.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheMiddleware.cs @@ -14,32 +14,32 @@ namespace Microsoft.AspNetCore.OutputCaching; /// /// Enable HTTP response caching. /// -public class OutputCachingMiddleware +internal class OutputCacheMiddleware { // see https://tools.ietf.org/html/rfc7232#section-4.1 private static readonly string[] HeadersToIncludeIn304 = new[] { "Cache-Control", "Content-Location", "Date", "ETag", "Expires", "Vary" }; private readonly RequestDelegate _next; - private readonly OutputCachingOptions _options; + private readonly OutputCacheOptions _options; private readonly ILogger _logger; - private readonly IOutputCachingPolicyProvider _policyProvider; + private readonly OutputCachePolicyProvider _policyProvider; private readonly IOutputCacheStore _store; private readonly IOutputCachingKeyProvider _keyProvider; private readonly WorkDispatcher _outputCacheEntryDispatcher; private readonly WorkDispatcher _requestDispatcher; /// - /// Creates a new . + /// Creates a new . /// /// The representing the next middleware in the pipeline. /// The options for this middleware. /// The used for logging. /// The store. /// The used for creating instances. - public OutputCachingMiddleware( + public OutputCacheMiddleware( RequestDelegate next, - IOptions options, + IOptions options, ILoggerFactory loggerFactory, IOutputCacheStore outputCache, ObjectPoolProvider poolProvider @@ -48,35 +48,32 @@ ObjectPoolProvider poolProvider next, options, loggerFactory, - new OutputCachingPolicyProvider(options), outputCache, - new OutputCachingKeyProvider(poolProvider, options)) + new OutputCacheKeyProvider(poolProvider, options)) { } // for testing - internal OutputCachingMiddleware( + internal OutputCacheMiddleware( RequestDelegate next, - IOptions options, + IOptions options, ILoggerFactory loggerFactory, - IOutputCachingPolicyProvider policyProvider, IOutputCacheStore cache, IOutputCachingKeyProvider keyProvider) { ArgumentNullException.ThrowIfNull(next); ArgumentNullException.ThrowIfNull(options); ArgumentNullException.ThrowIfNull(loggerFactory); - ArgumentNullException.ThrowIfNull(policyProvider); ArgumentNullException.ThrowIfNull(cache); ArgumentNullException.ThrowIfNull(keyProvider); _next = next; _options = options.Value; - _logger = loggerFactory.CreateLogger(); - _policyProvider = policyProvider; + _logger = loggerFactory.CreateLogger(); _store = cache; _keyProvider = keyProvider; _outputCacheEntryDispatcher = new(); _requestDispatcher = new(); + _policyProvider = new(_options); } /// @@ -93,10 +90,10 @@ public async Task Invoke(HttpContext httpContext) return; } - var context = new OutputCachingContext(httpContext, _store, _options, _logger); + var context = new OutputCacheContext(httpContext, _store, _options, _logger); - // Add IOutputCachingFeature - AddOutputCachingFeature(context); + // Add IOutputCacheFeature + AddOutputCacheFeature(context); try { @@ -182,7 +179,7 @@ public async Task Invoke(HttpContext httpContext) } } - internal async Task TryServeCachedResponseAsync(OutputCachingContext context, OutputCacheEntry? cacheEntry) + internal async Task TryServeCachedResponseAsync(OutputCacheContext context, OutputCacheEntry? cacheEntry) { if (cacheEntry == null) { @@ -262,7 +259,7 @@ internal async Task TryServeCachedResponseAsync(OutputCachingContext conte return false; } - internal async Task TryServeFromCacheAsync(OutputCachingContext cacheContext) + internal async Task TryServeFromCacheAsync(OutputCacheContext cacheContext) { CreateCacheKey(cacheContext); @@ -288,7 +285,7 @@ internal async Task TryServeFromCacheAsync(OutputCachingContext cacheConte return false; } - internal void CreateCacheKey(OutputCachingContext context) + internal void CreateCacheKey(OutputCacheContext context) { if (!string.IsNullOrEmpty(context.CacheKey)) { @@ -331,7 +328,7 @@ internal void CreateCacheKey(OutputCachingContext context) /// Finalize cache headers. /// /// - internal void FinalizeCacheHeaders(OutputCachingContext context) + internal void FinalizeCacheHeaders(OutputCacheContext context) { if (context.AllowCacheStorage) { @@ -370,7 +367,7 @@ internal void FinalizeCacheHeaders(OutputCachingContext context) /// /// Stores the response body /// - internal async ValueTask FinalizeCacheBodyAsync(OutputCachingContext context) + internal async ValueTask FinalizeCacheBodyAsync(OutputCacheContext context) { if (context.AllowCacheStorage && context.OutputCachingStream.BufferingEnabled) { @@ -416,7 +413,7 @@ internal async ValueTask FinalizeCacheBodyAsync(OutputCachingContext context) /// /// /// true if the response was not started before this call; otherwise false. - private bool OnStartResponse(OutputCachingContext context) + private bool OnStartResponse(OutputCacheContext context) { if (!context.ResponseStarted) { @@ -428,7 +425,7 @@ private bool OnStartResponse(OutputCachingContext context) return false; } - internal void StartResponse(OutputCachingContext context) + internal void StartResponse(OutputCacheContext context) { if (OnStartResponse(context)) { @@ -436,21 +433,21 @@ internal void StartResponse(OutputCachingContext context) } } - internal static void AddOutputCachingFeature(OutputCachingContext context) + internal static void AddOutputCacheFeature(OutputCacheContext context) { - if (context.HttpContext.Features.Get() != null) + if (context.HttpContext.Features.Get() != null) { - throw new InvalidOperationException($"Another instance of {nameof(OutputCachingFeature)} already exists. Only one instance of {nameof(OutputCachingMiddleware)} can be configured for an application."); + throw new InvalidOperationException($"Another instance of {nameof(OutputCacheFeature)} already exists. Only one instance of {nameof(OutputCacheMiddleware)} can be configured for an application."); } - context.HttpContext.Features.Set(new OutputCachingFeature(context)); + context.HttpContext.Features.Set(new OutputCacheFeature(context)); } - internal void ShimResponseStream(OutputCachingContext context) + internal void ShimResponseStream(OutputCacheContext context) { // Shim response stream context.OriginalResponseStream = context.HttpContext.Response.Body; - context.OutputCachingStream = new OutputCachingStream( + context.OutputCachingStream = new OutputCacheStream( context.OriginalResponseStream, _options.MaximumBodySize, StreamUtilities.BodySegmentSize, @@ -459,9 +456,9 @@ internal void ShimResponseStream(OutputCachingContext context) } internal static void RemoveOutputCachingFeature(HttpContext context) => - context.Features.Set(null); + context.Features.Set(null); - internal static void UnshimResponseStream(OutputCachingContext context) + internal static void UnshimResponseStream(OutputCacheContext context) { // Unshim response stream context.HttpContext.Response.Body = context.OriginalResponseStream; @@ -470,7 +467,7 @@ internal static void UnshimResponseStream(OutputCachingContext context) RemoveOutputCachingFeature(context.HttpContext); } - internal static bool ContentIsNotModified(OutputCachingContext context) + internal static bool ContentIsNotModified(OutputCacheContext context) { var cachedResponseHeaders = context.CachedResponse.Headers; var ifNoneMatchHeader = context.HttpContext.Request.Headers.IfNoneMatch; diff --git a/src/Middleware/OutputCaching/src/OutputCachingOptions.cs b/src/Middleware/OutputCaching/src/OutputCacheOptions.cs similarity index 73% rename from src/Middleware/OutputCaching/src/OutputCachingOptions.cs rename to src/Middleware/OutputCaching/src/OutputCacheOptions.cs index 9a25acfa8804..5006cc0a9da1 100644 --- a/src/Middleware/OutputCaching/src/OutputCachingOptions.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheOptions.cs @@ -7,18 +7,10 @@ namespace Microsoft.AspNetCore.OutputCaching; /// -/// Options for configuring the . +/// Options for configuring the . /// -public class OutputCachingOptions +public class OutputCacheOptions { - /// - /// Creates a new instance. - /// - public OutputCachingOptions() - { - BasePolicies = new(this); - } - /// /// The size limit for the output cache middleware in bytes. The default is set to 100 MB. /// When this limit is exceeded, no new responses will be cached until older entries are @@ -28,7 +20,7 @@ public OutputCachingOptions() /// /// The largest cacheable size for the response body in bytes. The default is set to 64 MB. - /// If the response body exceeds this limit, it will not be cached by the . + /// If the response body exceeds this limit, it will not be cached by the . /// public long MaximumBodySize { get; set; } = 64 * 1024 * 1024; @@ -45,14 +37,14 @@ public OutputCachingOptions() /// /// Gets the policy applied to all requests. /// - public PoliciesCollection BasePolicies { get; internal set; } + public PoliciesCollection BasePolicies { get; internal set; } = new(); /// /// Gets the application . /// public IServiceProvider ApplicationServices { get; set; } = default!; - internal IDictionary? NamedPolicies { get; set; } + internal IDictionary? NamedPolicies { get; set; } /// /// For testing purposes only. @@ -61,18 +53,18 @@ public OutputCachingOptions() internal ISystemClock SystemClock { get; set; } = new SystemClock(); /// - /// Defines a which can be referenced by name. + /// Defines a which can be referenced by name. /// /// The name of the policy. /// The policy to add - public void AddPolicy(string name, IOutputCachingPolicy policy) + public void AddPolicy(string name, IOutputCachePolicy policy) { - NamedPolicies ??= new Dictionary(StringComparer.OrdinalIgnoreCase); + NamedPolicies ??= new Dictionary(StringComparer.OrdinalIgnoreCase); NamedPolicies[name] = policy; } /// - /// Defines a which can be referenced by name. + /// Defines a which can be referenced by name. /// /// The name of the policy. /// an action on . @@ -80,7 +72,7 @@ public void AddPolicy(string name, Action build) { var builder = new OutputCachePolicyBuilder(); build(builder); - NamedPolicies ??= new Dictionary(StringComparer.OrdinalIgnoreCase); + NamedPolicies ??= new Dictionary(StringComparer.OrdinalIgnoreCase); NamedPolicies[name] = builder.Build(); } } diff --git a/src/Middleware/OutputCaching/src/OutputCachingOptionsSetup.cs b/src/Middleware/OutputCaching/src/OutputCacheOptionsSetup.cs similarity index 64% rename from src/Middleware/OutputCaching/src/OutputCachingOptionsSetup.cs rename to src/Middleware/OutputCaching/src/OutputCacheOptionsSetup.cs index 2e57df7abce3..76353e2d798a 100644 --- a/src/Middleware/OutputCaching/src/OutputCachingOptionsSetup.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheOptionsSetup.cs @@ -5,16 +5,16 @@ namespace Microsoft.AspNetCore.OutputCaching; -internal sealed class OutputCachingOptionsSetup : IConfigureOptions +internal sealed class OutputCacheOptionsSetup : IConfigureOptions { private readonly IServiceProvider _services; - public OutputCachingOptionsSetup(IServiceProvider services) + public OutputCacheOptionsSetup(IServiceProvider services) { _services = services; } - public void Configure(OutputCachingOptions options) + public void Configure(OutputCacheOptions options) { options.ApplicationServices = _services; } diff --git a/src/Middleware/OutputCaching/src/OutputCachePolicyBuilder.cs b/src/Middleware/OutputCaching/src/OutputCachePolicyBuilder.cs index f6d1b7640598..390501b1d6be 100644 --- a/src/Middleware/OutputCaching/src/OutputCachePolicyBuilder.cs +++ b/src/Middleware/OutputCaching/src/OutputCachePolicyBuilder.cs @@ -11,13 +11,13 @@ namespace Microsoft.AspNetCore.OutputCaching; /// /// Provides helper methods to create custom policies. /// -public sealed class OutputCachePolicyBuilder : IOutputCachingPolicy +public sealed class OutputCachePolicyBuilder : IOutputCachePolicy { private const DynamicallyAccessedMemberTypes ActivatorAccessibility = DynamicallyAccessedMemberTypes.PublicConstructors; - private IOutputCachingPolicy? _builtPolicy; - private readonly List _policies = new(); - private List>>? _requirements; + private IOutputCachePolicy? _builtPolicy; + private readonly List _policies = new(); + private List>>? _requirements; /// /// Creates a new instance. @@ -31,7 +31,7 @@ public OutputCachePolicyBuilder() /// /// Adds a policy instance. /// - public OutputCachePolicyBuilder Add(IOutputCachingPolicy policy) + public OutputCachePolicyBuilder AddPolicy(IOutputCachePolicy policy) { _builtPolicy = null; _policies.Add(policy); @@ -42,7 +42,7 @@ public OutputCachePolicyBuilder Add(IOutputCachingPolicy policy) /// Adds a requirement to the current policy. /// /// The predicate applied to the policy. - public OutputCachePolicyBuilder WithCondition(Func> predicate) + public OutputCachePolicyBuilder With(Func> predicate) { ArgumentNullException.ThrowIfNull(predicate); @@ -53,76 +53,16 @@ public OutputCachePolicyBuilder WithCondition(Func - /// Adds a requirement to the current policy based on the request path. - /// - /// The base path to limit the policy to. - public OutputCachePolicyBuilder WithPathBase(PathString pathBase) - { - ArgumentNullException.ThrowIfNull(pathBase); - - _builtPolicy = null; - _requirements ??= new(); - _requirements.Add(context => - { - var match = context.HttpContext.Request.Path.StartsWithSegments(pathBase); - return Task.FromResult(match); - }); - return this; - } - - /// - /// Adds a requirement to the current policy based on the request path. - /// - /// The base paths to limit the policy to. - public OutputCachePolicyBuilder WithPathBase(params PathString[] pathBases) - { - ArgumentNullException.ThrowIfNull(pathBases); - - _builtPolicy = null; - _requirements ??= new(); - _requirements.Add(context => - { - var match = pathBases.Any(x => context.HttpContext.Request.Path.StartsWithSegments(x)); - return Task.FromResult(match); - }); - return this; - } - - /// - /// Adds a requirement to the current policy based on the request method. - /// - /// The method to limit the policy to. - public OutputCachePolicyBuilder WithMethod(string method) - { - ArgumentNullException.ThrowIfNull(method); - - _builtPolicy = null; - _requirements ??= new(); - _requirements.Add(context => - { - var upperMethod = method.ToUpperInvariant(); - var match = context.HttpContext.Request.Method.ToUpperInvariant() == upperMethod; - return Task.FromResult(match); - }); - return this; - } - - /// - /// Adds a requirement to the current policy based on the request method. + /// Adds a requirement to the current policy. /// - /// The methods to limit the policy to. - public OutputCachePolicyBuilder WithMethod(params string[] methods) + /// The predicate applied to the policy. + public OutputCachePolicyBuilder With(Func predicate) { - ArgumentNullException.ThrowIfNull(methods); + ArgumentNullException.ThrowIfNull(predicate); _builtPolicy = null; _requirements ??= new(); - _requirements.Add(context => - { - var upperMethods = methods.Select(m => m.ToUpperInvariant()).ToArray(); - var match = methods.Any(m => context.HttpContext.Request.Method.ToUpperInvariant() == m); - return Task.FromResult(match); - }); + _requirements.Add(c => Task.FromResult(predicate(c))); return this; } @@ -248,7 +188,7 @@ public OutputCachePolicyBuilder Expire(TimeSpan expiration) /// Adds a policy to change the request locking strategy. /// /// Whether the request should be locked. - public OutputCachePolicyBuilder Lock(bool lockResponse = true) + public OutputCachePolicyBuilder AllowLocking(bool lockResponse = true) { _builtPolicy = null; _policies.Add(lockResponse ? LockingPolicy.Enabled : LockingPolicy.Disabled); @@ -269,32 +209,6 @@ public OutputCachePolicyBuilder Clear() return this; } - /// - /// Adds a policy to prevent the response from being cached automatically. - /// - /// - /// The cache key will still be computed for lookups. - /// - public OutputCachePolicyBuilder NoStore() - { - _builtPolicy = null; - _policies.Add(NoStorePolicy.Instance); - return this; - } - - /// - /// Adds a policy to prevent the response from being served from cached automatically. - /// - /// - /// The cache key will still be computed for storage. - /// - public OutputCachePolicyBuilder NoLookup() - { - _builtPolicy = null; - _policies.Add(NoLookupPolicy.Instance); - return this; - } - /// /// Clears the policies and adds one preventing any caching logic to happen. /// @@ -305,11 +219,11 @@ public OutputCachePolicyBuilder NoCache() { _builtPolicy = null; _policies.Clear(); - _policies.Add(EnableCachingPolicy.Disabled); + _policies.Add(EnableCachePolicy.Disabled); return this; } - internal IOutputCachingPolicy Build() + internal IOutputCachePolicy Build() { if (_builtPolicy != null) { @@ -342,31 +256,32 @@ internal IOutputCachingPolicy Build() /// Adds a dynamically resolved policy. /// /// The type of policy to add - public void Add([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type policyType) + public OutputCachePolicyBuilder AddPolicy([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type policyType) { - Add(new TypedPolicy(policyType)); + AddPolicy(new TypedPolicy(policyType)); + return this; } /// /// Adds a dynamically resolved policy. /// /// The policy type. - public void Add<[DynamicallyAccessedMembers(ActivatorAccessibility)] T>() where T : IOutputCachingPolicy + public OutputCachePolicyBuilder AddPolicy<[DynamicallyAccessedMembers(ActivatorAccessibility)] T>() where T : IOutputCachePolicy { - Add(typeof(T)); + return AddPolicy(typeof(T)); } - Task IOutputCachingPolicy.OnRequestAsync(OutputCachingContext context) + Task IOutputCachePolicy.OnRequestAsync(OutputCacheContext context) { return Build().OnRequestAsync(context); } - Task IOutputCachingPolicy.OnServeFromCacheAsync(OutputCachingContext context) + Task IOutputCachePolicy.OnServeFromCacheAsync(OutputCacheContext context) { return Build().OnServeFromCacheAsync(context); } - Task IOutputCachingPolicy.OnServeResponseAsync(OutputCachingContext context) + Task IOutputCachePolicy.OnServeResponseAsync(OutputCacheContext context) { return Build().OnServeResponseAsync(context); } diff --git a/src/Middleware/OutputCaching/src/OutputCachingPolicyProvider.cs b/src/Middleware/OutputCaching/src/OutputCachePolicyProvider.cs similarity index 69% rename from src/Middleware/OutputCaching/src/OutputCachingPolicyProvider.cs rename to src/Middleware/OutputCaching/src/OutputCachePolicyProvider.cs index a0f2d2dd9024..af9635dd1eaa 100644 --- a/src/Middleware/OutputCaching/src/OutputCachingPolicyProvider.cs +++ b/src/Middleware/OutputCaching/src/OutputCachePolicyProvider.cs @@ -3,20 +3,18 @@ using System.Linq; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.OutputCaching; -internal sealed class OutputCachingPolicyProvider : IOutputCachingPolicyProvider +internal sealed class OutputCachePolicyProvider { - private readonly OutputCachingOptions _options; + private readonly OutputCacheOptions _options; - public OutputCachingPolicyProvider(IOptions options) + public OutputCachePolicyProvider(OutputCacheOptions options) { - _options = options.Value; + _options = options; } - // Not in interface public bool HasPolicies(HttpContext httpContext) { if (_options.BasePolicies != null) @@ -25,12 +23,12 @@ public bool HasPolicies(HttpContext httpContext) } // Remove check - if (httpContext.Features.Get()?.Policies.Any() ?? false) + if (httpContext.Features.Get()?.Policies.Any() ?? false) { return true; } - if (httpContext.GetEndpoint()?.Metadata.GetMetadata()?.Policy != null) + if (httpContext.GetEndpoint()?.Metadata.GetMetadata() != null) { return true; } @@ -38,7 +36,7 @@ public bool HasPolicies(HttpContext httpContext) return false; } - public async Task OnRequestAsync(OutputCachingContext context) + public async Task OnRequestAsync(OutputCacheContext context) { if (_options.BasePolicies != null) { @@ -48,7 +46,7 @@ public async Task OnRequestAsync(OutputCachingContext context) } } - var policiesMetadata = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata(); + var policiesMetadata = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata(); if (policiesMetadata != null) { @@ -59,11 +57,11 @@ public async Task OnRequestAsync(OutputCachingContext context) throw new InvalidOperationException("Can't define output caching policies after headers have been sent to client."); } - await policiesMetadata.Policy.OnRequestAsync(context); + await policiesMetadata.OnRequestAsync(context); } } - public async Task OnServeFromCacheAsync(OutputCachingContext context) + public async Task OnServeFromCacheAsync(OutputCacheContext context) { if (_options.BasePolicies != null) { @@ -75,7 +73,7 @@ public async Task OnServeFromCacheAsync(OutputCachingContext context) // Apply response policies defined on the feature, e.g. from action attributes - var responsePolicies = context.HttpContext.Features.Get()?.Policies; + var responsePolicies = context.HttpContext.Features.Get()?.Policies; if (responsePolicies != null) { @@ -85,15 +83,15 @@ public async Task OnServeFromCacheAsync(OutputCachingContext context) } } - var policiesMetadata = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata(); + var policiesMetadata = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata(); if (policiesMetadata != null) { - await policiesMetadata.Policy.OnServeFromCacheAsync(context); + await policiesMetadata.OnServeFromCacheAsync(context); } } - public async Task OnServeResponseAsync(OutputCachingContext context) + public async Task OnServeResponseAsync(OutputCacheContext context) { if (_options.BasePolicies != null) { @@ -105,7 +103,7 @@ public async Task OnServeResponseAsync(OutputCachingContext context) // Apply response policies defined on the feature, e.g. from action attributes - var responsePolicies = context.HttpContext.Features.Get()?.Policies; + var responsePolicies = context.HttpContext.Features.Get()?.Policies; if (responsePolicies != null) { @@ -115,11 +113,11 @@ public async Task OnServeResponseAsync(OutputCachingContext context) } } - var policiesMetadata = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata(); + var policiesMetadata = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata(); if (policiesMetadata != null) { - await policiesMetadata.Policy.OnServeResponseAsync(context); + await policiesMetadata.OnServeResponseAsync(context); } } } diff --git a/src/Middleware/OutputCaching/src/OutputCachingServicesExtensions.cs b/src/Middleware/OutputCaching/src/OutputCacheServicesExtensions.cs similarity index 86% rename from src/Middleware/OutputCaching/src/OutputCachingServicesExtensions.cs rename to src/Middleware/OutputCaching/src/OutputCacheServicesExtensions.cs index 3787b839aedf..1348d7766416 100644 --- a/src/Middleware/OutputCaching/src/OutputCachingServicesExtensions.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheServicesExtensions.cs @@ -13,7 +13,7 @@ namespace Microsoft.Extensions.DependencyInjection; /// /// Extension methods for the OutputCaching middleware. /// -public static class OutputCachingServicesExtensions +public static class OutputCacheServicesExtensions { /// /// Add output caching services. @@ -24,13 +24,13 @@ public static IServiceCollection AddOutputCaching(this IServiceCollection servic { ArgumentNullException.ThrowIfNull(services); - services.AddTransient, OutputCachingOptionsSetup>(); + services.AddTransient, OutputCacheOptionsSetup>(); services.TryAddSingleton(); services.TryAddSingleton(sp => { - var outputCacheOptions = sp.GetRequiredService>(); + var outputCacheOptions = sp.GetRequiredService>(); return new MemoryOutputCacheStore(new MemoryCache(new MemoryCacheOptions { SizeLimit = outputCacheOptions.Value.SizeLimit @@ -43,9 +43,9 @@ public static IServiceCollection AddOutputCaching(this IServiceCollection servic /// Add output caching services and configure the related options. /// /// The for adding services. - /// A delegate to configure the . + /// A delegate to configure the . /// - public static IServiceCollection AddOutputCaching(this IServiceCollection services, Action configureOptions) + public static IServiceCollection AddOutputCaching(this IServiceCollection services, Action configureOptions) { ArgumentNullException.ThrowIfNull(services); ArgumentNullException.ThrowIfNull(configureOptions); diff --git a/src/Middleware/OutputCaching/src/OutputCachingFeature.cs b/src/Middleware/OutputCaching/src/OutputCachingFeature.cs deleted file mode 100644 index 62ead74846fa..000000000000 --- a/src/Middleware/OutputCaching/src/OutputCachingFeature.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.AspNetCore.OutputCaching; - -internal class OutputCachingFeature : IOutputCachingFeature -{ - public OutputCachingFeature(OutputCachingContext context) - { - Context = context; - } - - public OutputCachingContext Context { get; } - - public List Policies { get; } = new(); -} diff --git a/src/Middleware/OutputCaching/src/Policies/CompositePolicy.cs b/src/Middleware/OutputCaching/src/Policies/CompositePolicy.cs index cf4e6c2fdd63..47420aacff20 100644 --- a/src/Middleware/OutputCaching/src/Policies/CompositePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/CompositePolicy.cs @@ -6,21 +6,21 @@ namespace Microsoft.AspNetCore.OutputCaching.Policies; /// /// A composite policy. /// -internal sealed class CompositePolicy : IOutputCachingPolicy +internal sealed class CompositePolicy : IOutputCachePolicy { - private readonly IOutputCachingPolicy[] _policies; + private readonly IOutputCachePolicy[] _policies; /// /// Creates a new instance of /// /// The policies to include. - public CompositePolicy(params IOutputCachingPolicy[] policies!!) + public CompositePolicy(params IOutputCachePolicy[] policies!!) { _policies = policies; } /// - async Task IOutputCachingPolicy.OnRequestAsync(OutputCachingContext context) + async Task IOutputCachePolicy.OnRequestAsync(OutputCacheContext context) { foreach (var policy in _policies) { @@ -29,7 +29,7 @@ async Task IOutputCachingPolicy.OnRequestAsync(OutputCachingContext context) } /// - async Task IOutputCachingPolicy.OnServeFromCacheAsync(OutputCachingContext context) + async Task IOutputCachePolicy.OnServeFromCacheAsync(OutputCacheContext context) { foreach (var policy in _policies) { @@ -38,7 +38,7 @@ async Task IOutputCachingPolicy.OnServeFromCacheAsync(OutputCachingContext conte } /// - async Task IOutputCachingPolicy.OnServeResponseAsync(OutputCachingContext context) + async Task IOutputCachePolicy.OnServeResponseAsync(OutputCacheContext context) { foreach (var policy in _policies) { diff --git a/src/Middleware/OutputCaching/src/Policies/DefaultOutputCachePolicy.cs b/src/Middleware/OutputCaching/src/Policies/DefaultOutputCachePolicy.cs index 8e84509e7a5b..96aaa273c1ad 100644 --- a/src/Middleware/OutputCaching/src/Policies/DefaultOutputCachePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/DefaultOutputCachePolicy.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.OutputCaching; /// /// A policy which caches un-authenticated, GET and HEAD, 200 responses. /// -internal sealed class DefaultOutputCachePolicy : IOutputCachingPolicy +internal sealed class DefaultOutputCachePolicy : IOutputCachePolicy { public static readonly DefaultOutputCachePolicy Instance = new(); @@ -18,7 +18,7 @@ private DefaultOutputCachePolicy() } /// - Task IOutputCachingPolicy.OnRequestAsync(OutputCachingContext context) + Task IOutputCachePolicy.OnRequestAsync(OutputCacheContext context) { var attemptOutputCaching = AttemptOutputCaching(context); context.EnableOutputCaching = true; @@ -33,13 +33,13 @@ Task IOutputCachingPolicy.OnRequestAsync(OutputCachingContext context) } /// - Task IOutputCachingPolicy.OnServeFromCacheAsync(OutputCachingContext context) + Task IOutputCachePolicy.OnServeFromCacheAsync(OutputCacheContext context) { return Task.CompletedTask; } /// - Task IOutputCachingPolicy.OnServeResponseAsync(OutputCachingContext context) + Task IOutputCachePolicy.OnServeResponseAsync(OutputCacheContext context) { var response = context.HttpContext.Response; @@ -62,7 +62,7 @@ Task IOutputCachingPolicy.OnServeResponseAsync(OutputCachingContext context) return Task.CompletedTask; } - private static bool AttemptOutputCaching(OutputCachingContext context) + private static bool AttemptOutputCaching(OutputCacheContext context) { // Check if the current request fulfisls the requirements to be cached diff --git a/src/Middleware/OutputCaching/src/Policies/EnableCachingPolicy.cs b/src/Middleware/OutputCaching/src/Policies/EnableCachePolicy.cs similarity index 53% rename from src/Middleware/OutputCaching/src/Policies/EnableCachingPolicy.cs rename to src/Middleware/OutputCaching/src/Policies/EnableCachePolicy.cs index 39ce03fc0c5d..920db57de904 100644 --- a/src/Middleware/OutputCaching/src/Policies/EnableCachingPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/EnableCachePolicy.cs @@ -6,17 +6,17 @@ namespace Microsoft.AspNetCore.OutputCaching; /// /// A policy that enables caching /// -internal sealed class EnableCachingPolicy : IOutputCachingPolicy +internal sealed class EnableCachePolicy : IOutputCachePolicy { - public static readonly EnableCachingPolicy Enabled = new(); - public static readonly EnableCachingPolicy Disabled = new(); + public static readonly EnableCachePolicy Enabled = new(); + public static readonly EnableCachePolicy Disabled = new(); - private EnableCachingPolicy() + private EnableCachePolicy() { } /// - Task IOutputCachingPolicy.OnRequestAsync(OutputCachingContext context) + Task IOutputCachePolicy.OnRequestAsync(OutputCacheContext context) { context.EnableOutputCaching = this == Enabled; @@ -24,13 +24,13 @@ Task IOutputCachingPolicy.OnRequestAsync(OutputCachingContext context) } /// - Task IOutputCachingPolicy.OnServeResponseAsync(OutputCachingContext context) + Task IOutputCachePolicy.OnServeResponseAsync(OutputCacheContext context) { return Task.CompletedTask; } /// - Task IOutputCachingPolicy.OnServeFromCacheAsync(OutputCachingContext context) + Task IOutputCachePolicy.OnServeFromCacheAsync(OutputCacheContext context) { return Task.CompletedTask; } diff --git a/src/Middleware/OutputCaching/src/Policies/ExpirationPolicy.cs b/src/Middleware/OutputCaching/src/Policies/ExpirationPolicy.cs index 2f9c3e3f74df..c5e453e16f5d 100644 --- a/src/Middleware/OutputCaching/src/Policies/ExpirationPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/ExpirationPolicy.cs @@ -6,7 +6,7 @@ namespace Microsoft.AspNetCore.OutputCaching; /// /// A policy that defines a custom expiration duration. /// -internal sealed class ExpirationPolicy : IOutputCachingPolicy +internal sealed class ExpirationPolicy : IOutputCachePolicy { private readonly TimeSpan _expiration; @@ -20,7 +20,7 @@ public ExpirationPolicy(TimeSpan expiration) } /// - Task IOutputCachingPolicy.OnRequestAsync(OutputCachingContext context) + Task IOutputCachePolicy.OnRequestAsync(OutputCacheContext context) { context.ResponseExpirationTimeSpan = _expiration; @@ -28,13 +28,13 @@ Task IOutputCachingPolicy.OnRequestAsync(OutputCachingContext context) } /// - Task IOutputCachingPolicy.OnServeFromCacheAsync(OutputCachingContext context) + Task IOutputCachePolicy.OnServeFromCacheAsync(OutputCacheContext context) { return Task.CompletedTask; } /// - Task IOutputCachingPolicy.OnServeResponseAsync(OutputCachingContext context) + Task IOutputCachePolicy.OnServeResponseAsync(OutputCacheContext context) { return Task.CompletedTask; } diff --git a/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs b/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs index 39639f0d7260..d0a3470021b7 100644 --- a/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs @@ -6,7 +6,7 @@ namespace Microsoft.AspNetCore.OutputCaching; /// /// A policy that changes the locking behavior. /// -internal sealed class LockingPolicy : IOutputCachingPolicy +internal sealed class LockingPolicy : IOutputCachePolicy { private readonly bool _lockResponse; @@ -26,7 +26,7 @@ private LockingPolicy(bool lockResponse) public static readonly LockingPolicy Disabled = new(false); /// - Task IOutputCachingPolicy.OnRequestAsync(OutputCachingContext context) + Task IOutputCachePolicy.OnRequestAsync(OutputCacheContext context) { context.AllowLocking = _lockResponse; @@ -34,13 +34,13 @@ Task IOutputCachingPolicy.OnRequestAsync(OutputCachingContext context) } /// - Task IOutputCachingPolicy.OnServeFromCacheAsync(OutputCachingContext context) + Task IOutputCachePolicy.OnServeFromCacheAsync(OutputCacheContext context) { return Task.CompletedTask; } /// - Task IOutputCachingPolicy.OnServeResponseAsync(OutputCachingContext context) + Task IOutputCachePolicy.OnServeResponseAsync(OutputCacheContext context) { return Task.CompletedTask; } diff --git a/src/Middleware/OutputCaching/src/Policies/NoLookupPolicy.cs b/src/Middleware/OutputCaching/src/Policies/NoLookupPolicy.cs index 40558f402ecd..85d0806d12f2 100644 --- a/src/Middleware/OutputCaching/src/Policies/NoLookupPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/NoLookupPolicy.cs @@ -6,7 +6,7 @@ namespace Microsoft.AspNetCore.OutputCaching; /// /// A policy that prevents the response from being served from cached. /// -internal class NoLookupPolicy : IOutputCachingPolicy +internal class NoLookupPolicy : IOutputCachePolicy { public static NoLookupPolicy Instance = new(); @@ -15,7 +15,7 @@ private NoLookupPolicy() } /// - Task IOutputCachingPolicy.OnServeResponseAsync(OutputCachingContext context) + Task IOutputCachePolicy.OnServeResponseAsync(OutputCacheContext context) { context.AllowCacheLookup = false; @@ -23,13 +23,13 @@ Task IOutputCachingPolicy.OnServeResponseAsync(OutputCachingContext context) } /// - Task IOutputCachingPolicy.OnServeFromCacheAsync(OutputCachingContext context) + Task IOutputCachePolicy.OnServeFromCacheAsync(OutputCacheContext context) { return Task.CompletedTask; } /// - Task IOutputCachingPolicy.OnRequestAsync(OutputCachingContext context) + Task IOutputCachePolicy.OnRequestAsync(OutputCacheContext context) { return Task.CompletedTask; } diff --git a/src/Middleware/OutputCaching/src/Policies/NoStorePolicy.cs b/src/Middleware/OutputCaching/src/Policies/NoStorePolicy.cs index 540f966a6840..79d05b0b456c 100644 --- a/src/Middleware/OutputCaching/src/Policies/NoStorePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/NoStorePolicy.cs @@ -6,7 +6,7 @@ namespace Microsoft.AspNetCore.OutputCaching; /// /// A policy that prevents the response from being cached. /// -internal class NoStorePolicy : IOutputCachingPolicy +internal class NoStorePolicy : IOutputCachePolicy { public static NoStorePolicy Instance = new(); @@ -15,7 +15,7 @@ private NoStorePolicy() } /// - Task IOutputCachingPolicy.OnServeResponseAsync(OutputCachingContext context) + Task IOutputCachePolicy.OnServeResponseAsync(OutputCacheContext context) { context.AllowCacheStorage = false; @@ -23,13 +23,13 @@ Task IOutputCachingPolicy.OnServeResponseAsync(OutputCachingContext context) } /// - Task IOutputCachingPolicy.OnServeFromCacheAsync(OutputCachingContext context) + Task IOutputCachePolicy.OnServeFromCacheAsync(OutputCacheContext context) { return Task.CompletedTask; } /// - Task IOutputCachingPolicy.OnRequestAsync(OutputCachingContext context) + Task IOutputCachePolicy.OnRequestAsync(OutputCacheContext context) { return Task.CompletedTask; } diff --git a/src/Middleware/OutputCaching/src/Policies/PoliciesCollection.cs b/src/Middleware/OutputCaching/src/Policies/PoliciesCollection.cs index 8b175f48f6dd..433a296c9b53 100644 --- a/src/Middleware/OutputCaching/src/Policies/PoliciesCollection.cs +++ b/src/Middleware/OutputCaching/src/Policies/PoliciesCollection.cs @@ -2,36 +2,25 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; using System.Linq; -using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.OutputCaching.Policies; /// /// A collection of policies. /// -public sealed class PoliciesCollection : IReadOnlyCollection +public sealed class PoliciesCollection : IReadOnlyCollection { - private const DynamicallyAccessedMemberTypes ActivatorAccessibility = DynamicallyAccessedMemberTypes.PublicConstructors; - - private readonly OutputCachingOptions _options; - private List? _policies; - - internal PoliciesCollection(OutputCachingOptions options) - { - _options = options; - } + private List? _policies; /// public int Count => _policies == null ? 0 : _policies.Count; /// - /// Adds an instance. + /// Adds an instance. /// /// The policy - public void AddPolicy(IOutputCachingPolicy policy) + public void AddPolicy(IOutputCachePolicy policy) { ArgumentNullException.ThrowIfNull(policy); @@ -40,30 +29,7 @@ public void AddPolicy(IOutputCachingPolicy policy) } /// - /// Adds a dynamically resolved instance. - /// - /// The type of policy to add. - public void AddPolicy([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type policyType) - { - if (ActivatorUtilities.GetServiceOrCreateInstance(_options.ApplicationServices, policyType) is not IOutputCachingPolicy policy) - { - throw new InvalidOperationException(string.Format(CultureInfo.CurrentCulture, Resources.Policy_InvalidType)); - } - - AddPolicy(policy); - } - - /// - /// Adds a dynamically resolved instance. - /// - /// The type of policy to add. - public void AddPolicy<[DynamicallyAccessedMembers(ActivatorAccessibility)] T>(string name) where T : IOutputCachingPolicy - { - AddPolicy(typeof(T)); - } - - /// - /// Adds instance. + /// Adds instance. /// public void AddPolicy(Action build) { @@ -81,9 +47,9 @@ public void Clear() } /// - public IEnumerator GetEnumerator() + public IEnumerator GetEnumerator() { - return _policies == null ? Enumerable.Empty().GetEnumerator() : _policies.GetEnumerator(); + return _policies == null ? Enumerable.Empty().GetEnumerator() : _policies.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() diff --git a/src/Middleware/OutputCaching/src/Policies/PoliciesMetadata.cs b/src/Middleware/OutputCaching/src/Policies/PoliciesMetadata.cs deleted file mode 100644 index 4990fd8138e6..000000000000 --- a/src/Middleware/OutputCaching/src/Policies/PoliciesMetadata.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.AspNetCore.OutputCaching.Policies; - -internal sealed class PoliciesMetadata : IPoliciesMetadata -{ - public PoliciesMetadata(IOutputCachingPolicy policy) - { - Policy = policy; - } - - public IOutputCachingPolicy Policy { get; } -} diff --git a/src/Middleware/OutputCaching/src/Policies/PolicyExtensions.cs b/src/Middleware/OutputCaching/src/Policies/PolicyExtensions.cs index 3da963fe28a7..b9022999803f 100644 --- a/src/Middleware/OutputCaching/src/Policies/PolicyExtensions.cs +++ b/src/Middleware/OutputCaching/src/Policies/PolicyExtensions.cs @@ -3,15 +3,13 @@ using Microsoft.AspNetCore.Builder; -namespace Microsoft.AspNetCore.OutputCaching.Policies; +namespace Microsoft.AspNetCore.OutputCaching; /// /// A set of endpoint extension methods. /// public static class PolicyExtensions { - private static readonly IOutputCachingPolicy _defaultEnabledPolicy = new OutputCachePolicyBuilder(); - /// /// Marks an endpoint to be cached with the default policy. /// @@ -20,11 +18,10 @@ public static TBuilder CacheOutput(this TBuilder builder) where TBuild ArgumentNullException.ThrowIfNull(builder); // Enable caching if this method is invoked on an endpoint, extra policies can disable it - var policiesMetadata = new PoliciesMetadata(_defaultEnabledPolicy); builder.Add(endpointBuilder => { - endpointBuilder.Metadata.Add(policiesMetadata); + endpointBuilder.Metadata.Add(DefaultOutputCachePolicy.Instance); }); return builder; } @@ -32,16 +29,15 @@ public static TBuilder CacheOutput(this TBuilder builder) where TBuild /// /// Marks an endpoint to be cached with the specified policy. /// - public static TBuilder CacheOutput(this TBuilder builder, IOutputCachingPolicy policy) where TBuilder : IEndpointConventionBuilder + public static TBuilder CacheOutput(this TBuilder builder, IOutputCachePolicy policy) where TBuilder : IEndpointConventionBuilder { ArgumentNullException.ThrowIfNull(builder); // Enable caching if this method is invoked on an endpoint, extra policies can disable it - var policiesMetadata = new PoliciesMetadata(new OutputCachePolicyBuilder().Add(policy).Build()); builder.Add(endpointBuilder => { - endpointBuilder.Metadata.Add(policiesMetadata); + endpointBuilder.Metadata.Add(policy); }); return builder; } @@ -57,11 +53,9 @@ public static TBuilder CacheOutput(this TBuilder builder, Action { - endpointBuilder.Metadata.Add(policiesMetadata); + endpointBuilder.Metadata.Add(outputCachePolicyBuilder.Build()); }); return builder; diff --git a/src/Middleware/OutputCaching/src/Policies/PredicatePolicy.cs b/src/Middleware/OutputCaching/src/Policies/PredicatePolicy.cs index e159f1fca2c1..3b2ce2997776 100644 --- a/src/Middleware/OutputCaching/src/Policies/PredicatePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/PredicatePolicy.cs @@ -6,43 +6,43 @@ namespace Microsoft.AspNetCore.OutputCaching.Policies; /// /// A policy that adds a requirement to another policy. /// -internal sealed class PredicatePolicy : IOutputCachingPolicy +internal sealed class PredicatePolicy : IOutputCachePolicy { // TODO: Accept a non async predicate too? - private readonly Func> _predicate; - private readonly IOutputCachingPolicy _policy; + private readonly Func> _predicate; + private readonly IOutputCachePolicy _policy; /// /// Creates a new instance. /// /// The predicate. /// The policy. - public PredicatePolicy(Func> predicate, IOutputCachingPolicy policy) + public PredicatePolicy(Func> predicate, IOutputCachePolicy policy) { _predicate = predicate; _policy = policy; } /// - Task IOutputCachingPolicy.OnRequestAsync(OutputCachingContext context) + Task IOutputCachePolicy.OnRequestAsync(OutputCacheContext context) { return ExecuteAwaited(static (policy, context) => policy.OnRequestAsync(context), _policy, context); } /// - Task IOutputCachingPolicy.OnServeFromCacheAsync(OutputCachingContext context) + Task IOutputCachePolicy.OnServeFromCacheAsync(OutputCacheContext context) { return ExecuteAwaited(static (policy, context) => policy.OnServeFromCacheAsync(context), _policy, context); } /// - Task IOutputCachingPolicy.OnServeResponseAsync(OutputCachingContext context) + Task IOutputCachePolicy.OnServeResponseAsync(OutputCacheContext context) { return ExecuteAwaited(static (policy, context) => policy.OnServeResponseAsync(context), _policy, context); } - private Task ExecuteAwaited(Func action, IOutputCachingPolicy policy, OutputCachingContext context) + private Task ExecuteAwaited(Func action, IOutputCachePolicy policy, OutputCacheContext context) { ArgumentNullException.ThrowIfNull(action); diff --git a/src/Middleware/OutputCaching/src/Policies/ProfilePolicy.cs b/src/Middleware/OutputCaching/src/Policies/ProfilePolicy.cs index 9b3aa635fb5b..a270a1c9d453 100644 --- a/src/Middleware/OutputCaching/src/Policies/ProfilePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/ProfilePolicy.cs @@ -6,7 +6,7 @@ namespace Microsoft.AspNetCore.OutputCaching; /// /// A policy represented by a named profile. /// -internal sealed class ProfilePolicy : IOutputCachingPolicy +internal sealed class ProfilePolicy : IOutputCachePolicy { private readonly string _profileName; @@ -20,7 +20,7 @@ public ProfilePolicy(string profileName) } /// - Task IOutputCachingPolicy.OnServeResponseAsync(OutputCachingContext context) + Task IOutputCachePolicy.OnServeResponseAsync(OutputCacheContext context) { var policy = GetProfilePolicy(context); @@ -33,7 +33,7 @@ Task IOutputCachingPolicy.OnServeResponseAsync(OutputCachingContext context) } /// - Task IOutputCachingPolicy.OnServeFromCacheAsync(OutputCachingContext context) + Task IOutputCachePolicy.OnServeFromCacheAsync(OutputCacheContext context) { var policy = GetProfilePolicy(context); @@ -46,7 +46,7 @@ Task IOutputCachingPolicy.OnServeFromCacheAsync(OutputCachingContext context) } /// - Task IOutputCachingPolicy.OnRequestAsync(OutputCachingContext context) + Task IOutputCachePolicy.OnRequestAsync(OutputCacheContext context) { var policy = GetProfilePolicy(context); @@ -58,7 +58,7 @@ Task IOutputCachingPolicy.OnRequestAsync(OutputCachingContext context) return policy.OnRequestAsync(context); ; } - internal IOutputCachingPolicy? GetProfilePolicy(OutputCachingContext context) + internal IOutputCachePolicy? GetProfilePolicy(OutputCacheContext context) { var policies = context.Options.NamedPolicies; diff --git a/src/Middleware/OutputCaching/src/Policies/TagsPolicy.cs b/src/Middleware/OutputCaching/src/Policies/TagsPolicy.cs index 3771ca3cd561..416cfde94405 100644 --- a/src/Middleware/OutputCaching/src/Policies/TagsPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/TagsPolicy.cs @@ -6,7 +6,7 @@ namespace Microsoft.AspNetCore.OutputCaching; /// /// A policy that defines custom tags on the cache entry. /// -internal sealed class TagsPolicy : IOutputCachingPolicy +internal sealed class TagsPolicy : IOutputCachePolicy { private readonly string[] _tags; @@ -20,7 +20,7 @@ public TagsPolicy(params string[] tags) } /// - Task IOutputCachingPolicy.OnRequestAsync(OutputCachingContext context) + Task IOutputCachePolicy.OnRequestAsync(OutputCacheContext context) { foreach (var tag in _tags) { @@ -31,13 +31,13 @@ Task IOutputCachingPolicy.OnRequestAsync(OutputCachingContext context) } /// - Task IOutputCachingPolicy.OnServeFromCacheAsync(OutputCachingContext context) + Task IOutputCachePolicy.OnServeFromCacheAsync(OutputCacheContext context) { return Task.CompletedTask; } /// - Task IOutputCachingPolicy.OnServeResponseAsync(OutputCachingContext context) + Task IOutputCachePolicy.OnServeResponseAsync(OutputCacheContext context) { return Task.CompletedTask; } diff --git a/src/Middleware/OutputCaching/src/Policies/TypedPolicy.cs b/src/Middleware/OutputCaching/src/Policies/TypedPolicy.cs index 3bc4a46b2f0d..d47219c2c621 100644 --- a/src/Middleware/OutputCaching/src/Policies/TypedPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/TypedPolicy.cs @@ -9,9 +9,9 @@ namespace Microsoft.AspNetCore.OutputCaching.Policies; /// /// A type base policy. /// -internal sealed class TypedPolicy : IOutputCachingPolicy +internal sealed class TypedPolicy : IOutputCachePolicy { - private IOutputCachingPolicy? _instance; + private IOutputCachePolicy? _instance; [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] private readonly Type _policyType; @@ -27,25 +27,25 @@ public TypedPolicy([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Pu _policyType = policyType; } - private IOutputCachingPolicy? CreatePolicy(OutputCachingContext context) + private IOutputCachePolicy? CreatePolicy(OutputCacheContext context) { - return _instance ??= ActivatorUtilities.CreateInstance(context.Options.ApplicationServices, _policyType) as IOutputCachingPolicy; + return _instance ??= ActivatorUtilities.CreateInstance(context.Options.ApplicationServices, _policyType) as IOutputCachePolicy; } /// - Task IOutputCachingPolicy.OnRequestAsync(OutputCachingContext context) + Task IOutputCachePolicy.OnRequestAsync(OutputCacheContext context) { return CreatePolicy(context)?.OnRequestAsync(context) ?? Task.CompletedTask; } /// - Task IOutputCachingPolicy.OnServeFromCacheAsync(OutputCachingContext context) + Task IOutputCachePolicy.OnServeFromCacheAsync(OutputCacheContext context) { return CreatePolicy(context)?.OnServeFromCacheAsync(context) ?? Task.CompletedTask; } /// - Task IOutputCachingPolicy.OnServeResponseAsync(OutputCachingContext context) + Task IOutputCachePolicy.OnServeResponseAsync(OutputCacheContext context) { return CreatePolicy(context)?.OnServeResponseAsync(context) ?? Task.CompletedTask; } diff --git a/src/Middleware/OutputCaching/src/Policies/VaryByHeaderPolicy.cs b/src/Middleware/OutputCaching/src/Policies/VaryByHeaderPolicy.cs index db6ee7e990cd..547e1054ddb0 100644 --- a/src/Middleware/OutputCaching/src/Policies/VaryByHeaderPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/VaryByHeaderPolicy.cs @@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.OutputCaching; /// /// When applied, the cached content will be different for every value of the provided headers. /// -internal sealed class VaryByHeaderPolicy : IOutputCachingPolicy +internal sealed class VaryByHeaderPolicy : IOutputCachePolicy { private StringValues _headers { get; set; } @@ -36,7 +36,7 @@ public VaryByHeaderPolicy(params string[] headers) } /// - Task IOutputCachingPolicy.OnRequestAsync(OutputCachingContext context) + Task IOutputCachePolicy.OnRequestAsync(OutputCacheContext context) { // No vary by header? if (_headers.Count == 0) @@ -51,13 +51,13 @@ Task IOutputCachingPolicy.OnRequestAsync(OutputCachingContext context) } /// - Task IOutputCachingPolicy.OnServeFromCacheAsync(OutputCachingContext context) + Task IOutputCachePolicy.OnServeFromCacheAsync(OutputCacheContext context) { return Task.CompletedTask; } /// - Task IOutputCachingPolicy.OnServeResponseAsync(OutputCachingContext context) + Task IOutputCachePolicy.OnServeResponseAsync(OutputCacheContext context) { return Task.CompletedTask; } diff --git a/src/Middleware/OutputCaching/src/Policies/VaryByQueryPolicy.cs b/src/Middleware/OutputCaching/src/Policies/VaryByQueryPolicy.cs index 33af9a1e5623..467d568a0eeb 100644 --- a/src/Middleware/OutputCaching/src/Policies/VaryByQueryPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/VaryByQueryPolicy.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.OutputCaching; /// When applied, the cached content will be different for every value of the provided query string keys. /// It also disables the default behavior which is to vary on all query string keys. /// -internal sealed class VaryByQueryPolicy : IOutputCachingPolicy +internal sealed class VaryByQueryPolicy : IOutputCachePolicy { private StringValues _queryKeys { get; set; } @@ -37,7 +37,7 @@ public VaryByQueryPolicy(params string[] queryKeys) } /// - Task IOutputCachingPolicy.OnRequestAsync(OutputCachingContext context) + Task IOutputCachePolicy.OnRequestAsync(OutputCacheContext context) { // No vary by query? if (_queryKeys.Count == 0) @@ -59,13 +59,13 @@ Task IOutputCachingPolicy.OnRequestAsync(OutputCachingContext context) } /// - Task IOutputCachingPolicy.OnServeFromCacheAsync(OutputCachingContext context) + Task IOutputCachePolicy.OnServeFromCacheAsync(OutputCacheContext context) { return Task.CompletedTask; } /// - Task IOutputCachingPolicy.OnServeResponseAsync(OutputCachingContext context) + Task IOutputCachePolicy.OnServeResponseAsync(OutputCacheContext context) { return Task.CompletedTask; } diff --git a/src/Middleware/OutputCaching/src/Policies/VaryByValuePolicy.cs b/src/Middleware/OutputCaching/src/Policies/VaryByValuePolicy.cs index 6d04c9c3113b..12ac17a678a4 100644 --- a/src/Middleware/OutputCaching/src/Policies/VaryByValuePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/VaryByValuePolicy.cs @@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.OutputCaching; /// /// When applied, the cached content will be different for every provided value. /// -internal sealed class VaryByValuePolicy : IOutputCachingPolicy +internal sealed class VaryByValuePolicy : IOutputCachePolicy { private readonly Action? _varyBy; private readonly Func? _varyByAsync; @@ -61,7 +61,7 @@ public VaryByValuePolicy(Func> varyBy) } /// - Task IOutputCachingPolicy.OnRequestAsync(OutputCachingContext context) + Task IOutputCachePolicy.OnRequestAsync(OutputCacheContext context) { _varyBy?.Invoke(context.HttpContext, context.CachedVaryByRules); @@ -69,13 +69,13 @@ Task IOutputCachingPolicy.OnRequestAsync(OutputCachingContext context) } /// - Task IOutputCachingPolicy.OnServeFromCacheAsync(OutputCachingContext context) + Task IOutputCachePolicy.OnServeFromCacheAsync(OutputCacheContext context) { return Task.CompletedTask; } /// - Task IOutputCachingPolicy.OnServeResponseAsync(OutputCachingContext context) + Task IOutputCachePolicy.OnServeResponseAsync(OutputCacheContext context) { return Task.CompletedTask; } diff --git a/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt b/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt index c1ddfc47e3a5..f6c49bdec992 100644 --- a/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt @@ -1,5 +1,5 @@ #nullable enable -Microsoft.AspNetCore.Builder.OutputCachingExtensions +Microsoft.AspNetCore.Builder.OutputCacheExtensions Microsoft.AspNetCore.OutputCaching.CachedResponseBody Microsoft.AspNetCore.OutputCaching.CachedResponseBody.CachedResponseBody(System.Collections.Generic.List! segments, long length) -> void Microsoft.AspNetCore.OutputCaching.CachedResponseBody.CopyToAsync(System.IO.Pipelines.PipeWriter! destination, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! @@ -14,37 +14,31 @@ Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.QueryKeys.set -> void Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.SetVaryByCustom(string! key, string! value) -> void Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.VaryByPrefix.get -> Microsoft.Extensions.Primitives.StringValues Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.VaryByPrefix.set -> void +Microsoft.AspNetCore.OutputCaching.IOutputCacheFeature +Microsoft.AspNetCore.OutputCaching.IOutputCacheFeature.Context.get -> Microsoft.AspNetCore.OutputCaching.OutputCacheContext! +Microsoft.AspNetCore.OutputCaching.IOutputCacheFeature.Policies.get -> Microsoft.AspNetCore.OutputCaching.Policies.PoliciesCollection! Microsoft.AspNetCore.OutputCaching.IOutputCacheStore Microsoft.AspNetCore.OutputCaching.IOutputCacheStore.EvictByTagAsync(string! tag, System.Threading.CancellationToken token) -> System.Threading.Tasks.ValueTask Microsoft.AspNetCore.OutputCaching.IOutputCacheStore.GetAsync(string! key, System.Threading.CancellationToken token) -> System.Threading.Tasks.ValueTask Microsoft.AspNetCore.OutputCaching.IOutputCacheStore.SetAsync(string! key, Microsoft.AspNetCore.OutputCaching.OutputCacheEntry! entry, System.TimeSpan validFor, System.Threading.CancellationToken token) -> System.Threading.Tasks.ValueTask -Microsoft.AspNetCore.OutputCaching.IOutputCachingFeature -Microsoft.AspNetCore.OutputCaching.IOutputCachingFeature.Context.get -> Microsoft.AspNetCore.OutputCaching.OutputCachingContext! -Microsoft.AspNetCore.OutputCaching.IOutputCachingFeature.Policies.get -> System.Collections.Generic.List! -Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy -Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy.OnRequestAsync(Microsoft.AspNetCore.OutputCaching.OutputCachingContext! context) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy.OnServeFromCacheAsync(Microsoft.AspNetCore.OutputCaching.OutputCachingContext! context) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy.OnServeResponseAsync(Microsoft.AspNetCore.OutputCaching.OutputCachingContext! context) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicyProvider -Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicyProvider.HasPolicies(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> bool -Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicyProvider.OnRequestAsync(Microsoft.AspNetCore.OutputCaching.OutputCachingContext! context) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicyProvider.OnServeFromCacheAsync(Microsoft.AspNetCore.OutputCaching.OutputCachingContext! context) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicyProvider.OnServeResponseAsync(Microsoft.AspNetCore.OutputCaching.OutputCachingContext! context) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.OutputCaching.IPoliciesMetadata -Microsoft.AspNetCore.OutputCaching.IPoliciesMetadata.Policy.get -> Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy! +Microsoft.AspNetCore.OutputCaching.IOutputCachePolicy +Microsoft.AspNetCore.OutputCaching.IOutputCachePolicy.OnRequestAsync(Microsoft.AspNetCore.OutputCaching.OutputCacheContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.IOutputCachePolicy.OnServeFromCacheAsync(Microsoft.AspNetCore.OutputCaching.OutputCacheContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.IOutputCachePolicy.OnServeResponseAsync(Microsoft.AspNetCore.OutputCaching.OutputCacheContext! context) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.Duration.get -> int Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.Duration.set -> void -Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.NoStore.get -> bool -Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.NoStore.set -> void +Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.NoCache.get -> bool +Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.NoCache.set -> void Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.OutputCacheAttribute() -> void -Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.Policy.get -> Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy! Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.PolicyName.get -> string? Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.PolicyName.set -> void Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.VaryByHeaders.get -> string![]? Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.VaryByHeaders.set -> void Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.VaryByQueryKeys.get -> string![]? Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.VaryByQueryKeys.set -> void +Microsoft.AspNetCore.OutputCaching.OutputCacheContext.EnableOutputCaching.get -> bool +Microsoft.AspNetCore.OutputCaching.OutputCacheContext.EnableOutputCaching.set -> void Microsoft.AspNetCore.OutputCaching.OutputCacheEntry Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.Body.get -> Microsoft.AspNetCore.OutputCaching.CachedResponseBody! Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.Body.set -> void @@ -58,15 +52,13 @@ Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.StatusCode.set -> void Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.Tags.get -> string![]? Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.Tags.set -> void Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder -Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Add(Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy! policy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! -Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Add(System.Type! policyType) -> void -Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Add() -> void +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.AddPolicy(Microsoft.AspNetCore.OutputCaching.IOutputCachePolicy! policy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.AddPolicy(System.Type! policyType) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.AddPolicy() -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.AllowLocking(bool lockResponse = true) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Clear() -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Expire(System.TimeSpan expiration) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! -Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Lock(bool lockResponse = true) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.NoCache() -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! -Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.NoLookup() -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! -Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.NoStore() -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.OutputCachePolicyBuilder() -> void Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Policy(string! profileName) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Tag(params string![]! tags) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! @@ -76,61 +68,49 @@ Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.F Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.Func!>! varyBy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.Func!>! varyBy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.Func! varyBy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! -Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.WithCondition(System.Func!>! predicate) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! -Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.WithMethod(params string![]! methods) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! -Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.WithMethod(string! method) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! -Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.WithPathBase(Microsoft.AspNetCore.Http.PathString pathBase) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! -Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.WithPathBase(params Microsoft.AspNetCore.Http.PathString[]! pathBases) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! -Microsoft.AspNetCore.OutputCaching.OutputCachingContext -Microsoft.AspNetCore.OutputCaching.OutputCachingContext.AllowCacheLookup.get -> bool -Microsoft.AspNetCore.OutputCaching.OutputCachingContext.AllowCacheLookup.set -> void -Microsoft.AspNetCore.OutputCaching.OutputCachingContext.AllowCacheStorage.get -> bool -Microsoft.AspNetCore.OutputCaching.OutputCachingContext.AllowCacheStorage.set -> void -Microsoft.AspNetCore.OutputCaching.OutputCachingContext.AllowLocking.get -> bool -Microsoft.AspNetCore.OutputCaching.OutputCachingContext.AllowLocking.set -> void -Microsoft.AspNetCore.OutputCaching.OutputCachingContext.EnableOutputCaching.get -> bool -Microsoft.AspNetCore.OutputCaching.OutputCachingContext.EnableOutputCaching.set -> void -Microsoft.AspNetCore.OutputCaching.OutputCachingContext.ResponseExpirationTimeSpan.get -> System.TimeSpan? -Microsoft.AspNetCore.OutputCaching.OutputCachingContext.ResponseExpirationTimeSpan.set -> void -Microsoft.AspNetCore.OutputCaching.OutputCachingContext.ResponseTime.get -> System.DateTimeOffset? -Microsoft.AspNetCore.OutputCaching.OutputCachingMiddleware -Microsoft.AspNetCore.OutputCaching.OutputCachingMiddleware.Invoke(Microsoft.AspNetCore.Http.HttpContext! httpContext) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.OutputCaching.OutputCachingMiddleware.OutputCachingMiddleware(Microsoft.AspNetCore.Http.RequestDelegate! next, Microsoft.Extensions.Options.IOptions! options, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory, Microsoft.AspNetCore.OutputCaching.IOutputCacheStore! outputCache, Microsoft.Extensions.ObjectPool.ObjectPoolProvider! poolProvider) -> void -Microsoft.AspNetCore.OutputCaching.OutputCachingOptions -Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.AddPolicy(string! name, Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy! policy) -> void -Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.AddPolicy(string! name, System.Action! build) -> void -Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.ApplicationServices.get -> System.IServiceProvider! -Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.ApplicationServices.set -> void -Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.BasePolicies.get -> Microsoft.AspNetCore.OutputCaching.Policies.PoliciesCollection! -Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.DefaultExpirationTimeSpan.get -> System.TimeSpan -Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.DefaultExpirationTimeSpan.set -> void -Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.MaximumBodySize.get -> long -Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.MaximumBodySize.set -> void -Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.OutputCachingOptions() -> void -Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.SizeLimit.get -> long -Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.SizeLimit.set -> void -Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.UseCaseSensitivePaths.get -> bool -Microsoft.AspNetCore.OutputCaching.OutputCachingOptions.UseCaseSensitivePaths.set -> void +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.With(System.Func!>! predicate) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCacheContext +Microsoft.AspNetCore.OutputCaching.OutputCacheContext.AllowCacheLookup.get -> bool +Microsoft.AspNetCore.OutputCaching.OutputCacheContext.AllowCacheLookup.set -> void +Microsoft.AspNetCore.OutputCaching.OutputCacheContext.AllowCacheStorage.get -> bool +Microsoft.AspNetCore.OutputCaching.OutputCacheContext.AllowCacheStorage.set -> void +Microsoft.AspNetCore.OutputCaching.OutputCacheContext.AllowLocking.get -> bool +Microsoft.AspNetCore.OutputCaching.OutputCacheContext.AllowLocking.set -> void +Microsoft.AspNetCore.OutputCaching.OutputCacheContext.ResponseExpirationTimeSpan.get -> System.TimeSpan? +Microsoft.AspNetCore.OutputCaching.OutputCacheContext.ResponseExpirationTimeSpan.set -> void +Microsoft.AspNetCore.OutputCaching.OutputCacheContext.ResponseTime.get -> System.DateTimeOffset? +Microsoft.AspNetCore.OutputCaching.OutputCacheOptions +Microsoft.AspNetCore.OutputCaching.OutputCacheOptions.AddPolicy(string! name, Microsoft.AspNetCore.OutputCaching.IOutputCachePolicy! policy) -> void +Microsoft.AspNetCore.OutputCaching.OutputCacheOptions.AddPolicy(string! name, System.Action! build) -> void +Microsoft.AspNetCore.OutputCaching.OutputCacheOptions.ApplicationServices.get -> System.IServiceProvider! +Microsoft.AspNetCore.OutputCaching.OutputCacheOptions.ApplicationServices.set -> void +Microsoft.AspNetCore.OutputCaching.OutputCacheOptions.BasePolicies.get -> Microsoft.AspNetCore.OutputCaching.Policies.PoliciesCollection! +Microsoft.AspNetCore.OutputCaching.OutputCacheOptions.DefaultExpirationTimeSpan.get -> System.TimeSpan +Microsoft.AspNetCore.OutputCaching.OutputCacheOptions.DefaultExpirationTimeSpan.set -> void +Microsoft.AspNetCore.OutputCaching.OutputCacheOptions.MaximumBodySize.get -> long +Microsoft.AspNetCore.OutputCaching.OutputCacheOptions.MaximumBodySize.set -> void +Microsoft.AspNetCore.OutputCaching.OutputCacheOptions.OutputCacheOptions() -> void +Microsoft.AspNetCore.OutputCaching.OutputCacheOptions.SizeLimit.get -> long +Microsoft.AspNetCore.OutputCaching.OutputCacheOptions.SizeLimit.set -> void +Microsoft.AspNetCore.OutputCaching.OutputCacheOptions.UseCaseSensitivePaths.get -> bool +Microsoft.AspNetCore.OutputCaching.OutputCacheOptions.UseCaseSensitivePaths.set -> void +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.With(System.Func! predicate) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.Policies.PoliciesCollection -Microsoft.AspNetCore.OutputCaching.Policies.PoliciesCollection.AddPolicy(Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy! policy) -> void +Microsoft.AspNetCore.OutputCaching.Policies.PoliciesCollection.AddPolicy(Microsoft.AspNetCore.OutputCaching.IOutputCachePolicy! policy) -> void Microsoft.AspNetCore.OutputCaching.Policies.PoliciesCollection.AddPolicy(System.Action! build) -> void -Microsoft.AspNetCore.OutputCaching.Policies.PoliciesCollection.AddPolicy(System.Type! policyType) -> void -Microsoft.AspNetCore.OutputCaching.Policies.PoliciesCollection.AddPolicy(string! name) -> void Microsoft.AspNetCore.OutputCaching.Policies.PoliciesCollection.Clear() -> void Microsoft.AspNetCore.OutputCaching.Policies.PoliciesCollection.Count.get -> int -Microsoft.AspNetCore.OutputCaching.Policies.PoliciesCollection.GetEnumerator() -> System.Collections.Generic.IEnumerator! -Microsoft.AspNetCore.OutputCaching.Policies.PolicyExtensions -Microsoft.Extensions.DependencyInjection.OutputCachingServicesExtensions -static Microsoft.AspNetCore.Builder.OutputCachingExtensions.UseOutputCaching(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! -static Microsoft.AspNetCore.OutputCaching.Policies.PolicyExtensions.CacheOutput(this TBuilder builder) -> TBuilder -static Microsoft.AspNetCore.OutputCaching.Policies.PolicyExtensions.CacheOutput(this TBuilder builder, Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy! policy) -> TBuilder -static Microsoft.AspNetCore.OutputCaching.Policies.PolicyExtensions.CacheOutput(this TBuilder builder, System.Action! policy) -> TBuilder -static Microsoft.Extensions.DependencyInjection.OutputCachingServicesExtensions.AddOutputCaching(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.Extensions.DependencyInjection.OutputCachingServicesExtensions.AddOutputCaching(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -~Microsoft.AspNetCore.OutputCaching.OutputCachingContext.CachedVaryByRules.get -> Microsoft.AspNetCore.OutputCaching.CachedVaryByRules -~Microsoft.AspNetCore.OutputCaching.OutputCachingContext.CachedVaryByRules.set -> void -~Microsoft.AspNetCore.OutputCaching.OutputCachingContext.HttpContext.get -> Microsoft.AspNetCore.Http.HttpContext -~Microsoft.AspNetCore.OutputCaching.OutputCachingContext.Logger.get -> Microsoft.Extensions.Logging.ILogger -~Microsoft.AspNetCore.OutputCaching.OutputCachingContext.Options.get -> Microsoft.AspNetCore.OutputCaching.OutputCachingOptions -~Microsoft.AspNetCore.OutputCaching.OutputCachingContext.Store.get -> Microsoft.AspNetCore.OutputCaching.IOutputCacheStore -~Microsoft.AspNetCore.OutputCaching.OutputCachingContext.Tags.get -> System.Collections.Generic.HashSet +Microsoft.AspNetCore.OutputCaching.Policies.PoliciesCollection.GetEnumerator() -> System.Collections.Generic.IEnumerator! +Microsoft.AspNetCore.OutputCaching.Policies.PoliciesCollection.PoliciesCollection() -> void +Microsoft.AspNetCore.OutputCaching.PolicyExtensions +Microsoft.Extensions.DependencyInjection.OutputCacheServicesExtensions +static Microsoft.AspNetCore.Builder.OutputCacheExtensions.UseOutputCache(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! +static Microsoft.AspNetCore.OutputCaching.PolicyExtensions.CacheOutput(this TBuilder builder) -> TBuilder +static Microsoft.AspNetCore.OutputCaching.PolicyExtensions.CacheOutput(this TBuilder builder, Microsoft.AspNetCore.OutputCaching.IOutputCachePolicy! policy) -> TBuilder +static Microsoft.AspNetCore.OutputCaching.PolicyExtensions.CacheOutput(this TBuilder builder, System.Action! policy) -> TBuilder +static Microsoft.Extensions.DependencyInjection.OutputCacheServicesExtensions.AddOutputCaching(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.DependencyInjection.OutputCacheServicesExtensions.AddOutputCaching(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +~Microsoft.AspNetCore.OutputCaching.OutputCacheContext.CachedVaryByRules.get -> Microsoft.AspNetCore.OutputCaching.CachedVaryByRules +~Microsoft.AspNetCore.OutputCaching.OutputCacheContext.CachedVaryByRules.set -> void +~Microsoft.AspNetCore.OutputCaching.OutputCacheContext.HttpContext.get -> Microsoft.AspNetCore.Http.HttpContext +~Microsoft.AspNetCore.OutputCaching.OutputCacheContext.Tags.get -> System.Collections.Generic.HashSet diff --git a/src/Middleware/OutputCaching/src/Streams/OutputCachingStream.cs b/src/Middleware/OutputCaching/src/Streams/OutputCacheStream.cs similarity index 96% rename from src/Middleware/OutputCaching/src/Streams/OutputCachingStream.cs rename to src/Middleware/OutputCaching/src/Streams/OutputCacheStream.cs index 3759cabbe40b..d868586d6999 100644 --- a/src/Middleware/OutputCaching/src/Streams/OutputCachingStream.cs +++ b/src/Middleware/OutputCaching/src/Streams/OutputCacheStream.cs @@ -3,7 +3,7 @@ namespace Microsoft.AspNetCore.OutputCaching; -internal sealed class OutputCachingStream : Stream +internal sealed class OutputCacheStream : Stream { private readonly Stream _innerStream; private readonly long _maxBufferSize; @@ -11,7 +11,7 @@ internal sealed class OutputCachingStream : Stream private readonly SegmentWriteStream _segmentWriteStream; private readonly Action _startResponseCallback; - internal OutputCachingStream(Stream innerStream, long maxBufferSize, int segmentSize, Action startResponseCallback) + internal OutputCacheStream(Stream innerStream, long maxBufferSize, int segmentSize, Action startResponseCallback) { _innerStream = innerStream; _maxBufferSize = maxBufferSize; diff --git a/src/Middleware/OutputCaching/test/OutputCachingKeyProviderTests.cs b/src/Middleware/OutputCaching/test/OutputCachingKeyProviderTests.cs index 34ffb714588b..eba7cc13ef61 100644 --- a/src/Middleware/OutputCaching/test/OutputCachingKeyProviderTests.cs +++ b/src/Middleware/OutputCaching/test/OutputCachingKeyProviderTests.cs @@ -28,7 +28,7 @@ public void OutputCachingKeyProvider_CreateStorageKey_IncludesOnlyNormalizedMeth [Fact] public void OutputCachingKeyProvider_CreateStorageKey_CaseInsensitivePath_NormalizesPath() { - var cacheKeyProvider = TestUtils.CreateTestKeyProvider(new OutputCachingOptions() + var cacheKeyProvider = TestUtils.CreateTestKeyProvider(new OutputCacheOptions() { UseCaseSensitivePaths = false }); @@ -42,7 +42,7 @@ public void OutputCachingKeyProvider_CreateStorageKey_CaseInsensitivePath_Normal [Fact] public void OutputCachingKeyProvider_CreateStorageKey_CaseSensitivePath_PreservesPathCase() { - var cacheKeyProvider = TestUtils.CreateTestKeyProvider(new OutputCachingOptions() + var cacheKeyProvider = TestUtils.CreateTestKeyProvider(new OutputCacheOptions() { UseCaseSensitivePaths = true }); diff --git a/src/Middleware/OutputCaching/test/OutputCachingMiddlewareTests.cs b/src/Middleware/OutputCaching/test/OutputCachingMiddlewareTests.cs index 674c5c5e2666..659b3f5a6c53 100644 --- a/src/Middleware/OutputCaching/test/OutputCachingMiddlewareTests.cs +++ b/src/Middleware/OutputCaching/test/OutputCachingMiddlewareTests.cs @@ -136,7 +136,7 @@ public void ContentIsNotModified_NotConditionalRequest_False() var context = TestUtils.CreateTestContext(sink); context.CachedResponse = new OutputCacheEntry { Headers = new HeaderDictionary() }; - Assert.False(OutputCachingMiddleware.ContentIsNotModified(context)); + Assert.False(OutputCacheMiddleware.ContentIsNotModified(context)); Assert.Empty(sink.Writes); } @@ -152,17 +152,17 @@ public void ContentIsNotModified_IfModifiedSince_FallsbackToDateHeader() // Verify modifications in the past succeeds context.CachedResponse.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10)); - Assert.True(OutputCachingMiddleware.ContentIsNotModified(context)); + Assert.True(OutputCacheMiddleware.ContentIsNotModified(context)); Assert.Single(sink.Writes); // Verify modifications at present succeeds context.CachedResponse.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow); - Assert.True(OutputCachingMiddleware.ContentIsNotModified(context)); + Assert.True(OutputCacheMiddleware.ContentIsNotModified(context)); Assert.Equal(2, sink.Writes.Count); // Verify modifications in the future fails context.CachedResponse.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); - Assert.False(OutputCachingMiddleware.ContentIsNotModified(context)); + Assert.False(OutputCacheMiddleware.ContentIsNotModified(context)); // Verify logging TestUtils.AssertLoggedMessages( @@ -184,19 +184,19 @@ public void ContentIsNotModified_IfModifiedSince_LastModifiedOverridesDateHeader // Verify modifications in the past succeeds context.CachedResponse.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); context.CachedResponse.Headers[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10)); - Assert.True(OutputCachingMiddleware.ContentIsNotModified(context)); + Assert.True(OutputCacheMiddleware.ContentIsNotModified(context)); Assert.Single(sink.Writes); // Verify modifications at present context.CachedResponse.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); context.CachedResponse.Headers[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow); - Assert.True(OutputCachingMiddleware.ContentIsNotModified(context)); + Assert.True(OutputCacheMiddleware.ContentIsNotModified(context)); Assert.Equal(2, sink.Writes.Count); // Verify modifications in the future fails context.CachedResponse.Headers[HeaderNames.Date] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10)); context.CachedResponse.Headers[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); - Assert.False(OutputCachingMiddleware.ContentIsNotModified(context)); + Assert.False(OutputCacheMiddleware.ContentIsNotModified(context)); // Verify logging TestUtils.AssertLoggedMessages( @@ -218,7 +218,7 @@ public void ContentIsNotModified_IfNoneMatch_Overrides_IfModifiedSince_ToTrue() context.CachedResponse.Headers[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow + TimeSpan.FromSeconds(10)); context.HttpContext.Request.Headers.IfNoneMatch = EntityTagHeaderValue.Any.ToString(); - Assert.True(OutputCachingMiddleware.ContentIsNotModified(context)); + Assert.True(OutputCacheMiddleware.ContentIsNotModified(context)); TestUtils.AssertLoggedMessages( sink.Writes, LoggedMessage.NotModifiedIfNoneMatchStar); @@ -237,7 +237,7 @@ public void ContentIsNotModified_IfNoneMatch_Overrides_IfModifiedSince_ToFalse() context.CachedResponse.Headers[HeaderNames.LastModified] = HeaderUtilities.FormatDate(utcNow - TimeSpan.FromSeconds(10)); context.HttpContext.Request.Headers.IfNoneMatch = "\"E1\""; - Assert.False(OutputCachingMiddleware.ContentIsNotModified(context)); + Assert.False(OutputCacheMiddleware.ContentIsNotModified(context)); Assert.Empty(sink.Writes); } @@ -249,7 +249,7 @@ public void ContentIsNotModified_IfNoneMatch_AnyWithoutETagInResponse_False() context.CachedResponse = new OutputCacheEntry { Headers = new HeaderDictionary() }; context.HttpContext.Request.Headers.IfNoneMatch = "\"E1\""; - Assert.False(OutputCachingMiddleware.ContentIsNotModified(context)); + Assert.False(OutputCacheMiddleware.ContentIsNotModified(context)); Assert.Empty(sink.Writes); } @@ -277,7 +277,7 @@ public void ContentIsNotModified_IfNoneMatch_ExplicitWithMatch_True(EntityTagHea context.CachedResponse.Headers[HeaderNames.ETag] = responseETag.ToString(); context.HttpContext.Request.Headers.IfNoneMatch = requestETag.ToString(); - Assert.True(OutputCachingMiddleware.ContentIsNotModified(context)); + Assert.True(OutputCacheMiddleware.ContentIsNotModified(context)); TestUtils.AssertLoggedMessages( sink.Writes, LoggedMessage.NotModifiedIfNoneMatchMatched); @@ -292,7 +292,7 @@ public void ContentIsNotModified_IfNoneMatch_ExplicitWithoutMatch_False() context.CachedResponse.Headers[HeaderNames.ETag] = "\"E2\""; context.HttpContext.Request.Headers.IfNoneMatch = "\"E1\""; - Assert.False(OutputCachingMiddleware.ContentIsNotModified(context)); + Assert.False(OutputCacheMiddleware.ContentIsNotModified(context)); Assert.Empty(sink.Writes); } @@ -305,7 +305,7 @@ public void ContentIsNotModified_IfNoneMatch_MatchesAtLeastOneValue_True() context.CachedResponse.Headers[HeaderNames.ETag] = "\"E2\""; context.HttpContext.Request.Headers.IfNoneMatch = new string[] { "\"E0\", \"E1\"", "\"E1\", \"E2\"" }; - Assert.True(OutputCachingMiddleware.ContentIsNotModified(context)); + Assert.True(OutputCacheMiddleware.ContentIsNotModified(context)); TestUtils.AssertLoggedMessages( sink.Writes, LoggedMessage.NotModifiedIfNoneMatchMatched); @@ -318,7 +318,7 @@ public void StartResponsegAsync_IfAllowResponseCaptureIsTrue_SetsResponseTime() { UtcNow = DateTimeOffset.UtcNow }; - var middleware = TestUtils.CreateTestMiddleware(options: new OutputCachingOptions + var middleware = TestUtils.CreateTestMiddleware(options: new OutputCacheOptions { SystemClock = clock }); @@ -337,7 +337,7 @@ public void StartResponseAsync_IfAllowResponseCaptureIsTrue_SetsResponseTimeOnly { UtcNow = DateTimeOffset.UtcNow }; - var middleware = TestUtils.CreateTestMiddleware(options: new OutputCachingOptions + var middleware = TestUtils.CreateTestMiddleware(options: new OutputCacheOptions { SystemClock = clock }); @@ -377,7 +377,7 @@ public void FinalizeCacheHeadersAsync_ResponseValidity_IgnoresExpiryIfAvailable( { UtcNow = DateTimeOffset.MinValue }; - var options = new OutputCachingOptions + var options = new OutputCacheOptions { SystemClock = clock }; @@ -404,7 +404,7 @@ public void FinalizeCacheHeadersAsync_ResponseValidity_UseMaxAgeIfAvailable() UtcNow = DateTimeOffset.UtcNow }; var sink = new TestSink(); - var options = new OutputCachingOptions + var options = new OutputCacheOptions { SystemClock = clock }; @@ -433,7 +433,7 @@ public void FinalizeCacheHeadersAsync_ResponseValidity_UseSharedMaxAgeIfAvailabl UtcNow = DateTimeOffset.UtcNow }; var sink = new TestSink(); - var options = new OutputCachingOptions + var options = new OutputCacheOptions { SystemClock = clock }; @@ -482,7 +482,7 @@ public void FinalizeCacheHeadersAsync_UpdateCachedVaryByRules_NullOrEmptyRules(S var context = TestUtils.CreateTestContext(cache); context.HttpContext.Response.Headers.Vary = vary; - context.HttpContext.Features.Set(new OutputCachingFeature(context)); + context.HttpContext.Features.Set(new OutputCacheFeature(context)); context.CachedVaryByRules.QueryKeys = vary; middleware.FinalizeCacheHeaders(context); @@ -751,10 +751,10 @@ public void AddOutputCachingFeature_SecondInvocation_Throws() var context = TestUtils.CreateTestContext(httpContext); // Should not throw - OutputCachingMiddleware.AddOutputCachingFeature(context); + OutputCacheMiddleware.AddOutputCacheFeature(context); // Should throw - Assert.ThrowsAny(() => OutputCachingMiddleware.AddOutputCachingFeature(context)); + Assert.ThrowsAny(() => OutputCacheMiddleware.AddOutputCacheFeature(context)); } private class FakeResponseFeature : HttpResponseFeature @@ -762,45 +762,13 @@ private class FakeResponseFeature : HttpResponseFeature public override void OnStarting(Func callback, object state) { } } - [Theory] - // If allowResponseCaching is false, other settings will not matter but are included for completeness - [InlineData(false, false, false)] - [InlineData(false, false, true)] - [InlineData(false, true, false)] - [InlineData(false, true, true)] - [InlineData(true, false, false)] - [InlineData(true, false, true)] - [InlineData(true, true, false)] - [InlineData(true, true, true)] - public async Task Invoke_AddsOutputCachingFeature_Always(bool enableOutputCaching, bool allowCacheLookup, bool allowCacheStorage) - { - var responseCachingFeatureAdded = false; - var middleware = TestUtils.CreateTestMiddleware(next: httpContext => - { - responseCachingFeatureAdded = httpContext.Features.Get() != null; - return Task.CompletedTask; - }, - policyProvider: new TestOutputCachingPolicyProvider - { - EnableOutputCaching = enableOutputCaching, - AllowCacheLookupValue = allowCacheLookup, - AllowCacheStorageValue = allowCacheStorage - }); - - var context = new DefaultHttpContext(); - context.Features.Set(new FakeResponseFeature()); - await middleware.Invoke(context); - - Assert.True(responseCachingFeatureAdded); - } - [Fact] public void GetOrderCasingNormalizedStringValues_NormalizesCasingToUpper() { var uppercaseStrings = new StringValues(new[] { "STRINGA", "STRINGB" }); var lowercaseStrings = new StringValues(new[] { "stringA", "stringB" }); - var normalizedStrings = OutputCachingMiddleware.GetOrderCasingNormalizedStringValues(lowercaseStrings); + var normalizedStrings = OutputCacheMiddleware.GetOrderCasingNormalizedStringValues(lowercaseStrings); Assert.Equal(uppercaseStrings, normalizedStrings); } @@ -811,7 +779,7 @@ public void GetOrderCasingNormalizedStringValues_NormalizesOrder() var orderedStrings = new StringValues(new[] { "STRINGA", "STRINGB" }); var reverseOrderStrings = new StringValues(new[] { "STRINGB", "STRINGA" }); - var normalizedStrings = OutputCachingMiddleware.GetOrderCasingNormalizedStringValues(reverseOrderStrings); + var normalizedStrings = OutputCacheMiddleware.GetOrderCasingNormalizedStringValues(reverseOrderStrings); Assert.Equal(orderedStrings, normalizedStrings); } @@ -821,7 +789,7 @@ public void GetOrderCasingNormalizedStringValues_PreservesCommas() { var originalStrings = new StringValues(new[] { "STRINGA, STRINGB" }); - var normalizedStrings = OutputCachingMiddleware.GetOrderCasingNormalizedStringValues(originalStrings); + var normalizedStrings = OutputCacheMiddleware.GetOrderCasingNormalizedStringValues(originalStrings); Assert.Equal(originalStrings, normalizedStrings); } diff --git a/src/Middleware/OutputCaching/test/OutputCachingPolicyProviderTests.cs b/src/Middleware/OutputCaching/test/OutputCachingPolicyProviderTests.cs index d5009290133b..dcea6581d0c9 100644 --- a/src/Middleware/OutputCaching/test/OutputCachingPolicyProviderTests.cs +++ b/src/Middleware/OutputCaching/test/OutputCachingPolicyProviderTests.cs @@ -47,11 +47,11 @@ public async Task AttemptOutputCaching_CacheableMethods_Allowed(string method) { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - var options = new OutputCachingOptions(); + var options = new OutputCacheOptions(); options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); context.HttpContext.Request.Method = method; - await new OutputCachingPolicyProvider(Options.Create(options)).OnRequestAsync(context); + await new OutputCachePolicyProvider(options).OnRequestAsync(context); Assert.True(context.AllowCacheStorage); Assert.True(context.AllowCacheLookup); @@ -64,11 +64,11 @@ public async Task AttemptOutputCaching_UncacheableMethods_NotAllowed(string meth { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - var options = new OutputCachingOptions(); + var options = new OutputCacheOptions(); options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); context.HttpContext.Request.Method = method; - await new OutputCachingPolicyProvider(Options.Create(options)).OnRequestAsync(context); + await new OutputCachePolicyProvider(options).OnRequestAsync(context); Assert.False(context.AllowCacheLookup); Assert.False(context.AllowCacheStorage); @@ -85,10 +85,10 @@ public async Task AttemptResponseCaching_AuthorizationHeaders_NotAllowed() context.HttpContext.Request.Method = HttpMethods.Get; context.HttpContext.Request.Headers.Authorization = "Placeholder"; - var options = new OutputCachingOptions(); + var options = new OutputCacheOptions(); options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); - await new OutputCachingPolicyProvider(Options.Create(options)).OnRequestAsync(context); + await new OutputCachePolicyProvider(options).OnRequestAsync(context); Assert.False(context.AllowCacheStorage); Assert.False(context.AllowCacheLookup); @@ -109,9 +109,9 @@ public async Task AllowCacheStorage_NoStore_Allowed() NoStore = true }.ToString(); - var options = new OutputCachingOptions(); + var options = new OutputCacheOptions(); options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); - await new OutputCachingPolicyProvider(Options.Create(options)).OnRequestAsync(context); + await new OutputCachePolicyProvider(options).OnRequestAsync(context); Assert.True(context.AllowCacheStorage); Assert.Empty(sink.Writes); @@ -126,9 +126,9 @@ public async Task AllowCacheLookup_LegacyDirectives_OverridenByCacheControl() context.HttpContext.Request.Headers.Pragma = "no-cache"; context.HttpContext.Request.Headers.CacheControl = "max-age=10"; - var options = new OutputCachingOptions(); + var options = new OutputCacheOptions(); options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); - await new OutputCachingPolicyProvider(Options.Create(options)).OnRequestAsync(context); + await new OutputCachePolicyProvider(options).OnRequestAsync(context); Assert.True(context.AllowCacheLookup); Assert.Empty(sink.Writes); @@ -140,9 +140,9 @@ public async Task IsResponseCacheable_NoPublic_Allowed() var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - var options = new OutputCachingOptions(); + var options = new OutputCacheOptions(); options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); - await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); + await new OutputCachePolicyProvider(options).OnServeResponseAsync(context); Assert.True(context.AllowCacheStorage); Assert.True(context.AllowCacheLookup); @@ -159,9 +159,9 @@ public async Task IsResponseCacheable_Public_Allowed() Public = true }.ToString(); - var options = new OutputCachingOptions(); + var options = new OutputCacheOptions(); options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); - await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); + await new OutputCachePolicyProvider(options).OnServeResponseAsync(context); Assert.True(context.AllowCacheStorage); Assert.True(context.AllowCacheLookup); @@ -178,9 +178,9 @@ public async Task IsResponseCacheable_NoCache_Allowed() NoCache = true }.ToString(); - var options = new OutputCachingOptions(); + var options = new OutputCacheOptions(); options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); - await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); + await new OutputCachePolicyProvider(options).OnServeResponseAsync(context); Assert.True(context.AllowCacheStorage); Assert.True(context.AllowCacheLookup); @@ -197,9 +197,9 @@ public async Task IsResponseCacheable_ResponseNoStore_Allowed() NoStore = true }.ToString(); - var options = new OutputCachingOptions(); + var options = new OutputCacheOptions(); options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); - await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); + await new OutputCachePolicyProvider(options).OnServeResponseAsync(context); Assert.True(context.AllowCacheStorage); Assert.True(context.AllowCacheLookup); @@ -213,9 +213,9 @@ public async Task IsResponseCacheable_SetCookieHeader_NotAllowed() var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.Headers.SetCookie = "cookieName=cookieValue"; - var options = new OutputCachingOptions(); + var options = new OutputCacheOptions(); options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); - await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); + await new OutputCachePolicyProvider(options).OnServeResponseAsync(context); Assert.False(context.AllowCacheStorage); Assert.True(context.AllowCacheLookup); @@ -230,9 +230,9 @@ public async Task IsResponseCacheable_VaryHeaderByStar_Allowed() var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.Headers.Vary = "*"; - var options = new OutputCachingOptions(); + var options = new OutputCacheOptions(); options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); - await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); + await new OutputCachePolicyProvider(options).OnServeResponseAsync(context); Assert.True(context.AllowCacheStorage); Assert.True(context.AllowCacheLookup); @@ -249,9 +249,9 @@ public async Task IsResponseCacheable_Private_Allowed() Private = true }.ToString(); - var options = new OutputCachingOptions(); + var options = new OutputCacheOptions(); options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); - await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); + await new OutputCachePolicyProvider(options).OnServeResponseAsync(context); Assert.True(context.AllowCacheStorage); Assert.True(context.AllowCacheLookup); @@ -266,9 +266,9 @@ public async Task IsResponseCacheable_SuccessStatusCodes_Allowed(int statusCode) var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.StatusCode = statusCode; - var options = new OutputCachingOptions(); + var options = new OutputCacheOptions(); options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); - await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); + await new OutputCachePolicyProvider(options).OnServeResponseAsync(context); Assert.True(context.AllowCacheStorage); Assert.True(context.AllowCacheLookup); @@ -343,9 +343,9 @@ public async Task IsResponseCacheable_NonSuccessStatusCodes_NotAllowed(int statu var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.StatusCode = statusCode; - var options = new OutputCachingOptions(); + var options = new OutputCacheOptions(); options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); - await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); + await new OutputCachePolicyProvider(options).OnServeResponseAsync(context); Assert.True(context.AllowCacheLookup); Assert.False(context.AllowCacheStorage); @@ -365,9 +365,9 @@ public async Task IsResponseCacheable_NoExpiryRequirements_IsAllowed() context.HttpContext.Response.Headers.Date = HeaderUtilities.FormatDate(utcNow); context.ResponseTime = DateTimeOffset.MaxValue; - var options = new OutputCachingOptions(); + var options = new OutputCacheOptions(); options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); - await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); + await new OutputCachePolicyProvider(options).OnServeResponseAsync(context); Assert.True(context.AllowCacheStorage); Assert.True(context.AllowCacheLookup); @@ -389,9 +389,9 @@ public async Task IsResponseCacheable_MaxAgeOverridesExpiry_ToAllowed() context.HttpContext.Response.Headers.Date = HeaderUtilities.FormatDate(utcNow); context.ResponseTime = utcNow + TimeSpan.FromSeconds(9); - var options = new OutputCachingOptions(); + var options = new OutputCacheOptions(); options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); - await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); + await new OutputCachePolicyProvider(options).OnServeResponseAsync(context); Assert.True(context.AllowCacheStorage); Assert.True(context.AllowCacheLookup); @@ -413,9 +413,9 @@ public async Task IsResponseCacheable_SharedMaxAgeOverridesMaxAge_ToAllowed() context.HttpContext.Response.Headers.Date = HeaderUtilities.FormatDate(utcNow); context.ResponseTime = utcNow + TimeSpan.FromSeconds(11); - var options = new OutputCachingOptions(); + var options = new OutputCacheOptions(); options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); - await new OutputCachingPolicyProvider(Options.Create(options)).OnServeResponseAsync(context); + await new OutputCachePolicyProvider(options).OnServeResponseAsync(context); Assert.True(context.AllowCacheStorage); Assert.True(context.AllowCacheLookup); diff --git a/src/Middleware/OutputCaching/test/OutputCachingTests.cs b/src/Middleware/OutputCaching/test/OutputCachingTests.cs index da2667680280..8a2cbbd3a398 100644 --- a/src/Middleware/OutputCaching/test/OutputCachingTests.cs +++ b/src/Middleware/OutputCaching/test/OutputCachingTests.cs @@ -219,7 +219,7 @@ public async Task ServesCachedContent_If_PathCasingDiffers(string method) [InlineData("HEAD")] public async Task ServesFreshContent_If_ResponseExpired(string method) { - var options = new OutputCachingOptions(); + var options = new OutputCacheOptions(); options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().VaryByHeader(HeaderNames.From).Build()); options.DefaultExpirationTimeSpan = TimeSpan.FromMicroseconds(100); @@ -294,7 +294,7 @@ public async Task ServesCachedContent_IfVaryHeader_Matches() [Fact] public async Task ServesFreshContent_IfVaryHeader_Mismatches() { - var options = new OutputCachingOptions(); + var options = new OutputCacheOptions(); options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().VaryByHeader(HeaderNames.From).Build()); var builders = TestUtils.CreateBuildersWithOutputCaching(options: options); @@ -321,7 +321,7 @@ public async Task ServesFreshContent_IfVaryHeader_Mismatches() [Fact] public async Task ServesCachedContent_IfVaryQueryKeys_Matches() { - var options = new OutputCachingOptions(); + var options = new OutputCacheOptions(); options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().VaryByQuery("query").Build()); var builders = TestUtils.CreateBuildersWithOutputCaching(options: options); @@ -346,7 +346,7 @@ public async Task ServesCachedContent_IfVaryQueryKeys_Matches() [Fact] public async Task ServesCachedContent_IfVaryQueryKeysExplicit_Matches_QueryKeyCaseInsensitive() { - var options = new OutputCachingOptions(); + var options = new OutputCacheOptions(); options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().VaryByQuery("QueryA", "queryb").Build()); var builders = TestUtils.CreateBuildersWithOutputCaching(options: options); @@ -371,7 +371,7 @@ public async Task ServesCachedContent_IfVaryQueryKeysExplicit_Matches_QueryKeyCa [Fact] public async Task ServesCachedContent_IfVaryQueryKeyStar_Matches_QueryKeyCaseInsensitive() { - var options = new OutputCachingOptions(); + var options = new OutputCacheOptions(); options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().VaryByQuery("*").Build()); var builders = TestUtils.CreateBuildersWithOutputCaching(options: options); @@ -396,7 +396,7 @@ public async Task ServesCachedContent_IfVaryQueryKeyStar_Matches_QueryKeyCaseIns [Fact] public async Task ServesCachedContent_IfVaryQueryKeyExplicit_Matches_OrderInsensitive() { - var options = new OutputCachingOptions(); + var options = new OutputCacheOptions(); options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().VaryByQuery("QueryB", "QueryA").Build()); var builders = TestUtils.CreateBuildersWithOutputCaching(options: options); @@ -421,7 +421,7 @@ public async Task ServesCachedContent_IfVaryQueryKeyExplicit_Matches_OrderInsens [Fact] public async Task ServesCachedContent_IfVaryQueryKeyStar_Matches_OrderInsensitive() { - var options = new OutputCachingOptions(); + var options = new OutputCacheOptions(); options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().VaryByQuery("*").Build()); var builders = TestUtils.CreateBuildersWithOutputCaching(options: options); @@ -446,7 +446,7 @@ public async Task ServesCachedContent_IfVaryQueryKeyStar_Matches_OrderInsensitiv [Fact] public async Task ServesFreshContent_IfVaryQueryKey_Mismatches() { - var options = new OutputCachingOptions(); + var options = new OutputCacheOptions(); options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().VaryByQuery("query").Build()); var builders = TestUtils.CreateBuildersWithOutputCaching(options: options); @@ -470,7 +470,7 @@ public async Task ServesFreshContent_IfVaryQueryKey_Mismatches() [Fact] public async Task ServesCachedContent_IfOtherVaryQueryKey_Mismatches() { - var options = new OutputCachingOptions(); + var options = new OutputCacheOptions(); options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().VaryByQuery("query").Build()); var builders = TestUtils.CreateBuildersWithOutputCaching(options: options); @@ -495,7 +495,7 @@ public async Task ServesCachedContent_IfOtherVaryQueryKey_Mismatches() [Fact] public async Task ServesFreshContent_IfVaryQueryKeyExplicit_Mismatch_QueryKeyCaseSensitive() { - var builders = TestUtils.CreateBuildersWithOutputCaching(contextAction: context => context.Features.Get().Policies.Add(new VaryByQueryPolicy("QueryA", "QueryB"))); + var builders = TestUtils.CreateBuildersWithOutputCaching(contextAction: context => context.Features.Get().Policies.AddPolicy(new VaryByQueryPolicy("QueryA", "QueryB"))); foreach (var builder in builders) { @@ -517,7 +517,7 @@ public async Task ServesFreshContent_IfVaryQueryKeyExplicit_Mismatch_QueryKeyCas [Fact] public async Task ServesFreshContent_IfVaryQueryKeyStar_Mismatch_QueryKeyValueCaseSensitive() { - var builders = TestUtils.CreateBuildersWithOutputCaching(contextAction: context => context.Features.Get().Policies.Add(new VaryByQueryPolicy("*"))); + var builders = TestUtils.CreateBuildersWithOutputCaching(contextAction: context => context.Features.Get().Policies.AddPolicy(new VaryByQueryPolicy("*"))); foreach (var builder in builders) { @@ -795,7 +795,7 @@ public async Task ServesCachedContent_IfIfNoneMatch_NotSatisfied() [Fact] public async Task ServesCachedContent_IfBodySize_IsCacheable() { - var options = new OutputCachingOptions(); + var options = new OutputCacheOptions(); options.MaximumBodySize = 1000; options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); @@ -821,7 +821,7 @@ public async Task ServesCachedContent_IfBodySize_IsCacheable() [Fact] public async Task ServesFreshContent_IfBodySize_IsNotCacheable() { - var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions() + var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCacheOptions() { MaximumBodySize = 1 }); @@ -846,7 +846,7 @@ public async Task ServesFreshContent_IfBodySize_IsNotCacheable() [Fact] public async Task ServesFreshContent_CaseSensitivePaths_IsNotCacheable() { - var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCachingOptions() + var builders = TestUtils.CreateBuildersWithOutputCaching(options: new OutputCacheOptions() { UseCaseSensitivePaths = true }); diff --git a/src/Middleware/OutputCaching/test/TestUtils.cs b/src/Middleware/OutputCaching/test/TestUtils.cs index b2cd880b4632..0d19dcc69898 100644 --- a/src/Middleware/OutputCaching/test/TestUtils.cs +++ b/src/Middleware/OutputCaching/test/TestUtils.cs @@ -76,17 +76,17 @@ internal static Task TestRequestDelegateWrite(HttpContext context) internal static IOutputCachingKeyProvider CreateTestKeyProvider() { - return CreateTestKeyProvider(new OutputCachingOptions()); + return CreateTestKeyProvider(new OutputCacheOptions()); } - internal static IOutputCachingKeyProvider CreateTestKeyProvider(OutputCachingOptions options) + internal static IOutputCachingKeyProvider CreateTestKeyProvider(OutputCacheOptions options) { - return new OutputCachingKeyProvider(new DefaultObjectPoolProvider(), Options.Create(options)); + return new OutputCacheKeyProvider(new DefaultObjectPoolProvider(), Options.Create(options)); } internal static IEnumerable CreateBuildersWithOutputCaching( Action configureDelegate = null, - OutputCachingOptions options = null, + OutputCacheOptions options = null, Action contextAction = null) { return CreateBuildersWithOutputCaching(configureDelegate, options, new RequestDelegate[] @@ -111,7 +111,7 @@ internal static IEnumerable CreateBuildersWithOutputCaching( private static IEnumerable CreateBuildersWithOutputCaching( Action configureDelegate = null, - OutputCachingOptions options = null, + OutputCacheOptions options = null, IEnumerable requestDelegates = null) { if (configureDelegate == null) @@ -157,20 +157,20 @@ private static IEnumerable CreateBuildersWithOutputCaching( .Configure(app => { configureDelegate(app); - app.UseOutputCaching(); + app.UseOutputCache(); app.Run(requestDelegate); }); }); } } - internal static OutputCachingMiddleware CreateTestMiddleware( + internal static OutputCacheMiddleware CreateTestMiddleware( RequestDelegate next = null, IOutputCacheStore cache = null, - OutputCachingOptions options = null, + OutputCacheOptions options = null, TestSink testSink = null, IOutputCachingKeyProvider keyProvider = null, - IOutputCachingPolicyProvider policyProvider = null) + OutputCachePolicyProvider policyProvider = null) { if (next == null) { @@ -182,29 +182,24 @@ internal static OutputCachingMiddleware CreateTestMiddleware( } if (options == null) { - options = new OutputCachingOptions(); + options = new OutputCacheOptions(); } if (keyProvider == null) { - keyProvider = new OutputCachingKeyProvider(new DefaultObjectPoolProvider(), Options.Create(options)); - } - if (policyProvider == null) - { - policyProvider = new TestOutputCachingPolicyProvider(); + keyProvider = new OutputCacheKeyProvider(new DefaultObjectPoolProvider(), Options.Create(options)); } - return new OutputCachingMiddleware( + return new OutputCacheMiddleware( next, Options.Create(options), testSink == null ? (ILoggerFactory)NullLoggerFactory.Instance : new TestLoggerFactory(testSink, true), - policyProvider, cache, keyProvider); } - internal static OutputCachingContext CreateTestContext(IOutputCacheStore cache = null, OutputCachingOptions options = null) + internal static OutputCacheContext CreateTestContext(IOutputCacheStore cache = null, OutputCacheOptions options = null) { - return new OutputCachingContext(new DefaultHttpContext(), cache ?? new TestOutputCache(), options ?? Options.Create(new OutputCachingOptions()).Value, NullLogger.Instance) + return new OutputCacheContext(new DefaultHttpContext(), cache ?? new TestOutputCache(), options ?? Options.Create(new OutputCacheOptions()).Value, NullLogger.Instance) { EnableOutputCaching = true, AllowCacheStorage = true, @@ -213,9 +208,9 @@ internal static OutputCachingContext CreateTestContext(IOutputCacheStore cache = }; } - internal static OutputCachingContext CreateTestContext(HttpContext httpContext, IOutputCacheStore cache = null, OutputCachingOptions options = null) + internal static OutputCacheContext CreateTestContext(HttpContext httpContext, IOutputCacheStore cache = null, OutputCacheOptions options = null) { - return new OutputCachingContext(httpContext, cache ?? new TestOutputCache(), options ?? Options.Create(new OutputCachingOptions()).Value, NullLogger.Instance) + return new OutputCacheContext(httpContext, cache ?? new TestOutputCache(), options ?? Options.Create(new OutputCacheOptions()).Value, NullLogger.Instance) { EnableOutputCaching = true, AllowCacheStorage = true, @@ -224,9 +219,9 @@ internal static OutputCachingContext CreateTestContext(HttpContext httpContext, }; } - internal static OutputCachingContext CreateTestContext(ITestSink testSink, IOutputCacheStore cache = null, OutputCachingOptions options = null) + internal static OutputCacheContext CreateTestContext(ITestSink testSink, IOutputCacheStore cache = null, OutputCacheOptions options = null) { - return new OutputCachingContext(new DefaultHttpContext(), cache ?? new TestOutputCache(), options ?? Options.Create(new OutputCachingOptions()).Value, new TestLogger("OutputCachingTests", testSink, true)) + return new OutputCacheContext(new DefaultHttpContext(), cache ?? new TestOutputCache(), options ?? Options.Create(new OutputCacheOptions()).Value, new TestLogger("OutputCachingTests", testSink, true)) { EnableOutputCaching = true, AllowCacheStorage = true, @@ -315,42 +310,6 @@ private LoggedMessage(int evenId, LogLevel logLevel) internal LogLevel LogLevel { get; } } -internal class TestOutputCachingPolicyProvider : IOutputCachingPolicyProvider -{ - public bool AllowCacheLookupValue { get; set; } - public bool AllowCacheStorageValue { get; set; } - public bool EnableOutputCaching { get; set; } = true; - - public bool HasPolicies(HttpContext httpContext) - { - return true; - } - - public Task OnRequestAsync(OutputCachingContext context) - { - context.EnableOutputCaching = EnableOutputCaching; - context.AllowCacheLookup = AllowCacheLookupValue; - context.AllowCacheStorage = AllowCacheStorageValue; - - return Task.CompletedTask; - } - - public Task OnServeFromCacheAsync(OutputCachingContext context) - { - context.AllowCacheLookup = AllowCacheLookupValue; - context.AllowCacheStorage = AllowCacheStorageValue; - - return Task.CompletedTask; - } - - public Task OnServeResponseAsync(OutputCachingContext context) - { - context.AllowCacheStorage = AllowCacheStorageValue; - - return Task.CompletedTask; - } -} - internal class TestResponseCachingKeyProvider : IOutputCachingKeyProvider { private readonly string _key; @@ -360,7 +319,7 @@ public TestResponseCachingKeyProvider(string key = null) _key = key; } - public string CreateStorageKey(OutputCachingContext context) + public string CreateStorageKey(OutputCacheContext context) { return _key; } diff --git a/src/Mvc/Mvc.Core/src/Filters/OutputCacheFilter.cs b/src/Mvc/Mvc.Core/src/Filters/OutputCacheFilter.cs index 39e95f553511..12e5ed96670c 100644 --- a/src/Mvc/Mvc.Core/src/Filters/OutputCacheFilter.cs +++ b/src/Mvc/Mvc.Core/src/Filters/OutputCacheFilter.cs @@ -25,10 +25,7 @@ public OutputCacheFilter(ILoggerFactory loggerFactory) public void OnActionExecuting(ActionExecutingContext context) { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } + ArgumentNullException.ThrowIfNull(context); // If there are more filters which can override the values written by this filter, // then skip execution of this filter. @@ -39,7 +36,7 @@ public void OnActionExecuting(ActionExecutingContext context) return; } - var outputCachingFeature = context.HttpContext.Features.Get(); + var outputCachingFeature = context.HttpContext.Features.Get(); if (outputCachingFeature == null) { throw new InvalidOperationException( diff --git a/src/Mvc/Mvc.Core/src/OutputCacheAttribute.cs b/src/Mvc/Mvc.Core/src/OutputCacheAttribute.cs index d9c7549bd5a6..86e6edd85393 100644 --- a/src/Mvc/Mvc.Core/src/OutputCacheAttribute.cs +++ b/src/Mvc/Mvc.Core/src/OutputCacheAttribute.cs @@ -10,15 +10,14 @@ namespace Microsoft.AspNetCore.Mvc; /// Specifies the parameters necessary for setting appropriate headers in output caching. /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] -public class OutputCacheAttribute : Attribute, IOrderedFilter, IPoliciesMetadata +public class OutputCacheAttribute : Attribute, IOrderedFilter, IOutputCachePolicy { // A nullable-int cannot be used as an Attribute parameter. // Hence this nullable-int is present to back the Duration property. // The same goes for nullable-ResponseCacheLocation and nullable-bool. private int? _duration; - private bool? _noStore; - - private IOutputCachingPolicy? _policy; + private bool? _noCache; + private IOutputCachePolicy? _policy; /// /// Gets or sets the duration in seconds for which the response is cached. @@ -30,13 +29,13 @@ public int Duration } /// - /// Gets or sets the value which determines whether the data should be stored or not. + /// Gets or sets the value which determines whether the reponse should be cached or not. /// When set to , the response won't be cached. /// - public bool NoStore + public bool NoCache { - get => _noStore ?? false; - set => _noStore = value; + get => _noCache ?? false; + set => _noCache = value; } /// @@ -55,16 +54,33 @@ public bool NoStore /// public int Order { get; set; } - /// - public IOutputCachingPolicy Policy => _policy ??= GetPolicy(); + Task IOutputCachePolicy.OnRequestAsync(OutputCacheContext context) + { + return GetPolicy().OnRequestAsync(context); + } + + Task IOutputCachePolicy.OnServeFromCacheAsync(OutputCacheContext context) + { + return GetPolicy().OnServeFromCacheAsync(context); + } - private IOutputCachingPolicy GetPolicy() + Task IOutputCachePolicy.OnServeResponseAsync(OutputCacheContext context) { + return GetPolicy().OnServeResponseAsync(context); + } + + private IOutputCachePolicy GetPolicy() + { + if (_policy != null) + { + return _policy; + } + var builder = new OutputCachePolicyBuilder(); - if (_noStore != null && _noStore.Value) + if (_noCache != null && _noCache.Value) { - builder.NoStore(); + builder.NoCache(); } if (PolicyName != null) @@ -82,6 +98,6 @@ private IOutputCachingPolicy GetPolicy() builder.Expire(TimeSpan.FromSeconds(_duration.Value)); } - return builder; + return _policy = builder; } } diff --git a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt index d50a40462ecb..e29c21bd5a3e 100644 --- a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt +++ b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt @@ -16,12 +16,11 @@ Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.ValidationMetadata.ValidationMode Microsoft.AspNetCore.Mvc.OutputCacheAttribute Microsoft.AspNetCore.Mvc.OutputCacheAttribute.Duration.get -> int Microsoft.AspNetCore.Mvc.OutputCacheAttribute.Duration.set -> void -Microsoft.AspNetCore.Mvc.OutputCacheAttribute.NoStore.get -> bool -Microsoft.AspNetCore.Mvc.OutputCacheAttribute.NoStore.set -> void +Microsoft.AspNetCore.Mvc.OutputCacheAttribute.NoCache.get -> bool +Microsoft.AspNetCore.Mvc.OutputCacheAttribute.NoCache.set -> void Microsoft.AspNetCore.Mvc.OutputCacheAttribute.Order.get -> int Microsoft.AspNetCore.Mvc.OutputCacheAttribute.Order.set -> void Microsoft.AspNetCore.Mvc.OutputCacheAttribute.OutputCacheAttribute() -> void -Microsoft.AspNetCore.Mvc.OutputCacheAttribute.Policy.get -> Microsoft.AspNetCore.OutputCaching.IOutputCachingPolicy! Microsoft.AspNetCore.Mvc.OutputCacheAttribute.PolicyName.get -> string? Microsoft.AspNetCore.Mvc.OutputCacheAttribute.PolicyName.set -> void Microsoft.AspNetCore.Mvc.OutputCacheAttribute.VaryByQueryKeys.get -> string![]? From 47b7f49743fea6e8530665e877a54739407243af Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Mon, 20 Jun 2022 16:15:38 -0700 Subject: [PATCH 41/48] Apply API review changes --- .../samples/OutputCachingSample/Startup.cs | 10 +- .../OutputCaching/src/CachedVaryByRules.cs | 17 +-- .../OutputCaching/src/IOutputCacheFeature.cs | 10 +- ...Provider.cs => IOutputCacheKeyProvider.cs} | 2 +- .../OutputCaching/src/IOutputCachePolicy.cs | 10 +- .../OutputCaching/src/IOutputCacheStore.cs | 7 +- .../src/Memory/MemoryOutputCacheResponse.cs | 19 --- .../src/Memory/MemoryOutputCacheStore.cs | 40 ++---- ...utputCacheApplicationBuilderExtensions.cs} | 2 +- .../OutputCaching/src/OutputCacheAttribute.cs | 37 ++---- .../OutputCaching/src/OutputCacheContext.cs | 4 +- .../OutputCaching/src/OutputCacheEntry.cs | 4 +- .../src/OutputCacheEntryFormatter.cs | 81 ++++++++++++ .../OutputCaching/src/OutputCacheFeature.cs | 6 +- .../src/OutputCacheKeyProvider.cs | 2 +- .../src/OutputCacheMiddleware.cs | 101 ++++++++++---- .../OutputCaching/src/OutputCacheOptions.cs | 34 +++-- .../src/OutputCachePolicyBuilder.cs | 80 +++++------- .../src/OutputCachePolicyProvider.cs | 123 ------------------ ...OutputCacheServiceCollectionExtensions.cs} | 8 +- .../src/Policies/CompositePolicy.cs | 12 +- .../src/Policies/DefaultOutputCachePolicy.cs | 6 +- .../src/Policies/EnableCachePolicy.cs | 6 +- .../src/Policies/ExpirationPolicy.cs | 6 +- .../src/Policies/LockingPolicy.cs | 6 +- .../src/Policies/NoLookupPolicy.cs | 6 +- .../src/Policies/NoStorePolicy.cs | 6 +- ...OutputCacheConventionBuilderExtensions.cs} | 5 +- .../src/Policies/PoliciesCollection.cs | 59 --------- .../src/Policies/PredicatePolicy.cs | 12 +- .../src/Policies/ProfilePolicy.cs | 12 +- .../OutputCaching/src/Policies/TagsPolicy.cs | 6 +- .../OutputCaching/src/Policies/TypedPolicy.cs | 12 +- .../src/Policies/VaryByHeaderPolicy.cs | 6 +- .../src/Policies/VaryByQueryPolicy.cs | 6 +- .../src/Policies/VaryByValuePolicy.cs | 24 ++-- .../src/Properties/AssemblyInfo.cs | 1 - .../OutputCaching/src/PublicAPI.Unshipped.txt | 77 ++++------- .../src/Serialization/FormatterEntry.cs | 12 ++ .../FormatterEntrySerializerContext.cs | 12 ++ .../test/OutputCachingMiddlewareTests.cs | 35 ++--- .../test/OutputCachingPolicyProviderTests.cs | 88 ++++++------- .../OutputCaching/test/OutputCachingTests.cs | 28 ++-- .../OutputCaching/test/TestUtils.cs | 37 +++--- src/Mvc/Mvc.Core/src/OutputCacheAttribute.cs | 103 --------------- src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt | 12 -- .../MvcSandbox/Controllers/HomeController.cs | 1 - .../MvcSandbox/Views/Home/Index.cshtml | 1 - 48 files changed, 473 insertions(+), 721 deletions(-) rename src/Middleware/OutputCaching/src/{IOutputCachingKeyProvider.cs => IOutputCacheKeyProvider.cs} (90%) delete mode 100644 src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheResponse.cs rename src/Middleware/OutputCaching/src/{OutputCacheExtensions.cs => OutputCacheApplicationBuilderExtensions.cs} (92%) create mode 100644 src/Middleware/OutputCaching/src/OutputCacheEntryFormatter.cs delete mode 100644 src/Middleware/OutputCaching/src/OutputCachePolicyProvider.cs rename src/Middleware/OutputCaching/src/{OutputCacheServicesExtensions.cs => OutputCacheServiceCollectionExtensions.cs} (86%) rename src/Middleware/OutputCaching/src/Policies/{PolicyExtensions.cs => OutputCacheConventionBuilderExtensions.cs} (92%) delete mode 100644 src/Middleware/OutputCaching/src/Policies/PoliciesCollection.cs create mode 100644 src/Middleware/OutputCaching/src/Serialization/FormatterEntry.cs create mode 100644 src/Middleware/OutputCaching/src/Serialization/FormatterEntrySerializerContext.cs delete mode 100644 src/Mvc/Mvc.Core/src/OutputCacheAttribute.cs diff --git a/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs b/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs index 7b2fb9dd982b..c5003a86cec9 100644 --- a/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs +++ b/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs @@ -6,11 +6,11 @@ var builder = WebApplication.CreateBuilder(args); -builder.Services.AddOutputCaching(options => +builder.Services.AddOutputCache(options => { // Define policies for all requests which are not configured per endpoint or per request - options.BasePolicies.AddPolicy(b => b.With(c => c.HttpContext.Request.Path.StartsWithSegments("/js")).Expire(TimeSpan.FromDays(1)).AddPolicy()); - options.BasePolicies.AddPolicy(b => b.With(c => c.HttpContext.Request.Path.StartsWithSegments("/js")).NoCache()); + options.AddBasePolicy(builder => builder.With(c => c.HttpContext.Request.Path.StartsWithSegments("/js")).Expire(TimeSpan.FromDays(1))); + options.AddBasePolicy(builder => builder.With(c => c.HttpContext.Request.Path.StartsWithSegments("/js")).NoCache()); options.AddPolicy("NoCache", b => b.NoCache()); }); @@ -31,7 +31,7 @@ var blog = app.MapGroup("blog").CacheOutput(x => x.Tag("blog")); blog.MapGet("/", Gravatar.WriteGravatar); -blog.MapGet("/post/{id}", Gravatar.WriteGravatar).CacheOutput(x => x.Tag("byid")); // check we get the two tags. Or if we need to get the last one +blog.MapGet("/post/{id}", Gravatar.WriteGravatar).CacheOutput(x => x.Tag("blog", "byid")); // Calling CacheOutput() here overwrites the group's policy app.MapPost("/purge/{tag}", async (IOutputCacheStore cache, string tag) => { @@ -43,7 +43,7 @@ // Cached entries will vary by culture, but any other additional query is ignored and returns the same cached content app.MapGet("/query", Gravatar.WriteGravatar).CacheOutput(p => p.VaryByQuery("culture")); -app.MapGet("/vary", Gravatar.WriteGravatar).CacheOutput(c => c.VaryByValue((context) => ("time", (DateTime.Now.Second % 2).ToString(CultureInfo.InvariantCulture)))); +app.MapGet("/vary", Gravatar.WriteGravatar).CacheOutput(c => c.VaryByValue((context) => new KeyValuePair("time", (DateTime.Now.Second % 2).ToString(CultureInfo.InvariantCulture)))); long requests = 0; diff --git a/src/Middleware/OutputCaching/src/CachedVaryByRules.cs b/src/Middleware/OutputCaching/src/CachedVaryByRules.cs index d83fc9c903c9..6ad24052e153 100644 --- a/src/Middleware/OutputCaching/src/CachedVaryByRules.cs +++ b/src/Middleware/OutputCaching/src/CachedVaryByRules.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Linq; using Microsoft.Extensions.Primitives; namespace Microsoft.AspNetCore.OutputCaching; @@ -8,20 +9,16 @@ namespace Microsoft.AspNetCore.OutputCaching; /// /// Represents vary-by rules. /// -public class CachedVaryByRules +public sealed class CachedVaryByRules { - internal Dictionary? VaryByCustom; + private Dictionary? _varyByCustom; + + internal bool HasVaryByCustom => _varyByCustom != null && _varyByCustom.Any(); /// - /// Defines a custom key-pair value to vary the cache by. + /// Gets a dictionary of key-pair values to vary the cache by. /// - /// The key. - /// The value. - public void SetVaryByCustom(string key, string value) - { - VaryByCustom ??= new(); - VaryByCustom[key] = value; - } + public IDictionary VaryByCustom => _varyByCustom ??= new(); /// /// Gets or sets the list of headers to vary by. diff --git a/src/Middleware/OutputCaching/src/IOutputCacheFeature.cs b/src/Middleware/OutputCaching/src/IOutputCacheFeature.cs index 4722a86077ca..db651bd7c0c1 100644 --- a/src/Middleware/OutputCaching/src/IOutputCacheFeature.cs +++ b/src/Middleware/OutputCaching/src/IOutputCacheFeature.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.AspNetCore.OutputCaching.Policies; - namespace Microsoft.AspNetCore.OutputCaching; /// @@ -11,13 +9,7 @@ namespace Microsoft.AspNetCore.OutputCaching; public interface IOutputCacheFeature { /// - /// Gets the caching context. + /// Gets the cache context. /// OutputCacheContext Context { get; } - - // Remove - /// - /// Gets the policies. - /// - PoliciesCollection Policies { get; } } diff --git a/src/Middleware/OutputCaching/src/IOutputCachingKeyProvider.cs b/src/Middleware/OutputCaching/src/IOutputCacheKeyProvider.cs similarity index 90% rename from src/Middleware/OutputCaching/src/IOutputCachingKeyProvider.cs rename to src/Middleware/OutputCaching/src/IOutputCacheKeyProvider.cs index c74d1217787b..e86cf6c6797b 100644 --- a/src/Middleware/OutputCaching/src/IOutputCachingKeyProvider.cs +++ b/src/Middleware/OutputCaching/src/IOutputCacheKeyProvider.cs @@ -3,7 +3,7 @@ namespace Microsoft.AspNetCore.OutputCaching; -internal interface IOutputCachingKeyProvider +internal interface IOutputCacheKeyProvider { /// /// Create a key for storing cached responses. diff --git a/src/Middleware/OutputCaching/src/IOutputCachePolicy.cs b/src/Middleware/OutputCaching/src/IOutputCachePolicy.cs index 17ebe8f20aac..3f8a0fb7b6c9 100644 --- a/src/Middleware/OutputCaching/src/IOutputCachePolicy.cs +++ b/src/Middleware/OutputCaching/src/IOutputCachePolicy.cs @@ -12,19 +12,19 @@ public interface IOutputCachePolicy /// Updates the before the cache middleware is invoked. /// At that point the cache middleware can still be enabled or disabled for the request. /// - /// The current request's caching context. - Task OnRequestAsync(OutputCacheContext context); + /// The current request's cache context. + Task CacheRequestAsync(OutputCacheContext context); /// /// Updates the before the cached response is used. /// At that point the freshness of the cached response can be updated. /// - /// The current request's caching context. - Task OnServeFromCacheAsync(OutputCacheContext context); + /// The current request's cache context. + Task ServeFromCacheAsync(OutputCacheContext context); /// /// Updates the before the response is served and can be cached. /// At that point cacheability of the response can be updated. /// - Task OnServeResponseAsync(OutputCacheContext context); + Task ServeResponseAsync(OutputCacheContext context); } diff --git a/src/Middleware/OutputCaching/src/IOutputCacheStore.cs b/src/Middleware/OutputCaching/src/IOutputCacheStore.cs index afb5d390d093..2170fc7789d1 100644 --- a/src/Middleware/OutputCaching/src/IOutputCacheStore.cs +++ b/src/Middleware/OutputCaching/src/IOutputCacheStore.cs @@ -22,14 +22,15 @@ public interface IOutputCacheStore /// The cache key to look up. /// Indicates that the operation should be cancelled. /// The response cache entry if it exists; otherwise null. - ValueTask GetAsync(string key, CancellationToken token); + ValueTask GetAsync(string key, CancellationToken token); /// /// Stores the given response in the response cache. /// /// The cache key to store the response under. - /// The response cache entry to store. + /// The response cache entry to store. + /// The tags associated with the cache entry to store. /// The amount of time the entry will be kept in the cache before expiring, relative to now. /// Indicates that the operation should be cancelled. - ValueTask SetAsync(string key, OutputCacheEntry entry, TimeSpan validFor, CancellationToken token); + ValueTask SetAsync(string key, byte[] value, string[] tags, TimeSpan validFor, CancellationToken token); } diff --git a/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheResponse.cs b/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheResponse.cs deleted file mode 100644 index 09cbfca30bc7..000000000000 --- a/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheResponse.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.AspNetCore.Http; - -namespace Microsoft.AspNetCore.OutputCaching.Memory; - -internal sealed class MemoryOutputCacheResponse -{ - public DateTimeOffset Created { get; set; } - - public int StatusCode { get; set; } - - public IHeaderDictionary Headers { get; set; } = new HeaderDictionary(); - - public CachedResponseBody Body { get; set; } = default!; - - public string[] Tags { get; set; } = default!; -} diff --git a/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs b/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs index 43549d6f054b..daf0b9300513 100644 --- a/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs +++ b/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs @@ -31,32 +31,19 @@ public ValueTask EvictByTagAsync(string tag, CancellationToken token) return ValueTask.CompletedTask; } - public ValueTask GetAsync(string key, CancellationToken token) + /// + public ValueTask GetAsync(string key, CancellationToken token) { - var entry = _cache.Get(key); - - if (entry is MemoryOutputCacheResponse memoryCachedResponse) - { - var outputCacheEntry = new OutputCacheEntry - { - Created = memoryCachedResponse.Created, - StatusCode = memoryCachedResponse.StatusCode, - Headers = memoryCachedResponse.Headers, - Body = memoryCachedResponse.Body, - Tags = memoryCachedResponse.Tags - }; - - return ValueTask.FromResult(outputCacheEntry); - } - - return ValueTask.FromResult(default(OutputCacheEntry)); + var entry = _cache.Get(key) as byte[]; + return ValueTask.FromResult(entry); } - public ValueTask SetAsync(string key, OutputCacheEntry cachedResponse, TimeSpan validFor, CancellationToken token) + /// + public ValueTask SetAsync(string key, byte[] value, string[] tags, TimeSpan validFor, CancellationToken token) { - if (cachedResponse.Tags != null) + if (tags != null) { - foreach (var tag in cachedResponse.Tags) + foreach (var tag in tags) { var keys = _taggedEntries.GetOrAdd(tag, _ => new HashSet()); @@ -73,18 +60,11 @@ public ValueTask SetAsync(string key, OutputCacheEntry cachedResponse, TimeSpan _cache.Set( key, - new MemoryOutputCacheResponse - { - Created = cachedResponse.Created, - StatusCode = cachedResponse.StatusCode, - Headers = cachedResponse.Headers, - Body = cachedResponse.Body, - Tags = cachedResponse.Tags ?? Array.Empty() - }, + value, new MemoryCacheEntryOptions { AbsoluteExpirationRelativeToNow = validFor, - Size = CacheEntryHelpers.EstimateCachedResponseSize(cachedResponse) + Size = value.Length }); return ValueTask.CompletedTask; diff --git a/src/Middleware/OutputCaching/src/OutputCacheExtensions.cs b/src/Middleware/OutputCaching/src/OutputCacheApplicationBuilderExtensions.cs similarity index 92% rename from src/Middleware/OutputCaching/src/OutputCacheExtensions.cs rename to src/Middleware/OutputCaching/src/OutputCacheApplicationBuilderExtensions.cs index fba63af35ec4..4d6caa87d0cc 100644 --- a/src/Middleware/OutputCaching/src/OutputCacheExtensions.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheApplicationBuilderExtensions.cs @@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.Builder; /// /// Extension methods for adding the to an application. /// -public static class OutputCacheExtensions +public static class OutputCacheApplicationBuilderExtensions { /// /// Adds the for caching HTTP responses. diff --git a/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs b/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs index 2ac31b487b19..97b92fb8745a 100644 --- a/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs @@ -7,7 +7,7 @@ namespace Microsoft.AspNetCore.OutputCaching; /// Specifies the parameters necessary for setting appropriate headers in output caching. /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] -public class OutputCacheAttribute : Attribute, IOutputCachePolicy +public class OutputCacheAttribute : Attribute { // A nullable-int cannot be used as an Attribute parameter. // Hence this nullable-int is present to back the Duration property. @@ -15,7 +15,7 @@ public class OutputCacheAttribute : Attribute, IOutputCachePolicy private int? _duration; private bool? _noCache; - private IOutputCachePolicy? _policy; + private IOutputCachePolicy? _builtPolicy; /// /// Gets or sets the duration in seconds for which the response is cached. @@ -23,7 +23,7 @@ public class OutputCacheAttribute : Attribute, IOutputCachePolicy public int Duration { get => _duration ?? 0; - set => _duration = value; + init => _duration = value; } /// @@ -33,7 +33,7 @@ public int Duration public bool NoCache { get => _noCache ?? false; - set => _noCache = value; + init => _noCache = value; } /// @@ -42,7 +42,7 @@ public bool NoCache /// /// requires the output cache middleware. /// - public string[]? VaryByQueryKeys { get; set; } + public string[]? VaryByQueryKeys { get; init; } /// /// Gets or sets the headers to vary by. @@ -50,33 +50,18 @@ public bool NoCache /// /// requires the output cache middleware. /// - public string[]? VaryByHeaders { get; set; } + public string[]? VaryByHeaders { get; init; } /// /// Gets or sets the value of the cache policy name. /// - public string? PolicyName { get; set; } + public string? PolicyName { get; init; } - Task IOutputCachePolicy.OnRequestAsync(OutputCacheContext context) + internal IOutputCachePolicy BuildPolicy() { - return GetPolicy().OnRequestAsync(context); - } - - Task IOutputCachePolicy.OnServeFromCacheAsync(OutputCacheContext context) - { - return GetPolicy().OnServeFromCacheAsync(context); - } - - Task IOutputCachePolicy.OnServeResponseAsync(OutputCacheContext context) - { - return GetPolicy().OnServeResponseAsync(context); - } - - private IOutputCachePolicy GetPolicy() - { - if (_policy != null) + if (_builtPolicy != null) { - return _policy; + return _builtPolicy; } var builder = new OutputCachePolicyBuilder(); @@ -101,6 +86,6 @@ private IOutputCachePolicy GetPolicy() builder.Expire(TimeSpan.FromSeconds(_duration.Value)); } - return _policy = builder.Build(); + return _builtPolicy = builder.Build(); } } diff --git a/src/Middleware/OutputCaching/src/OutputCacheContext.cs b/src/Middleware/OutputCaching/src/OutputCacheContext.cs index 8c8406f8d897..0471c68acb9e 100644 --- a/src/Middleware/OutputCaching/src/OutputCacheContext.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheContext.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.OutputCaching; /// -/// Represent the current caching context for the request. +/// Represent the current cache context for the request. /// public sealed class OutputCacheContext { @@ -80,7 +80,7 @@ internal OutputCacheContext(HttpContext httpContext, IOutputCacheStore store, Ou internal Stream OriginalResponseStream { get; set; } - internal OutputCacheStream OutputCachingStream { get; set; } + internal OutputCacheStream OutputCacheStream { get; set; } internal ILogger Logger { get; } internal OutputCacheOptions Options { get; } internal IOutputCacheStore Store { get; } diff --git a/src/Middleware/OutputCaching/src/OutputCacheEntry.cs b/src/Middleware/OutputCaching/src/OutputCacheEntry.cs index e466a98e7cdd..ce77ac671a25 100644 --- a/src/Middleware/OutputCaching/src/OutputCacheEntry.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheEntry.cs @@ -6,7 +6,7 @@ namespace Microsoft.AspNetCore.OutputCaching; /// -public sealed class OutputCacheEntry +internal sealed class OutputCacheEntry { /// /// Gets the created date and time of the cache entry. @@ -21,7 +21,7 @@ public sealed class OutputCacheEntry /// /// Gets the headers of the cache entry. /// - public IHeaderDictionary Headers { get; set; } = default!; + public HeaderDictionary Headers { get; set; } = default!; /// /// Gets the body of the cache entry. diff --git a/src/Middleware/OutputCaching/src/OutputCacheEntryFormatter.cs b/src/Middleware/OutputCaching/src/OutputCacheEntryFormatter.cs new file mode 100644 index 000000000000..d806ebaccae1 --- /dev/null +++ b/src/Middleware/OutputCaching/src/OutputCacheEntryFormatter.cs @@ -0,0 +1,81 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using System.Text.Json; +using Microsoft.AspNetCore.OutputCaching.Serialization; + +namespace Microsoft.AspNetCore.OutputCaching; +/// +/// Formats instance to match structures supported by the implementations. +/// +internal class OutputCacheEntryFormatter +{ + public static async ValueTask GetAsync(string key, IOutputCacheStore store, CancellationToken token) + { + ArgumentNullException.ThrowIfNull(key); + + var content = await store.GetAsync(key, token); + + if (content == null) + { + return null; + } + + using var br = new MemoryStream(content); + + var formatter = await JsonSerializer.DeserializeAsync(br, FormatterEntrySerializerContext.Default.FormatterEntry, cancellationToken: token); + + if (formatter == null) + { + return null; + } + + var outputCacheEntry = new OutputCacheEntry + { + StatusCode = formatter.StatusCode, + Created = formatter.Created, + Tags = formatter.Tags + }; + + if (formatter.Headers != null) + { + outputCacheEntry.Headers = new(); + + foreach (var header in formatter.Headers) + { + outputCacheEntry.Headers.TryAdd(header.Key, header.Value); + } + } + var cachedResponseBody = new CachedResponseBody(formatter.Body, formatter.Body.Sum(x => x.Length)); + outputCacheEntry.Body = cachedResponseBody; + return outputCacheEntry; + } + + public static async ValueTask StoreAsync(string key, OutputCacheEntry value, TimeSpan duration, IOutputCacheStore store, CancellationToken token) + { + ArgumentNullException.ThrowIfNull(value); + + var formatterEntry = new FormatterEntry + { + StatusCode = value.StatusCode, + Created = value.Created, + Tags = value.Tags, + Body = value.Body.Segments + }; + + if (value.Headers != null) + { + formatterEntry.Headers = new(); + foreach (var header in value.Headers) + { + formatterEntry.Headers.TryAdd(header.Key, header.Value.ToArray()); + } + } + + using var br = new MemoryStream(); + + await JsonSerializer.SerializeAsync(br, formatterEntry, FormatterEntrySerializerContext.Default.FormatterEntry, token); + await store.SetAsync(key, br.ToArray(), value.Tags ?? Array.Empty(), duration, token); + } +} diff --git a/src/Middleware/OutputCaching/src/OutputCacheFeature.cs b/src/Middleware/OutputCaching/src/OutputCacheFeature.cs index b774e928feee..b1ce32b97a69 100644 --- a/src/Middleware/OutputCaching/src/OutputCacheFeature.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheFeature.cs @@ -1,11 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.AspNetCore.OutputCaching.Policies; - namespace Microsoft.AspNetCore.OutputCaching; -internal class OutputCacheFeature : IOutputCacheFeature +internal sealed class OutputCacheFeature : IOutputCacheFeature { public OutputCacheFeature(OutputCacheContext context) { @@ -13,6 +11,4 @@ public OutputCacheFeature(OutputCacheContext context) } public OutputCacheContext Context { get; } - - public PoliciesCollection Policies { get; } = new(); } diff --git a/src/Middleware/OutputCaching/src/OutputCacheKeyProvider.cs b/src/Middleware/OutputCaching/src/OutputCacheKeyProvider.cs index 04705fd80fed..9af98b3ff657 100644 --- a/src/Middleware/OutputCaching/src/OutputCacheKeyProvider.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheKeyProvider.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.OutputCaching; -internal sealed class OutputCacheKeyProvider : IOutputCachingKeyProvider +internal sealed class OutputCacheKeyProvider : IOutputCacheKeyProvider { // Use the record separator for delimiting components of the cache key to avoid possible collisions private const char KeyDelimiter = '\x1e'; diff --git a/src/Middleware/OutputCaching/src/OutputCacheMiddleware.cs b/src/Middleware/OutputCaching/src/OutputCacheMiddleware.cs index f9c00606e1f9..f5cfc5f91c8b 100644 --- a/src/Middleware/OutputCaching/src/OutputCacheMiddleware.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheMiddleware.cs @@ -23,9 +23,8 @@ internal class OutputCacheMiddleware private readonly RequestDelegate _next; private readonly OutputCacheOptions _options; private readonly ILogger _logger; - private readonly OutputCachePolicyProvider _policyProvider; private readonly IOutputCacheStore _store; - private readonly IOutputCachingKeyProvider _keyProvider; + private readonly IOutputCacheKeyProvider _keyProvider; private readonly WorkDispatcher _outputCacheEntryDispatcher; private readonly WorkDispatcher _requestDispatcher; @@ -58,7 +57,7 @@ internal OutputCacheMiddleware( IOptions options, ILoggerFactory loggerFactory, IOutputCacheStore cache, - IOutputCachingKeyProvider keyProvider) + IOutputCacheKeyProvider keyProvider) { ArgumentNullException.ThrowIfNull(next); ArgumentNullException.ThrowIfNull(options); @@ -73,7 +72,6 @@ internal OutputCacheMiddleware( _keyProvider = keyProvider; _outputCacheEntryDispatcher = new(); _requestDispatcher = new(); - _policyProvider = new(_options); } /// @@ -84,7 +82,7 @@ internal OutputCacheMiddleware( public async Task Invoke(HttpContext httpContext) { // Skip the middleware if there is no policy for the current request - if (!_policyProvider.HasPolicies(httpContext)) + if (!TryGetRequestPolicies(httpContext, out var policies)) { await _next(httpContext); return; @@ -97,7 +95,10 @@ public async Task Invoke(HttpContext httpContext) try { - await _policyProvider.OnRequestAsync(context); + foreach (var policy in policies) + { + await policy.CacheRequestAsync(context); + } // Should we attempt any caching logic? if (context.EnableOutputCaching) @@ -105,7 +106,7 @@ public async Task Invoke(HttpContext httpContext) // Can this request be served from cache? if (context.AllowCacheLookup) { - if (await TryServeFromCacheAsync(context)) + if (await TryServeFromCacheAsync(context, policies)) { return; } @@ -127,7 +128,7 @@ public async Task Invoke(HttpContext httpContext) // If the result was processed by another request, serve it from cache if (!executed) { - if (await TryServeFromCacheAsync(context)) + if (await TryServeFromCacheAsync(context, policies)) { return; } @@ -148,7 +149,10 @@ public async Task Invoke(HttpContext httpContext) await _next(httpContext); // The next middleware might change the policy - await _policyProvider.OnServeResponseAsync(context); + foreach (var policy in policies) + { + await policy.ServeResponseAsync(context); + } // If there was no response body, check the response headers now. We can cache things like redirects. StartResponse(context); @@ -175,11 +179,49 @@ public async Task Invoke(HttpContext httpContext) } finally { - RemoveOutputCachingFeature(httpContext); + RemoveOutputCacheFeature(httpContext); } } - internal async Task TryServeCachedResponseAsync(OutputCacheContext context, OutputCacheEntry? cacheEntry) + internal bool TryGetRequestPolicies(HttpContext httpContext, out IReadOnlyList policies) + { + policies = Array.Empty(); + List? result = null; + + if (_options.BasePolicies != null) + { + result = new(); + result.AddRange(_options.BasePolicies); + } + + var metadata = httpContext.GetEndpoint()?.Metadata; + + var policy = metadata?.GetMetadata(); + + if (policy != null) + { + result ??= new(); + result.Add(policy); + } + + var attribute = metadata?.GetMetadata(); + + if (attribute != null) + { + result ??= new(); + result.Add(attribute.BuildPolicy()); + } + + if (result != null) + { + policies = result; + return true; + } + + return false; + } + + internal async Task TryServeCachedResponseAsync(OutputCacheContext context, OutputCacheEntry? cacheEntry, IReadOnlyList policies) { if (cacheEntry == null) { @@ -191,7 +233,10 @@ internal async Task TryServeCachedResponseAsync(OutputCacheContext context var cacheEntryAge = context.ResponseTime.Value - context.CachedResponse.Created; context.CachedEntryAge = cacheEntryAge > TimeSpan.Zero ? cacheEntryAge : TimeSpan.Zero; - await _policyProvider.OnServeFromCacheAsync(context); + foreach (var policy in policies) + { + await policy.ServeFromCacheAsync(context); + } context.IsCacheEntryFresh = true; @@ -259,7 +304,7 @@ internal async Task TryServeCachedResponseAsync(OutputCacheContext context return false; } - internal async Task TryServeFromCacheAsync(OutputCacheContext cacheContext) + internal async Task TryServeFromCacheAsync(OutputCacheContext cacheContext, IReadOnlyList policies) { CreateCacheKey(cacheContext); @@ -267,9 +312,9 @@ internal async Task TryServeFromCacheAsync(OutputCacheContext cacheContext // TODO: should it be part of the cache implementations or can we assume all caches would benefit from it? // It makes sense for caches that use IO (disk, network) or need to deserialize the state but could also be a global option - var cacheEntry = await _outputCacheEntryDispatcher.ScheduleAsync(cacheContext.CacheKey, cacheContext, static async (key, cacheContext) => await cacheContext.Store.GetAsync(key, cacheContext.HttpContext.RequestAborted)); + var cacheEntry = await _outputCacheEntryDispatcher.ScheduleAsync(cacheContext.CacheKey, cacheContext, static async (key, cacheContext) => await OutputCacheEntryFormatter.GetAsync(key, cacheContext.Store, cacheContext.HttpContext.RequestAborted)); - if (await TryServeCachedResponseAsync(cacheContext, cacheEntry)) + if (await TryServeCachedResponseAsync(cacheContext, cacheEntry, policies)) { return true; } @@ -361,7 +406,7 @@ internal void FinalizeCacheHeaders(OutputCacheContext context) return; } - context.OutputCachingStream.DisableBuffering(); + context.OutputCacheStream.DisableBuffering(); } /// @@ -369,13 +414,13 @@ internal void FinalizeCacheHeaders(OutputCacheContext context) /// internal async ValueTask FinalizeCacheBodyAsync(OutputCacheContext context) { - if (context.AllowCacheStorage && context.OutputCachingStream.BufferingEnabled) + if (context.AllowCacheStorage && context.OutputCacheStream.BufferingEnabled) { // If AllowCacheLookup is false, the cache key was not created CreateCacheKey(context); var contentLength = context.HttpContext.Response.ContentLength; - var cachedResponseBody = context.OutputCachingStream.GetCachedResponseBody(); + var cachedResponseBody = context.OutputCacheStream.GetCachedResponseBody(); if (!contentLength.HasValue || contentLength == cachedResponseBody.Length || (cachedResponseBody.Length == 0 && HttpMethods.IsHead(context.HttpContext.Request.Method))) @@ -395,7 +440,7 @@ internal async ValueTask FinalizeCacheBodyAsync(OutputCacheContext context) throw new InvalidOperationException("Cache key must be defined"); } - await _store.SetAsync(context.CacheKey, context.CachedResponse, context.CachedResponseValidFor, context.HttpContext.RequestAborted); + await OutputCacheEntryFormatter.StoreAsync(context.CacheKey, context.CachedResponse, context.CachedResponseValidFor, _store, context.HttpContext.RequestAborted); } else { @@ -447,15 +492,15 @@ internal void ShimResponseStream(OutputCacheContext context) { // Shim response stream context.OriginalResponseStream = context.HttpContext.Response.Body; - context.OutputCachingStream = new OutputCacheStream( + context.OutputCacheStream = new OutputCacheStream( context.OriginalResponseStream, _options.MaximumBodySize, StreamUtilities.BodySegmentSize, () => StartResponse(context)); - context.HttpContext.Response.Body = context.OutputCachingStream; + context.HttpContext.Response.Body = context.OutputCacheStream; } - internal static void RemoveOutputCachingFeature(HttpContext context) => + internal static void RemoveOutputCacheFeature(HttpContext context) => context.Features.Set(null); internal static void UnshimResponseStream(OutputCacheContext context) @@ -464,7 +509,7 @@ internal static void UnshimResponseStream(OutputCacheContext context) context.HttpContext.Response.Body = context.OriginalResponseStream; // Remove IOutputCachingFeature - RemoveOutputCachingFeature(context.HttpContext); + RemoveOutputCacheFeature(context.HttpContext); } internal static bool ContentIsNotModified(OutputCacheContext context) @@ -480,11 +525,11 @@ internal static bool ContentIsNotModified(OutputCacheContext context) return true; } - if (!StringValues.IsNullOrEmpty(cachedResponseHeaders.ETag) - && EntityTagHeaderValue.TryParse(cachedResponseHeaders.ETag.ToString(), out var eTag) + if (!StringValues.IsNullOrEmpty(cachedResponseHeaders[HeaderNames.ETag]) + && EntityTagHeaderValue.TryParse(cachedResponseHeaders[HeaderNames.ETag].ToString(), out var eTag) && EntityTagHeaderValue.TryParseList(ifNoneMatchHeader, out var ifNoneMatchEtags)) { - for (var i = 0; i < ifNoneMatchEtags.Count; i++) + for (var i = 0; i < ifNoneMatchEtags?.Count; i++) { var requestETag = ifNoneMatchEtags[i]; if (eTag.Compare(requestETag, useStrongComparison: false)) @@ -500,8 +545,8 @@ internal static bool ContentIsNotModified(OutputCacheContext context) var ifModifiedSince = context.HttpContext.Request.Headers.IfModifiedSince; if (!StringValues.IsNullOrEmpty(ifModifiedSince)) { - if (!HeaderUtilities.TryParseDate(cachedResponseHeaders.LastModified.ToString(), out var modified) && - !HeaderUtilities.TryParseDate(cachedResponseHeaders.Date.ToString(), out modified)) + if (!HeaderUtilities.TryParseDate(cachedResponseHeaders[HeaderNames.LastModified].ToString(), out var modified) && + !HeaderUtilities.TryParseDate(cachedResponseHeaders[HeaderNames.Date].ToString(), out modified)) { return false; } diff --git a/src/Middleware/OutputCaching/src/OutputCacheOptions.cs b/src/Middleware/OutputCaching/src/OutputCacheOptions.cs index 5006cc0a9da1..fdd316d4d3be 100644 --- a/src/Middleware/OutputCaching/src/OutputCacheOptions.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheOptions.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.ComponentModel; -using Microsoft.AspNetCore.OutputCaching.Policies; namespace Microsoft.AspNetCore.OutputCaching; @@ -34,17 +33,14 @@ public class OutputCacheOptions /// public bool UseCaseSensitivePaths { get; set; } - /// - /// Gets the policy applied to all requests. - /// - public PoliciesCollection BasePolicies { get; internal set; } = new(); - /// /// Gets the application . /// - public IServiceProvider ApplicationServices { get; set; } = default!; + public IServiceProvider ApplicationServices { get; internal set; } = default!; - internal IDictionary? NamedPolicies { get; set; } + internal Dictionary? NamedPolicies { get; set; } + + internal List? BasePolicies { get; set; } /// /// For testing purposes only. @@ -75,4 +71,26 @@ public void AddPolicy(string name, Action build) NamedPolicies ??= new Dictionary(StringComparer.OrdinalIgnoreCase); NamedPolicies[name] = builder.Build(); } + + /// + /// Adds an instance to base policies. + /// + /// The policy to add + public void AddBasePolicy(IOutputCachePolicy policy) + { + BasePolicies ??= new(); + BasePolicies.Add(policy); + } + + /// + /// Builds and adds an instance to base policies. + /// + /// an action on . + public void AddBasePolicy(Action build) + { + var builder = new OutputCachePolicyBuilder(); + build(builder); + BasePolicies ??= new(); + BasePolicies.Add(builder.Build()); + } } diff --git a/src/Middleware/OutputCaching/src/OutputCachePolicyBuilder.cs b/src/Middleware/OutputCaching/src/OutputCachePolicyBuilder.cs index 390501b1d6be..1ef23e0f7f2b 100644 --- a/src/Middleware/OutputCaching/src/OutputCachePolicyBuilder.cs +++ b/src/Middleware/OutputCaching/src/OutputCachePolicyBuilder.cs @@ -11,13 +11,13 @@ namespace Microsoft.AspNetCore.OutputCaching; /// /// Provides helper methods to create custom policies. /// -public sealed class OutputCachePolicyBuilder : IOutputCachePolicy +public sealed class OutputCachePolicyBuilder { private const DynamicallyAccessedMemberTypes ActivatorAccessibility = DynamicallyAccessedMemberTypes.PublicConstructors; private IOutputCachePolicy? _builtPolicy; private readonly List _policies = new(); - private List>>? _requirements; + private List>>? _requirements; /// /// Creates a new instance. @@ -29,20 +29,30 @@ public OutputCachePolicyBuilder() } /// - /// Adds a policy instance. + /// Adds a dynamically resolved policy. /// - public OutputCachePolicyBuilder AddPolicy(IOutputCachePolicy policy) + /// The type of policy to add + public OutputCachePolicyBuilder AddPolicy([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type policyType) { _builtPolicy = null; - _policies.Add(policy); + _policies.Add(new TypedPolicy(policyType)); return this; } + /// + /// Adds a dynamically resolved policy. + /// + /// The policy type. + public OutputCachePolicyBuilder AddPolicy<[DynamicallyAccessedMembers(ActivatorAccessibility)] T>() where T : IOutputCachePolicy + { + return AddPolicy(typeof(T)); + } + /// /// Adds a requirement to the current policy. /// /// The predicate applied to the policy. - public OutputCachePolicyBuilder With(Func> predicate) + public OutputCachePolicyBuilder With(Func> predicate) { ArgumentNullException.ThrowIfNull(predicate); @@ -62,14 +72,14 @@ public OutputCachePolicyBuilder With(Func predicate) _builtPolicy = null; _requirements ??= new(); - _requirements.Add(c => Task.FromResult(predicate(c))); + _requirements.Add((c, t) => Task.FromResult(predicate(c))); return this; } /// /// Adds a policy to vary the cached responses by query strings. /// - /// The query keys to vary the cached responses by. + /// The query keys to vary the cached responses by. Leave empty to ignore all query strings. /// /// By default all query keys vary the cache entries. However when specific query keys are specified only these are then taken into account. /// @@ -99,7 +109,7 @@ public OutputCachePolicyBuilder VaryByHeader(params string[] headers) /// Adds a policy to vary the cached responses by custom values. /// /// The value to vary the cached responses by. - public OutputCachePolicyBuilder VaryByValue(Func> varyBy) + public OutputCachePolicyBuilder VaryByValue(Func> varyBy) { ArgumentNullException.ThrowIfNull(varyBy); @@ -112,7 +122,7 @@ public OutputCachePolicyBuilder VaryByValue(Func> vary /// Adds a policy to vary the cached responses by custom key/value. /// /// The key/value to vary the cached responses by. - public OutputCachePolicyBuilder VaryByValue(Func> varyBy) + public OutputCachePolicyBuilder VaryByValue(Func>> varyBy) { ArgumentNullException.ThrowIfNull(varyBy); @@ -138,7 +148,7 @@ public OutputCachePolicyBuilder VaryByValue(Func varyBy) /// Adds a policy to vary the cached responses by custom key/value. /// /// The key/value to vary the cached responses by. - public OutputCachePolicyBuilder VaryByValue(Func varyBy) + public OutputCachePolicyBuilder VaryByValue(Func> varyBy) { ArgumentNullException.ThrowIfNull(varyBy); @@ -198,6 +208,7 @@ public OutputCachePolicyBuilder AllowLocking(bool lockResponse = true) /// /// Clears the current policies. /// + /// It also removed the default cache policy. public OutputCachePolicyBuilder Clear() { _builtPolicy = null; @@ -223,6 +234,10 @@ public OutputCachePolicyBuilder NoCache() return this; } + /// + /// Creates the . + /// + /// The instance. internal IOutputCachePolicy Build() { if (_builtPolicy != null) @@ -230,16 +245,19 @@ internal IOutputCachePolicy Build() return _builtPolicy; } - var policies = new CompositePolicy(_policies.ToArray()); + var policies = _policies.Count == 1 + ? _policies[0] + : new CompositePolicy(_policies.ToArray()) + ; // If the policy was built with requirements, wrap it if (_requirements != null && _requirements.Any()) { - return new PredicatePolicy(async c => + policies = new PredicatePolicy(async c => { foreach (var r in _requirements) { - if (!await r(c)) + if (!await r(c, c.HttpContext.RequestAborted)) { return false; } @@ -251,38 +269,4 @@ internal IOutputCachePolicy Build() return _builtPolicy = policies; } - - /// - /// Adds a dynamically resolved policy. - /// - /// The type of policy to add - public OutputCachePolicyBuilder AddPolicy([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type policyType) - { - AddPolicy(new TypedPolicy(policyType)); - return this; - } - - /// - /// Adds a dynamically resolved policy. - /// - /// The policy type. - public OutputCachePolicyBuilder AddPolicy<[DynamicallyAccessedMembers(ActivatorAccessibility)] T>() where T : IOutputCachePolicy - { - return AddPolicy(typeof(T)); - } - - Task IOutputCachePolicy.OnRequestAsync(OutputCacheContext context) - { - return Build().OnRequestAsync(context); - } - - Task IOutputCachePolicy.OnServeFromCacheAsync(OutputCacheContext context) - { - return Build().OnServeFromCacheAsync(context); - } - - Task IOutputCachePolicy.OnServeResponseAsync(OutputCacheContext context) - { - return Build().OnServeResponseAsync(context); - } } diff --git a/src/Middleware/OutputCaching/src/OutputCachePolicyProvider.cs b/src/Middleware/OutputCaching/src/OutputCachePolicyProvider.cs deleted file mode 100644 index af9635dd1eaa..000000000000 --- a/src/Middleware/OutputCaching/src/OutputCachePolicyProvider.cs +++ /dev/null @@ -1,123 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Linq; -using Microsoft.AspNetCore.Http; - -namespace Microsoft.AspNetCore.OutputCaching; - -internal sealed class OutputCachePolicyProvider -{ - private readonly OutputCacheOptions _options; - - public OutputCachePolicyProvider(OutputCacheOptions options) - { - _options = options; - } - - public bool HasPolicies(HttpContext httpContext) - { - if (_options.BasePolicies != null) - { - return true; - } - - // Remove check - if (httpContext.Features.Get()?.Policies.Any() ?? false) - { - return true; - } - - if (httpContext.GetEndpoint()?.Metadata.GetMetadata() != null) - { - return true; - } - - return false; - } - - public async Task OnRequestAsync(OutputCacheContext context) - { - if (_options.BasePolicies != null) - { - foreach (var policy in _options.BasePolicies) - { - await policy.OnRequestAsync(context); - } - } - - var policiesMetadata = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata(); - - if (policiesMetadata != null) - { - // TODO: Log only? - - if (context.HttpContext.Response.HasStarted) - { - throw new InvalidOperationException("Can't define output caching policies after headers have been sent to client."); - } - - await policiesMetadata.OnRequestAsync(context); - } - } - - public async Task OnServeFromCacheAsync(OutputCacheContext context) - { - if (_options.BasePolicies != null) - { - foreach (var policy in _options.BasePolicies) - { - await policy.OnServeFromCacheAsync(context); - } - } - - // Apply response policies defined on the feature, e.g. from action attributes - - var responsePolicies = context.HttpContext.Features.Get()?.Policies; - - if (responsePolicies != null) - { - foreach (var policy in responsePolicies) - { - await policy.OnServeFromCacheAsync(context); - } - } - - var policiesMetadata = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata(); - - if (policiesMetadata != null) - { - await policiesMetadata.OnServeFromCacheAsync(context); - } - } - - public async Task OnServeResponseAsync(OutputCacheContext context) - { - if (_options.BasePolicies != null) - { - foreach (var policy in _options.BasePolicies) - { - await policy.OnServeResponseAsync(context); - } - } - - // Apply response policies defined on the feature, e.g. from action attributes - - var responsePolicies = context.HttpContext.Features.Get()?.Policies; - - if (responsePolicies != null) - { - foreach (var policy in responsePolicies) - { - await policy.OnServeResponseAsync(context); - } - } - - var policiesMetadata = context.HttpContext.GetEndpoint()?.Metadata.GetMetadata(); - - if (policiesMetadata != null) - { - await policiesMetadata.OnServeResponseAsync(context); - } - } -} diff --git a/src/Middleware/OutputCaching/src/OutputCacheServicesExtensions.cs b/src/Middleware/OutputCaching/src/OutputCacheServiceCollectionExtensions.cs similarity index 86% rename from src/Middleware/OutputCaching/src/OutputCacheServicesExtensions.cs rename to src/Middleware/OutputCaching/src/OutputCacheServiceCollectionExtensions.cs index 1348d7766416..b0184e1339a2 100644 --- a/src/Middleware/OutputCaching/src/OutputCacheServicesExtensions.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheServiceCollectionExtensions.cs @@ -13,14 +13,14 @@ namespace Microsoft.Extensions.DependencyInjection; /// /// Extension methods for the OutputCaching middleware. /// -public static class OutputCacheServicesExtensions +public static class OutputCacheServiceCollectionExtensions { /// /// Add output caching services. /// /// The for adding services. /// - public static IServiceCollection AddOutputCaching(this IServiceCollection services) + public static IServiceCollection AddOutputCache(this IServiceCollection services) { ArgumentNullException.ThrowIfNull(services); @@ -45,13 +45,13 @@ public static IServiceCollection AddOutputCaching(this IServiceCollection servic /// The for adding services. /// A delegate to configure the . /// - public static IServiceCollection AddOutputCaching(this IServiceCollection services, Action configureOptions) + public static IServiceCollection AddOutputCache(this IServiceCollection services, Action configureOptions) { ArgumentNullException.ThrowIfNull(services); ArgumentNullException.ThrowIfNull(configureOptions); services.Configure(configureOptions); - services.AddOutputCaching(); + services.AddOutputCache(); return services; } diff --git a/src/Middleware/OutputCaching/src/Policies/CompositePolicy.cs b/src/Middleware/OutputCaching/src/Policies/CompositePolicy.cs index 47420aacff20..2c2ba2fa67c9 100644 --- a/src/Middleware/OutputCaching/src/Policies/CompositePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/CompositePolicy.cs @@ -20,29 +20,29 @@ public CompositePolicy(params IOutputCachePolicy[] policies!!) } /// - async Task IOutputCachePolicy.OnRequestAsync(OutputCacheContext context) + async Task IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) { foreach (var policy in _policies) { - await policy.OnRequestAsync(context); + await policy.CacheRequestAsync(context); } } /// - async Task IOutputCachePolicy.OnServeFromCacheAsync(OutputCacheContext context) + async Task IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) { foreach (var policy in _policies) { - await policy.OnServeFromCacheAsync(context); + await policy.ServeFromCacheAsync(context); } } /// - async Task IOutputCachePolicy.OnServeResponseAsync(OutputCacheContext context) + async Task IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) { foreach (var policy in _policies) { - await policy.OnServeResponseAsync(context); + await policy.ServeResponseAsync(context); } } } diff --git a/src/Middleware/OutputCaching/src/Policies/DefaultOutputCachePolicy.cs b/src/Middleware/OutputCaching/src/Policies/DefaultOutputCachePolicy.cs index 96aaa273c1ad..4a5c7628494a 100644 --- a/src/Middleware/OutputCaching/src/Policies/DefaultOutputCachePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/DefaultOutputCachePolicy.cs @@ -18,7 +18,7 @@ private DefaultOutputCachePolicy() } /// - Task IOutputCachePolicy.OnRequestAsync(OutputCacheContext context) + Task IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) { var attemptOutputCaching = AttemptOutputCaching(context); context.EnableOutputCaching = true; @@ -33,13 +33,13 @@ Task IOutputCachePolicy.OnRequestAsync(OutputCacheContext context) } /// - Task IOutputCachePolicy.OnServeFromCacheAsync(OutputCacheContext context) + Task IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) { return Task.CompletedTask; } /// - Task IOutputCachePolicy.OnServeResponseAsync(OutputCacheContext context) + Task IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) { var response = context.HttpContext.Response; diff --git a/src/Middleware/OutputCaching/src/Policies/EnableCachePolicy.cs b/src/Middleware/OutputCaching/src/Policies/EnableCachePolicy.cs index 920db57de904..98e7e138a7ec 100644 --- a/src/Middleware/OutputCaching/src/Policies/EnableCachePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/EnableCachePolicy.cs @@ -16,7 +16,7 @@ private EnableCachePolicy() } /// - Task IOutputCachePolicy.OnRequestAsync(OutputCacheContext context) + Task IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) { context.EnableOutputCaching = this == Enabled; @@ -24,13 +24,13 @@ Task IOutputCachePolicy.OnRequestAsync(OutputCacheContext context) } /// - Task IOutputCachePolicy.OnServeResponseAsync(OutputCacheContext context) + Task IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) { return Task.CompletedTask; } /// - Task IOutputCachePolicy.OnServeFromCacheAsync(OutputCacheContext context) + Task IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) { return Task.CompletedTask; } diff --git a/src/Middleware/OutputCaching/src/Policies/ExpirationPolicy.cs b/src/Middleware/OutputCaching/src/Policies/ExpirationPolicy.cs index c5e453e16f5d..1f75adbbfef8 100644 --- a/src/Middleware/OutputCaching/src/Policies/ExpirationPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/ExpirationPolicy.cs @@ -20,7 +20,7 @@ public ExpirationPolicy(TimeSpan expiration) } /// - Task IOutputCachePolicy.OnRequestAsync(OutputCacheContext context) + Task IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) { context.ResponseExpirationTimeSpan = _expiration; @@ -28,13 +28,13 @@ Task IOutputCachePolicy.OnRequestAsync(OutputCacheContext context) } /// - Task IOutputCachePolicy.OnServeFromCacheAsync(OutputCacheContext context) + Task IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) { return Task.CompletedTask; } /// - Task IOutputCachePolicy.OnServeResponseAsync(OutputCacheContext context) + Task IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) { return Task.CompletedTask; } diff --git a/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs b/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs index d0a3470021b7..96d134aef0ab 100644 --- a/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs @@ -26,7 +26,7 @@ private LockingPolicy(bool lockResponse) public static readonly LockingPolicy Disabled = new(false); /// - Task IOutputCachePolicy.OnRequestAsync(OutputCacheContext context) + Task IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) { context.AllowLocking = _lockResponse; @@ -34,13 +34,13 @@ Task IOutputCachePolicy.OnRequestAsync(OutputCacheContext context) } /// - Task IOutputCachePolicy.OnServeFromCacheAsync(OutputCacheContext context) + Task IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) { return Task.CompletedTask; } /// - Task IOutputCachePolicy.OnServeResponseAsync(OutputCacheContext context) + Task IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) { return Task.CompletedTask; } diff --git a/src/Middleware/OutputCaching/src/Policies/NoLookupPolicy.cs b/src/Middleware/OutputCaching/src/Policies/NoLookupPolicy.cs index 85d0806d12f2..49ccd38b9bbc 100644 --- a/src/Middleware/OutputCaching/src/Policies/NoLookupPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/NoLookupPolicy.cs @@ -15,7 +15,7 @@ private NoLookupPolicy() } /// - Task IOutputCachePolicy.OnServeResponseAsync(OutputCacheContext context) + Task IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) { context.AllowCacheLookup = false; @@ -23,13 +23,13 @@ Task IOutputCachePolicy.OnServeResponseAsync(OutputCacheContext context) } /// - Task IOutputCachePolicy.OnServeFromCacheAsync(OutputCacheContext context) + Task IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) { return Task.CompletedTask; } /// - Task IOutputCachePolicy.OnRequestAsync(OutputCacheContext context) + Task IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) { return Task.CompletedTask; } diff --git a/src/Middleware/OutputCaching/src/Policies/NoStorePolicy.cs b/src/Middleware/OutputCaching/src/Policies/NoStorePolicy.cs index 79d05b0b456c..1a45ffd63f4d 100644 --- a/src/Middleware/OutputCaching/src/Policies/NoStorePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/NoStorePolicy.cs @@ -15,7 +15,7 @@ private NoStorePolicy() } /// - Task IOutputCachePolicy.OnServeResponseAsync(OutputCacheContext context) + Task IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) { context.AllowCacheStorage = false; @@ -23,13 +23,13 @@ Task IOutputCachePolicy.OnServeResponseAsync(OutputCacheContext context) } /// - Task IOutputCachePolicy.OnServeFromCacheAsync(OutputCacheContext context) + Task IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) { return Task.CompletedTask; } /// - Task IOutputCachePolicy.OnRequestAsync(OutputCacheContext context) + Task IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) { return Task.CompletedTask; } diff --git a/src/Middleware/OutputCaching/src/Policies/PolicyExtensions.cs b/src/Middleware/OutputCaching/src/Policies/OutputCacheConventionBuilderExtensions.cs similarity index 92% rename from src/Middleware/OutputCaching/src/Policies/PolicyExtensions.cs rename to src/Middleware/OutputCaching/src/Policies/OutputCacheConventionBuilderExtensions.cs index b9022999803f..bff780f254ea 100644 --- a/src/Middleware/OutputCaching/src/Policies/PolicyExtensions.cs +++ b/src/Middleware/OutputCaching/src/Policies/OutputCacheConventionBuilderExtensions.cs @@ -2,13 +2,14 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.OutputCaching; -namespace Microsoft.AspNetCore.OutputCaching; +namespace Microsoft.Extensions.DependencyInjection; /// /// A set of endpoint extension methods. /// -public static class PolicyExtensions +public static class OutputCacheConventionBuilderExtensions { /// /// Marks an endpoint to be cached with the default policy. diff --git a/src/Middleware/OutputCaching/src/Policies/PoliciesCollection.cs b/src/Middleware/OutputCaching/src/Policies/PoliciesCollection.cs deleted file mode 100644 index 433a296c9b53..000000000000 --- a/src/Middleware/OutputCaching/src/Policies/PoliciesCollection.cs +++ /dev/null @@ -1,59 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Collections; -using System.Linq; - -namespace Microsoft.AspNetCore.OutputCaching.Policies; - -/// -/// A collection of policies. -/// -public sealed class PoliciesCollection : IReadOnlyCollection -{ - private List? _policies; - - /// - public int Count => _policies == null ? 0 : _policies.Count; - - /// - /// Adds an instance. - /// - /// The policy - public void AddPolicy(IOutputCachePolicy policy) - { - ArgumentNullException.ThrowIfNull(policy); - - _policies ??= new(); - _policies.Add(policy); - } - - /// - /// Adds instance. - /// - public void AddPolicy(Action build) - { - var builder = new OutputCachePolicyBuilder(); - build(builder); - AddPolicy(builder.Build()); - } - - /// - /// Clears the collection. - /// - public void Clear() - { - _policies = null; - } - - /// - public IEnumerator GetEnumerator() - { - return _policies == null ? Enumerable.Empty().GetEnumerator() : _policies.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } -} diff --git a/src/Middleware/OutputCaching/src/Policies/PredicatePolicy.cs b/src/Middleware/OutputCaching/src/Policies/PredicatePolicy.cs index 3b2ce2997776..d3599a873714 100644 --- a/src/Middleware/OutputCaching/src/Policies/PredicatePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/PredicatePolicy.cs @@ -25,21 +25,21 @@ public PredicatePolicy(Func> predicate, IOutputCa } /// - Task IOutputCachePolicy.OnRequestAsync(OutputCacheContext context) + Task IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) { - return ExecuteAwaited(static (policy, context) => policy.OnRequestAsync(context), _policy, context); + return ExecuteAwaited(static (policy, context) => policy.CacheRequestAsync(context), _policy, context); } /// - Task IOutputCachePolicy.OnServeFromCacheAsync(OutputCacheContext context) + Task IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) { - return ExecuteAwaited(static (policy, context) => policy.OnServeFromCacheAsync(context), _policy, context); + return ExecuteAwaited(static (policy, context) => policy.ServeFromCacheAsync(context), _policy, context); } /// - Task IOutputCachePolicy.OnServeResponseAsync(OutputCacheContext context) + Task IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) { - return ExecuteAwaited(static (policy, context) => policy.OnServeResponseAsync(context), _policy, context); + return ExecuteAwaited(static (policy, context) => policy.ServeResponseAsync(context), _policy, context); } private Task ExecuteAwaited(Func action, IOutputCachePolicy policy, OutputCacheContext context) diff --git a/src/Middleware/OutputCaching/src/Policies/ProfilePolicy.cs b/src/Middleware/OutputCaching/src/Policies/ProfilePolicy.cs index a270a1c9d453..8f3acfd9b3f2 100644 --- a/src/Middleware/OutputCaching/src/Policies/ProfilePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/ProfilePolicy.cs @@ -20,7 +20,7 @@ public ProfilePolicy(string profileName) } /// - Task IOutputCachePolicy.OnServeResponseAsync(OutputCacheContext context) + Task IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) { var policy = GetProfilePolicy(context); @@ -29,11 +29,11 @@ Task IOutputCachePolicy.OnServeResponseAsync(OutputCacheContext context) return Task.CompletedTask; } - return policy.OnServeResponseAsync(context); + return policy.ServeResponseAsync(context); } /// - Task IOutputCachePolicy.OnServeFromCacheAsync(OutputCacheContext context) + Task IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) { var policy = GetProfilePolicy(context); @@ -42,11 +42,11 @@ Task IOutputCachePolicy.OnServeFromCacheAsync(OutputCacheContext context) return Task.CompletedTask; } - return policy.OnServeFromCacheAsync(context); + return policy.ServeFromCacheAsync(context); } /// - Task IOutputCachePolicy.OnRequestAsync(OutputCacheContext context) + Task IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) { var policy = GetProfilePolicy(context); @@ -55,7 +55,7 @@ Task IOutputCachePolicy.OnRequestAsync(OutputCacheContext context) return Task.CompletedTask; } - return policy.OnRequestAsync(context); ; + return policy.CacheRequestAsync(context); ; } internal IOutputCachePolicy? GetProfilePolicy(OutputCacheContext context) diff --git a/src/Middleware/OutputCaching/src/Policies/TagsPolicy.cs b/src/Middleware/OutputCaching/src/Policies/TagsPolicy.cs index 416cfde94405..3ddfec717c1c 100644 --- a/src/Middleware/OutputCaching/src/Policies/TagsPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/TagsPolicy.cs @@ -20,7 +20,7 @@ public TagsPolicy(params string[] tags) } /// - Task IOutputCachePolicy.OnRequestAsync(OutputCacheContext context) + Task IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) { foreach (var tag in _tags) { @@ -31,13 +31,13 @@ Task IOutputCachePolicy.OnRequestAsync(OutputCacheContext context) } /// - Task IOutputCachePolicy.OnServeFromCacheAsync(OutputCacheContext context) + Task IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) { return Task.CompletedTask; } /// - Task IOutputCachePolicy.OnServeResponseAsync(OutputCacheContext context) + Task IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) { return Task.CompletedTask; } diff --git a/src/Middleware/OutputCaching/src/Policies/TypedPolicy.cs b/src/Middleware/OutputCaching/src/Policies/TypedPolicy.cs index d47219c2c621..68e7fcba6e9c 100644 --- a/src/Middleware/OutputCaching/src/Policies/TypedPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/TypedPolicy.cs @@ -33,20 +33,20 @@ public TypedPolicy([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Pu } /// - Task IOutputCachePolicy.OnRequestAsync(OutputCacheContext context) + Task IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) { - return CreatePolicy(context)?.OnRequestAsync(context) ?? Task.CompletedTask; + return CreatePolicy(context)?.CacheRequestAsync(context) ?? Task.CompletedTask; } /// - Task IOutputCachePolicy.OnServeFromCacheAsync(OutputCacheContext context) + Task IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) { - return CreatePolicy(context)?.OnServeFromCacheAsync(context) ?? Task.CompletedTask; + return CreatePolicy(context)?.ServeFromCacheAsync(context) ?? Task.CompletedTask; } /// - Task IOutputCachePolicy.OnServeResponseAsync(OutputCacheContext context) + Task IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) { - return CreatePolicy(context)?.OnServeResponseAsync(context) ?? Task.CompletedTask; + return CreatePolicy(context)?.ServeResponseAsync(context) ?? Task.CompletedTask; } } diff --git a/src/Middleware/OutputCaching/src/Policies/VaryByHeaderPolicy.cs b/src/Middleware/OutputCaching/src/Policies/VaryByHeaderPolicy.cs index 547e1054ddb0..92c57fa710e6 100644 --- a/src/Middleware/OutputCaching/src/Policies/VaryByHeaderPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/VaryByHeaderPolicy.cs @@ -36,7 +36,7 @@ public VaryByHeaderPolicy(params string[] headers) } /// - Task IOutputCachePolicy.OnRequestAsync(OutputCacheContext context) + Task IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) { // No vary by header? if (_headers.Count == 0) @@ -51,13 +51,13 @@ Task IOutputCachePolicy.OnRequestAsync(OutputCacheContext context) } /// - Task IOutputCachePolicy.OnServeFromCacheAsync(OutputCacheContext context) + Task IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) { return Task.CompletedTask; } /// - Task IOutputCachePolicy.OnServeResponseAsync(OutputCacheContext context) + Task IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) { return Task.CompletedTask; } diff --git a/src/Middleware/OutputCaching/src/Policies/VaryByQueryPolicy.cs b/src/Middleware/OutputCaching/src/Policies/VaryByQueryPolicy.cs index 467d568a0eeb..2fa48099c09e 100644 --- a/src/Middleware/OutputCaching/src/Policies/VaryByQueryPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/VaryByQueryPolicy.cs @@ -37,7 +37,7 @@ public VaryByQueryPolicy(params string[] queryKeys) } /// - Task IOutputCachePolicy.OnRequestAsync(OutputCacheContext context) + Task IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) { // No vary by query? if (_queryKeys.Count == 0) @@ -59,13 +59,13 @@ Task IOutputCachePolicy.OnRequestAsync(OutputCacheContext context) } /// - Task IOutputCachePolicy.OnServeFromCacheAsync(OutputCacheContext context) + Task IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) { return Task.CompletedTask; } /// - Task IOutputCachePolicy.OnServeResponseAsync(OutputCacheContext context) + Task IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) { return Task.CompletedTask; } diff --git a/src/Middleware/OutputCaching/src/Policies/VaryByValuePolicy.cs b/src/Middleware/OutputCaching/src/Policies/VaryByValuePolicy.cs index 12ac17a678a4..14c02c336daa 100644 --- a/src/Middleware/OutputCaching/src/Policies/VaryByValuePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/VaryByValuePolicy.cs @@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.OutputCaching; internal sealed class VaryByValuePolicy : IOutputCachePolicy { private readonly Action? _varyBy; - private readonly Func? _varyByAsync; + private readonly Func? _varyByAsync; /// /// Creates a policy that doesn't vary the cached content based on values. @@ -31,51 +31,51 @@ public VaryByValuePolicy(Func varyBy) /// /// Creates a policy that vary the cached content based on the specified value. /// - public VaryByValuePolicy(Func> varyBy) + public VaryByValuePolicy(Func> varyBy) { - _varyByAsync = async (context, rules) => rules.VaryByPrefix += await varyBy(context); + _varyByAsync = async (context, rules, token) => rules.VaryByPrefix += await varyBy(context, token); } /// /// Creates a policy that vary the cached content based on the specified value. /// - public VaryByValuePolicy(Func varyBy) + public VaryByValuePolicy(Func> varyBy) { _varyBy = (context, rules) => { var result = varyBy(context); - rules.VaryByCustom?.TryAdd(result.Item1, result.Item2); + rules.VaryByCustom?.TryAdd(result.Key, result.Value); }; } /// /// Creates a policy that vary the cached content based on the specified value. /// - public VaryByValuePolicy(Func> varyBy) + public VaryByValuePolicy(Func>> varyBy) { _varyBy = async (context, rules) => { - var result = await varyBy(context); - rules.VaryByCustom?.TryAdd(result.Item1, result.Item2); + var result = await varyBy(context, context.RequestAborted); + rules.VaryByCustom?.TryAdd(result.Key, result.Value); }; } /// - Task IOutputCachePolicy.OnRequestAsync(OutputCacheContext context) + Task IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) { _varyBy?.Invoke(context.HttpContext, context.CachedVaryByRules); - return _varyByAsync?.Invoke(context.HttpContext, context.CachedVaryByRules) ?? Task.CompletedTask; + return _varyByAsync?.Invoke(context.HttpContext, context.CachedVaryByRules, context.HttpContext.RequestAborted) ?? Task.CompletedTask; } /// - Task IOutputCachePolicy.OnServeFromCacheAsync(OutputCacheContext context) + Task IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) { return Task.CompletedTask; } /// - Task IOutputCachePolicy.OnServeResponseAsync(OutputCacheContext context) + Task IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) { return Task.CompletedTask; } diff --git a/src/Middleware/OutputCaching/src/Properties/AssemblyInfo.cs b/src/Middleware/OutputCaching/src/Properties/AssemblyInfo.cs index b2ae4af0f142..a185ed9b3c63 100644 --- a/src/Middleware/OutputCaching/src/Properties/AssemblyInfo.cs +++ b/src/Middleware/OutputCaching/src/Properties/AssemblyInfo.cs @@ -4,4 +4,3 @@ using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Microsoft.AspNetCore.OutputCaching.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] -[assembly: InternalsVisibleTo("OutputCachingSample, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt b/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt index f6c49bdec992..32884134f11f 100644 --- a/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt @@ -1,5 +1,5 @@ #nullable enable -Microsoft.AspNetCore.Builder.OutputCacheExtensions +Microsoft.AspNetCore.Builder.OutputCacheApplicationBuilderExtensions Microsoft.AspNetCore.OutputCaching.CachedResponseBody Microsoft.AspNetCore.OutputCaching.CachedResponseBody.CachedResponseBody(System.Collections.Generic.List! segments, long length) -> void Microsoft.AspNetCore.OutputCaching.CachedResponseBody.CopyToAsync(System.IO.Pipelines.PipeWriter! destination, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! @@ -11,48 +11,38 @@ Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.Headers.get -> Microsoft.Ex Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.Headers.set -> void Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.QueryKeys.get -> Microsoft.Extensions.Primitives.StringValues Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.QueryKeys.set -> void -Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.SetVaryByCustom(string! key, string! value) -> void +Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.VaryByCustom.get -> System.Collections.Generic.IDictionary! Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.VaryByPrefix.get -> Microsoft.Extensions.Primitives.StringValues Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.VaryByPrefix.set -> void Microsoft.AspNetCore.OutputCaching.IOutputCacheFeature Microsoft.AspNetCore.OutputCaching.IOutputCacheFeature.Context.get -> Microsoft.AspNetCore.OutputCaching.OutputCacheContext! -Microsoft.AspNetCore.OutputCaching.IOutputCacheFeature.Policies.get -> Microsoft.AspNetCore.OutputCaching.Policies.PoliciesCollection! +Microsoft.AspNetCore.OutputCaching.IOutputCachePolicy.CacheRequestAsync(Microsoft.AspNetCore.OutputCaching.OutputCacheContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.IOutputCachePolicy.ServeFromCacheAsync(Microsoft.AspNetCore.OutputCaching.OutputCacheContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.IOutputCachePolicy.ServeResponseAsync(Microsoft.AspNetCore.OutputCaching.OutputCacheContext! context) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.OutputCaching.IOutputCacheStore Microsoft.AspNetCore.OutputCaching.IOutputCacheStore.EvictByTagAsync(string! tag, System.Threading.CancellationToken token) -> System.Threading.Tasks.ValueTask -Microsoft.AspNetCore.OutputCaching.IOutputCacheStore.GetAsync(string! key, System.Threading.CancellationToken token) -> System.Threading.Tasks.ValueTask -Microsoft.AspNetCore.OutputCaching.IOutputCacheStore.SetAsync(string! key, Microsoft.AspNetCore.OutputCaching.OutputCacheEntry! entry, System.TimeSpan validFor, System.Threading.CancellationToken token) -> System.Threading.Tasks.ValueTask Microsoft.AspNetCore.OutputCaching.IOutputCachePolicy -Microsoft.AspNetCore.OutputCaching.IOutputCachePolicy.OnRequestAsync(Microsoft.AspNetCore.OutputCaching.OutputCacheContext! context) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.OutputCaching.IOutputCachePolicy.OnServeFromCacheAsync(Microsoft.AspNetCore.OutputCaching.OutputCacheContext! context) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.OutputCaching.IOutputCachePolicy.OnServeResponseAsync(Microsoft.AspNetCore.OutputCaching.OutputCacheContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.IOutputCacheStore.GetAsync(string! key, System.Threading.CancellationToken token) -> System.Threading.Tasks.ValueTask +Microsoft.AspNetCore.OutputCaching.IOutputCacheStore.SetAsync(string! key, byte[]! value, string![]! tags, System.TimeSpan validFor, System.Threading.CancellationToken token) -> System.Threading.Tasks.ValueTask Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.Duration.get -> int -Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.Duration.set -> void +Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.Duration.init -> void Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.NoCache.get -> bool -Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.NoCache.set -> void +Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.NoCache.init -> void Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.OutputCacheAttribute() -> void Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.PolicyName.get -> string? -Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.PolicyName.set -> void +Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.PolicyName.init -> void Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.VaryByHeaders.get -> string![]? -Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.VaryByHeaders.set -> void +Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.VaryByHeaders.init -> void Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.VaryByQueryKeys.get -> string![]? -Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.VaryByQueryKeys.set -> void +Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.VaryByQueryKeys.init -> void Microsoft.AspNetCore.OutputCaching.OutputCacheContext.EnableOutputCaching.get -> bool Microsoft.AspNetCore.OutputCaching.OutputCacheContext.EnableOutputCaching.set -> void -Microsoft.AspNetCore.OutputCaching.OutputCacheEntry -Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.Body.get -> Microsoft.AspNetCore.OutputCaching.CachedResponseBody! -Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.Body.set -> void -Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.Created.get -> System.DateTimeOffset -Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.Created.set -> void -Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.Headers.get -> Microsoft.AspNetCore.Http.IHeaderDictionary! -Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.Headers.set -> void -Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.OutputCacheEntry() -> void -Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.StatusCode.get -> int -Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.StatusCode.set -> void -Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.Tags.get -> string![]? -Microsoft.AspNetCore.OutputCaching.OutputCacheEntry.Tags.set -> void +Microsoft.AspNetCore.OutputCaching.OutputCacheOptions.AddBasePolicy(Microsoft.AspNetCore.OutputCaching.IOutputCachePolicy! policy) -> void +Microsoft.AspNetCore.OutputCaching.OutputCacheOptions.AddBasePolicy(System.Action! build) -> void +Microsoft.AspNetCore.OutputCaching.OutputCacheOptions.AddPolicy(string! name, Microsoft.AspNetCore.OutputCaching.IOutputCachePolicy! policy) -> void +Microsoft.AspNetCore.OutputCaching.OutputCacheOptions.AddPolicy(string! name, System.Action! build) -> void Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder -Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.AddPolicy(Microsoft.AspNetCore.OutputCaching.IOutputCachePolicy! policy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.AddPolicy(System.Type! policyType) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.AddPolicy() -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.AllowLocking(bool lockResponse = true) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! @@ -64,11 +54,11 @@ Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Policy(string! profi Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Tag(params string![]! tags) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByHeader(params string![]! headers) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByQuery(params string![]! queryKeys) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! -Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.Func! varyBy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! -Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.Func!>! varyBy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! -Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.Func!>! varyBy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.Func>! varyBy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.Func!>! varyBy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.Func! varyBy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! -Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.With(System.Func!>! predicate) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.Func>!>! varyBy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.With(System.Func!>! predicate) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCacheContext Microsoft.AspNetCore.OutputCaching.OutputCacheContext.AllowCacheLookup.get -> bool Microsoft.AspNetCore.OutputCaching.OutputCacheContext.AllowCacheLookup.set -> void @@ -80,11 +70,7 @@ Microsoft.AspNetCore.OutputCaching.OutputCacheContext.ResponseExpirationTimeSpan Microsoft.AspNetCore.OutputCaching.OutputCacheContext.ResponseExpirationTimeSpan.set -> void Microsoft.AspNetCore.OutputCaching.OutputCacheContext.ResponseTime.get -> System.DateTimeOffset? Microsoft.AspNetCore.OutputCaching.OutputCacheOptions -Microsoft.AspNetCore.OutputCaching.OutputCacheOptions.AddPolicy(string! name, Microsoft.AspNetCore.OutputCaching.IOutputCachePolicy! policy) -> void -Microsoft.AspNetCore.OutputCaching.OutputCacheOptions.AddPolicy(string! name, System.Action! build) -> void Microsoft.AspNetCore.OutputCaching.OutputCacheOptions.ApplicationServices.get -> System.IServiceProvider! -Microsoft.AspNetCore.OutputCaching.OutputCacheOptions.ApplicationServices.set -> void -Microsoft.AspNetCore.OutputCaching.OutputCacheOptions.BasePolicies.get -> Microsoft.AspNetCore.OutputCaching.Policies.PoliciesCollection! Microsoft.AspNetCore.OutputCaching.OutputCacheOptions.DefaultExpirationTimeSpan.get -> System.TimeSpan Microsoft.AspNetCore.OutputCaching.OutputCacheOptions.DefaultExpirationTimeSpan.set -> void Microsoft.AspNetCore.OutputCaching.OutputCacheOptions.MaximumBodySize.get -> long @@ -95,21 +81,14 @@ Microsoft.AspNetCore.OutputCaching.OutputCacheOptions.SizeLimit.set -> void Microsoft.AspNetCore.OutputCaching.OutputCacheOptions.UseCaseSensitivePaths.get -> bool Microsoft.AspNetCore.OutputCaching.OutputCacheOptions.UseCaseSensitivePaths.set -> void Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.With(System.Func! predicate) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! -Microsoft.AspNetCore.OutputCaching.Policies.PoliciesCollection -Microsoft.AspNetCore.OutputCaching.Policies.PoliciesCollection.AddPolicy(Microsoft.AspNetCore.OutputCaching.IOutputCachePolicy! policy) -> void -Microsoft.AspNetCore.OutputCaching.Policies.PoliciesCollection.AddPolicy(System.Action! build) -> void -Microsoft.AspNetCore.OutputCaching.Policies.PoliciesCollection.Clear() -> void -Microsoft.AspNetCore.OutputCaching.Policies.PoliciesCollection.Count.get -> int -Microsoft.AspNetCore.OutputCaching.Policies.PoliciesCollection.GetEnumerator() -> System.Collections.Generic.IEnumerator! -Microsoft.AspNetCore.OutputCaching.Policies.PoliciesCollection.PoliciesCollection() -> void -Microsoft.AspNetCore.OutputCaching.PolicyExtensions -Microsoft.Extensions.DependencyInjection.OutputCacheServicesExtensions -static Microsoft.AspNetCore.Builder.OutputCacheExtensions.UseOutputCache(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! -static Microsoft.AspNetCore.OutputCaching.PolicyExtensions.CacheOutput(this TBuilder builder) -> TBuilder -static Microsoft.AspNetCore.OutputCaching.PolicyExtensions.CacheOutput(this TBuilder builder, Microsoft.AspNetCore.OutputCaching.IOutputCachePolicy! policy) -> TBuilder -static Microsoft.AspNetCore.OutputCaching.PolicyExtensions.CacheOutput(this TBuilder builder, System.Action! policy) -> TBuilder -static Microsoft.Extensions.DependencyInjection.OutputCacheServicesExtensions.AddOutputCaching(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -static Microsoft.Extensions.DependencyInjection.OutputCacheServicesExtensions.AddOutputCaching(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +Microsoft.Extensions.DependencyInjection.OutputCacheConventionBuilderExtensions +Microsoft.Extensions.DependencyInjection.OutputCacheServiceCollectionExtensions +static Microsoft.AspNetCore.Builder.OutputCacheApplicationBuilderExtensions.UseOutputCache(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! +static Microsoft.Extensions.DependencyInjection.OutputCacheConventionBuilderExtensions.CacheOutput(this TBuilder builder) -> TBuilder +static Microsoft.Extensions.DependencyInjection.OutputCacheConventionBuilderExtensions.CacheOutput(this TBuilder builder, Microsoft.AspNetCore.OutputCaching.IOutputCachePolicy! policy) -> TBuilder +static Microsoft.Extensions.DependencyInjection.OutputCacheConventionBuilderExtensions.CacheOutput(this TBuilder builder, System.Action! policy) -> TBuilder +static Microsoft.Extensions.DependencyInjection.OutputCacheServiceCollectionExtensions.AddOutputCache(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Microsoft.Extensions.DependencyInjection.OutputCacheServiceCollectionExtensions.AddOutputCache(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! ~Microsoft.AspNetCore.OutputCaching.OutputCacheContext.CachedVaryByRules.get -> Microsoft.AspNetCore.OutputCaching.CachedVaryByRules ~Microsoft.AspNetCore.OutputCaching.OutputCacheContext.CachedVaryByRules.set -> void ~Microsoft.AspNetCore.OutputCaching.OutputCacheContext.HttpContext.get -> Microsoft.AspNetCore.Http.HttpContext diff --git a/src/Middleware/OutputCaching/src/Serialization/FormatterEntry.cs b/src/Middleware/OutputCaching/src/Serialization/FormatterEntry.cs new file mode 100644 index 000000000000..674806979144 --- /dev/null +++ b/src/Middleware/OutputCaching/src/Serialization/FormatterEntry.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.OutputCaching.Serialization; +internal class FormatterEntry +{ + public DateTimeOffset Created { get; set; } + public int StatusCode { get; set; } + public Dictionary Headers { get; set; } = new(); + public List Body { get; set; } = new(); + public string[]? Tags { get; set; } +} diff --git a/src/Middleware/OutputCaching/src/Serialization/FormatterEntrySerializerContext.cs b/src/Middleware/OutputCaching/src/Serialization/FormatterEntrySerializerContext.cs new file mode 100644 index 000000000000..6f4936740993 --- /dev/null +++ b/src/Middleware/OutputCaching/src/Serialization/FormatterEntrySerializerContext.cs @@ -0,0 +1,12 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json.Serialization; + +namespace Microsoft.AspNetCore.OutputCaching.Serialization; + +[JsonSourceGenerationOptions(WriteIndented = false)] +[JsonSerializable(typeof(FormatterEntry))] +internal partial class FormatterEntrySerializerContext : JsonSerializerContext +{ +} diff --git a/src/Middleware/OutputCaching/test/OutputCachingMiddlewareTests.cs b/src/Middleware/OutputCaching/test/OutputCachingMiddlewareTests.cs index 659b3f5a6c53..e240c9362a35 100644 --- a/src/Middleware/OutputCaching/test/OutputCachingMiddlewareTests.cs +++ b/src/Middleware/OutputCaching/test/OutputCachingMiddlewareTests.cs @@ -6,7 +6,6 @@ using Microsoft.AspNetCore.OutputCaching.Memory; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging.Testing; -using Microsoft.Extensions.Options; using Microsoft.Extensions.Primitives; using Microsoft.Net.Http.Headers; @@ -25,8 +24,9 @@ public async Task TryServeFromCacheAsync_OnlyIfCached_Serves504() { OnlyIfCached = true }.ToString(); + middleware.TryGetRequestPolicies(context.HttpContext, out var policies); - Assert.True(await middleware.TryServeFromCacheAsync(context)); + Assert.True(await middleware.TryServeFromCacheAsync(context, policies)); Assert.Equal(StatusCodes.Status504GatewayTimeout, context.HttpContext.Response.StatusCode); TestUtils.AssertLoggedMessages( sink.Writes, @@ -40,8 +40,9 @@ public async Task TryServeFromCacheAsync_CachedResponseNotFound_Fails() var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider("BaseKey")); var context = TestUtils.CreateTestContext(cache); + middleware.TryGetRequestPolicies(context.HttpContext, out var policies); - Assert.False(await middleware.TryServeFromCacheAsync(context)); + Assert.False(await middleware.TryServeFromCacheAsync(context, policies)); Assert.Equal(1, cache.GetCount); TestUtils.AssertLoggedMessages( sink.Writes, @@ -55,8 +56,9 @@ public async Task TryServeFromCacheAsync_CachedResponseFound_Succeeds() var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider("BaseKey")); var context = TestUtils.CreateTestContext(cache); + middleware.TryGetRequestPolicies(context.HttpContext, out var policies); - await cache.SetAsync( + await OutputCacheEntryFormatter.StoreAsync( "BaseKey", new OutputCacheEntry() { @@ -64,9 +66,10 @@ await cache.SetAsync( Body = new CachedResponseBody(new List(0), 0) }, TimeSpan.Zero, + cache, default); - Assert.True(await middleware.TryServeFromCacheAsync(context)); + Assert.True(await middleware.TryServeFromCacheAsync(context, policies)); Assert.Equal(1, cache.GetCount); TestUtils.AssertLoggedMessages( sink.Writes, @@ -80,11 +83,11 @@ public async Task TryServeFromCacheAsync_CachedResponseFound_OverwritesExistingH var sink = new TestSink(); var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider("BaseKey")); var context = TestUtils.CreateTestContext(cache); + middleware.TryGetRequestPolicies(context.HttpContext, out var policies); context.CacheKey = "BaseKey"; context.HttpContext.Response.Headers["MyHeader"] = "OldValue"; - await cache.SetAsync( - context.CacheKey, + await OutputCacheEntryFormatter.StoreAsync(context.CacheKey, new OutputCacheEntry() { Headers = new HeaderDictionary() @@ -94,9 +97,10 @@ await cache.SetAsync( Body = new CachedResponseBody(new List(0), 0) }, TimeSpan.Zero, + cache, default); - Assert.True(await middleware.TryServeFromCacheAsync(context)); + Assert.True(await middleware.TryServeFromCacheAsync(context, policies)); Assert.Equal("NewValue", context.HttpContext.Response.Headers["MyHeader"]); Assert.Equal(1, cache.GetCount); TestUtils.AssertLoggedMessages( @@ -112,17 +116,18 @@ public async Task TryServeFromCacheAsync_CachedResponseFound_Serves304IfPossible var middleware = TestUtils.CreateTestMiddleware(testSink: sink, cache: cache, keyProvider: new TestResponseCachingKeyProvider("BaseKey")); var context = TestUtils.CreateTestContext(cache); context.HttpContext.Request.Headers.IfNoneMatch = "*"; + middleware.TryGetRequestPolicies(context.HttpContext, out var policies); - await cache.SetAsync( - "BaseKey", + await OutputCacheEntryFormatter.StoreAsync("BaseKey", new OutputCacheEntry() { Body = new CachedResponseBody(new List(0), 0) }, TimeSpan.Zero, + cache, default); - Assert.True(await middleware.TryServeFromCacheAsync(context)); + Assert.True(await middleware.TryServeFromCacheAsync(context, policies)); Assert.Equal(1, cache.GetCount); TestUtils.AssertLoggedMessages( sink.Writes, @@ -558,7 +563,7 @@ public void FinalizeCacheHeadersAsync_StoresHeaders() middleware.FinalizeCacheHeaders(context); - Assert.Equal(new StringValues(new[] { "HeaderB, heaDera" }), context.CachedResponse.Headers.Vary); + Assert.Equal(new StringValues(new[] { "HeaderB, heaDera" }), context.CachedResponse.Headers[HeaderNames.Vary]); } [Fact] @@ -702,7 +707,7 @@ public async Task FinalizeCacheBody_DoNotCache_IfBufferingDisabled() middleware.ShimResponseStream(context); await context.HttpContext.Response.WriteAsync(new string('0', 10)); - context.OutputCachingStream.DisableBuffering(); + context.OutputCacheStream.DisableBuffering(); await middleware.FinalizeCacheBodyAsync(context); @@ -724,7 +729,7 @@ public async Task FinalizeCacheBody_DoNotCache_IfSizeTooBig() SizeLimit = 100 }))); var context = TestUtils.CreateTestContext(); - + middleware.TryGetRequestPolicies(context.HttpContext, out var policies); middleware.ShimResponseStream(context); await context.HttpContext.Response.WriteAsync(new string('0', 101)); @@ -741,7 +746,7 @@ public async Task FinalizeCacheBody_DoNotCache_IfSizeTooBig() LoggedMessage.ResponseCached); // The entry cannot be retrieved - Assert.False(await middleware.TryServeFromCacheAsync(context)); + Assert.False(await middleware.TryServeFromCacheAsync(context, policies)); } [Fact] diff --git a/src/Middleware/OutputCaching/test/OutputCachingPolicyProviderTests.cs b/src/Middleware/OutputCaching/test/OutputCachingPolicyProviderTests.cs index dcea6581d0c9..3ee32f2c60fe 100644 --- a/src/Middleware/OutputCaching/test/OutputCachingPolicyProviderTests.cs +++ b/src/Middleware/OutputCaching/test/OutputCachingPolicyProviderTests.cs @@ -47,11 +47,13 @@ public async Task AttemptOutputCaching_CacheableMethods_Allowed(string method) { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - var options = new OutputCacheOptions(); - options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); + var policies = new[] { new OutputCachePolicyBuilder().Build() }; context.HttpContext.Request.Method = method; - await new OutputCachePolicyProvider(options).OnRequestAsync(context); + foreach (var policy in policies) + { + await policy.CacheRequestAsync(context); + } Assert.True(context.AllowCacheStorage); Assert.True(context.AllowCacheLookup); @@ -64,11 +66,10 @@ public async Task AttemptOutputCaching_UncacheableMethods_NotAllowed(string meth { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - var options = new OutputCacheOptions(); - options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); + var policy = new OutputCachePolicyBuilder().Build(); context.HttpContext.Request.Method = method; - await new OutputCachePolicyProvider(options).OnRequestAsync(context); + await policy.CacheRequestAsync(context); Assert.False(context.AllowCacheLookup); Assert.False(context.AllowCacheStorage); @@ -85,10 +86,9 @@ public async Task AttemptResponseCaching_AuthorizationHeaders_NotAllowed() context.HttpContext.Request.Method = HttpMethods.Get; context.HttpContext.Request.Headers.Authorization = "Placeholder"; - var options = new OutputCacheOptions(); - options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); + var policy = new OutputCachePolicyBuilder().Build(); - await new OutputCachePolicyProvider(options).OnRequestAsync(context); + await policy.CacheRequestAsync(context); Assert.False(context.AllowCacheStorage); Assert.False(context.AllowCacheLookup); @@ -109,9 +109,8 @@ public async Task AllowCacheStorage_NoStore_Allowed() NoStore = true }.ToString(); - var options = new OutputCacheOptions(); - options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); - await new OutputCachePolicyProvider(options).OnRequestAsync(context); + var policy = new OutputCachePolicyBuilder().Build(); + await policy.CacheRequestAsync(context); Assert.True(context.AllowCacheStorage); Assert.Empty(sink.Writes); @@ -126,9 +125,8 @@ public async Task AllowCacheLookup_LegacyDirectives_OverridenByCacheControl() context.HttpContext.Request.Headers.Pragma = "no-cache"; context.HttpContext.Request.Headers.CacheControl = "max-age=10"; - var options = new OutputCacheOptions(); - options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); - await new OutputCachePolicyProvider(options).OnRequestAsync(context); + var policy = new OutputCachePolicyBuilder().Build(); + await policy.CacheRequestAsync(context); Assert.True(context.AllowCacheLookup); Assert.Empty(sink.Writes); @@ -140,9 +138,8 @@ public async Task IsResponseCacheable_NoPublic_Allowed() var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); - var options = new OutputCacheOptions(); - options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); - await new OutputCachePolicyProvider(options).OnServeResponseAsync(context); + var policy = new OutputCachePolicyBuilder().Build(); + await policy.ServeResponseAsync(context); Assert.True(context.AllowCacheStorage); Assert.True(context.AllowCacheLookup); @@ -159,9 +156,8 @@ public async Task IsResponseCacheable_Public_Allowed() Public = true }.ToString(); - var options = new OutputCacheOptions(); - options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); - await new OutputCachePolicyProvider(options).OnServeResponseAsync(context); + var policy = new OutputCachePolicyBuilder().Build(); + await policy.ServeResponseAsync(context); Assert.True(context.AllowCacheStorage); Assert.True(context.AllowCacheLookup); @@ -178,9 +174,8 @@ public async Task IsResponseCacheable_NoCache_Allowed() NoCache = true }.ToString(); - var options = new OutputCacheOptions(); - options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); - await new OutputCachePolicyProvider(options).OnServeResponseAsync(context); + var policy = new OutputCachePolicyBuilder().Build(); + await policy.ServeResponseAsync(context); Assert.True(context.AllowCacheStorage); Assert.True(context.AllowCacheLookup); @@ -197,9 +192,8 @@ public async Task IsResponseCacheable_ResponseNoStore_Allowed() NoStore = true }.ToString(); - var options = new OutputCacheOptions(); - options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); - await new OutputCachePolicyProvider(options).OnServeResponseAsync(context); + var policy = new OutputCachePolicyBuilder().Build(); + await policy.ServeResponseAsync(context); Assert.True(context.AllowCacheStorage); Assert.True(context.AllowCacheLookup); @@ -213,9 +207,8 @@ public async Task IsResponseCacheable_SetCookieHeader_NotAllowed() var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.Headers.SetCookie = "cookieName=cookieValue"; - var options = new OutputCacheOptions(); - options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); - await new OutputCachePolicyProvider(options).OnServeResponseAsync(context); + var policy = new OutputCachePolicyBuilder().Build(); + await policy.ServeResponseAsync(context); Assert.False(context.AllowCacheStorage); Assert.True(context.AllowCacheLookup); @@ -230,9 +223,8 @@ public async Task IsResponseCacheable_VaryHeaderByStar_Allowed() var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.Headers.Vary = "*"; - var options = new OutputCacheOptions(); - options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); - await new OutputCachePolicyProvider(options).OnServeResponseAsync(context); + var policy = new OutputCachePolicyBuilder().Build(); + await policy.ServeResponseAsync(context); Assert.True(context.AllowCacheStorage); Assert.True(context.AllowCacheLookup); @@ -249,9 +241,8 @@ public async Task IsResponseCacheable_Private_Allowed() Private = true }.ToString(); - var options = new OutputCacheOptions(); - options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); - await new OutputCachePolicyProvider(options).OnServeResponseAsync(context); + var policy = new OutputCachePolicyBuilder().Build(); + await policy.ServeResponseAsync(context); Assert.True(context.AllowCacheStorage); Assert.True(context.AllowCacheLookup); @@ -266,9 +257,8 @@ public async Task IsResponseCacheable_SuccessStatusCodes_Allowed(int statusCode) var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.StatusCode = statusCode; - var options = new OutputCacheOptions(); - options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); - await new OutputCachePolicyProvider(options).OnServeResponseAsync(context); + var policy = new OutputCachePolicyBuilder().Build(); + await policy.ServeResponseAsync(context); Assert.True(context.AllowCacheStorage); Assert.True(context.AllowCacheLookup); @@ -343,9 +333,8 @@ public async Task IsResponseCacheable_NonSuccessStatusCodes_NotAllowed(int statu var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.StatusCode = statusCode; - var options = new OutputCacheOptions(); - options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); - await new OutputCachePolicyProvider(options).OnServeResponseAsync(context); + var policy = new OutputCachePolicyBuilder().Build(); + await policy.ServeResponseAsync(context); Assert.True(context.AllowCacheLookup); Assert.False(context.AllowCacheStorage); @@ -365,9 +354,8 @@ public async Task IsResponseCacheable_NoExpiryRequirements_IsAllowed() context.HttpContext.Response.Headers.Date = HeaderUtilities.FormatDate(utcNow); context.ResponseTime = DateTimeOffset.MaxValue; - var options = new OutputCacheOptions(); - options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); - await new OutputCachePolicyProvider(options).OnServeResponseAsync(context); + var policy = new OutputCachePolicyBuilder().Build(); + await policy.ServeResponseAsync(context); Assert.True(context.AllowCacheStorage); Assert.True(context.AllowCacheLookup); @@ -389,9 +377,8 @@ public async Task IsResponseCacheable_MaxAgeOverridesExpiry_ToAllowed() context.HttpContext.Response.Headers.Date = HeaderUtilities.FormatDate(utcNow); context.ResponseTime = utcNow + TimeSpan.FromSeconds(9); - var options = new OutputCacheOptions(); - options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); - await new OutputCachePolicyProvider(options).OnServeResponseAsync(context); + var policy = new OutputCachePolicyBuilder().Build(); + await policy.ServeResponseAsync(context); Assert.True(context.AllowCacheStorage); Assert.True(context.AllowCacheLookup); @@ -413,9 +400,8 @@ public async Task IsResponseCacheable_SharedMaxAgeOverridesMaxAge_ToAllowed() context.HttpContext.Response.Headers.Date = HeaderUtilities.FormatDate(utcNow); context.ResponseTime = utcNow + TimeSpan.FromSeconds(11); - var options = new OutputCacheOptions(); - options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); - await new OutputCachePolicyProvider(options).OnServeResponseAsync(context); + var policy = new OutputCachePolicyBuilder().Build(); + await policy.ServeResponseAsync(context); Assert.True(context.AllowCacheStorage); Assert.True(context.AllowCacheLookup); diff --git a/src/Middleware/OutputCaching/test/OutputCachingTests.cs b/src/Middleware/OutputCaching/test/OutputCachingTests.cs index 8a2cbbd3a398..ae9a4ebabf52 100644 --- a/src/Middleware/OutputCaching/test/OutputCachingTests.cs +++ b/src/Middleware/OutputCaching/test/OutputCachingTests.cs @@ -220,7 +220,7 @@ public async Task ServesCachedContent_If_PathCasingDiffers(string method) public async Task ServesFreshContent_If_ResponseExpired(string method) { var options = new OutputCacheOptions(); - options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().VaryByHeader(HeaderNames.From).Build()); + options.BasePolicies.Add(new OutputCachePolicyBuilder().VaryByHeader(HeaderNames.From).Build()); options.DefaultExpirationTimeSpan = TimeSpan.FromMicroseconds(100); var builders = TestUtils.CreateBuildersWithOutputCaching(options: options); @@ -295,7 +295,7 @@ public async Task ServesCachedContent_IfVaryHeader_Matches() public async Task ServesFreshContent_IfVaryHeader_Mismatches() { var options = new OutputCacheOptions(); - options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().VaryByHeader(HeaderNames.From).Build()); + options.BasePolicies.Add(new OutputCachePolicyBuilder().VaryByHeader(HeaderNames.From).Build()); var builders = TestUtils.CreateBuildersWithOutputCaching(options: options); @@ -322,7 +322,7 @@ public async Task ServesFreshContent_IfVaryHeader_Mismatches() public async Task ServesCachedContent_IfVaryQueryKeys_Matches() { var options = new OutputCacheOptions(); - options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().VaryByQuery("query").Build()); + options.BasePolicies.Add(new OutputCachePolicyBuilder().VaryByQuery("query").Build()); var builders = TestUtils.CreateBuildersWithOutputCaching(options: options); @@ -347,7 +347,7 @@ public async Task ServesCachedContent_IfVaryQueryKeys_Matches() public async Task ServesCachedContent_IfVaryQueryKeysExplicit_Matches_QueryKeyCaseInsensitive() { var options = new OutputCacheOptions(); - options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().VaryByQuery("QueryA", "queryb").Build()); + options.BasePolicies.Add(new OutputCachePolicyBuilder().VaryByQuery("QueryA", "queryb").Build()); var builders = TestUtils.CreateBuildersWithOutputCaching(options: options); @@ -372,7 +372,7 @@ public async Task ServesCachedContent_IfVaryQueryKeysExplicit_Matches_QueryKeyCa public async Task ServesCachedContent_IfVaryQueryKeyStar_Matches_QueryKeyCaseInsensitive() { var options = new OutputCacheOptions(); - options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().VaryByQuery("*").Build()); + options.BasePolicies.Add(new OutputCachePolicyBuilder().VaryByQuery("*").Build()); var builders = TestUtils.CreateBuildersWithOutputCaching(options: options); @@ -397,7 +397,7 @@ public async Task ServesCachedContent_IfVaryQueryKeyStar_Matches_QueryKeyCaseIns public async Task ServesCachedContent_IfVaryQueryKeyExplicit_Matches_OrderInsensitive() { var options = new OutputCacheOptions(); - options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().VaryByQuery("QueryB", "QueryA").Build()); + options.BasePolicies.Add(new OutputCachePolicyBuilder().VaryByQuery("QueryB", "QueryA").Build()); var builders = TestUtils.CreateBuildersWithOutputCaching(options: options); @@ -422,7 +422,7 @@ public async Task ServesCachedContent_IfVaryQueryKeyExplicit_Matches_OrderInsens public async Task ServesCachedContent_IfVaryQueryKeyStar_Matches_OrderInsensitive() { var options = new OutputCacheOptions(); - options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().VaryByQuery("*").Build()); + options.BasePolicies.Add(new OutputCachePolicyBuilder().VaryByQuery("*").Build()); var builders = TestUtils.CreateBuildersWithOutputCaching(options: options); @@ -447,7 +447,7 @@ public async Task ServesCachedContent_IfVaryQueryKeyStar_Matches_OrderInsensitiv public async Task ServesFreshContent_IfVaryQueryKey_Mismatches() { var options = new OutputCacheOptions(); - options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().VaryByQuery("query").Build()); + options.BasePolicies.Add(new OutputCachePolicyBuilder().VaryByQuery("query").Build()); var builders = TestUtils.CreateBuildersWithOutputCaching(options: options); foreach (var builder in builders) @@ -471,7 +471,7 @@ public async Task ServesFreshContent_IfVaryQueryKey_Mismatches() public async Task ServesCachedContent_IfOtherVaryQueryKey_Mismatches() { var options = new OutputCacheOptions(); - options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().VaryByQuery("query").Build()); + options.BasePolicies.Add(new OutputCachePolicyBuilder().VaryByQuery("query").Build()); var builders = TestUtils.CreateBuildersWithOutputCaching(options: options); @@ -495,7 +495,9 @@ public async Task ServesCachedContent_IfOtherVaryQueryKey_Mismatches() [Fact] public async Task ServesFreshContent_IfVaryQueryKeyExplicit_Mismatch_QueryKeyCaseSensitive() { - var builders = TestUtils.CreateBuildersWithOutputCaching(contextAction: context => context.Features.Get().Policies.AddPolicy(new VaryByQueryPolicy("QueryA", "QueryB"))); + var options = new OutputCacheOptions(); + options.BasePolicies.Add(new VaryByQueryPolicy("QueryA", "QueryB")); + var builders = TestUtils.CreateBuildersWithOutputCaching(options: options); foreach (var builder in builders) { @@ -517,7 +519,9 @@ public async Task ServesFreshContent_IfVaryQueryKeyExplicit_Mismatch_QueryKeyCas [Fact] public async Task ServesFreshContent_IfVaryQueryKeyStar_Mismatch_QueryKeyValueCaseSensitive() { - var builders = TestUtils.CreateBuildersWithOutputCaching(contextAction: context => context.Features.Get().Policies.AddPolicy(new VaryByQueryPolicy("*"))); + var options = new OutputCacheOptions(); + options.BasePolicies.Add(new VaryByQueryPolicy("*")); + var builders = TestUtils.CreateBuildersWithOutputCaching(options: options); foreach (var builder in builders) { @@ -797,7 +801,7 @@ public async Task ServesCachedContent_IfBodySize_IsCacheable() { var options = new OutputCacheOptions(); options.MaximumBodySize = 1000; - options.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); + options.BasePolicies.Add(new OutputCachePolicyBuilder().Build()); var builders = TestUtils.CreateBuildersWithOutputCaching(options: options); diff --git a/src/Middleware/OutputCaching/test/TestUtils.cs b/src/Middleware/OutputCaching/test/TestUtils.cs index 0d19dcc69898..a0d79d96c13f 100644 --- a/src/Middleware/OutputCaching/test/TestUtils.cs +++ b/src/Middleware/OutputCaching/test/TestUtils.cs @@ -74,12 +74,12 @@ internal static Task TestRequestDelegateWrite(HttpContext context) return Task.CompletedTask; } - internal static IOutputCachingKeyProvider CreateTestKeyProvider() + internal static IOutputCacheKeyProvider CreateTestKeyProvider() { return CreateTestKeyProvider(new OutputCacheOptions()); } - internal static IOutputCachingKeyProvider CreateTestKeyProvider(OutputCacheOptions options) + internal static IOutputCacheKeyProvider CreateTestKeyProvider(OutputCacheOptions options) { return new OutputCacheKeyProvider(new DefaultObjectPoolProvider(), Options.Create(options)); } @@ -137,7 +137,7 @@ private static IEnumerable CreateBuildersWithOutputCaching( .UseTestServer() .ConfigureServices(services => { - services.AddOutputCaching(outputCachingOptions => + services.AddOutputCache(outputCachingOptions => { if (options != null) { @@ -150,7 +150,7 @@ private static IEnumerable CreateBuildersWithOutputCaching( } else { - outputCachingOptions.BasePolicies.AddPolicy(new OutputCachePolicyBuilder().Build()); + outputCachingOptions.BasePolicies.Add(new OutputCachePolicyBuilder().Build()); } }); }) @@ -169,8 +169,8 @@ internal static OutputCacheMiddleware CreateTestMiddleware( IOutputCacheStore cache = null, OutputCacheOptions options = null, TestSink testSink = null, - IOutputCachingKeyProvider keyProvider = null, - OutputCachePolicyProvider policyProvider = null) + IOutputCacheKeyProvider keyProvider = null + ) { if (next == null) { @@ -252,17 +252,10 @@ internal static class HttpResponseWritingExtensions { internal static void Write(this HttpResponse response, string text) { - if (response == null) - { - throw new ArgumentNullException(nameof(response)); - } - - if (text == null) - { - throw new ArgumentNullException(nameof(text)); - } + ArgumentNullException.ThrowIfNull(response); + ArgumentNullException.ThrowIfNull(text); - byte[] data = Encoding.UTF8.GetBytes(text); + var data = Encoding.UTF8.GetBytes(text); response.Body.Write(data, 0, data.Length); } } @@ -310,7 +303,7 @@ private LoggedMessage(int evenId, LogLevel logLevel) internal LogLevel LogLevel { get; } } -internal class TestResponseCachingKeyProvider : IOutputCachingKeyProvider +internal class TestResponseCachingKeyProvider : IOutputCacheKeyProvider { private readonly string _key; @@ -327,7 +320,7 @@ public string CreateStorageKey(OutputCacheContext context) internal class TestOutputCache : IOutputCacheStore { - private readonly IDictionary _storage = new Dictionary(); + private readonly IDictionary _storage = new Dictionary(); public int GetCount { get; private set; } public int SetCount { get; private set; } @@ -336,20 +329,20 @@ public ValueTask EvictByTagAsync(string tag, CancellationToken token) throw new NotImplementedException(); } - public ValueTask GetAsync(string key, CancellationToken token) + public ValueTask GetAsync(string key, CancellationToken token) { GetCount++; try { - return new ValueTask(_storage[key]); + return ValueTask.FromResult(_storage[key]); } catch { - return new ValueTask(default(OutputCacheEntry)); + return ValueTask.FromResult(default(byte[])); } } - public ValueTask SetAsync(string key, OutputCacheEntry entry, TimeSpan validFor, CancellationToken token) + public ValueTask SetAsync(string key, byte[] entry, string[] tags, TimeSpan validFor, CancellationToken token) { SetCount++; _storage[key] = entry; diff --git a/src/Mvc/Mvc.Core/src/OutputCacheAttribute.cs b/src/Mvc/Mvc.Core/src/OutputCacheAttribute.cs deleted file mode 100644 index 86e6edd85393..000000000000 --- a/src/Mvc/Mvc.Core/src/OutputCacheAttribute.cs +++ /dev/null @@ -1,103 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.AspNetCore.Mvc.Filters; -using Microsoft.AspNetCore.OutputCaching; - -namespace Microsoft.AspNetCore.Mvc; - -/// -/// Specifies the parameters necessary for setting appropriate headers in output caching. -/// -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] -public class OutputCacheAttribute : Attribute, IOrderedFilter, IOutputCachePolicy -{ - // A nullable-int cannot be used as an Attribute parameter. - // Hence this nullable-int is present to back the Duration property. - // The same goes for nullable-ResponseCacheLocation and nullable-bool. - private int? _duration; - private bool? _noCache; - private IOutputCachePolicy? _policy; - - /// - /// Gets or sets the duration in seconds for which the response is cached. - /// - public int Duration - { - get => _duration ?? 0; - set => _duration = value; - } - - /// - /// Gets or sets the value which determines whether the reponse should be cached or not. - /// When set to , the response won't be cached. - /// - public bool NoCache - { - get => _noCache ?? false; - set => _noCache = value; - } - - /// - /// Gets or sets the query keys to vary by. - /// - /// - /// requires the output cache middleware. - /// - public string[]? VaryByQueryKeys { get; set; } - - /// - /// Gets or sets the value of the cache policy name. - /// - public string? PolicyName { get; set; } - - /// - public int Order { get; set; } - - Task IOutputCachePolicy.OnRequestAsync(OutputCacheContext context) - { - return GetPolicy().OnRequestAsync(context); - } - - Task IOutputCachePolicy.OnServeFromCacheAsync(OutputCacheContext context) - { - return GetPolicy().OnServeFromCacheAsync(context); - } - - Task IOutputCachePolicy.OnServeResponseAsync(OutputCacheContext context) - { - return GetPolicy().OnServeResponseAsync(context); - } - - private IOutputCachePolicy GetPolicy() - { - if (_policy != null) - { - return _policy; - } - - var builder = new OutputCachePolicyBuilder(); - - if (_noCache != null && _noCache.Value) - { - builder.NoCache(); - } - - if (PolicyName != null) - { - builder.Policy(PolicyName); - } - - if (VaryByQueryKeys != null) - { - builder.VaryByQuery(VaryByQueryKeys); - } - - if (_duration != null) - { - builder.Expire(TimeSpan.FromSeconds(_duration.Value)); - } - - return _policy = builder; - } -} diff --git a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt index 70251dbe4cc7..820ab9896cb4 100644 --- a/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt +++ b/src/Mvc/Mvc.Core/src/PublicAPI.Unshipped.txt @@ -13,17 +13,5 @@ Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.SystemTextJsonValidationMetadataP Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.SystemTextJsonValidationMetadataProvider.SystemTextJsonValidationMetadataProvider(System.Text.Json.JsonNamingPolicy! namingPolicy) -> void Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.ValidationMetadata.ValidationModelName.get -> string? Microsoft.AspNetCore.Mvc.ModelBinding.Metadata.ValidationMetadata.ValidationModelName.set -> void -Microsoft.AspNetCore.Mvc.OutputCacheAttribute -Microsoft.AspNetCore.Mvc.OutputCacheAttribute.Duration.get -> int -Microsoft.AspNetCore.Mvc.OutputCacheAttribute.Duration.set -> void -Microsoft.AspNetCore.Mvc.OutputCacheAttribute.NoCache.get -> bool -Microsoft.AspNetCore.Mvc.OutputCacheAttribute.NoCache.set -> void -Microsoft.AspNetCore.Mvc.OutputCacheAttribute.Order.get -> int -Microsoft.AspNetCore.Mvc.OutputCacheAttribute.Order.set -> void -Microsoft.AspNetCore.Mvc.OutputCacheAttribute.OutputCacheAttribute() -> void -Microsoft.AspNetCore.Mvc.OutputCacheAttribute.PolicyName.get -> string? -Microsoft.AspNetCore.Mvc.OutputCacheAttribute.PolicyName.set -> void -Microsoft.AspNetCore.Mvc.OutputCacheAttribute.VaryByQueryKeys.get -> string![]? -Microsoft.AspNetCore.Mvc.OutputCacheAttribute.VaryByQueryKeys.set -> void virtual Microsoft.AspNetCore.Mvc.Infrastructure.ConfigureCompatibilityOptions.PostConfigure(string? name, TOptions! options) -> void static Microsoft.AspNetCore.Mvc.ControllerBase.Empty.get -> Microsoft.AspNetCore.Mvc.EmptyResult! diff --git a/src/Mvc/samples/MvcSandbox/Controllers/HomeController.cs b/src/Mvc/samples/MvcSandbox/Controllers/HomeController.cs index 96a6a3a64ded..f5d5c9df5662 100644 --- a/src/Mvc/samples/MvcSandbox/Controllers/HomeController.cs +++ b/src/Mvc/samples/MvcSandbox/Controllers/HomeController.cs @@ -10,7 +10,6 @@ public class HomeController : Controller [ModelBinder] public string Id { get; set; } - [OutputCache(Duration = 10, VaryByQueryKeys = new [] { "culture" })] public IActionResult Index() { return View(); diff --git a/src/Mvc/samples/MvcSandbox/Views/Home/Index.cshtml b/src/Mvc/samples/MvcSandbox/Views/Home/Index.cshtml index 8572a64234e1..b1c5cc559876 100644 --- a/src/Mvc/samples/MvcSandbox/Views/Home/Index.cshtml +++ b/src/Mvc/samples/MvcSandbox/Views/Home/Index.cshtml @@ -5,5 +5,4 @@

Sandbox

This sandbox should give you a quick view of a basic MVC application.

- @DateTime.UtcNow.ToString("o")
From 343b25e0458bf570007c2f2e746d39cbf868644f Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Mon, 20 Jun 2022 18:58:14 -0700 Subject: [PATCH 42/48] Add more tests --- .../OutputCaching/src/IOutputCachePolicy.cs | 6 +- .../src/Memory/MemoryOutputCacheStore.cs | 7 + .../src/OutputCachePolicyBuilder.cs | 6 +- .../src/Policies/CompositePolicy.cs | 6 +- ...tOutputCachePolicy.cs => DefaultPolicy.cs} | 22 +- .../src/Policies/EnableCachePolicy.cs | 12 +- .../src/Policies/ExpirationPolicy.cs | 12 +- .../src/Policies/LockingPolicy.cs | 12 +- .../src/Policies/NoLookupPolicy.cs | 12 +- .../src/Policies/NoStorePolicy.cs | 12 +- .../OutputCacheConventionBuilderExtensions.cs | 2 +- .../src/Policies/PredicatePolicy.cs | 20 +- .../src/Policies/ProfilePolicy.cs | 12 +- .../OutputCaching/src/Policies/TagsPolicy.cs | 12 +- .../OutputCaching/src/Policies/TypedPolicy.cs | 12 +- .../src/Policies/VaryByHeaderPolicy.cs | 18 +- .../src/Policies/VaryByQueryPolicy.cs | 16 +- .../src/Policies/VaryByValuePolicy.cs | 18 +- .../OutputCaching/src/PublicAPI.Unshipped.txt | 10 +- .../test/MemoryOutputCacheStoreTests.cs | 155 +++++++++ ...ests.cs => OutputCacheKeyProviderTests.cs} | 2 +- ...Tests.cs => OutputCacheMiddlewareTests.cs} | 2 +- .../test/OutputCachePoliciesTests.cs | 294 ++++++++++++++++++ ...s.cs => OutputCachePolicyProviderTests.cs} | 2 +- ...putCachingTests.cs => OutputCacheTests.cs} | 26 +- .../OutputCaching/test/TestUtils.cs | 8 + 26 files changed, 592 insertions(+), 124 deletions(-) rename src/Middleware/OutputCaching/src/Policies/{DefaultOutputCachePolicy.cs => DefaultPolicy.cs} (78%) create mode 100644 src/Middleware/OutputCaching/test/MemoryOutputCacheStoreTests.cs rename src/Middleware/OutputCaching/test/{OutputCachingKeyProviderTests.cs => OutputCacheKeyProviderTests.cs} (99%) rename src/Middleware/OutputCaching/test/{OutputCachingMiddlewareTests.cs => OutputCacheMiddlewareTests.cs} (99%) create mode 100644 src/Middleware/OutputCaching/test/OutputCachePoliciesTests.cs rename src/Middleware/OutputCaching/test/{OutputCachingPolicyProviderTests.cs => OutputCachePolicyProviderTests.cs} (99%) rename src/Middleware/OutputCaching/test/{OutputCachingTests.cs => OutputCacheTests.cs} (96%) diff --git a/src/Middleware/OutputCaching/src/IOutputCachePolicy.cs b/src/Middleware/OutputCaching/src/IOutputCachePolicy.cs index 3f8a0fb7b6c9..6b8e0e405d2d 100644 --- a/src/Middleware/OutputCaching/src/IOutputCachePolicy.cs +++ b/src/Middleware/OutputCaching/src/IOutputCachePolicy.cs @@ -13,18 +13,18 @@ public interface IOutputCachePolicy /// At that point the cache middleware can still be enabled or disabled for the request. ///
/// The current request's cache context. - Task CacheRequestAsync(OutputCacheContext context); + ValueTask CacheRequestAsync(OutputCacheContext context); /// /// Updates the before the cached response is used. /// At that point the freshness of the cached response can be updated. /// /// The current request's cache context. - Task ServeFromCacheAsync(OutputCacheContext context); + ValueTask ServeFromCacheAsync(OutputCacheContext context); /// /// Updates the before the response is served and can be cached. /// At that point cacheability of the response can be updated. /// - Task ServeResponseAsync(OutputCacheContext context); + ValueTask ServeResponseAsync(OutputCacheContext context); } diff --git a/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs b/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs index daf0b9300513..fc6497d3b3ad 100644 --- a/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs +++ b/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs @@ -20,6 +20,8 @@ internal MemoryOutputCacheStore(IMemoryCache cache) public ValueTask EvictByTagAsync(string tag, CancellationToken token) { + ArgumentNullException.ThrowIfNull(tag); + if (_taggedEntries.TryGetValue(tag, out var keys)) { foreach (var key in keys) @@ -34,6 +36,8 @@ public ValueTask EvictByTagAsync(string tag, CancellationToken token) /// public ValueTask GetAsync(string key, CancellationToken token) { + ArgumentNullException.ThrowIfNull(key); + var entry = _cache.Get(key) as byte[]; return ValueTask.FromResult(entry); } @@ -41,6 +45,9 @@ public ValueTask EvictByTagAsync(string tag, CancellationToken token) /// public ValueTask SetAsync(string key, byte[] value, string[] tags, TimeSpan validFor, CancellationToken token) { + ArgumentNullException.ThrowIfNull(key); + ArgumentNullException.ThrowIfNull(value); + if (tags != null) { foreach (var tag in tags) diff --git a/src/Middleware/OutputCaching/src/OutputCachePolicyBuilder.cs b/src/Middleware/OutputCaching/src/OutputCachePolicyBuilder.cs index 1ef23e0f7f2b..8256174059d0 100644 --- a/src/Middleware/OutputCaching/src/OutputCachePolicyBuilder.cs +++ b/src/Middleware/OutputCaching/src/OutputCachePolicyBuilder.cs @@ -25,7 +25,7 @@ public sealed class OutputCachePolicyBuilder public OutputCachePolicyBuilder() { _builtPolicy = null; - _policies.Add(DefaultOutputCachePolicy.Instance); + _policies.Add(DefaultPolicy.Instance); } /// @@ -109,7 +109,7 @@ public OutputCachePolicyBuilder VaryByHeader(params string[] headers) /// Adds a policy to vary the cached responses by custom values. /// /// The value to vary the cached responses by. - public OutputCachePolicyBuilder VaryByValue(Func> varyBy) + public OutputCachePolicyBuilder VaryByValue(Func> varyBy) { ArgumentNullException.ThrowIfNull(varyBy); @@ -122,7 +122,7 @@ public OutputCachePolicyBuilder VaryByValue(Func /// The key/value to vary the cached responses by. - public OutputCachePolicyBuilder VaryByValue(Func>> varyBy) + public OutputCachePolicyBuilder VaryByValue(Func>> varyBy) { ArgumentNullException.ThrowIfNull(varyBy); diff --git a/src/Middleware/OutputCaching/src/Policies/CompositePolicy.cs b/src/Middleware/OutputCaching/src/Policies/CompositePolicy.cs index 2c2ba2fa67c9..dff9efc9e583 100644 --- a/src/Middleware/OutputCaching/src/Policies/CompositePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/CompositePolicy.cs @@ -20,7 +20,7 @@ public CompositePolicy(params IOutputCachePolicy[] policies!!) } /// - async Task IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) + async ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) { foreach (var policy in _policies) { @@ -29,7 +29,7 @@ async Task IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) } /// - async Task IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) + async ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) { foreach (var policy in _policies) { @@ -38,7 +38,7 @@ async Task IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) } /// - async Task IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) + async ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) { foreach (var policy in _policies) { diff --git a/src/Middleware/OutputCaching/src/Policies/DefaultOutputCachePolicy.cs b/src/Middleware/OutputCaching/src/Policies/DefaultPolicy.cs similarity index 78% rename from src/Middleware/OutputCaching/src/Policies/DefaultOutputCachePolicy.cs rename to src/Middleware/OutputCaching/src/Policies/DefaultPolicy.cs index 4a5c7628494a..cbe78433579f 100644 --- a/src/Middleware/OutputCaching/src/Policies/DefaultOutputCachePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/DefaultPolicy.cs @@ -9,16 +9,16 @@ namespace Microsoft.AspNetCore.OutputCaching; /// /// A policy which caches un-authenticated, GET and HEAD, 200 responses. /// -internal sealed class DefaultOutputCachePolicy : IOutputCachePolicy +internal sealed class DefaultPolicy : IOutputCachePolicy { - public static readonly DefaultOutputCachePolicy Instance = new(); + public static readonly DefaultPolicy Instance = new(); - private DefaultOutputCachePolicy() + private DefaultPolicy() { } /// - Task IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) { var attemptOutputCaching = AttemptOutputCaching(context); context.EnableOutputCaching = true; @@ -29,17 +29,17 @@ Task IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) // Vary by any query by default context.CachedVaryByRules.QueryKeys = "*"; - return Task.CompletedTask; + return ValueTask.CompletedTask; } /// - Task IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) { - return Task.CompletedTask; + return ValueTask.CompletedTask; } /// - Task IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) { var response = context.HttpContext.Response; @@ -48,7 +48,7 @@ Task IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) { context.Logger.ResponseWithSetCookieNotCacheable(); context.AllowCacheStorage = false; - return Task.CompletedTask; + return ValueTask.CompletedTask; } // Check response code @@ -56,10 +56,10 @@ Task IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) { context.Logger.ResponseWithUnsuccessfulStatusCodeNotCacheable(response.StatusCode); context.AllowCacheStorage = false; - return Task.CompletedTask; + return ValueTask.CompletedTask; } - return Task.CompletedTask; + return ValueTask.CompletedTask; } private static bool AttemptOutputCaching(OutputCacheContext context) diff --git a/src/Middleware/OutputCaching/src/Policies/EnableCachePolicy.cs b/src/Middleware/OutputCaching/src/Policies/EnableCachePolicy.cs index 98e7e138a7ec..1894c8a5d0a4 100644 --- a/src/Middleware/OutputCaching/src/Policies/EnableCachePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/EnableCachePolicy.cs @@ -16,22 +16,22 @@ private EnableCachePolicy() } /// - Task IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) { context.EnableOutputCaching = this == Enabled; - return Task.CompletedTask; + return ValueTask.CompletedTask; } /// - Task IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) { - return Task.CompletedTask; + return ValueTask.CompletedTask; } /// - Task IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) { - return Task.CompletedTask; + return ValueTask.CompletedTask; } } diff --git a/src/Middleware/OutputCaching/src/Policies/ExpirationPolicy.cs b/src/Middleware/OutputCaching/src/Policies/ExpirationPolicy.cs index 1f75adbbfef8..29589a4c1ed9 100644 --- a/src/Middleware/OutputCaching/src/Policies/ExpirationPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/ExpirationPolicy.cs @@ -20,22 +20,22 @@ public ExpirationPolicy(TimeSpan expiration) } /// - Task IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) { context.ResponseExpirationTimeSpan = _expiration; - return Task.CompletedTask; + return ValueTask.CompletedTask; } /// - Task IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) { - return Task.CompletedTask; + return ValueTask.CompletedTask; } /// - Task IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) { - return Task.CompletedTask; + return ValueTask.CompletedTask; } } diff --git a/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs b/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs index 96d134aef0ab..26a5f83fa813 100644 --- a/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs @@ -26,22 +26,22 @@ private LockingPolicy(bool lockResponse) public static readonly LockingPolicy Disabled = new(false); /// - Task IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) { context.AllowLocking = _lockResponse; - return Task.CompletedTask; + return ValueTask.CompletedTask; } /// - Task IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) { - return Task.CompletedTask; + return ValueTask.CompletedTask; } /// - Task IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) { - return Task.CompletedTask; + return ValueTask.CompletedTask; } } diff --git a/src/Middleware/OutputCaching/src/Policies/NoLookupPolicy.cs b/src/Middleware/OutputCaching/src/Policies/NoLookupPolicy.cs index 49ccd38b9bbc..c7ad348fbf6c 100644 --- a/src/Middleware/OutputCaching/src/Policies/NoLookupPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/NoLookupPolicy.cs @@ -15,22 +15,22 @@ private NoLookupPolicy() } /// - Task IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) { context.AllowCacheLookup = false; - return Task.CompletedTask; + return ValueTask.CompletedTask; } /// - Task IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) { - return Task.CompletedTask; + return ValueTask.CompletedTask; } /// - Task IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) { - return Task.CompletedTask; + return ValueTask.CompletedTask; } } diff --git a/src/Middleware/OutputCaching/src/Policies/NoStorePolicy.cs b/src/Middleware/OutputCaching/src/Policies/NoStorePolicy.cs index 1a45ffd63f4d..ce8a6a234f46 100644 --- a/src/Middleware/OutputCaching/src/Policies/NoStorePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/NoStorePolicy.cs @@ -15,22 +15,22 @@ private NoStorePolicy() } /// - Task IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) { context.AllowCacheStorage = false; - return Task.CompletedTask; + return ValueTask.CompletedTask; } /// - Task IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) { - return Task.CompletedTask; + return ValueTask.CompletedTask; } /// - Task IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) { - return Task.CompletedTask; + return ValueTask.CompletedTask; } } diff --git a/src/Middleware/OutputCaching/src/Policies/OutputCacheConventionBuilderExtensions.cs b/src/Middleware/OutputCaching/src/Policies/OutputCacheConventionBuilderExtensions.cs index bff780f254ea..3cdf507e0fc1 100644 --- a/src/Middleware/OutputCaching/src/Policies/OutputCacheConventionBuilderExtensions.cs +++ b/src/Middleware/OutputCaching/src/Policies/OutputCacheConventionBuilderExtensions.cs @@ -22,7 +22,7 @@ public static TBuilder CacheOutput(this TBuilder builder) where TBuild builder.Add(endpointBuilder => { - endpointBuilder.Metadata.Add(DefaultOutputCachePolicy.Instance); + endpointBuilder.Metadata.Add(DefaultPolicy.Instance); }); return builder; } diff --git a/src/Middleware/OutputCaching/src/Policies/PredicatePolicy.cs b/src/Middleware/OutputCaching/src/Policies/PredicatePolicy.cs index d3599a873714..353d1ce223dc 100644 --- a/src/Middleware/OutputCaching/src/Policies/PredicatePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/PredicatePolicy.cs @@ -10,39 +10,39 @@ internal sealed class PredicatePolicy : IOutputCachePolicy { // TODO: Accept a non async predicate too? - private readonly Func> _predicate; + private readonly Func> _predicate; private readonly IOutputCachePolicy _policy; /// /// Creates a new instance. /// - /// The predicate. + /// The predicate. /// The policy. - public PredicatePolicy(Func> predicate, IOutputCachePolicy policy) + public PredicatePolicy(Func> asyncPredicate, IOutputCachePolicy policy) { - _predicate = predicate; + _predicate = asyncPredicate; _policy = policy; } /// - Task IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) { return ExecuteAwaited(static (policy, context) => policy.CacheRequestAsync(context), _policy, context); } /// - Task IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) { return ExecuteAwaited(static (policy, context) => policy.ServeFromCacheAsync(context), _policy, context); } /// - Task IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) { return ExecuteAwaited(static (policy, context) => policy.ServeResponseAsync(context), _policy, context); } - private Task ExecuteAwaited(Func action, IOutputCachePolicy policy, OutputCacheContext context) + private ValueTask ExecuteAwaited(Func action, IOutputCachePolicy policy, OutputCacheContext context) { ArgumentNullException.ThrowIfNull(action); @@ -60,12 +60,12 @@ private Task ExecuteAwaited(Func a return action(policy, context); } - return Task.CompletedTask; + return ValueTask.CompletedTask; } return Awaited(task); - async Task Awaited(Task task) + async ValueTask Awaited(ValueTask task) { if (await task) { diff --git a/src/Middleware/OutputCaching/src/Policies/ProfilePolicy.cs b/src/Middleware/OutputCaching/src/Policies/ProfilePolicy.cs index 8f3acfd9b3f2..2b154b547cb7 100644 --- a/src/Middleware/OutputCaching/src/Policies/ProfilePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/ProfilePolicy.cs @@ -20,39 +20,39 @@ public ProfilePolicy(string profileName) } /// - Task IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) { var policy = GetProfilePolicy(context); if (policy == null) { - return Task.CompletedTask; + return ValueTask.CompletedTask; } return policy.ServeResponseAsync(context); } /// - Task IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) { var policy = GetProfilePolicy(context); if (policy == null) { - return Task.CompletedTask; + return ValueTask.CompletedTask; } return policy.ServeFromCacheAsync(context); } /// - Task IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) { var policy = GetProfilePolicy(context); if (policy == null) { - return Task.CompletedTask; + return ValueTask.CompletedTask; } return policy.CacheRequestAsync(context); ; diff --git a/src/Middleware/OutputCaching/src/Policies/TagsPolicy.cs b/src/Middleware/OutputCaching/src/Policies/TagsPolicy.cs index 3ddfec717c1c..622af389bdda 100644 --- a/src/Middleware/OutputCaching/src/Policies/TagsPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/TagsPolicy.cs @@ -20,25 +20,25 @@ public TagsPolicy(params string[] tags) } /// - Task IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) { foreach (var tag in _tags) { context.Tags.Add(tag); } - return Task.CompletedTask; + return ValueTask.CompletedTask; } /// - Task IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) { - return Task.CompletedTask; + return ValueTask.CompletedTask; } /// - Task IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) { - return Task.CompletedTask; + return ValueTask.CompletedTask; } } diff --git a/src/Middleware/OutputCaching/src/Policies/TypedPolicy.cs b/src/Middleware/OutputCaching/src/Policies/TypedPolicy.cs index 68e7fcba6e9c..adfba1fef1a8 100644 --- a/src/Middleware/OutputCaching/src/Policies/TypedPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/TypedPolicy.cs @@ -33,20 +33,20 @@ public TypedPolicy([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Pu } /// - Task IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) { - return CreatePolicy(context)?.CacheRequestAsync(context) ?? Task.CompletedTask; + return CreatePolicy(context)?.CacheRequestAsync(context) ?? ValueTask.CompletedTask; } /// - Task IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) { - return CreatePolicy(context)?.ServeFromCacheAsync(context) ?? Task.CompletedTask; + return CreatePolicy(context)?.ServeFromCacheAsync(context) ?? ValueTask.CompletedTask; } /// - Task IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) { - return CreatePolicy(context)?.ServeResponseAsync(context) ?? Task.CompletedTask; + return CreatePolicy(context)?.ServeResponseAsync(context) ?? ValueTask.CompletedTask; } } diff --git a/src/Middleware/OutputCaching/src/Policies/VaryByHeaderPolicy.cs b/src/Middleware/OutputCaching/src/Policies/VaryByHeaderPolicy.cs index 92c57fa710e6..11faf33694ae 100644 --- a/src/Middleware/OutputCaching/src/Policies/VaryByHeaderPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/VaryByHeaderPolicy.cs @@ -24,6 +24,8 @@ public VaryByHeaderPolicy() ///
public VaryByHeaderPolicy(string header) { + ArgumentNullException.ThrowIfNull(header); + _headers = header; } @@ -32,33 +34,35 @@ public VaryByHeaderPolicy(string header) ///
public VaryByHeaderPolicy(params string[] headers) { + ArgumentNullException.ThrowIfNull(headers); + _headers = headers; } /// - Task IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) { // No vary by header? if (_headers.Count == 0) { context.CachedVaryByRules.Headers = _headers; - return Task.CompletedTask; + return ValueTask.CompletedTask; } context.CachedVaryByRules.Headers = StringValues.Concat(context.CachedVaryByRules.Headers, _headers); - return Task.CompletedTask; + return ValueTask.CompletedTask; } /// - Task IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) { - return Task.CompletedTask; + return ValueTask.CompletedTask; } /// - Task IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) { - return Task.CompletedTask; + return ValueTask.CompletedTask; } } diff --git a/src/Middleware/OutputCaching/src/Policies/VaryByQueryPolicy.cs b/src/Middleware/OutputCaching/src/Policies/VaryByQueryPolicy.cs index 2fa48099c09e..93835ce690f6 100644 --- a/src/Middleware/OutputCaching/src/Policies/VaryByQueryPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/VaryByQueryPolicy.cs @@ -37,36 +37,36 @@ public VaryByQueryPolicy(params string[] queryKeys) } /// - Task IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) { // No vary by query? if (_queryKeys.Count == 0) { context.CachedVaryByRules.QueryKeys = _queryKeys; - return Task.CompletedTask; + return ValueTask.CompletedTask; } // If the current key is "*" (default) replace it if (context.CachedVaryByRules.QueryKeys.Count == 1 && string.Equals(context.CachedVaryByRules.QueryKeys[0], "*", StringComparison.Ordinal)) { context.CachedVaryByRules.QueryKeys = _queryKeys; - return Task.CompletedTask; + return ValueTask.CompletedTask; } context.CachedVaryByRules.QueryKeys = StringValues.Concat(context.CachedVaryByRules.QueryKeys, _queryKeys); - return Task.CompletedTask; + return ValueTask.CompletedTask; } /// - Task IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) { - return Task.CompletedTask; + return ValueTask.CompletedTask; } /// - Task IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) { - return Task.CompletedTask; + return ValueTask.CompletedTask; } } diff --git a/src/Middleware/OutputCaching/src/Policies/VaryByValuePolicy.cs b/src/Middleware/OutputCaching/src/Policies/VaryByValuePolicy.cs index 14c02c336daa..58eb99e49420 100644 --- a/src/Middleware/OutputCaching/src/Policies/VaryByValuePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/VaryByValuePolicy.cs @@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.OutputCaching; internal sealed class VaryByValuePolicy : IOutputCachePolicy { private readonly Action? _varyBy; - private readonly Func? _varyByAsync; + private readonly Func? _varyByAsync; /// /// Creates a policy that doesn't vary the cached content based on values. @@ -31,7 +31,7 @@ public VaryByValuePolicy(Func varyBy) /// /// Creates a policy that vary the cached content based on the specified value. /// - public VaryByValuePolicy(Func> varyBy) + public VaryByValuePolicy(Func> varyBy) { _varyByAsync = async (context, rules, token) => rules.VaryByPrefix += await varyBy(context, token); } @@ -51,7 +51,7 @@ public VaryByValuePolicy(Func> varyBy) /// /// Creates a policy that vary the cached content based on the specified value. /// - public VaryByValuePolicy(Func>> varyBy) + public VaryByValuePolicy(Func>> varyBy) { _varyBy = async (context, rules) => { @@ -61,22 +61,22 @@ public VaryByValuePolicy(Func - Task IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) { _varyBy?.Invoke(context.HttpContext, context.CachedVaryByRules); - return _varyByAsync?.Invoke(context.HttpContext, context.CachedVaryByRules, context.HttpContext.RequestAborted) ?? Task.CompletedTask; + return _varyByAsync?.Invoke(context.HttpContext, context.CachedVaryByRules, context.HttpContext.RequestAborted) ?? ValueTask.CompletedTask; } /// - Task IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) { - return Task.CompletedTask; + return ValueTask.CompletedTask; } /// - Task IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) { - return Task.CompletedTask; + return ValueTask.CompletedTask; } } diff --git a/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt b/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt index 32884134f11f..6c831506774c 100644 --- a/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt @@ -16,9 +16,9 @@ Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.VaryByPrefix.get -> Microso Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.VaryByPrefix.set -> void Microsoft.AspNetCore.OutputCaching.IOutputCacheFeature Microsoft.AspNetCore.OutputCaching.IOutputCacheFeature.Context.get -> Microsoft.AspNetCore.OutputCaching.OutputCacheContext! -Microsoft.AspNetCore.OutputCaching.IOutputCachePolicy.CacheRequestAsync(Microsoft.AspNetCore.OutputCaching.OutputCacheContext! context) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.OutputCaching.IOutputCachePolicy.ServeFromCacheAsync(Microsoft.AspNetCore.OutputCaching.OutputCacheContext! context) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.OutputCaching.IOutputCachePolicy.ServeResponseAsync(Microsoft.AspNetCore.OutputCaching.OutputCacheContext! context) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.OutputCaching.IOutputCachePolicy.CacheRequestAsync(Microsoft.AspNetCore.OutputCaching.OutputCacheContext! context) -> System.Threading.Tasks.ValueTask +Microsoft.AspNetCore.OutputCaching.IOutputCachePolicy.ServeFromCacheAsync(Microsoft.AspNetCore.OutputCaching.OutputCacheContext! context) -> System.Threading.Tasks.ValueTask +Microsoft.AspNetCore.OutputCaching.IOutputCachePolicy.ServeResponseAsync(Microsoft.AspNetCore.OutputCaching.OutputCacheContext! context) -> System.Threading.Tasks.ValueTask Microsoft.AspNetCore.OutputCaching.IOutputCacheStore Microsoft.AspNetCore.OutputCaching.IOutputCacheStore.EvictByTagAsync(string! tag, System.Threading.CancellationToken token) -> System.Threading.Tasks.ValueTask Microsoft.AspNetCore.OutputCaching.IOutputCachePolicy @@ -55,9 +55,9 @@ Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Tag(params string![] Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByHeader(params string![]! headers) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByQuery(params string![]! queryKeys) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.Func>! varyBy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! -Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.Func!>! varyBy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.Func! varyBy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! -Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.Func>!>! varyBy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.Func>! varyBy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! +Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByValue(System.Func>>! varyBy) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.With(System.Func!>! predicate) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCacheContext Microsoft.AspNetCore.OutputCaching.OutputCacheContext.AllowCacheLookup.get -> bool diff --git a/src/Middleware/OutputCaching/test/MemoryOutputCacheStoreTests.cs b/src/Middleware/OutputCaching/test/MemoryOutputCacheStoreTests.cs new file mode 100644 index 000000000000..1b1158c66229 --- /dev/null +++ b/src/Middleware/OutputCaching/test/MemoryOutputCacheStoreTests.cs @@ -0,0 +1,155 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Http; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.OutputCaching.Memory; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.OutputCaching.Tests; + +public class MemoryOutputCacheStoreTests +{ + [Fact] + public async Task StoreAndGetValue_Succeeds() + { + var store = new MemoryOutputCacheStore(new MemoryCache(new MemoryCacheOptions())); + var value = "abc"u8; + var key = "abc"; + + await store.SetAsync(key, value, null, TimeSpan.FromMinutes(1), default); + + var result = await store.GetAsync(key, default); + + Assert.Equal(value, result); + } + + [Fact] + public async Task StoreAndGetValue_TimesOut() + { + var testClock = new TestMemoryOptionsClock { UtcNow = DateTimeOffset.UtcNow }; + var store = new MemoryOutputCacheStore(new MemoryCache(new MemoryCacheOptions { Clock = testClock })); + var value = "abc"u8; + var key = "abc"; + + await store.SetAsync(key, value, null, TimeSpan.FromMilliseconds(5), default); + testClock.Advance(TimeSpan.FromMilliseconds(10)); + + var result = await store.GetAsync(key, default); + + Assert.Null(result); + } + + [Fact] + public async Task StoreNullKey_ThrowsException() + { + var store = new MemoryOutputCacheStore(new MemoryCache(new MemoryCacheOptions())); + var value = "abc"u8; + string key = null; + + _ = await Assert.ThrowsAsync("key", () => store.SetAsync(key, value, null, TimeSpan.FromMilliseconds(5), default).AsTask()); + } + + [Fact] + public async Task StoreNullValue_ThrowsException() + { + var store = new MemoryOutputCacheStore(new MemoryCache(new MemoryCacheOptions())); + var value = default(byte[]); + string key = "abc"; + + _ = await Assert.ThrowsAsync("value", () => store.SetAsync(key, value, null, TimeSpan.FromMilliseconds(5), default).AsTask()); + } + + [Fact] + public async Task EvictByTag_SingleTag_SingleEntry() + { + var testClock = new TestMemoryOptionsClock { UtcNow = DateTimeOffset.UtcNow }; + var store = new MemoryOutputCacheStore(new MemoryCache(new MemoryCacheOptions { Clock = testClock })); + var value = "abc"u8; + var key = "abc"; + var tags = new string[] { "tag1" }; + + await store.SetAsync(key, value, tags, TimeSpan.FromDays(1), default); + await store.EvictByTagAsync("tag1", default); + var result = await store.GetAsync(key, default); + + Assert.Null(result); + } + + [Fact] + public async Task EvictByTag_SingleTag_MultipleEntries() + { + var testClock = new TestMemoryOptionsClock { UtcNow = DateTimeOffset.UtcNow }; + var store = new MemoryOutputCacheStore(new MemoryCache(new MemoryCacheOptions { Clock = testClock })); + var value = "abc"u8; + var key1 = "abc"; + var key2 = "def"; + var tags = new string[] { "tag1" }; + + await store.SetAsync(key1, value, tags, TimeSpan.FromDays(1), default); + await store.SetAsync(key2, value, tags, TimeSpan.FromDays(1), default); + await store.EvictByTagAsync("tag1", default); + var result1 = await store.GetAsync(key1, default); + var result2 = await store.GetAsync(key2, default); + + Assert.Null(result1); + Assert.Null(result2); + } + + [Fact] + public async Task EvictByTag_MultipleTags_SingleEntry() + { + var testClock = new TestMemoryOptionsClock { UtcNow = DateTimeOffset.UtcNow }; + var store = new MemoryOutputCacheStore(new MemoryCache(new MemoryCacheOptions { Clock = testClock })); + var value = "abc"u8; + var key = "abc"; + var tags = new string[] { "tag1", "tag2" }; + + await store.SetAsync(key, value, tags, TimeSpan.FromDays(1), default); + await store.EvictByTagAsync("tag1", default); + var result1 = await store.GetAsync(key, default); + + Assert.Null(result1); + } + + [Fact] + public async Task EvictByTag_MultipleTags_MultipleEntries() + { + var testClock = new TestMemoryOptionsClock { UtcNow = DateTimeOffset.UtcNow }; + var store = new MemoryOutputCacheStore(new MemoryCache(new MemoryCacheOptions { Clock = testClock })); + var value = "abc"u8; + var key1 = "abc"; + var key2 = "def"; + var tags1 = new string[] { "tag1", "tag2" }; + var tags2 = new string[] { "tag2", "tag3" }; + + await store.SetAsync(key1, value, tags1, TimeSpan.FromDays(1), default); + await store.SetAsync(key2, value, tags2, TimeSpan.FromDays(1), default); + await store.EvictByTagAsync("tag1", default); + + var result1 = await store.GetAsync(key1, default); + var result2 = await store.GetAsync(key2, default); + + Assert.Null(result1); + Assert.NotNull(result2); + + await store.EvictByTagAsync("tag3", default); + + result1 = await store.GetAsync(key1, default); + result2 = await store.GetAsync(key2, default); + + Assert.Null(result1); + Assert.Null(result2); + } + + private class TestMemoryOptionsClock : Extensions.Internal.ISystemClock + { + public DateTimeOffset UtcNow { get; set; } + public void Advance(TimeSpan duration) + { + UtcNow += duration; + } + } +} diff --git a/src/Middleware/OutputCaching/test/OutputCachingKeyProviderTests.cs b/src/Middleware/OutputCaching/test/OutputCacheKeyProviderTests.cs similarity index 99% rename from src/Middleware/OutputCaching/test/OutputCachingKeyProviderTests.cs rename to src/Middleware/OutputCaching/test/OutputCacheKeyProviderTests.cs index eba7cc13ef61..8b7a3d2c5bd6 100644 --- a/src/Middleware/OutputCaching/test/OutputCachingKeyProviderTests.cs +++ b/src/Middleware/OutputCaching/test/OutputCacheKeyProviderTests.cs @@ -5,7 +5,7 @@ namespace Microsoft.AspNetCore.OutputCaching.Tests; -public class OutputCachingKeyProviderTests +public class OutputCacheKeyProviderTests { private const char KeyDelimiter = '\x1e'; private const char KeySubDelimiter = '\x1f'; diff --git a/src/Middleware/OutputCaching/test/OutputCachingMiddlewareTests.cs b/src/Middleware/OutputCaching/test/OutputCacheMiddlewareTests.cs similarity index 99% rename from src/Middleware/OutputCaching/test/OutputCachingMiddlewareTests.cs rename to src/Middleware/OutputCaching/test/OutputCacheMiddlewareTests.cs index e240c9362a35..0a71b50ea5f9 100644 --- a/src/Middleware/OutputCaching/test/OutputCachingMiddlewareTests.cs +++ b/src/Middleware/OutputCaching/test/OutputCacheMiddlewareTests.cs @@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.OutputCaching.Tests; -public class OutputCachingMiddlewareTests +public class OutputCacheMiddlewareTests { [Fact] public async Task TryServeFromCacheAsync_OnlyIfCached_Serves504() diff --git a/src/Middleware/OutputCaching/test/OutputCachePoliciesTests.cs b/src/Middleware/OutputCaching/test/OutputCachePoliciesTests.cs new file mode 100644 index 000000000000..c84e5cb93cc8 --- /dev/null +++ b/src/Middleware/OutputCaching/test/OutputCachePoliciesTests.cs @@ -0,0 +1,294 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Http; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.OutputCaching.Memory; +using Microsoft.AspNetCore.OutputCaching.Policies; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.OutputCaching.Tests; + +public class OutputCachePoliciesTests +{ + [Fact] + public async Task DefaultCachePolicy_EnablesCache() + { + IOutputCachePolicy policy = DefaultPolicy.Instance; + var context = TestUtils.CreateUninitializedContext(); + + await policy.CacheRequestAsync(context); + + Assert.True(context.EnableOutputCaching); + } + + [Fact] + public async Task DefaultCachePolicy_AllowsLocking() + { + IOutputCachePolicy policy = DefaultPolicy.Instance; + var context = TestUtils.CreateUninitializedContext(); + + await policy.CacheRequestAsync(context); + + Assert.True(context.AllowLocking); + } + + [Fact] + public async Task DefaultCachePolicy_VariesByStar() + { + IOutputCachePolicy policy = DefaultPolicy.Instance; + var context = TestUtils.CreateUninitializedContext(); + + await policy.CacheRequestAsync(context); + + Assert.Equal("*", context.CachedVaryByRules.QueryKeys); + } + + [Fact] + public async Task EnableCachePolicy_DisablesCache() + { + IOutputCachePolicy policy = EnableCachePolicy.Disabled; + var context = TestUtils.CreateUninitializedContext(); + context.EnableOutputCaching = true; + + await policy.CacheRequestAsync(context); + + Assert.False(context.EnableOutputCaching); + } + + [Fact] + public async Task ExpirationPolicy_SetsResponseExpirationTimeSpan() + { + var duration = TimeSpan.FromDays(1); + IOutputCachePolicy policy = new ExpirationPolicy(duration); + var context = TestUtils.CreateUninitializedContext(); + + await policy.CacheRequestAsync(context); + + Assert.Equal(duration, context.ResponseExpirationTimeSpan); + } + + [Fact] + public async Task LockingPolicy_EnablesLocking() + { + IOutputCachePolicy policy = LockingPolicy.Enabled; + var context = TestUtils.CreateUninitializedContext(); + + await policy.CacheRequestAsync(context); + + Assert.True(context.AllowLocking); + } + + [Fact] + public async Task LockingPolicy_DisablesLocking() + { + IOutputCachePolicy policy = LockingPolicy.Disabled; + var context = TestUtils.CreateUninitializedContext(); + + await policy.CacheRequestAsync(context); + + Assert.False(context.AllowLocking); + } + + [Fact] + public async Task NoLookupPolicy_DisablesLookup() + { + IOutputCachePolicy policy = NoLookupPolicy.Instance; + var context = TestUtils.CreateUninitializedContext(); + + await policy.CacheRequestAsync(context); + + Assert.False(context.AllowCacheLookup); + } + + [Fact] + public async Task NoStorePolicy_DisablesStore() + { + IOutputCachePolicy policy = NoStorePolicy.Instance; + var context = TestUtils.CreateUninitializedContext(); + + await policy.CacheRequestAsync(context); + + Assert.False(context.AllowCacheStorage); + } + + [Theory] + [InlineData(true, true, true)] + [InlineData(true, false, false)] + [InlineData(false, true, false)] + [InlineData(false, false, false)] + public async Task PredicatePolicy_Filters(bool filter, bool enabled, bool expected) + { + IOutputCachePolicy predicate = new PredicatePolicy(c => ValueTask.FromResult(filter), enabled ? EnableCachePolicy.Enabled : EnableCachePolicy.Disabled); + var context = TestUtils.CreateUninitializedContext(); + + await predicate.CacheRequestAsync(context); + + Assert.Equal(expected, context.EnableOutputCaching); + } + + [Fact] + public async Task ProfilePolicy_UsesNamedProfile() + { + var context = TestUtils.CreateUninitializedContext(); + context.Options.AddPolicy("enabled", EnableCachePolicy.Enabled); + context.Options.AddPolicy("disabled", EnableCachePolicy.Disabled); + + IOutputCachePolicy policy = new ProfilePolicy("enabled"); + + await policy.CacheRequestAsync(context); + + Assert.True(context.EnableOutputCaching); + + policy = new ProfilePolicy("disabled"); + + await policy.CacheRequestAsync(context); + + Assert.False(context.EnableOutputCaching); + } + + [Fact] + public async Task TagsPolicy_Tags() + { + var context = TestUtils.CreateUninitializedContext(); + + IOutputCachePolicy policy = new TagsPolicy("tag1", "tag2"); + + await policy.CacheRequestAsync(context); + + Assert.Contains("tag1", context.Tags); + Assert.Contains("tag2", context.Tags); + } + + [Fact] + public async Task VaryByHeadersPolicy_IsEmpty() + { + var context = TestUtils.CreateUninitializedContext(); + + IOutputCachePolicy policy = new VaryByHeaderPolicy(); + + await policy.CacheRequestAsync(context); + + Assert.Empty(context.CachedVaryByRules.Headers); + } + + [Fact] + public async Task VaryByHeadersPolicy_AddsSingleHeader() + { + var context = TestUtils.CreateUninitializedContext(); + var header = "header"; + + IOutputCachePolicy policy = new VaryByHeaderPolicy(header); + + await policy.CacheRequestAsync(context); + + Assert.Equal(header, context.CachedVaryByRules.Headers); + } + + [Fact] + public async Task VaryByHeadersPolicy_AddsMultipleHeaders() + { + var context = TestUtils.CreateUninitializedContext(); + var headers = new[] { "header1", "header2" }; + + IOutputCachePolicy policy = new VaryByHeaderPolicy(headers); + + await policy.CacheRequestAsync(context); + + Assert.Equal(headers, context.CachedVaryByRules.Headers); + } + + [Fact] + public async Task VaryByQueryPolicy_IsEmpty() + { + var context = TestUtils.CreateUninitializedContext(); + + IOutputCachePolicy policy = new VaryByQueryPolicy(); + + await policy.CacheRequestAsync(context); + + Assert.Empty(context.CachedVaryByRules.QueryKeys); + } + + [Fact] + public async Task VaryByQueryPolicy_AddsSingleHeader() + { + var context = TestUtils.CreateUninitializedContext(); + var query = "query"; + + IOutputCachePolicy policy = new VaryByQueryPolicy(query); + + await policy.CacheRequestAsync(context); + + Assert.Equal(query, context.CachedVaryByRules.QueryKeys); + } + + [Fact] + public async Task VaryByQueryPolicy_AddsMultipleHeaders() + { + var context = TestUtils.CreateUninitializedContext(); + var queries = new[] { "query1", "query2" }; + + IOutputCachePolicy policy = new VaryByQueryPolicy(queries); + + await policy.CacheRequestAsync(context); + + Assert.Equal(queries, context.CachedVaryByRules.QueryKeys); + } + + [Fact] + public async Task VaryByValuePolicy_SingleValue() + { + var context = TestUtils.CreateUninitializedContext(); + var value = "value"; + + IOutputCachePolicy policy = new VaryByValuePolicy(context => value); + + await policy.CacheRequestAsync(context); + + Assert.Equal(value, context.CachedVaryByRules.VaryByPrefix); + } + + [Fact] + public async Task VaryByValuePolicy_SingleValueAsync() + { + var context = TestUtils.CreateUninitializedContext(); + var value = "value"; + + IOutputCachePolicy policy = new VaryByValuePolicy((context, token) => ValueTask.FromResult(value)); + + await policy.CacheRequestAsync(context); + + Assert.Equal(value, context.CachedVaryByRules.VaryByPrefix); + } + + [Fact] + public async Task VaryByValuePolicy_KeyValuePair() + { + var context = TestUtils.CreateUninitializedContext(); + var key = "key"; + var value = "value"; + + IOutputCachePolicy policy = new VaryByValuePolicy(context => new KeyValuePair(key, value)); + + await policy.CacheRequestAsync(context); + + Assert.Contains(new KeyValuePair(key, value), context.CachedVaryByRules.VaryByCustom); + } + + [Fact] + public async Task VaryByValuePolicy_KeyValuePairAsync() + { + var context = TestUtils.CreateUninitializedContext(); + var key = "key"; + var value = "value"; + + IOutputCachePolicy policy = new VaryByValuePolicy((context, token) => ValueTask.FromResult(new KeyValuePair(key, value))); + + await policy.CacheRequestAsync(context); + + Assert.Contains(new KeyValuePair(key, value), context.CachedVaryByRules.VaryByCustom); + } +} diff --git a/src/Middleware/OutputCaching/test/OutputCachingPolicyProviderTests.cs b/src/Middleware/OutputCaching/test/OutputCachePolicyProviderTests.cs similarity index 99% rename from src/Middleware/OutputCaching/test/OutputCachingPolicyProviderTests.cs rename to src/Middleware/OutputCaching/test/OutputCachePolicyProviderTests.cs index 3ee32f2c60fe..d3d00347e2e6 100644 --- a/src/Middleware/OutputCaching/test/OutputCachingPolicyProviderTests.cs +++ b/src/Middleware/OutputCaching/test/OutputCachePolicyProviderTests.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.OutputCaching.Tests; -public class OutputCachingPolicyProviderTests +public class OutputCachePolicyProviderTests { public static TheoryData CacheableMethods { diff --git a/src/Middleware/OutputCaching/test/OutputCachingTests.cs b/src/Middleware/OutputCaching/test/OutputCacheTests.cs similarity index 96% rename from src/Middleware/OutputCaching/test/OutputCachingTests.cs rename to src/Middleware/OutputCaching/test/OutputCacheTests.cs index ae9a4ebabf52..6e8bfb5455c4 100644 --- a/src/Middleware/OutputCaching/test/OutputCachingTests.cs +++ b/src/Middleware/OutputCaching/test/OutputCacheTests.cs @@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.OutputCaching.Tests; -public class OutputCachingTests +public class OutputCacheTests { [Theory] [InlineData("GET")] @@ -220,7 +220,7 @@ public async Task ServesCachedContent_If_PathCasingDiffers(string method) public async Task ServesFreshContent_If_ResponseExpired(string method) { var options = new OutputCacheOptions(); - options.BasePolicies.Add(new OutputCachePolicyBuilder().VaryByHeader(HeaderNames.From).Build()); + options.AddBasePolicy(b => b.VaryByHeader(HeaderNames.From)); options.DefaultExpirationTimeSpan = TimeSpan.FromMicroseconds(100); var builders = TestUtils.CreateBuildersWithOutputCaching(options: options); @@ -295,7 +295,7 @@ public async Task ServesCachedContent_IfVaryHeader_Matches() public async Task ServesFreshContent_IfVaryHeader_Mismatches() { var options = new OutputCacheOptions(); - options.BasePolicies.Add(new OutputCachePolicyBuilder().VaryByHeader(HeaderNames.From).Build()); + options.AddBasePolicy(b => b.VaryByHeader(HeaderNames.From).Build()); var builders = TestUtils.CreateBuildersWithOutputCaching(options: options); @@ -322,7 +322,7 @@ public async Task ServesFreshContent_IfVaryHeader_Mismatches() public async Task ServesCachedContent_IfVaryQueryKeys_Matches() { var options = new OutputCacheOptions(); - options.BasePolicies.Add(new OutputCachePolicyBuilder().VaryByQuery("query").Build()); + options.AddBasePolicy(b => b.VaryByQuery("query")); var builders = TestUtils.CreateBuildersWithOutputCaching(options: options); @@ -347,7 +347,7 @@ public async Task ServesCachedContent_IfVaryQueryKeys_Matches() public async Task ServesCachedContent_IfVaryQueryKeysExplicit_Matches_QueryKeyCaseInsensitive() { var options = new OutputCacheOptions(); - options.BasePolicies.Add(new OutputCachePolicyBuilder().VaryByQuery("QueryA", "queryb").Build()); + options.AddBasePolicy(b => b.VaryByQuery("QueryA", "queryb")); var builders = TestUtils.CreateBuildersWithOutputCaching(options: options); @@ -372,7 +372,7 @@ public async Task ServesCachedContent_IfVaryQueryKeysExplicit_Matches_QueryKeyCa public async Task ServesCachedContent_IfVaryQueryKeyStar_Matches_QueryKeyCaseInsensitive() { var options = new OutputCacheOptions(); - options.BasePolicies.Add(new OutputCachePolicyBuilder().VaryByQuery("*").Build()); + options.AddBasePolicy(b => b.VaryByQuery("*")); var builders = TestUtils.CreateBuildersWithOutputCaching(options: options); @@ -397,7 +397,7 @@ public async Task ServesCachedContent_IfVaryQueryKeyStar_Matches_QueryKeyCaseIns public async Task ServesCachedContent_IfVaryQueryKeyExplicit_Matches_OrderInsensitive() { var options = new OutputCacheOptions(); - options.BasePolicies.Add(new OutputCachePolicyBuilder().VaryByQuery("QueryB", "QueryA").Build()); + options.AddBasePolicy(b => b.VaryByQuery("QueryB", "QueryA")); var builders = TestUtils.CreateBuildersWithOutputCaching(options: options); @@ -422,7 +422,7 @@ public async Task ServesCachedContent_IfVaryQueryKeyExplicit_Matches_OrderInsens public async Task ServesCachedContent_IfVaryQueryKeyStar_Matches_OrderInsensitive() { var options = new OutputCacheOptions(); - options.BasePolicies.Add(new OutputCachePolicyBuilder().VaryByQuery("*").Build()); + options.AddBasePolicy(b => b.VaryByQuery("*")); var builders = TestUtils.CreateBuildersWithOutputCaching(options: options); @@ -447,7 +447,7 @@ public async Task ServesCachedContent_IfVaryQueryKeyStar_Matches_OrderInsensitiv public async Task ServesFreshContent_IfVaryQueryKey_Mismatches() { var options = new OutputCacheOptions(); - options.BasePolicies.Add(new OutputCachePolicyBuilder().VaryByQuery("query").Build()); + options.AddBasePolicy(b => b.VaryByQuery("query").Build()); var builders = TestUtils.CreateBuildersWithOutputCaching(options: options); foreach (var builder in builders) @@ -471,7 +471,7 @@ public async Task ServesFreshContent_IfVaryQueryKey_Mismatches() public async Task ServesCachedContent_IfOtherVaryQueryKey_Mismatches() { var options = new OutputCacheOptions(); - options.BasePolicies.Add(new OutputCachePolicyBuilder().VaryByQuery("query").Build()); + options.AddBasePolicy(b => b.VaryByQuery("query").Build()); var builders = TestUtils.CreateBuildersWithOutputCaching(options: options); @@ -496,7 +496,7 @@ public async Task ServesCachedContent_IfOtherVaryQueryKey_Mismatches() public async Task ServesFreshContent_IfVaryQueryKeyExplicit_Mismatch_QueryKeyCaseSensitive() { var options = new OutputCacheOptions(); - options.BasePolicies.Add(new VaryByQueryPolicy("QueryA", "QueryB")); + options.AddBasePolicy(new VaryByQueryPolicy("QueryA", "QueryB")); var builders = TestUtils.CreateBuildersWithOutputCaching(options: options); foreach (var builder in builders) @@ -520,7 +520,7 @@ public async Task ServesFreshContent_IfVaryQueryKeyExplicit_Mismatch_QueryKeyCas public async Task ServesFreshContent_IfVaryQueryKeyStar_Mismatch_QueryKeyValueCaseSensitive() { var options = new OutputCacheOptions(); - options.BasePolicies.Add(new VaryByQueryPolicy("*")); + options.AddBasePolicy(new VaryByQueryPolicy("*")); var builders = TestUtils.CreateBuildersWithOutputCaching(options: options); foreach (var builder in builders) @@ -801,7 +801,7 @@ public async Task ServesCachedContent_IfBodySize_IsCacheable() { var options = new OutputCacheOptions(); options.MaximumBodySize = 1000; - options.BasePolicies.Add(new OutputCachePolicyBuilder().Build()); + options.AddBasePolicy(b => b.Build()); var builders = TestUtils.CreateBuildersWithOutputCaching(options: options); diff --git a/src/Middleware/OutputCaching/test/TestUtils.cs b/src/Middleware/OutputCaching/test/TestUtils.cs index a0d79d96c13f..066dc91cfb9b 100644 --- a/src/Middleware/OutputCaching/test/TestUtils.cs +++ b/src/Middleware/OutputCaching/test/TestUtils.cs @@ -150,6 +150,7 @@ private static IEnumerable CreateBuildersWithOutputCaching( } else { + outputCachingOptions.BasePolicies = new(); outputCachingOptions.BasePolicies.Add(new OutputCachePolicyBuilder().Build()); } }); @@ -230,6 +231,13 @@ internal static OutputCacheContext CreateTestContext(ITestSink testSink, IOutput }; } + internal static OutputCacheContext CreateUninitializedContext(IOutputCacheStore cache = null, OutputCacheOptions options = null) + { + return new OutputCacheContext(new DefaultHttpContext(), cache ?? new TestOutputCache(), options ?? Options.Create(new OutputCacheOptions()).Value, NullLogger.Instance) + { + }; + } + internal static void AssertLoggedMessages(IEnumerable messages, params LoggedMessage[] expectedMessages) { var messageList = messages.ToList(); From fcbea078ee92bb752196032376d0401325b724c0 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Tue, 21 Jun 2022 10:59:32 -0700 Subject: [PATCH 43/48] Add more tests --- .../OutputCaching/test/OutputCacheTests.cs | 620 +++++++++--------- .../OutputCaching/test/TestUtils.cs | 20 + 2 files changed, 320 insertions(+), 320 deletions(-) diff --git a/src/Middleware/OutputCaching/test/OutputCacheTests.cs b/src/Middleware/OutputCaching/test/OutputCacheTests.cs index 6e8bfb5455c4..83080b67efcc 100644 --- a/src/Middleware/OutputCaching/test/OutputCacheTests.cs +++ b/src/Middleware/OutputCaching/test/OutputCacheTests.cs @@ -23,14 +23,12 @@ public async Task ServesCachedContent_IfAvailable(string method) await host.StartAsync(); - using (var server = host.GetTestServer()) - { - var client = server.CreateClient(); - var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); - var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); + using var server = host.GetTestServer(); + var client = server.CreateClient(); + var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); + var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); - await AssertCachedResponseAsync(initialResponse, subsequentResponse); - } + await AssertCachedResponseAsync(initialResponse, subsequentResponse); } } @@ -47,14 +45,12 @@ public async Task ServesFreshContent_IfNotAvailable(string method) await host.StartAsync(); - using (var server = host.GetTestServer()) - { - var client = server.CreateClient(); - var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); - var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, "different")); + using var server = host.GetTestServer(); + var client = server.CreateClient(); + var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); + var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, "different")); - await AssertFreshResponseAsync(initialResponse, subsequentResponse); - } + await AssertFreshResponseAsync(initialResponse, subsequentResponse); } } @@ -69,14 +65,12 @@ public async Task ServesFreshContent_Post() await host.StartAsync(); - using (var server = host.GetTestServer()) - { - var client = server.CreateClient(); - var initialResponse = await client.PostAsync("", new StringContent(string.Empty)); - var subsequentResponse = await client.PostAsync("", new StringContent(string.Empty)); + using var server = host.GetTestServer(); + var client = server.CreateClient(); + var initialResponse = await client.PostAsync("", new StringContent(string.Empty)); + var subsequentResponse = await client.PostAsync("", new StringContent(string.Empty)); - await AssertFreshResponseAsync(initialResponse, subsequentResponse); - } + await AssertFreshResponseAsync(initialResponse, subsequentResponse); } } @@ -91,14 +85,12 @@ public async Task ServesFreshContent_Head_Get() await host.StartAsync(); - using (var server = host.GetTestServer()) - { - var client = server.CreateClient(); - var subsequentResponse = await client.SendAsync(new HttpRequestMessage(HttpMethod.Head, "")); - var initialResponse = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "")); + using var server = host.GetTestServer(); + var client = server.CreateClient(); + var subsequentResponse = await client.SendAsync(new HttpRequestMessage(HttpMethod.Head, "")); + var initialResponse = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "")); - await AssertFreshResponseAsync(initialResponse, subsequentResponse); - } + await AssertFreshResponseAsync(initialResponse, subsequentResponse); } } @@ -113,14 +105,12 @@ public async Task ServesFreshContent_Get_Head() await host.StartAsync(); - using (var server = host.GetTestServer()) - { - var client = server.CreateClient(); - var initialResponse = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "")); - var subsequentResponse = await client.SendAsync(new HttpRequestMessage(HttpMethod.Head, "")); + using var server = host.GetTestServer(); + var client = server.CreateClient(); + var initialResponse = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "")); + var subsequentResponse = await client.SendAsync(new HttpRequestMessage(HttpMethod.Head, "")); - await AssertFreshResponseAsync(initialResponse, subsequentResponse); - } + await AssertFreshResponseAsync(initialResponse, subsequentResponse); } } @@ -137,23 +127,21 @@ public async Task ServesCachedContent_If_CacheControlNoCache(string method) await host.StartAsync(); - using (var server = host.GetTestServer()) - { - var client = server.CreateClient(); + using var server = host.GetTestServer(); + var client = server.CreateClient(); - var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); + var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); - // verify the response is cached - var cachedResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); - await AssertCachedResponseAsync(initialResponse, cachedResponse); + // verify the response is cached + var cachedResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); + await AssertCachedResponseAsync(initialResponse, cachedResponse); - // assert cached response still served - client.DefaultRequestHeaders.CacheControl = - new System.Net.Http.Headers.CacheControlHeaderValue { NoCache = true }; - var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); + // assert cached response still served + client.DefaultRequestHeaders.CacheControl = + new System.Net.Http.Headers.CacheControlHeaderValue { NoCache = true }; + var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); - await AssertCachedResponseAsync(initialResponse, subsequentResponse); - } + await AssertCachedResponseAsync(initialResponse, subsequentResponse); } } @@ -170,23 +158,21 @@ public async Task ServesCachedContent_If_PragmaNoCache(string method) await host.StartAsync(); - using (var server = host.GetTestServer()) - { - var client = server.CreateClient(); + using var server = host.GetTestServer(); + var client = server.CreateClient(); - var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); + var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); - // verify the response is cached - var cachedResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); - await AssertCachedResponseAsync(initialResponse, cachedResponse); + // verify the response is cached + var cachedResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); + await AssertCachedResponseAsync(initialResponse, cachedResponse); - // assert cached response still served - client.DefaultRequestHeaders.Pragma.Clear(); - client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("no-cache")); - var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); + // assert cached response still served + client.DefaultRequestHeaders.Pragma.Clear(); + client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("no-cache")); + var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); - await AssertCachedResponseAsync(initialResponse, subsequentResponse); - } + await AssertCachedResponseAsync(initialResponse, subsequentResponse); } } @@ -203,14 +189,35 @@ public async Task ServesCachedContent_If_PathCasingDiffers(string method) await host.StartAsync(); - using (var server = host.GetTestServer()) - { - var client = server.CreateClient(); - var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, "path")); - var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, "PATH")); + using var server = host.GetTestServer(); + var client = server.CreateClient(); + var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, "path")); + var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, "PATH")); - await AssertCachedResponseAsync(initialResponse, subsequentResponse); - } + await AssertCachedResponseAsync(initialResponse, subsequentResponse); + } + } + + [Theory] + [InlineData("GET")] + [InlineData("HEAD")] + public async Task ServesFreshContent_If_PathCasingDiffers(string method) + { + var options = new OutputCacheOptions { UseCaseSensitivePaths = true }; + var builders = TestUtils.CreateBuildersWithOutputCaching(options: options); + + foreach (var builder in builders) + { + using var host = builder.Build(); + + await host.StartAsync(); + + using var server = host.GetTestServer(); + var client = server.CreateClient(); + var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, "path")); + var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, "PATH")); + + await AssertFreshResponseAsync(initialResponse, subsequentResponse); } } @@ -219,9 +226,10 @@ public async Task ServesCachedContent_If_PathCasingDiffers(string method) [InlineData("HEAD")] public async Task ServesFreshContent_If_ResponseExpired(string method) { - var options = new OutputCacheOptions(); - options.AddBasePolicy(b => b.VaryByHeader(HeaderNames.From)); - options.DefaultExpirationTimeSpan = TimeSpan.FromMicroseconds(100); + var options = new OutputCacheOptions + { + DefaultExpirationTimeSpan = TimeSpan.FromMicroseconds(100) + }; var builders = TestUtils.CreateBuildersWithOutputCaching(options: options); @@ -231,15 +239,13 @@ public async Task ServesFreshContent_If_ResponseExpired(string method) await host.StartAsync(); - using (var server = host.GetTestServer()) - { - var client = server.CreateClient(); - var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); - await Task.Delay(1); - var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); + using var server = host.GetTestServer(); + var client = server.CreateClient(); + var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); + await Task.Delay(1); + var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); - await AssertFreshResponseAsync(initialResponse, subsequentResponse); - } + await AssertFreshResponseAsync(initialResponse, subsequentResponse); } } @@ -256,15 +262,41 @@ public async Task ServesFreshContent_If_Authorization_HeaderExists(string method await host.StartAsync(); - using (var server = host.GetTestServer()) - { - var client = server.CreateClient(); - client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("abc"); - var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); - var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); + using var server = host.GetTestServer(); + var client = server.CreateClient(); + client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("abc"); + var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); + var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); - await AssertFreshResponseAsync(initialResponse, subsequentResponse); - } + await AssertFreshResponseAsync(initialResponse, subsequentResponse); + } + } + + [Theory] + [InlineData("GET")] + [InlineData("HEAD")] + public async Task ServesCachedContent_If_Authorization_HeaderExists(string method) + { + var options = new OutputCacheOptions(); + + var builders = TestUtils.CreateBuildersWithOutputCaching(options: options); + + // This is added after the DefaultPolicy which disables caching for authenticated requests + options.AddBasePolicy(b => b.AddPolicy()); + + foreach (var builder in builders) + { + using var host = builder.Build(); + + await host.StartAsync(); + + using var server = host.GetTestServer(); + var client = server.CreateClient(); + client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("abc"); + var initialResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); + var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest(method, "")); + + await AssertCachedResponseAsync(initialResponse, subsequentResponse); } } @@ -279,15 +311,13 @@ public async Task ServesCachedContent_IfVaryHeader_Matches() await host.StartAsync(); - using (var server = host.GetTestServer()) - { - var client = server.CreateClient(); - client.DefaultRequestHeaders.From = "user@example.com"; - var initialResponse = await client.GetAsync(""); - var subsequentResponse = await client.GetAsync(""); + using var server = host.GetTestServer(); + var client = server.CreateClient(); + client.DefaultRequestHeaders.From = "user@example.com"; + var initialResponse = await client.GetAsync(""); + var subsequentResponse = await client.GetAsync(""); - await AssertCachedResponseAsync(initialResponse, subsequentResponse); - } + await AssertCachedResponseAsync(initialResponse, subsequentResponse); } } @@ -305,16 +335,14 @@ public async Task ServesFreshContent_IfVaryHeader_Mismatches() await host.StartAsync(); - using (var server = host.GetTestServer()) - { - var client = server.CreateClient(); - client.DefaultRequestHeaders.From = "user@example.com"; - var initialResponse = await client.GetAsync(""); - client.DefaultRequestHeaders.From = "user2@example.com"; - var subsequentResponse = await client.GetAsync(""); + using var server = host.GetTestServer(); + var client = server.CreateClient(); + client.DefaultRequestHeaders.From = "user@example.com"; + var initialResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.From = "user2@example.com"; + var subsequentResponse = await client.GetAsync(""); - await AssertFreshResponseAsync(initialResponse, subsequentResponse); - } + await AssertFreshResponseAsync(initialResponse, subsequentResponse); } } @@ -332,14 +360,12 @@ public async Task ServesCachedContent_IfVaryQueryKeys_Matches() await host.StartAsync(); - using (var server = host.GetTestServer()) - { - var client = server.CreateClient(); - var initialResponse = await client.GetAsync("?query=value"); - var subsequentResponse = await client.GetAsync("?query=value"); + using var server = host.GetTestServer(); + var client = server.CreateClient(); + var initialResponse = await client.GetAsync("?query=value"); + var subsequentResponse = await client.GetAsync("?query=value"); - await AssertCachedResponseAsync(initialResponse, subsequentResponse); - } + await AssertCachedResponseAsync(initialResponse, subsequentResponse); } } @@ -357,14 +383,12 @@ public async Task ServesCachedContent_IfVaryQueryKeysExplicit_Matches_QueryKeyCa await host.StartAsync(); - using (var server = host.GetTestServer()) - { - var client = server.CreateClient(); - var initialResponse = await client.GetAsync("?querya=valuea&queryb=valueb"); - var subsequentResponse = await client.GetAsync("?QueryA=valuea&QueryB=valueb"); + using var server = host.GetTestServer(); + var client = server.CreateClient(); + var initialResponse = await client.GetAsync("?querya=valuea&queryb=valueb"); + var subsequentResponse = await client.GetAsync("?QueryA=valuea&QueryB=valueb"); - await AssertCachedResponseAsync(initialResponse, subsequentResponse); - } + await AssertCachedResponseAsync(initialResponse, subsequentResponse); } } @@ -382,14 +406,12 @@ public async Task ServesCachedContent_IfVaryQueryKeyStar_Matches_QueryKeyCaseIns await host.StartAsync(); - using (var server = host.GetTestServer()) - { - var client = server.CreateClient(); - var initialResponse = await client.GetAsync("?querya=valuea&queryb=valueb"); - var subsequentResponse = await client.GetAsync("?QueryA=valuea&QueryB=valueb"); + using var server = host.GetTestServer(); + var client = server.CreateClient(); + var initialResponse = await client.GetAsync("?querya=valuea&queryb=valueb"); + var subsequentResponse = await client.GetAsync("?QueryA=valuea&QueryB=valueb"); - await AssertCachedResponseAsync(initialResponse, subsequentResponse); - } + await AssertCachedResponseAsync(initialResponse, subsequentResponse); } } @@ -407,14 +429,12 @@ public async Task ServesCachedContent_IfVaryQueryKeyExplicit_Matches_OrderInsens await host.StartAsync(); - using (var server = host.GetTestServer()) - { - var client = server.CreateClient(); - var initialResponse = await client.GetAsync("?QueryA=ValueA&QueryB=ValueB"); - var subsequentResponse = await client.GetAsync("?QueryB=ValueB&QueryA=ValueA"); + using var server = host.GetTestServer(); + var client = server.CreateClient(); + var initialResponse = await client.GetAsync("?QueryA=ValueA&QueryB=ValueB"); + var subsequentResponse = await client.GetAsync("?QueryB=ValueB&QueryA=ValueA"); - await AssertCachedResponseAsync(initialResponse, subsequentResponse); - } + await AssertCachedResponseAsync(initialResponse, subsequentResponse); } } @@ -432,14 +452,12 @@ public async Task ServesCachedContent_IfVaryQueryKeyStar_Matches_OrderInsensitiv await host.StartAsync(); - using (var server = host.GetTestServer()) - { - var client = server.CreateClient(); - var initialResponse = await client.GetAsync("?QueryA=ValueA&QueryB=ValueB"); - var subsequentResponse = await client.GetAsync("?QueryB=ValueB&QueryA=ValueA"); + using var server = host.GetTestServer(); + var client = server.CreateClient(); + var initialResponse = await client.GetAsync("?QueryA=ValueA&QueryB=ValueB"); + var subsequentResponse = await client.GetAsync("?QueryB=ValueB&QueryA=ValueA"); - await AssertCachedResponseAsync(initialResponse, subsequentResponse); - } + await AssertCachedResponseAsync(initialResponse, subsequentResponse); } } @@ -456,14 +474,12 @@ public async Task ServesFreshContent_IfVaryQueryKey_Mismatches() await host.StartAsync(); - using (var server = host.GetTestServer()) - { - var client = server.CreateClient(); - var initialResponse = await client.GetAsync("?query=value"); - var subsequentResponse = await client.GetAsync("?query=value2"); + using var server = host.GetTestServer(); + var client = server.CreateClient(); + var initialResponse = await client.GetAsync("?query=value"); + var subsequentResponse = await client.GetAsync("?query=value2"); - await AssertFreshResponseAsync(initialResponse, subsequentResponse); - } + await AssertFreshResponseAsync(initialResponse, subsequentResponse); } } @@ -481,14 +497,12 @@ public async Task ServesCachedContent_IfOtherVaryQueryKey_Mismatches() await host.StartAsync(); - using (var server = host.GetTestServer()) - { - var client = server.CreateClient(); - var initialResponse = await client.GetAsync("?other=value1"); - var subsequentResponse = await client.GetAsync("?other=value2"); + using var server = host.GetTestServer(); + var client = server.CreateClient(); + var initialResponse = await client.GetAsync("?other=value1"); + var subsequentResponse = await client.GetAsync("?other=value2"); - await AssertCachedResponseAsync(initialResponse, subsequentResponse); - } + await AssertCachedResponseAsync(initialResponse, subsequentResponse); } } @@ -505,14 +519,12 @@ public async Task ServesFreshContent_IfVaryQueryKeyExplicit_Mismatch_QueryKeyCas await host.StartAsync(); - using (var server = host.GetTestServer()) - { - var client = server.CreateClient(); - var initialResponse = await client.GetAsync("?querya=valuea&queryb=valueb"); - var subsequentResponse = await client.GetAsync("?querya=ValueA&queryb=ValueB"); + using var server = host.GetTestServer(); + var client = server.CreateClient(); + var initialResponse = await client.GetAsync("?querya=valuea&queryb=valueb"); + var subsequentResponse = await client.GetAsync("?querya=ValueA&queryb=ValueB"); - await AssertFreshResponseAsync(initialResponse, subsequentResponse); - } + await AssertFreshResponseAsync(initialResponse, subsequentResponse); } } @@ -529,14 +541,12 @@ public async Task ServesFreshContent_IfVaryQueryKeyStar_Mismatch_QueryKeyValueCa await host.StartAsync(); - using (var server = host.GetTestServer()) - { - var client = server.CreateClient(); - var initialResponse = await client.GetAsync("?querya=valuea&queryb=valueb"); - var subsequentResponse = await client.GetAsync("?querya=ValueA&queryb=ValueB"); + using var server = host.GetTestServer(); + var client = server.CreateClient(); + var initialResponse = await client.GetAsync("?querya=valuea&queryb=valueb"); + var subsequentResponse = await client.GetAsync("?querya=ValueA&queryb=ValueB"); - await AssertFreshResponseAsync(initialResponse, subsequentResponse); - } + await AssertFreshResponseAsync(initialResponse, subsequentResponse); } } @@ -551,18 +561,16 @@ public async Task ServesCachedContent_IfRequestRequirements_NotMet() await host.StartAsync(); - using (var server = host.GetTestServer()) + using var server = host.GetTestServer(); + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue() { - var client = server.CreateClient(); - var initialResponse = await client.GetAsync(""); - client.DefaultRequestHeaders.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue() - { - MaxAge = TimeSpan.FromSeconds(0) - }; - var subsequentResponse = await client.GetAsync(""); + MaxAge = TimeSpan.FromSeconds(0) + }; + var subsequentResponse = await client.GetAsync(""); - await AssertCachedResponseAsync(initialResponse, subsequentResponse); - } + await AssertCachedResponseAsync(initialResponse, subsequentResponse); } } @@ -577,19 +585,17 @@ public async Task Serves504_IfOnlyIfCachedHeader_IsSpecified() await host.StartAsync(); - using (var server = host.GetTestServer()) + using var server = host.GetTestServer(); + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue() { - var client = server.CreateClient(); - var initialResponse = await client.GetAsync(""); - client.DefaultRequestHeaders.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue() - { - OnlyIfCached = true - }; - var subsequentResponse = await client.GetAsync("/different"); + OnlyIfCached = true + }; + var subsequentResponse = await client.GetAsync("/different"); - initialResponse.EnsureSuccessStatusCode(); - Assert.Equal(System.Net.HttpStatusCode.GatewayTimeout, subsequentResponse.StatusCode); - } + initialResponse.EnsureSuccessStatusCode(); + Assert.Equal(System.Net.HttpStatusCode.GatewayTimeout, subsequentResponse.StatusCode); } } @@ -604,14 +610,12 @@ public async Task ServesFreshContent_IfSetCookie_IsSpecified() await host.StartAsync(); - using (var server = host.GetTestServer()) - { - var client = server.CreateClient(); - var initialResponse = await client.GetAsync(""); - var subsequentResponse = await client.GetAsync(""); + using var server = host.GetTestServer(); + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + var subsequentResponse = await client.GetAsync(""); - await AssertFreshResponseAsync(initialResponse, subsequentResponse); - } + await AssertFreshResponseAsync(initialResponse, subsequentResponse); } } @@ -626,18 +630,16 @@ public async Task ServesCachedContent_IfSubsequentRequestContainsNoStore() await host.StartAsync(); - using (var server = host.GetTestServer()) + using var server = host.GetTestServer(); + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue() { - var client = server.CreateClient(); - var initialResponse = await client.GetAsync(""); - client.DefaultRequestHeaders.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue() - { - NoStore = true - }; - var subsequentResponse = await client.GetAsync(""); + NoStore = true + }; + var subsequentResponse = await client.GetAsync(""); - await AssertCachedResponseAsync(initialResponse, subsequentResponse); - } + await AssertCachedResponseAsync(initialResponse, subsequentResponse); } } @@ -652,18 +654,16 @@ public async Task ServesCachedContent_IfInitialRequestContainsNoStore() await host.StartAsync(); - using (var server = host.GetTestServer()) + using var server = host.GetTestServer(); + var client = server.CreateClient(); + client.DefaultRequestHeaders.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue() { - var client = server.CreateClient(); - client.DefaultRequestHeaders.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue() - { - NoStore = true - }; - var initialResponse = await client.GetAsync(""); - var subsequentResponse = await client.GetAsync(""); + NoStore = true + }; + var initialResponse = await client.GetAsync(""); + var subsequentResponse = await client.GetAsync(""); - await AssertCachedResponseAsync(initialResponse, subsequentResponse); - } + await AssertCachedResponseAsync(initialResponse, subsequentResponse); } } @@ -678,14 +678,12 @@ public async Task ServesCachedContent_IfInitialResponseContainsNoStore() await host.StartAsync(); - using (var server = host.GetTestServer()) - { - var client = server.CreateClient(); - var initialResponse = await client.GetAsync(""); - var subsequentResponse = await client.GetAsync(""); + using var server = host.GetTestServer(); + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + var subsequentResponse = await client.GetAsync(""); - await AssertCachedResponseAsync(initialResponse, subsequentResponse); - } + await AssertCachedResponseAsync(initialResponse, subsequentResponse); } } @@ -706,17 +704,15 @@ public async Task Serves304_IfIfModifiedSince_Satisfied() await host.StartAsync(); - using (var server = host.GetTestServer()) - { - var client = server.CreateClient(); - var initialResponse = await client.GetAsync(""); - client.DefaultRequestHeaders.IfModifiedSince = DateTimeOffset.MaxValue; - var subsequentResponse = await client.GetAsync(""); + using var server = host.GetTestServer(); + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.IfModifiedSince = DateTimeOffset.MaxValue; + var subsequentResponse = await client.GetAsync(""); - initialResponse.EnsureSuccessStatusCode(); - Assert.Equal(System.Net.HttpStatusCode.NotModified, subsequentResponse.StatusCode); - Assert304Headers(initialResponse, subsequentResponse); - } + initialResponse.EnsureSuccessStatusCode(); + Assert.Equal(System.Net.HttpStatusCode.NotModified, subsequentResponse.StatusCode); + Assert304Headers(initialResponse, subsequentResponse); } } @@ -731,15 +727,13 @@ public async Task ServesCachedContent_IfIfModifiedSince_NotSatisfied() await host.StartAsync(); - using (var server = host.GetTestServer()) - { - var client = server.CreateClient(); - var initialResponse = await client.GetAsync(""); - client.DefaultRequestHeaders.IfModifiedSince = DateTimeOffset.MinValue; - var subsequentResponse = await client.GetAsync(""); + using var server = host.GetTestServer(); + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.IfModifiedSince = DateTimeOffset.MinValue; + var subsequentResponse = await client.GetAsync(""); - await AssertCachedResponseAsync(initialResponse, subsequentResponse); - } + await AssertCachedResponseAsync(initialResponse, subsequentResponse); } } @@ -759,17 +753,15 @@ public async Task Serves304_IfIfNoneMatch_Satisfied() await host.StartAsync(); - using (var server = host.GetTestServer()) - { - var client = server.CreateClient(); - var initialResponse = await client.GetAsync(""); - client.DefaultRequestHeaders.IfNoneMatch.Add(new System.Net.Http.Headers.EntityTagHeaderValue("\"E1\"")); - var subsequentResponse = await client.GetAsync(""); + using var server = host.GetTestServer(); + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.IfNoneMatch.Add(new System.Net.Http.Headers.EntityTagHeaderValue("\"E1\"")); + var subsequentResponse = await client.GetAsync(""); - initialResponse.EnsureSuccessStatusCode(); - Assert.Equal(System.Net.HttpStatusCode.NotModified, subsequentResponse.StatusCode); - Assert304Headers(initialResponse, subsequentResponse); - } + initialResponse.EnsureSuccessStatusCode(); + Assert.Equal(System.Net.HttpStatusCode.NotModified, subsequentResponse.StatusCode); + Assert304Headers(initialResponse, subsequentResponse); } } @@ -784,23 +776,23 @@ public async Task ServesCachedContent_IfIfNoneMatch_NotSatisfied() await host.StartAsync(); - using (var server = host.GetTestServer()) - { - var client = server.CreateClient(); - var initialResponse = await client.GetAsync(""); - client.DefaultRequestHeaders.IfNoneMatch.Add(new System.Net.Http.Headers.EntityTagHeaderValue("\"E2\"")); - var subsequentResponse = await client.GetAsync(""); + using var server = host.GetTestServer(); + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.IfNoneMatch.Add(new System.Net.Http.Headers.EntityTagHeaderValue("\"E2\"")); + var subsequentResponse = await client.GetAsync(""); - await AssertCachedResponseAsync(initialResponse, subsequentResponse); - } + await AssertCachedResponseAsync(initialResponse, subsequentResponse); } } [Fact] public async Task ServesCachedContent_IfBodySize_IsCacheable() { - var options = new OutputCacheOptions(); - options.MaximumBodySize = 1000; + var options = new OutputCacheOptions + { + MaximumBodySize = 1000 + }; options.AddBasePolicy(b => b.Build()); var builders = TestUtils.CreateBuildersWithOutputCaching(options: options); @@ -811,14 +803,12 @@ public async Task ServesCachedContent_IfBodySize_IsCacheable() await host.StartAsync(); - using (var server = host.GetTestServer()) - { - var client = server.CreateClient(); - var initialResponse = await client.GetAsync(""); - var subsequentResponse = await client.GetAsync(""); + using var server = host.GetTestServer(); + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + var subsequentResponse = await client.GetAsync(""); - await AssertCachedResponseAsync(initialResponse, subsequentResponse); - } + await AssertCachedResponseAsync(initialResponse, subsequentResponse); } } @@ -836,14 +826,12 @@ public async Task ServesFreshContent_IfBodySize_IsNotCacheable() await host.StartAsync(); - using (var server = host.GetTestServer()) - { - var client = server.CreateClient(); - var initialResponse = await client.GetAsync(""); - var subsequentResponse = await client.GetAsync("/different"); + using var server = host.GetTestServer(); + var client = server.CreateClient(); + var initialResponse = await client.GetAsync(""); + var subsequentResponse = await client.GetAsync("/different"); - await AssertFreshResponseAsync(initialResponse, subsequentResponse); - } + await AssertFreshResponseAsync(initialResponse, subsequentResponse); } } @@ -861,14 +849,12 @@ public async Task ServesFreshContent_CaseSensitivePaths_IsNotCacheable() await host.StartAsync(); - using (var server = host.GetTestServer()) - { - var client = server.CreateClient(); - var initialResponse = await client.GetAsync("/path"); - var subsequentResponse = await client.GetAsync("/Path"); + using var server = host.GetTestServer(); + var client = server.CreateClient(); + var initialResponse = await client.GetAsync("/path"); + var subsequentResponse = await client.GetAsync("/Path"); - await AssertFreshResponseAsync(initialResponse, subsequentResponse); - } + await AssertFreshResponseAsync(initialResponse, subsequentResponse); } } @@ -883,18 +869,16 @@ public async Task ServesCachedContent_WithoutReplacingCachedVaryBy_OnCacheMiss() await host.StartAsync(); - using (var server = host.GetTestServer()) - { - var client = server.CreateClient(); - client.DefaultRequestHeaders.From = "user@example.com"; - var initialResponse = await client.GetAsync(""); - client.DefaultRequestHeaders.From = "user2@example.com"; - var otherResponse = await client.GetAsync(""); - client.DefaultRequestHeaders.From = "user@example.com"; - var subsequentResponse = await client.GetAsync(""); + using var server = host.GetTestServer(); + var client = server.CreateClient(); + client.DefaultRequestHeaders.From = "user@example.com"; + var initialResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.From = "user2@example.com"; + var otherResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.From = "user@example.com"; + var subsequentResponse = await client.GetAsync(""); - await AssertCachedResponseAsync(initialResponse, subsequentResponse); - } + await AssertCachedResponseAsync(initialResponse, subsequentResponse); } } @@ -909,27 +893,25 @@ public async Task ServesCachedContent_IfCachedVaryByNotUpdated_OnCacheMiss() await host.StartAsync(); - using (var server = host.GetTestServer()) - { - var client = server.CreateClient(); - client.DefaultRequestHeaders.From = "user@example.com"; - client.DefaultRequestHeaders.Pragma.Clear(); - client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("From")); - client.DefaultRequestHeaders.MaxForwards = 1; - var initialResponse = await client.GetAsync(""); - client.DefaultRequestHeaders.From = "user2@example.com"; - client.DefaultRequestHeaders.Pragma.Clear(); - client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("From")); - client.DefaultRequestHeaders.MaxForwards = 2; - var otherResponse = await client.GetAsync(""); - client.DefaultRequestHeaders.From = "user@example.com"; - client.DefaultRequestHeaders.Pragma.Clear(); - client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("From")); - client.DefaultRequestHeaders.MaxForwards = 1; - var subsequentResponse = await client.GetAsync(""); - - await AssertCachedResponseAsync(initialResponse, subsequentResponse); - } + using var server = host.GetTestServer(); + var client = server.CreateClient(); + client.DefaultRequestHeaders.From = "user@example.com"; + client.DefaultRequestHeaders.Pragma.Clear(); + client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("From")); + client.DefaultRequestHeaders.MaxForwards = 1; + var initialResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.From = "user2@example.com"; + client.DefaultRequestHeaders.Pragma.Clear(); + client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("From")); + client.DefaultRequestHeaders.MaxForwards = 2; + var otherResponse = await client.GetAsync(""); + client.DefaultRequestHeaders.From = "user@example.com"; + client.DefaultRequestHeaders.Pragma.Clear(); + client.DefaultRequestHeaders.Pragma.Add(new System.Net.Http.Headers.NameValueHeaderValue("From")); + client.DefaultRequestHeaders.MaxForwards = 1; + var subsequentResponse = await client.GetAsync(""); + + await AssertCachedResponseAsync(initialResponse, subsequentResponse); } } @@ -944,14 +926,12 @@ public async Task ServesCachedContent_IfAvailable_UsingHead_WithContentLength() await host.StartAsync(); - using (var server = host.GetTestServer()) - { - var client = server.CreateClient(); - var initialResponse = await client.SendAsync(TestUtils.CreateRequest("HEAD", "?contentLength=10")); - var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest("HEAD", "?contentLength=10")); + using var server = host.GetTestServer(); + var client = server.CreateClient(); + var initialResponse = await client.SendAsync(TestUtils.CreateRequest("HEAD", "?contentLength=10")); + var subsequentResponse = await client.SendAsync(TestUtils.CreateRequest("HEAD", "?contentLength=10")); - await AssertCachedResponseAsync(initialResponse, subsequentResponse); - } + await AssertCachedResponseAsync(initialResponse, subsequentResponse); } } diff --git a/src/Middleware/OutputCaching/test/TestUtils.cs b/src/Middleware/OutputCaching/test/TestUtils.cs index 066dc91cfb9b..2ebee391cb99 100644 --- a/src/Middleware/OutputCaching/test/TestUtils.cs +++ b/src/Middleware/OutputCaching/test/TestUtils.cs @@ -363,3 +363,23 @@ internal class TestClock : ISystemClock { public DateTimeOffset UtcNow { get; set; } } + +internal class AllowTestPolicy : IOutputCachePolicy +{ + public ValueTask CacheRequestAsync(OutputCacheContext context) + { + context.AllowCacheLookup = true; + context.AllowCacheStorage = true; + return ValueTask.CompletedTask; + } + + public ValueTask ServeFromCacheAsync(OutputCacheContext context) + { + return ValueTask.CompletedTask; + } + + public ValueTask ServeResponseAsync(OutputCacheContext context) + { + return ValueTask.CompletedTask; + } +} From aa89f7c7fe2723ca4fdd8d15c8da3a4a7be63b96 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Tue, 21 Jun 2022 14:21:12 -0700 Subject: [PATCH 44/48] Apply PR feedback --- .../OutputCaching/src/CachedResponseBody.cs | 2 +- .../OutputCaching/src/IOutputCacheStore.cs | 12 ++++++------ .../src/Memory/MemoryOutputCacheStore.cs | 6 +++--- .../OutputCaching/src/OutputCacheAttribute.cs | 2 +- .../OutputCaching/src/OutputCacheEntryFormatter.cs | 14 +++++++------- .../OutputCaching/src/OutputCacheKeyProvider.cs | 2 +- .../OutputCaching/src/OutputCacheMiddleware.cs | 12 ++++++++---- .../src/Policies/VaryByQueryPolicy.cs | 2 +- .../OutputCaching/src/PublicAPI.Unshipped.txt | 11 +++-------- 9 files changed, 31 insertions(+), 32 deletions(-) diff --git a/src/Middleware/OutputCaching/src/CachedResponseBody.cs b/src/Middleware/OutputCaching/src/CachedResponseBody.cs index d17627bdcd20..4efdc97035fe 100644 --- a/src/Middleware/OutputCaching/src/CachedResponseBody.cs +++ b/src/Middleware/OutputCaching/src/CachedResponseBody.cs @@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.OutputCaching; /// /// Represents a cached response body. /// -public class CachedResponseBody +internal sealed class CachedResponseBody { /// /// Creates a new instance. diff --git a/src/Middleware/OutputCaching/src/IOutputCacheStore.cs b/src/Middleware/OutputCaching/src/IOutputCacheStore.cs index 2170fc7789d1..5f1c3bf08073 100644 --- a/src/Middleware/OutputCaching/src/IOutputCacheStore.cs +++ b/src/Middleware/OutputCaching/src/IOutputCacheStore.cs @@ -12,17 +12,17 @@ public interface IOutputCacheStore /// Evicts cached responses by tag. /// /// The tag to evict. - /// Indicates that the operation should be cancelled. - ValueTask EvictByTagAsync(string tag, CancellationToken token); + /// Indicates that the operation should be cancelled. + ValueTask EvictByTagAsync(string tag, CancellationToken cancellationToken); /// /// Gets the cached response for the given key, if it exists. /// If no cached response exists for the given key, null is returned. /// /// The cache key to look up. - /// Indicates that the operation should be cancelled. + /// Indicates that the operation should be cancelled. /// The response cache entry if it exists; otherwise null. - ValueTask GetAsync(string key, CancellationToken token); + ValueTask GetAsync(string key, CancellationToken cancellationToken); /// /// Stores the given response in the response cache. @@ -31,6 +31,6 @@ public interface IOutputCacheStore /// The response cache entry to store. /// The tags associated with the cache entry to store. /// The amount of time the entry will be kept in the cache before expiring, relative to now. - /// Indicates that the operation should be cancelled. - ValueTask SetAsync(string key, byte[] value, string[] tags, TimeSpan validFor, CancellationToken token); + /// Indicates that the operation should be cancelled. + ValueTask SetAsync(string key, byte[] value, string[] tags, TimeSpan validFor, CancellationToken cancellationToken); } diff --git a/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs b/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs index fc6497d3b3ad..63f8e0b5146f 100644 --- a/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs +++ b/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs @@ -18,7 +18,7 @@ internal MemoryOutputCacheStore(IMemoryCache cache) _cache = cache; } - public ValueTask EvictByTagAsync(string tag, CancellationToken token) + public ValueTask EvictByTagAsync(string tag, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(tag); @@ -34,7 +34,7 @@ public ValueTask EvictByTagAsync(string tag, CancellationToken token) } /// - public ValueTask GetAsync(string key, CancellationToken token) + public ValueTask GetAsync(string key, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(key); @@ -43,7 +43,7 @@ public ValueTask EvictByTagAsync(string tag, CancellationToken token) } /// - public ValueTask SetAsync(string key, byte[] value, string[] tags, TimeSpan validFor, CancellationToken token) + public ValueTask SetAsync(string key, byte[] value, string[] tags, TimeSpan validFor, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(key); ArgumentNullException.ThrowIfNull(value); diff --git a/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs b/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs index 97b92fb8745a..a214639c3a44 100644 --- a/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs @@ -7,7 +7,7 @@ namespace Microsoft.AspNetCore.OutputCaching; /// Specifies the parameters necessary for setting appropriate headers in output caching. /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] -public class OutputCacheAttribute : Attribute +public sealed class OutputCacheAttribute : Attribute { // A nullable-int cannot be used as an Attribute parameter. // Hence this nullable-int is present to back the Duration property. diff --git a/src/Middleware/OutputCaching/src/OutputCacheEntryFormatter.cs b/src/Middleware/OutputCaching/src/OutputCacheEntryFormatter.cs index d806ebaccae1..59f911e18682 100644 --- a/src/Middleware/OutputCaching/src/OutputCacheEntryFormatter.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheEntryFormatter.cs @@ -9,13 +9,13 @@ namespace Microsoft.AspNetCore.OutputCaching; /// /// Formats instance to match structures supported by the implementations. /// -internal class OutputCacheEntryFormatter +internal static class OutputCacheEntryFormatter { - public static async ValueTask GetAsync(string key, IOutputCacheStore store, CancellationToken token) + public static async ValueTask GetAsync(string key, IOutputCacheStore store, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(key); - var content = await store.GetAsync(key, token); + var content = await store.GetAsync(key, cancellationToken); if (content == null) { @@ -24,7 +24,7 @@ internal class OutputCacheEntryFormatter using var br = new MemoryStream(content); - var formatter = await JsonSerializer.DeserializeAsync(br, FormatterEntrySerializerContext.Default.FormatterEntry, cancellationToken: token); + var formatter = await JsonSerializer.DeserializeAsync(br, FormatterEntrySerializerContext.Default.FormatterEntry, cancellationToken: cancellationToken); if (formatter == null) { @@ -52,7 +52,7 @@ internal class OutputCacheEntryFormatter return outputCacheEntry; } - public static async ValueTask StoreAsync(string key, OutputCacheEntry value, TimeSpan duration, IOutputCacheStore store, CancellationToken token) + public static async ValueTask StoreAsync(string key, OutputCacheEntry value, TimeSpan duration, IOutputCacheStore store, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(value); @@ -75,7 +75,7 @@ public static async ValueTask StoreAsync(string key, OutputCacheEntry value, Tim using var br = new MemoryStream(); - await JsonSerializer.SerializeAsync(br, formatterEntry, FormatterEntrySerializerContext.Default.FormatterEntry, token); - await store.SetAsync(key, br.ToArray(), value.Tags ?? Array.Empty(), duration, token); + await JsonSerializer.SerializeAsync(br, formatterEntry, FormatterEntrySerializerContext.Default.FormatterEntry, cancellationToken); + await store.SetAsync(key, br.ToArray(), value.Tags ?? Array.Empty(), duration, cancellationToken); } } diff --git a/src/Middleware/OutputCaching/src/OutputCacheKeyProvider.cs b/src/Middleware/OutputCaching/src/OutputCacheKeyProvider.cs index 9af98b3ff657..d3943e0d5feb 100644 --- a/src/Middleware/OutputCaching/src/OutputCacheKeyProvider.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheKeyProvider.cs @@ -174,7 +174,7 @@ public string CreateStorageKey(OutputCacheContext context) } } - private class QueryKeyComparer : IComparer> + private sealed class QueryKeyComparer : IComparer> { private readonly StringComparer _stringComparer; diff --git a/src/Middleware/OutputCaching/src/OutputCacheMiddleware.cs b/src/Middleware/OutputCaching/src/OutputCacheMiddleware.cs index f5cfc5f91c8b..ac05c0829d06 100644 --- a/src/Middleware/OutputCaching/src/OutputCacheMiddleware.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheMiddleware.cs @@ -14,7 +14,7 @@ namespace Microsoft.AspNetCore.OutputCaching; /// /// Enable HTTP response caching. /// -internal class OutputCacheMiddleware +internal sealed class OutputCacheMiddleware { // see https://tools.ietf.org/html/rfc7232#section-4.1 private static readonly string[] HeadersToIncludeIn304 = @@ -79,15 +79,19 @@ internal OutputCacheMiddleware( /// /// The . /// A that completes when the middleware has completed processing. - public async Task Invoke(HttpContext httpContext) + public Task Invoke(HttpContext httpContext) { // Skip the middleware if there is no policy for the current request if (!TryGetRequestPolicies(httpContext, out var policies)) { - await _next(httpContext); - return; + return _next(httpContext); } + return InvokeAwaited(httpContext, policies); + } + + private async Task InvokeAwaited(HttpContext httpContext, IReadOnlyList policies) + { var context = new OutputCacheContext(httpContext, _store, _options, _logger); // Add IOutputCacheFeature diff --git a/src/Middleware/OutputCaching/src/Policies/VaryByQueryPolicy.cs b/src/Middleware/OutputCaching/src/Policies/VaryByQueryPolicy.cs index 93835ce690f6..f6053a4eb35c 100644 --- a/src/Middleware/OutputCaching/src/Policies/VaryByQueryPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/VaryByQueryPolicy.cs @@ -11,7 +11,7 @@ namespace Microsoft.AspNetCore.OutputCaching; ///
internal sealed class VaryByQueryPolicy : IOutputCachePolicy { - private StringValues _queryKeys { get; set; } + private readonly StringValues _queryKeys; /// /// Creates a policy that doesn't vary the cached content based on query string. diff --git a/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt b/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt index 6c831506774c..7a130db6d3a3 100644 --- a/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt @@ -1,10 +1,5 @@ #nullable enable Microsoft.AspNetCore.Builder.OutputCacheApplicationBuilderExtensions -Microsoft.AspNetCore.OutputCaching.CachedResponseBody -Microsoft.AspNetCore.OutputCaching.CachedResponseBody.CachedResponseBody(System.Collections.Generic.List! segments, long length) -> void -Microsoft.AspNetCore.OutputCaching.CachedResponseBody.CopyToAsync(System.IO.Pipelines.PipeWriter! destination, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.Task! -Microsoft.AspNetCore.OutputCaching.CachedResponseBody.Length.get -> long -Microsoft.AspNetCore.OutputCaching.CachedResponseBody.Segments.get -> System.Collections.Generic.List! Microsoft.AspNetCore.OutputCaching.CachedVaryByRules Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.CachedVaryByRules() -> void Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.Headers.get -> Microsoft.Extensions.Primitives.StringValues @@ -20,10 +15,10 @@ Microsoft.AspNetCore.OutputCaching.IOutputCachePolicy.CacheRequestAsync(Microsof Microsoft.AspNetCore.OutputCaching.IOutputCachePolicy.ServeFromCacheAsync(Microsoft.AspNetCore.OutputCaching.OutputCacheContext! context) -> System.Threading.Tasks.ValueTask Microsoft.AspNetCore.OutputCaching.IOutputCachePolicy.ServeResponseAsync(Microsoft.AspNetCore.OutputCaching.OutputCacheContext! context) -> System.Threading.Tasks.ValueTask Microsoft.AspNetCore.OutputCaching.IOutputCacheStore -Microsoft.AspNetCore.OutputCaching.IOutputCacheStore.EvictByTagAsync(string! tag, System.Threading.CancellationToken token) -> System.Threading.Tasks.ValueTask +Microsoft.AspNetCore.OutputCaching.IOutputCacheStore.EvictByTagAsync(string! tag, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask Microsoft.AspNetCore.OutputCaching.IOutputCachePolicy -Microsoft.AspNetCore.OutputCaching.IOutputCacheStore.GetAsync(string! key, System.Threading.CancellationToken token) -> System.Threading.Tasks.ValueTask -Microsoft.AspNetCore.OutputCaching.IOutputCacheStore.SetAsync(string! key, byte[]! value, string![]! tags, System.TimeSpan validFor, System.Threading.CancellationToken token) -> System.Threading.Tasks.ValueTask +Microsoft.AspNetCore.OutputCaching.IOutputCacheStore.GetAsync(string! key, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask +Microsoft.AspNetCore.OutputCaching.IOutputCacheStore.SetAsync(string! key, byte[]! value, string![]! tags, System.TimeSpan validFor, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.Duration.get -> int Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.Duration.init -> void From edaf9ffb6d6256fd514d99d537a58d0913e012bf Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Tue, 21 Jun 2022 14:31:18 -0700 Subject: [PATCH 45/48] PR feedback --- src/Middleware/OutputCaching/src/Policies/NoLookupPolicy.cs | 2 +- src/Middleware/OutputCaching/src/Policies/NoStorePolicy.cs | 2 +- src/Middleware/OutputCaching/src/Policies/VaryByHeaderPolicy.cs | 2 +- .../OutputCaching/src/Serialization/FormatterEntry.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Middleware/OutputCaching/src/Policies/NoLookupPolicy.cs b/src/Middleware/OutputCaching/src/Policies/NoLookupPolicy.cs index c7ad348fbf6c..443987ac0d04 100644 --- a/src/Middleware/OutputCaching/src/Policies/NoLookupPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/NoLookupPolicy.cs @@ -6,7 +6,7 @@ namespace Microsoft.AspNetCore.OutputCaching; /// /// A policy that prevents the response from being served from cached. /// -internal class NoLookupPolicy : IOutputCachePolicy +internal sealed class NoLookupPolicy : IOutputCachePolicy { public static NoLookupPolicy Instance = new(); diff --git a/src/Middleware/OutputCaching/src/Policies/NoStorePolicy.cs b/src/Middleware/OutputCaching/src/Policies/NoStorePolicy.cs index ce8a6a234f46..11b7d420f892 100644 --- a/src/Middleware/OutputCaching/src/Policies/NoStorePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/NoStorePolicy.cs @@ -6,7 +6,7 @@ namespace Microsoft.AspNetCore.OutputCaching; /// /// A policy that prevents the response from being cached. /// -internal class NoStorePolicy : IOutputCachePolicy +internal sealed class NoStorePolicy : IOutputCachePolicy { public static NoStorePolicy Instance = new(); diff --git a/src/Middleware/OutputCaching/src/Policies/VaryByHeaderPolicy.cs b/src/Middleware/OutputCaching/src/Policies/VaryByHeaderPolicy.cs index 11faf33694ae..1827a9feae70 100644 --- a/src/Middleware/OutputCaching/src/Policies/VaryByHeaderPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/VaryByHeaderPolicy.cs @@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.OutputCaching; /// internal sealed class VaryByHeaderPolicy : IOutputCachePolicy { - private StringValues _headers { get; set; } + private readonly StringValues _headers; /// /// Creates a policy that doesn't vary the cached content based on headers. diff --git a/src/Middleware/OutputCaching/src/Serialization/FormatterEntry.cs b/src/Middleware/OutputCaching/src/Serialization/FormatterEntry.cs index 674806979144..f3fc56f4bc63 100644 --- a/src/Middleware/OutputCaching/src/Serialization/FormatterEntry.cs +++ b/src/Middleware/OutputCaching/src/Serialization/FormatterEntry.cs @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. namespace Microsoft.AspNetCore.OutputCaching.Serialization; -internal class FormatterEntry +internal sealed class FormatterEntry { public DateTimeOffset Created { get; set; } public int StatusCode { get; set; } From 4013de55a1870b96fdffe5059f4b936807e2393c Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Tue, 21 Jun 2022 18:02:32 -0700 Subject: [PATCH 46/48] PR feedback --- .../OutputCachingSample.csproj | 6 -- .../OutputCaching/src/IOutputCacheStore.cs | 2 +- .../OutputCaching/src/LoggerExtensions.cs | 92 ++++--------------- .../src/Memory/MemoryOutputCacheStore.cs | 2 +- .../Microsoft.AspNetCore.OutputCaching.csproj | 21 +---- .../src/OutputCacheMiddleware.cs | 2 +- .../src/Policies/LockingPolicy.cs | 2 +- .../src/Policies/NoLookupPolicy.cs | 2 +- .../src/Properties/AssemblyInfo.cs | 6 -- .../OutputCaching/src/PublicAPI.Unshipped.txt | 2 +- .../OutputCaching/src/Resources.Designer.cs | 72 --------------- .../test/OutputCachePolicyProviderTests.cs | 22 ++--- .../OutputCaching/test/TestUtils.cs | 85 ++++++++--------- src/Mvc/samples/MvcSandbox/MvcSandbox.csproj | 1 - .../samples/MvcSandbox/Pages/PagesHome.cshtml | 3 +- 15 files changed, 76 insertions(+), 244 deletions(-) delete mode 100644 src/Middleware/OutputCaching/src/Properties/AssemblyInfo.cs delete mode 100644 src/Middleware/OutputCaching/src/Resources.Designer.cs diff --git a/src/Middleware/OutputCaching/samples/OutputCachingSample/OutputCachingSample.csproj b/src/Middleware/OutputCaching/samples/OutputCachingSample/OutputCachingSample.csproj index 8b0e88781139..8e76982e4dfc 100644 --- a/src/Middleware/OutputCaching/samples/OutputCachingSample/OutputCachingSample.csproj +++ b/src/Middleware/OutputCaching/samples/OutputCachingSample/OutputCachingSample.csproj @@ -6,15 +6,9 @@ - - - - - - \ No newline at end of file diff --git a/src/Middleware/OutputCaching/src/IOutputCacheStore.cs b/src/Middleware/OutputCaching/src/IOutputCacheStore.cs index 5f1c3bf08073..ffba017ef6f9 100644 --- a/src/Middleware/OutputCaching/src/IOutputCacheStore.cs +++ b/src/Middleware/OutputCaching/src/IOutputCacheStore.cs @@ -32,5 +32,5 @@ public interface IOutputCacheStore /// The tags associated with the cache entry to store. /// The amount of time the entry will be kept in the cache before expiring, relative to now. /// Indicates that the operation should be cancelled. - ValueTask SetAsync(string key, byte[] value, string[] tags, TimeSpan validFor, CancellationToken cancellationToken); + ValueTask SetAsync(string key, byte[] value, string[]? tags, TimeSpan validFor, CancellationToken cancellationToken); } diff --git a/src/Middleware/OutputCaching/src/LoggerExtensions.cs b/src/Middleware/OutputCaching/src/LoggerExtensions.cs index 4b4db1455d35..642f7252a623 100644 --- a/src/Middleware/OutputCaching/src/LoggerExtensions.cs +++ b/src/Middleware/OutputCaching/src/LoggerExtensions.cs @@ -7,7 +7,7 @@ namespace Microsoft.AspNetCore.OutputCaching; /// -/// Defines *all* the logger messages produced by response caching +/// Defines the logger messages produced by output caching /// internal static partial class LoggerExtensions { @@ -19,109 +19,51 @@ internal static partial class LoggerExtensions EventName = "RequestWithAuthorizationNotCacheable")] internal static partial void RequestWithAuthorizationNotCacheable(this ILogger logger); - [LoggerMessage(3, LogLevel.Debug, "The request cannot be served from cache because it contains a 'no-cache' cache directive.", - EventName = "RequestWithNoCacheNotCacheable")] - internal static partial void RequestWithNoCacheNotCacheable(this ILogger logger); - - [LoggerMessage(4, LogLevel.Debug, "The request cannot be served from cache because it contains a 'no-cache' pragma directive.", - EventName = "RequestWithPragmaNoCacheNotCacheable")] - internal static partial void RequestWithPragmaNoCacheNotCacheable(this ILogger logger); - - [LoggerMessage(5, LogLevel.Debug, "Adding a minimum freshness requirement of {Duration} specified by the 'min-fresh' cache directive.", - EventName = "LogRequestMethodNotCacheable")] - internal static partial void ExpirationMinFreshAdded(this ILogger logger, TimeSpan duration); - - [LoggerMessage(6, LogLevel.Debug, "The age of the entry is {Age} and has exceeded the maximum age for shared caches of {SharedMaxAge} specified by the 's-maxage' cache directive.", - EventName = "ExpirationSharedMaxAgeExceeded")] - internal static partial void ExpirationSharedMaxAgeExceeded(this ILogger logger, TimeSpan age, TimeSpan sharedMaxAge); - - [LoggerMessage(7, LogLevel.Debug, "The age of the entry is {Age} and has exceeded the maximum age of {MaxAge} specified by the 'max-age' cache directive. " + - "It must be revalidated because the 'must-revalidate' or 'proxy-revalidate' cache directive is specified.", - EventName = "ExpirationMustRevalidate")] - internal static partial void ExpirationMustRevalidate(this ILogger logger, TimeSpan age, TimeSpan maxAge); - - [LoggerMessage(8, LogLevel.Debug, "The age of the entry is {Age} and has exceeded the maximum age of {MaxAge} specified by the 'max-age' cache directive. " + - "However, it satisfied the maximum stale allowance of {MaxStale} specified by the 'max-stale' cache directive.", - EventName = "ExpirationMaxStaleSatisfied")] - internal static partial void ExpirationMaxStaleSatisfied(this ILogger logger, TimeSpan age, TimeSpan maxAge, TimeSpan maxStale); - - [LoggerMessage(9, LogLevel.Debug, "The age of the entry is {Age} and has exceeded the maximum age of {MaxAge} specified by the 'max-age' cache directive.", EventName = "ExpirationMaxAgeExceeded")] - internal static partial void ExpirationMaxAgeExceeded(this ILogger logger, TimeSpan age, TimeSpan maxAge); - - [LoggerMessage(10, LogLevel.Debug, "The response time of the entry is {ResponseTime} and has exceeded its expiry date of {Expired} specified by the 'Expires' header.", - EventName = "ExpirationExpiresExceeded")] - internal static partial void ExpirationExpiresExceeded(this ILogger logger, DateTimeOffset responseTime, DateTimeOffset expired); - - [LoggerMessage(11, LogLevel.Debug, "Response is not cacheable because it does not contain the 'public' cache directive.", - EventName = "ResponseWithoutPublicNotCacheable")] - internal static partial void ResponseWithoutPublicNotCacheable(this ILogger logger); - - [LoggerMessage(12, LogLevel.Debug, "Response is not cacheable because it or its corresponding request contains a 'no-store' cache directive.", - EventName = "ResponseWithNoStoreNotCacheable")] - internal static partial void ResponseWithNoStoreNotCacheable(this ILogger logger); - [LoggerMessage(13, LogLevel.Debug, "Response is not cacheable because it contains a 'no-cache' cache directive.", - EventName = "ResponseWithNoCacheNotCacheable")] - internal static partial void ResponseWithNoCacheNotCacheable(this ILogger logger); - - [LoggerMessage(14, LogLevel.Debug, "Response is not cacheable because it contains a 'SetCookie' header.", EventName = "ResponseWithSetCookieNotCacheable")] + [LoggerMessage(3, LogLevel.Debug, "Response is not cacheable because it contains a 'SetCookie' header.", EventName = "ResponseWithSetCookieNotCacheable")] internal static partial void ResponseWithSetCookieNotCacheable(this ILogger logger); - [LoggerMessage(15, LogLevel.Debug, "Response is not cacheable because it contains a '.Vary' header with a value of *.", - EventName = "ResponseWithVaryStarNotCacheable")] - internal static partial void ResponseWithVaryStarNotCacheable(this ILogger logger); - - [LoggerMessage(16, LogLevel.Debug, "Response is not cacheable because it contains the 'private' cache directive.", - EventName = "ResponseWithPrivateNotCacheable")] - internal static partial void ResponseWithPrivateNotCacheable(this ILogger logger); - - [LoggerMessage(17, LogLevel.Debug, "Response is not cacheable because its status code {StatusCode} does not indicate success.", + [LoggerMessage(4, LogLevel.Debug, "Response is not cacheable because its status code {StatusCode} does not indicate success.", EventName = "ResponseWithUnsuccessfulStatusCodeNotCacheable")] internal static partial void ResponseWithUnsuccessfulStatusCodeNotCacheable(this ILogger logger, int statusCode); - [LoggerMessage(18, LogLevel.Debug, "The 'IfNoneMatch' header of the request contains a value of *.", EventName = "ExpirationExpiresExceeded")] + [LoggerMessage(5, LogLevel.Debug, "The 'IfNoneMatch' header of the request contains a value of *.", EventName = "NotModifiedIfNoneMatchStar")] internal static partial void NotModifiedIfNoneMatchStar(this ILogger logger); - [LoggerMessage(19, LogLevel.Debug, "The ETag {ETag} in the 'IfNoneMatch' header matched the ETag of a cached entry.", + [LoggerMessage(6, LogLevel.Debug, "The ETag {ETag} in the 'IfNoneMatch' header matched the ETag of a cached entry.", EventName = "NotModifiedIfNoneMatchMatched")] internal static partial void NotModifiedIfNoneMatchMatched(this ILogger logger, EntityTagHeaderValue etag); - [LoggerMessage(20, LogLevel.Debug, "The last modified date of {LastModified} is before the date {IfModifiedSince} specified in the 'IfModifiedSince' header.", + [LoggerMessage(7, LogLevel.Debug, "The last modified date of {LastModified} is before the date {IfModifiedSince} specified in the 'IfModifiedSince' header.", EventName = "NotModifiedIfModifiedSinceSatisfied")] internal static partial void NotModifiedIfModifiedSinceSatisfied(this ILogger logger, DateTimeOffset lastModified, DateTimeOffset ifModifiedSince); - [LoggerMessage(21, LogLevel.Information, "The content requested has not been modified.", EventName = "NotModifiedServed")] + [LoggerMessage(8, LogLevel.Information, "The content requested has not been modified.", EventName = "NotModifiedServed")] internal static partial void NotModifiedServed(this ILogger logger); - [LoggerMessage(22, LogLevel.Information, "Serving response from cache.", EventName = "CachedResponseServed")] + [LoggerMessage(9, LogLevel.Information, "Serving response from cache.", EventName = "CachedResponseServed")] internal static partial void CachedResponseServed(this ILogger logger); - [LoggerMessage(23, LogLevel.Information, "No cached response available for this request and the 'only-if-cached' cache directive was specified.", + [LoggerMessage(10, LogLevel.Information, "No cached response available for this request and the 'only-if-cached' cache directive was specified.", EventName = "GatewayTimeoutServed")] internal static partial void GatewayTimeoutServed(this ILogger logger); - [LoggerMessage(24, LogLevel.Information, "No cached response available for this request.", EventName = "NoResponseServed")] + [LoggerMessage(11, LogLevel.Information, "No cached response available for this request.", EventName = "NoResponseServed")] internal static partial void NoResponseServed(this ILogger logger); - [LoggerMessage(25, LogLevel.Debug, "Vary by rules were updated. Headers: {Headers}, Query keys: {QueryKeys}", EventName = "VaryByRulesUpdated")] + [LoggerMessage(12, LogLevel.Debug, "Vary by rules were updated. Headers: {Headers}, Query keys: {QueryKeys}", EventName = "VaryByRulesUpdated")] internal static partial void VaryByRulesUpdated(this ILogger logger, string headers, string queryKeys); - [LoggerMessage(26, LogLevel.Information, "The response has been cached.", EventName = "ResponseCached")] + [LoggerMessage(13, LogLevel.Information, "The response has been cached.", EventName = "ResponseCached")] internal static partial void ResponseCached(this ILogger logger); - [LoggerMessage(27, LogLevel.Information, "The response could not be cached for this request.", EventName = "ResponseNotCached")] - internal static partial void LogResponseNotCached(this ILogger logger); + [LoggerMessage(14, LogLevel.Information, "The response could not be cached for this request.", EventName = "ResponseNotCached")] + internal static partial void ResponseNotCached(this ILogger logger); - [LoggerMessage(28, LogLevel.Warning, "The response could not be cached for this request because the 'Content-Length' did not match the body length.", - EventName = "responseContentLengthMismatchNotCached")] + [LoggerMessage(15, LogLevel.Warning, "The response could not be cached for this request because the 'Content-Length' did not match the body length.", + EventName = "ResponseContentLengthMismatchNotCached")] internal static partial void ResponseContentLengthMismatchNotCached(this ILogger logger); - [LoggerMessage(29, LogLevel.Debug, - "The age of the entry is {Age} and has exceeded the maximum age of {MaxAge} specified by the 'max-age' cache directive. " + - "However, the 'max-stale' cache directive was specified without an assigned value and a stale response of any age is accepted.", - EventName = "ExpirationInfiniteMaxStaleSatisfied")] - internal static partial void ExpirationInfiniteMaxStaleSatisfied(this ILogger logger, TimeSpan age, TimeSpan maxAge); - - [LoggerMessage(30, LogLevel.Debug, "The response time of the entry is {ResponseTime} and has exceeded its expiry date.", + [LoggerMessage(16, LogLevel.Debug, "The response time of the entry is {ResponseTime} and has exceeded its expiry date.", EventName = "ExpirationExpiresExceeded")] internal static partial void ExpirationExpiresExceeded(this ILogger logger, DateTimeOffset responseTime); diff --git a/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs b/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs index 63f8e0b5146f..71cecdedcc21 100644 --- a/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs +++ b/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs @@ -43,7 +43,7 @@ public ValueTask EvictByTagAsync(string tag, CancellationToken cancellationToken } /// - public ValueTask SetAsync(string key, byte[] value, string[] tags, TimeSpan validFor, CancellationToken cancellationToken) + public ValueTask SetAsync(string key, byte[] value, string[]? tags, TimeSpan validFor, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(key); ArgumentNullException.ThrowIfNull(value); diff --git a/src/Middleware/OutputCaching/src/Microsoft.AspNetCore.OutputCaching.csproj b/src/Middleware/OutputCaching/src/Microsoft.AspNetCore.OutputCaching.csproj index 0b8ad8e44ffb..e9a0cbc7ff23 100644 --- a/src/Middleware/OutputCaching/src/Microsoft.AspNetCore.OutputCaching.csproj +++ b/src/Middleware/OutputCaching/src/Microsoft.AspNetCore.OutputCaching.csproj @@ -5,12 +5,14 @@ $(DefaultNetCoreTargetFramework) true true - true - aspnetcore;cache;caching false true + + + + @@ -22,19 +24,4 @@ - - - True - True - Resources.resx - - - - - - ResXFileCodeGenerator - Resources.Designer.cs - - - diff --git a/src/Middleware/OutputCaching/src/OutputCacheMiddleware.cs b/src/Middleware/OutputCaching/src/OutputCacheMiddleware.cs index ac05c0829d06..f9902be98305 100644 --- a/src/Middleware/OutputCaching/src/OutputCacheMiddleware.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheMiddleware.cs @@ -453,7 +453,7 @@ internal async ValueTask FinalizeCacheBodyAsync(OutputCacheContext context) } else { - _logger.LogResponseNotCached(); + _logger.ResponseNotCached(); } } diff --git a/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs b/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs index 26a5f83fa813..c21dc5ecd9a1 100644 --- a/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs @@ -21,7 +21,7 @@ private LockingPolicy(bool lockResponse) public static readonly LockingPolicy Enabled = new(true); /// - /// A policy that disabled locking/ + /// A policy that disables locking. /// public static readonly LockingPolicy Disabled = new(false); diff --git a/src/Middleware/OutputCaching/src/Policies/NoLookupPolicy.cs b/src/Middleware/OutputCaching/src/Policies/NoLookupPolicy.cs index 443987ac0d04..c01db72338e7 100644 --- a/src/Middleware/OutputCaching/src/Policies/NoLookupPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/NoLookupPolicy.cs @@ -4,7 +4,7 @@ namespace Microsoft.AspNetCore.OutputCaching; /// -/// A policy that prevents the response from being served from cached. +/// A policy that prevents the response from being served from cache. /// internal sealed class NoLookupPolicy : IOutputCachePolicy { diff --git a/src/Middleware/OutputCaching/src/Properties/AssemblyInfo.cs b/src/Middleware/OutputCaching/src/Properties/AssemblyInfo.cs deleted file mode 100644 index a185ed9b3c63..000000000000 --- a/src/Middleware/OutputCaching/src/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,6 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System.Runtime.CompilerServices; - -[assembly: InternalsVisibleTo("Microsoft.AspNetCore.OutputCaching.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt b/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt index 7a130db6d3a3..1bd7e92e8e66 100644 --- a/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt @@ -18,7 +18,7 @@ Microsoft.AspNetCore.OutputCaching.IOutputCacheStore Microsoft.AspNetCore.OutputCaching.IOutputCacheStore.EvictByTagAsync(string! tag, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask Microsoft.AspNetCore.OutputCaching.IOutputCachePolicy Microsoft.AspNetCore.OutputCaching.IOutputCacheStore.GetAsync(string! key, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask -Microsoft.AspNetCore.OutputCaching.IOutputCacheStore.SetAsync(string! key, byte[]! value, string![]! tags, System.TimeSpan validFor, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask +Microsoft.AspNetCore.OutputCaching.IOutputCacheStore.SetAsync(string! key, byte[]! value, string![]? tags, System.TimeSpan validFor, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.Duration.get -> int Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.Duration.init -> void diff --git a/src/Middleware/OutputCaching/src/Resources.Designer.cs b/src/Middleware/OutputCaching/src/Resources.Designer.cs deleted file mode 100644 index da8d0c09ec09..000000000000 --- a/src/Middleware/OutputCaching/src/Resources.Designer.cs +++ /dev/null @@ -1,72 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// Runtime Version:4.0.30319.42000 -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace Microsoft.AspNetCore.OutputCaching { - using System; - - - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "17.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] - internal class Resources { - - private static global::System.Resources.ResourceManager resourceMan; - - private static global::System.Globalization.CultureInfo resourceCulture; - - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] - internal Resources() { - } - - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { - get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Microsoft.AspNetCore.OutputCaching.Resources", typeof(Resources).Assembly); - resourceMan = temp; - } - return resourceMan; - } - } - - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { - get { - return resourceCulture; - } - set { - resourceCulture = value; - } - } - - /// - /// Looks up a localized string similar to The type '{0}' is not a valid output policy.. - /// - internal static string Policy_InvalidType { - get { - return ResourceManager.GetString("Policy_InvalidType", resourceCulture); - } - } - } -} diff --git a/src/Middleware/OutputCaching/test/OutputCachePolicyProviderTests.cs b/src/Middleware/OutputCaching/test/OutputCachePolicyProviderTests.cs index d3d00347e2e6..64d8b62f8085 100644 --- a/src/Middleware/OutputCaching/test/OutputCachePolicyProviderTests.cs +++ b/src/Middleware/OutputCaching/test/OutputCachePolicyProviderTests.cs @@ -43,7 +43,7 @@ public static TheoryData NonCacheableMethods [Theory] [MemberData(nameof(CacheableMethods))] - public async Task AttemptOutputCaching_CacheableMethods_Allowed(string method) + public async Task AttemptOutputCaching_CacheableMethods_IsAllowed(string method) { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); @@ -99,7 +99,7 @@ public async Task AttemptResponseCaching_AuthorizationHeaders_NotAllowed() } [Fact] - public async Task AllowCacheStorage_NoStore_Allowed() + public async Task AllowCacheStorage_NoStore_IsAllowed() { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); @@ -133,7 +133,7 @@ public async Task AllowCacheLookup_LegacyDirectives_OverridenByCacheControl() } [Fact] - public async Task IsResponseCacheable_NoPublic_Allowed() + public async Task IsResponseCacheable_NoPublic_IsAllowed() { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); @@ -147,7 +147,7 @@ public async Task IsResponseCacheable_NoPublic_Allowed() } [Fact] - public async Task IsResponseCacheable_Public_Allowed() + public async Task IsResponseCacheable_Public_IsAllowed() { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); @@ -165,7 +165,7 @@ public async Task IsResponseCacheable_Public_Allowed() } [Fact] - public async Task IsResponseCacheable_NoCache_Allowed() + public async Task IsResponseCacheable_NoCache_IsAllowed() { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); @@ -183,7 +183,7 @@ public async Task IsResponseCacheable_NoCache_Allowed() } [Fact] - public async Task IsResponseCacheable_ResponseNoStore_Allowed() + public async Task IsResponseCacheable_ResponseNoStore_IsAllowed() { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); @@ -218,7 +218,7 @@ public async Task IsResponseCacheable_SetCookieHeader_NotAllowed() } [Fact] - public async Task IsResponseCacheable_VaryHeaderByStar_Allowed() + public async Task IsResponseCacheable_VaryHeaderByStar_IsAllowed() { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); @@ -232,7 +232,7 @@ public async Task IsResponseCacheable_VaryHeaderByStar_Allowed() } [Fact] - public async Task IsResponseCacheable_Private_Allowed() + public async Task IsResponseCacheable_Private_IsAllowed() { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); @@ -251,7 +251,7 @@ public async Task IsResponseCacheable_Private_Allowed() [Theory] [InlineData(StatusCodes.Status200OK)] - public async Task IsResponseCacheable_SuccessStatusCodes_Allowed(int statusCode) + public async Task IsResponseCacheable_SuccessStatusCodes_IsAllowed(int statusCode) { var sink = new TestSink(); var context = TestUtils.CreateTestContext(sink); @@ -363,7 +363,7 @@ public async Task IsResponseCacheable_NoExpiryRequirements_IsAllowed() } [Fact] - public async Task IsResponseCacheable_MaxAgeOverridesExpiry_ToAllowed() + public async Task IsResponseCacheable_MaxAgeOverridesExpiry_IsAllowed() { var utcNow = DateTimeOffset.UtcNow; var sink = new TestSink(); @@ -386,7 +386,7 @@ public async Task IsResponseCacheable_MaxAgeOverridesExpiry_ToAllowed() } [Fact] - public async Task IsResponseCacheable_SharedMaxAgeOverridesMaxAge_ToAllowed() + public async Task IsResponseCacheable_SharedMaxAgeOverridesMaxAge_IsAllowed() { var utcNow = DateTimeOffset.UtcNow; var sink = new TestSink(); diff --git a/src/Middleware/OutputCaching/test/TestUtils.cs b/src/Middleware/OutputCaching/test/TestUtils.cs index 2ebee391cb99..99020db5d213 100644 --- a/src/Middleware/OutputCaching/test/TestUtils.cs +++ b/src/Middleware/OutputCaching/test/TestUtils.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#nullable enable using System.Net.Http; using System.Text; using Microsoft.AspNetCore.Builder; @@ -85,9 +86,9 @@ internal static IOutputCacheKeyProvider CreateTestKeyProvider(OutputCacheOptions } internal static IEnumerable CreateBuildersWithOutputCaching( - Action configureDelegate = null, - OutputCacheOptions options = null, - Action contextAction = null) + Action? configureDelegate = null, + OutputCacheOptions? options = null, + Action? contextAction = null) { return CreateBuildersWithOutputCaching(configureDelegate, options, new RequestDelegate[] { @@ -110,9 +111,9 @@ internal static IEnumerable CreateBuildersWithOutputCaching( } private static IEnumerable CreateBuildersWithOutputCaching( - Action configureDelegate = null, - OutputCacheOptions options = null, - IEnumerable requestDelegates = null) + Action? configureDelegate = null, + OutputCacheOptions? options = null, + IEnumerable? requestDelegates = null) { if (configureDelegate == null) { @@ -166,11 +167,11 @@ private static IEnumerable CreateBuildersWithOutputCaching( } internal static OutputCacheMiddleware CreateTestMiddleware( - RequestDelegate next = null, - IOutputCacheStore cache = null, - OutputCacheOptions options = null, - TestSink testSink = null, - IOutputCacheKeyProvider keyProvider = null + RequestDelegate? next = null, + IOutputCacheStore? cache = null, + OutputCacheOptions? options = null, + TestSink? testSink = null, + IOutputCacheKeyProvider? keyProvider = null ) { if (next == null) @@ -198,7 +199,7 @@ internal static OutputCacheMiddleware CreateTestMiddleware( keyProvider); } - internal static OutputCacheContext CreateTestContext(IOutputCacheStore cache = null, OutputCacheOptions options = null) + internal static OutputCacheContext CreateTestContext(IOutputCacheStore? cache = null, OutputCacheOptions? options = null) { return new OutputCacheContext(new DefaultHttpContext(), cache ?? new TestOutputCache(), options ?? Options.Create(new OutputCacheOptions()).Value, NullLogger.Instance) { @@ -209,7 +210,7 @@ internal static OutputCacheContext CreateTestContext(IOutputCacheStore cache = n }; } - internal static OutputCacheContext CreateTestContext(HttpContext httpContext, IOutputCacheStore cache = null, OutputCacheOptions options = null) + internal static OutputCacheContext CreateTestContext(HttpContext httpContext, IOutputCacheStore? cache = null, OutputCacheOptions? options = null) { return new OutputCacheContext(httpContext, cache ?? new TestOutputCache(), options ?? Options.Create(new OutputCacheOptions()).Value, NullLogger.Instance) { @@ -220,7 +221,7 @@ internal static OutputCacheContext CreateTestContext(HttpContext httpContext, IO }; } - internal static OutputCacheContext CreateTestContext(ITestSink testSink, IOutputCacheStore cache = null, OutputCacheOptions options = null) + internal static OutputCacheContext CreateTestContext(ITestSink testSink, IOutputCacheStore? cache = null, OutputCacheOptions? options = null) { return new OutputCacheContext(new DefaultHttpContext(), cache ?? new TestOutputCache(), options ?? Options.Create(new OutputCacheOptions()).Value, new TestLogger("OutputCachingTests", testSink, true)) { @@ -231,7 +232,7 @@ internal static OutputCacheContext CreateTestContext(ITestSink testSink, IOutput }; } - internal static OutputCacheContext CreateUninitializedContext(IOutputCacheStore cache = null, OutputCacheOptions options = null) + internal static OutputCacheContext CreateUninitializedContext(IOutputCacheStore? cache = null, OutputCacheOptions? options = null) { return new OutputCacheContext(new DefaultHttpContext(), cache ?? new TestOutputCache(), options ?? Options.Create(new OutputCacheOptions()).Value, NullLogger.Instance) { @@ -272,34 +273,20 @@ internal class LoggedMessage { internal static LoggedMessage RequestMethodNotCacheable => new LoggedMessage(1, LogLevel.Debug); internal static LoggedMessage RequestWithAuthorizationNotCacheable => new LoggedMessage(2, LogLevel.Debug); - internal static LoggedMessage RequestWithNoCacheNotCacheable => new LoggedMessage(3, LogLevel.Debug); - internal static LoggedMessage RequestWithPragmaNoCacheNotCacheable => new LoggedMessage(4, LogLevel.Debug); - internal static LoggedMessage ExpirationMinFreshAdded => new LoggedMessage(5, LogLevel.Debug); - internal static LoggedMessage ExpirationSharedMaxAgeExceeded => new LoggedMessage(6, LogLevel.Debug); - internal static LoggedMessage ExpirationMustRevalidate => new LoggedMessage(7, LogLevel.Debug); - internal static LoggedMessage ExpirationMaxStaleSatisfied => new LoggedMessage(8, LogLevel.Debug); - internal static LoggedMessage ExpirationMaxAgeExceeded => new LoggedMessage(9, LogLevel.Debug); - internal static LoggedMessage ExpirationExpiresExceeded => new LoggedMessage(10, LogLevel.Debug); - internal static LoggedMessage ResponseWithoutPublicNotCacheable => new LoggedMessage(11, LogLevel.Debug); - internal static LoggedMessage ResponseWithNoStoreNotCacheable => new LoggedMessage(12, LogLevel.Debug); - internal static LoggedMessage ResponseWithNoCacheNotCacheable => new LoggedMessage(13, LogLevel.Debug); - internal static LoggedMessage ResponseWithSetCookieNotCacheable => new LoggedMessage(14, LogLevel.Debug); - internal static LoggedMessage ResponseWithVaryStarNotCacheable => new LoggedMessage(15, LogLevel.Debug); - internal static LoggedMessage ResponseWithPrivateNotCacheable => new LoggedMessage(16, LogLevel.Debug); - internal static LoggedMessage ResponseWithUnsuccessfulStatusCodeNotCacheable => new LoggedMessage(17, LogLevel.Debug); - internal static LoggedMessage NotModifiedIfNoneMatchStar => new LoggedMessage(18, LogLevel.Debug); - internal static LoggedMessage NotModifiedIfNoneMatchMatched => new LoggedMessage(19, LogLevel.Debug); - internal static LoggedMessage NotModifiedIfModifiedSinceSatisfied => new LoggedMessage(20, LogLevel.Debug); - internal static LoggedMessage NotModifiedServed => new LoggedMessage(21, LogLevel.Information); - internal static LoggedMessage CachedResponseServed => new LoggedMessage(22, LogLevel.Information); - internal static LoggedMessage GatewayTimeoutServed => new LoggedMessage(23, LogLevel.Information); - internal static LoggedMessage NoResponseServed => new LoggedMessage(24, LogLevel.Information); - internal static LoggedMessage VaryByRulesUpdated => new LoggedMessage(25, LogLevel.Debug); - internal static LoggedMessage ResponseCached => new LoggedMessage(26, LogLevel.Information); - internal static LoggedMessage ResponseNotCached => new LoggedMessage(27, LogLevel.Information); - internal static LoggedMessage ResponseContentLengthMismatchNotCached => new LoggedMessage(28, LogLevel.Warning); - internal static LoggedMessage ExpirationInfiniteMaxStaleSatisfied => new LoggedMessage(29, LogLevel.Debug); - internal static LoggedMessage ExpirationExpiresExceededNoExpiration => new LoggedMessage(30, LogLevel.Debug); + internal static LoggedMessage ResponseWithSetCookieNotCacheable => new LoggedMessage(3, LogLevel.Debug); + internal static LoggedMessage ResponseWithUnsuccessfulStatusCodeNotCacheable => new LoggedMessage(4, LogLevel.Debug); + internal static LoggedMessage NotModifiedIfNoneMatchStar => new LoggedMessage(5, LogLevel.Debug); + internal static LoggedMessage NotModifiedIfNoneMatchMatched => new LoggedMessage(6, LogLevel.Debug); + internal static LoggedMessage NotModifiedIfModifiedSinceSatisfied => new LoggedMessage(7, LogLevel.Debug); + internal static LoggedMessage NotModifiedServed => new LoggedMessage(8, LogLevel.Information); + internal static LoggedMessage CachedResponseServed => new LoggedMessage(9, LogLevel.Information); + internal static LoggedMessage GatewayTimeoutServed => new LoggedMessage(10, LogLevel.Information); + internal static LoggedMessage NoResponseServed => new LoggedMessage(11, LogLevel.Information); + internal static LoggedMessage VaryByRulesUpdated => new LoggedMessage(12, LogLevel.Debug); + internal static LoggedMessage ResponseCached => new LoggedMessage(13, LogLevel.Information); + internal static LoggedMessage ResponseNotCached => new LoggedMessage(14, LogLevel.Information); + internal static LoggedMessage ResponseContentLengthMismatchNotCached => new LoggedMessage(15, LogLevel.Warning); + internal static LoggedMessage ExpirationExpiresExceeded => new LoggedMessage(15, LogLevel.Debug); private LoggedMessage(int evenId, LogLevel logLevel) { @@ -315,12 +302,12 @@ internal class TestResponseCachingKeyProvider : IOutputCacheKeyProvider { private readonly string _key; - public TestResponseCachingKeyProvider(string key = null) + public TestResponseCachingKeyProvider(string key) { _key = key; } - public string CreateStorageKey(OutputCacheContext context) + public string CreateStorageKey(OutputCacheContext? context) { return _key; } @@ -328,7 +315,7 @@ public string CreateStorageKey(OutputCacheContext context) internal class TestOutputCache : IOutputCacheStore { - private readonly IDictionary _storage = new Dictionary(); + private readonly Dictionary _storage = new(); public int GetCount { get; private set; } public int SetCount { get; private set; } @@ -337,8 +324,10 @@ public ValueTask EvictByTagAsync(string tag, CancellationToken token) throw new NotImplementedException(); } - public ValueTask GetAsync(string key, CancellationToken token) + public ValueTask GetAsync(string? key, CancellationToken token) { + ArgumentNullException.ThrowIfNull(key); + GetCount++; try { @@ -350,7 +339,7 @@ public ValueTask GetAsync(string key, CancellationToken token) } } - public ValueTask SetAsync(string key, byte[] entry, string[] tags, TimeSpan validFor, CancellationToken token) + public ValueTask SetAsync(string key, byte[] entry, string[]? tags, TimeSpan validFor, CancellationToken token) { SetCount++; _storage[key] = entry; diff --git a/src/Mvc/samples/MvcSandbox/MvcSandbox.csproj b/src/Mvc/samples/MvcSandbox/MvcSandbox.csproj index aecdc3456b06..6c3cf21a5ac2 100644 --- a/src/Mvc/samples/MvcSandbox/MvcSandbox.csproj +++ b/src/Mvc/samples/MvcSandbox/MvcSandbox.csproj @@ -11,7 +11,6 @@ - diff --git a/src/Mvc/samples/MvcSandbox/Pages/PagesHome.cshtml b/src/Mvc/samples/MvcSandbox/Pages/PagesHome.cshtml index 7d28b1821905..d4d5241a6072 100644 --- a/src/Mvc/samples/MvcSandbox/Pages/PagesHome.cshtml +++ b/src/Mvc/samples/MvcSandbox/Pages/PagesHome.cshtml @@ -14,5 +14,4 @@

Pages Home

This sandbox should give you a quick view of a basic MVC application.

- @DateTime.UtcNow.ToString("o") -
+ \ No newline at end of file From 37e30285e452e0210c7f4f58db26405c22cfb38f Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Wed, 22 Jun 2022 14:36:09 -0700 Subject: [PATCH 47/48] Brennan's --- .../samples/OutputCachingSample/Startup.cs | 2 +- ...chedVaryByRules.cs => CacheVaryByRules.cs} | 2 +- .../OutputCaching/src/CachedResponseBody.cs | 14 +-- .../OutputCaching/src/IOutputCachePolicy.cs | 6 +- .../src/Memory/MemoryOutputCacheStore.cs | 47 ++++++---- .../Microsoft.AspNetCore.OutputCaching.csproj | 1 - .../OutputCaching/src/OutputCacheAttribute.cs | 19 ++-- .../OutputCaching/src/OutputCacheContext.cs | 15 ++-- .../OutputCaching/src/OutputCacheEntry.cs | 3 +- .../src/OutputCacheEntryFormatter.cs | 22 ++--- .../src/OutputCacheKeyProvider.cs | 4 +- .../src/OutputCacheMiddleware.cs | 90 +++++++++---------- .../src/OutputCachePolicyBuilder.cs | 64 ++++--------- .../src/Policies/CompositePolicy.cs | 14 +-- .../src/Policies/DefaultPolicy.cs | 8 +- .../src/Policies/EnableCachePolicy.cs | 6 +- .../src/Policies/ExpirationPolicy.cs | 6 +- .../src/Policies/LockingPolicy.cs | 6 +- .../{ProfilePolicy.cs => NamedPolicy.cs} | 28 +++--- .../src/Policies/NoLookupPolicy.cs | 6 +- .../src/Policies/NoStorePolicy.cs | 6 +- .../OutputCacheConventionBuilderExtensions.cs | 17 ++++ .../src/Policies/PredicatePolicy.cs | 20 ++--- .../OutputCaching/src/Policies/TagsPolicy.cs | 6 +- .../OutputCaching/src/Policies/TypedPolicy.cs | 12 +-- .../src/Policies/VaryByHeaderPolicy.cs | 10 +-- .../src/Policies/VaryByQueryPolicy.cs | 14 +-- .../src/Policies/VaryByValuePolicy.cs | 14 +-- .../OutputCaching/src/PublicAPI.Unshipped.txt | 38 ++++---- .../src/Serialization/FormatterEntry.cs | 6 +- .../test/OutputCacheEntryFormatterTests.cs | 66 ++++++++++++++ .../test/OutputCacheKeyProviderTests.cs | 34 +++---- .../test/OutputCacheMiddlewareTests.cs | 9 +- .../test/OutputCachePoliciesTests.cs | 78 ++++++++-------- .../test/OutputCachePolicyProviderTests.cs | 34 +++---- .../OutputCaching/test/TestUtils.cs | 6 +- .../Mvc.Core/src/Filters/OutputCacheFilter.cs | 2 +- 37 files changed, 393 insertions(+), 342 deletions(-) rename src/Middleware/OutputCaching/src/{CachedVaryByRules.cs => CacheVaryByRules.cs} (96%) rename src/Middleware/OutputCaching/src/Policies/{ProfilePolicy.cs => NamedPolicy.cs} (60%) create mode 100644 src/Middleware/OutputCaching/test/OutputCacheEntryFormatterTests.cs diff --git a/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs b/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs index c5003a86cec9..ca91981bc6b7 100644 --- a/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs +++ b/src/Middleware/OutputCaching/samples/OutputCachingSample/Startup.cs @@ -25,7 +25,7 @@ app.MapGet("/nocache", Gravatar.WriteGravatar).CacheOutput(x => x.NoCache()); -app.MapGet("/profile", Gravatar.WriteGravatar).CacheOutput(x => x.Policy("NoCache")); +app.MapGet("/profile", Gravatar.WriteGravatar).CacheOutput("NoCache"); app.MapGet("/attribute", [OutputCache(PolicyName = "NoCache")] () => Gravatar.WriteGravatar); diff --git a/src/Middleware/OutputCaching/src/CachedVaryByRules.cs b/src/Middleware/OutputCaching/src/CacheVaryByRules.cs similarity index 96% rename from src/Middleware/OutputCaching/src/CachedVaryByRules.cs rename to src/Middleware/OutputCaching/src/CacheVaryByRules.cs index 6ad24052e153..28caea7acea2 100644 --- a/src/Middleware/OutputCaching/src/CachedVaryByRules.cs +++ b/src/Middleware/OutputCaching/src/CacheVaryByRules.cs @@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.OutputCaching; /// /// Represents vary-by rules. /// -public sealed class CachedVaryByRules +public sealed class CacheVaryByRules { private Dictionary? _varyByCustom; diff --git a/src/Middleware/OutputCaching/src/CachedResponseBody.cs b/src/Middleware/OutputCaching/src/CachedResponseBody.cs index 4efdc97035fe..dd46a5ece715 100644 --- a/src/Middleware/OutputCaching/src/CachedResponseBody.cs +++ b/src/Middleware/OutputCaching/src/CachedResponseBody.cs @@ -17,6 +17,8 @@ internal sealed class CachedResponseBody /// The length. public CachedResponseBody(List segments, long length) { + ArgumentNullException.ThrowIfNull(segments); + Segments = segments; Length = length; } @@ -45,17 +47,7 @@ public async Task CopyToAsync(PipeWriter destination, CancellationToken cancella { cancellationToken.ThrowIfCancellationRequested(); - Copy(segment, destination); - - await destination.FlushAsync(cancellationToken); + await destination.WriteAsync(segment, cancellationToken); } } - - private static void Copy(byte[] segment, PipeWriter destination) - { - var span = destination.GetSpan(segment.Length); - - segment.CopyTo(span); - destination.Advance(segment.Length); - } } diff --git a/src/Middleware/OutputCaching/src/IOutputCachePolicy.cs b/src/Middleware/OutputCaching/src/IOutputCachePolicy.cs index 6b8e0e405d2d..aae03b76dbaf 100644 --- a/src/Middleware/OutputCaching/src/IOutputCachePolicy.cs +++ b/src/Middleware/OutputCaching/src/IOutputCachePolicy.cs @@ -13,18 +13,18 @@ public interface IOutputCachePolicy /// At that point the cache middleware can still be enabled or disabled for the request. ///
/// The current request's cache context. - ValueTask CacheRequestAsync(OutputCacheContext context); + ValueTask CacheRequestAsync(OutputCacheContext context, CancellationToken cancellation); /// /// Updates the before the cached response is used. /// At that point the freshness of the cached response can be updated. /// /// The current request's cache context. - ValueTask ServeFromCacheAsync(OutputCacheContext context); + ValueTask ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellation); /// /// Updates the before the response is served and can be cached. /// At that point cacheability of the response can be updated. /// - ValueTask ServeResponseAsync(OutputCacheContext context); + ValueTask ServeResponseAsync(OutputCacheContext context, CancellationToken cancellation); } diff --git a/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs b/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs index 71cecdedcc21..3f1df73c7b80 100644 --- a/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs +++ b/src/Middleware/OutputCaching/src/Memory/MemoryOutputCacheStore.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Collections.Concurrent; using Microsoft.Extensions.Caching.Memory; namespace Microsoft.AspNetCore.OutputCaching.Memory; @@ -9,7 +8,8 @@ namespace Microsoft.AspNetCore.OutputCaching.Memory; internal sealed class MemoryOutputCacheStore : IOutputCacheStore { private readonly IMemoryCache _cache; - private readonly ConcurrentDictionary> _taggedEntries = new(); + private readonly Dictionary> _taggedEntries = new(); + private readonly object _tagsLock = new(); internal MemoryOutputCacheStore(IMemoryCache cache) { @@ -22,11 +22,16 @@ public ValueTask EvictByTagAsync(string tag, CancellationToken cancellationToken { ArgumentNullException.ThrowIfNull(tag); - if (_taggedEntries.TryGetValue(tag, out var keys)) + lock (_tagsLock) { - foreach (var key in keys) + if (_taggedEntries.TryGetValue(tag, out var keys)) { - _cache.Remove(key); + foreach (var key in keys) + { + _cache.Remove(key); + } + + _taggedEntries.Remove(tag); } } @@ -50,22 +55,33 @@ public ValueTask SetAsync(string key, byte[] value, string[]? tags, TimeSpan val if (tags != null) { - foreach (var tag in tags) - { - var keys = _taggedEntries.GetOrAdd(tag, _ => new HashSet()); + // Lock with SetEntry() to prevent EvictByTagAsync() from trying to remove a tag whose entry hasn't been added yet. + // It might be acceptable to not lock SetEntry() since in this case Remove(key) would just no-op and the user retry to evict. - // Copy the list of tags to prevent locking - - var local = new HashSet(keys) + lock (_tagsLock) + { + foreach (var tag in tags) { - key - }; + if (!_taggedEntries.TryGetValue(tag, out var keys)) + { + keys = new HashSet(); + _taggedEntries[tag] = keys; + } - _taggedEntries[tag] = local; + keys.Add(key); + } + + SetEntry(); } } + else + { + SetEntry(); + } - _cache.Set( + void SetEntry() + { + _cache.Set( key, value, new MemoryCacheEntryOptions @@ -73,6 +89,7 @@ public ValueTask SetAsync(string key, byte[] value, string[]? tags, TimeSpan val AbsoluteExpirationRelativeToNow = validFor, Size = value.Length }); + } return ValueTask.CompletedTask; } diff --git a/src/Middleware/OutputCaching/src/Microsoft.AspNetCore.OutputCaching.csproj b/src/Middleware/OutputCaching/src/Microsoft.AspNetCore.OutputCaching.csproj index e9a0cbc7ff23..1396a66e27f1 100644 --- a/src/Middleware/OutputCaching/src/Microsoft.AspNetCore.OutputCaching.csproj +++ b/src/Middleware/OutputCaching/src/Microsoft.AspNetCore.OutputCaching.csproj @@ -4,7 +4,6 @@ ASP.NET Core middleware for caching HTTP responses on the server. $(DefaultNetCoreTargetFramework) true - true false true diff --git a/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs b/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs index a214639c3a44..ab48a0355609 100644 --- a/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheAttribute.cs @@ -6,6 +6,9 @@ namespace Microsoft.AspNetCore.OutputCaching; /// /// Specifies the parameters necessary for setting appropriate headers in output caching. /// +/// +/// This attribute requires the output cache middleware. +/// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] public sealed class OutputCacheAttribute : Attribute { @@ -30,7 +33,7 @@ public int Duration /// Gets or sets the value which determines whether the reponse should be cached or not. /// When set to , the response won't be cached. ///
- public bool NoCache + public bool NoStore { get => _noCache ?? false; init => _noCache = value; @@ -39,17 +42,11 @@ public bool NoCache /// /// Gets or sets the query keys to vary by. /// - /// - /// requires the output cache middleware. - /// public string[]? VaryByQueryKeys { get; init; } /// /// Gets or sets the headers to vary by. /// - /// - /// requires the output cache middleware. - /// public string[]? VaryByHeaders { get; init; } /// @@ -66,14 +63,14 @@ internal IOutputCachePolicy BuildPolicy() var builder = new OutputCachePolicyBuilder(); - if (_noCache != null && _noCache.Value) + if (PolicyName != null) { - builder.NoCache(); + builder.AddPolicy(new NamedPolicy(PolicyName)); } - if (PolicyName != null) + if (_noCache != null && _noCache.Value) { - builder.Policy(PolicyName); + builder.NoCache(); } if (VaryByQueryKeys != null) diff --git a/src/Middleware/OutputCaching/src/OutputCacheContext.cs b/src/Middleware/OutputCaching/src/OutputCacheContext.cs index 0471c68acb9e..667ca5dcce94 100644 --- a/src/Middleware/OutputCaching/src/OutputCacheContext.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheContext.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; @@ -52,9 +50,9 @@ internal OutputCacheContext(HttpContext httpContext, IOutputCacheStore store, Ou public DateTimeOffset? ResponseTime { get; internal set; } /// - /// Gets the instance. + /// Gets the instance. /// - public CachedVaryByRules CachedVaryByRules { get; set; } = new(); + public CacheVaryByRules CacheVaryByRules { get; set; } = new(); /// /// Gets the tags of the cached response. @@ -66,7 +64,7 @@ internal OutputCacheContext(HttpContext httpContext, IOutputCacheStore store, Ou /// public TimeSpan? ResponseExpirationTimeSpan { get; set; } - internal string CacheKey { get; set; } + internal string CacheKey { get; set; } = default!; internal TimeSpan CachedResponseValidFor { get; set; } @@ -74,15 +72,14 @@ internal OutputCacheContext(HttpContext httpContext, IOutputCacheStore store, Ou internal TimeSpan CachedEntryAge { get; set; } - internal OutputCacheEntry CachedResponse { get; set; } + internal OutputCacheEntry CachedResponse { get; set; } = default!; internal bool ResponseStarted { get; set; } - internal Stream OriginalResponseStream { get; set; } + internal Stream OriginalResponseStream { get; set; } = default!; - internal OutputCacheStream OutputCacheStream { get; set; } + internal OutputCacheStream OutputCacheStream { get; set; } = default!; internal ILogger Logger { get; } internal OutputCacheOptions Options { get; } internal IOutputCacheStore Store { get; } - } diff --git a/src/Middleware/OutputCaching/src/OutputCacheEntry.cs b/src/Middleware/OutputCaching/src/OutputCacheEntry.cs index ce77ac671a25..325f2e660718 100644 --- a/src/Middleware/OutputCaching/src/OutputCacheEntry.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheEntry.cs @@ -5,7 +5,6 @@ namespace Microsoft.AspNetCore.OutputCaching; -/// internal sealed class OutputCacheEntry { /// @@ -31,5 +30,5 @@ internal sealed class OutputCacheEntry /// /// Gets the tags of the cache entry. /// - public string[]? Tags { get; set; } + public string[] Tags { get; set; } = Array.Empty(); } diff --git a/src/Middleware/OutputCaching/src/OutputCacheEntryFormatter.cs b/src/Middleware/OutputCaching/src/OutputCacheEntryFormatter.cs index 59f911e18682..b6dbef1c2c51 100644 --- a/src/Middleware/OutputCaching/src/OutputCacheEntryFormatter.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheEntryFormatter.cs @@ -22,9 +22,7 @@ internal static class OutputCacheEntryFormatter return null; } - using var br = new MemoryStream(content); - - var formatter = await JsonSerializer.DeserializeAsync(br, FormatterEntrySerializerContext.Default.FormatterEntry, cancellationToken: cancellationToken); + var formatter = JsonSerializer.Deserialize(content, FormatterEntrySerializerContext.Default.FormatterEntry); if (formatter == null) { @@ -35,26 +33,27 @@ internal static class OutputCacheEntryFormatter { StatusCode = formatter.StatusCode, Created = formatter.Created, - Tags = formatter.Tags + Tags = formatter.Tags, + Headers = new(), + Body = new CachedResponseBody(formatter.Body, formatter.Body.Sum(x => x.Length)) }; if (formatter.Headers != null) { - outputCacheEntry.Headers = new(); - foreach (var header in formatter.Headers) { outputCacheEntry.Headers.TryAdd(header.Key, header.Value); } } - var cachedResponseBody = new CachedResponseBody(formatter.Body, formatter.Body.Sum(x => x.Length)); - outputCacheEntry.Body = cachedResponseBody; + return outputCacheEntry; } public static async ValueTask StoreAsync(string key, OutputCacheEntry value, TimeSpan duration, IOutputCacheStore store, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(value); + ArgumentNullException.ThrowIfNull(value.Body); + ArgumentNullException.ThrowIfNull(value.Headers); var formatterEntry = new FormatterEntry { @@ -73,9 +72,10 @@ public static async ValueTask StoreAsync(string key, OutputCacheEntry value, Tim } } - using var br = new MemoryStream(); + using var bufferStream = new MemoryStream(); + + JsonSerializer.Serialize(bufferStream, formatterEntry, FormatterEntrySerializerContext.Default.FormatterEntry); - await JsonSerializer.SerializeAsync(br, formatterEntry, FormatterEntrySerializerContext.Default.FormatterEntry, cancellationToken); - await store.SetAsync(key, br.ToArray(), value.Tags ?? Array.Empty(), duration, cancellationToken); + await store.SetAsync(key, bufferStream.ToArray(), value.Tags ?? Array.Empty(), duration, cancellationToken); } } diff --git a/src/Middleware/OutputCaching/src/OutputCacheKeyProvider.cs b/src/Middleware/OutputCaching/src/OutputCacheKeyProvider.cs index d3943e0d5feb..dc8cce4afc15 100644 --- a/src/Middleware/OutputCaching/src/OutputCacheKeyProvider.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheKeyProvider.cs @@ -33,10 +33,10 @@ public string CreateStorageKey(OutputCacheContext context) { ArgumentNullException.ThrowIfNull(_builderPool); - var varyByRules = context.CachedVaryByRules; + var varyByRules = context.CacheVaryByRules; if (varyByRules == null) { - throw new InvalidOperationException($"{nameof(CachedVaryByRules)} must not be null on the {nameof(OutputCacheContext)}"); + throw new InvalidOperationException($"{nameof(CacheVaryByRules)} must not be null on the {nameof(OutputCacheContext)}"); } var request = context.HttpContext.Request; diff --git a/src/Middleware/OutputCaching/src/OutputCacheMiddleware.cs b/src/Middleware/OutputCaching/src/OutputCacheMiddleware.cs index f9902be98305..a526358e114b 100644 --- a/src/Middleware/OutputCaching/src/OutputCacheMiddleware.cs +++ b/src/Middleware/OutputCaching/src/OutputCacheMiddleware.cs @@ -101,7 +101,7 @@ private async Task InvokeAwaited(HttpContext httpContext, IReadOnlyList ExecuteResponseAsync()); - if (context.AllowLocking) + // The current request was processed, nothing more to do + if (executed) { - var cacheEntry = await _requestDispatcher.ScheduleAsync(context.CacheKey, key => ExecuteResponseAsync()); - - // If the result was processed by another request, serve it from cache - if (!executed) - { - if (await TryServeFromCacheAsync(context, policies)) - { - return; - } - } - } - else - { - await ExecuteResponseAsync(); + return; } - async Task ExecuteResponseAsync() + // If the result was processed by another request, try to serve it from cache entry (no lookup) + if (await TryServeCachedResponseAsync(context, cacheEntry, policies)) { - // Hook up to listen to the response stream - ShimResponseStream(context); + return; + } - try - { - await _next(httpContext); + // If the cache entry couldn't be served, continue to processing the request as usual + } - // The next middleware might change the policy - foreach (var policy in policies) - { - await policy.ServeResponseAsync(context); - } + await ExecuteResponseAsync(); - // If there was no response body, check the response headers now. We can cache things like redirects. - StartResponse(context); + async Task ExecuteResponseAsync() + { + // Hook up to listen to the response stream + ShimResponseStream(context); - // Finalize the cache entry - await FinalizeCacheBodyAsync(context); + try + { + await _next(httpContext); - executed = true; - } - finally + // The next middleware might change the policy + foreach (var policy in policies) { - UnshimResponseStream(context); + await policy.ServeResponseAsync(context, httpContext.RequestAborted); } - return context.CachedResponse; + // If there was no response body, check the response headers now. We can cache things like redirects. + StartResponse(context); + + // Finalize the cache entry + await FinalizeCacheBodyAsync(context); + + executed = true; + } + finally + { + UnshimResponseStream(context); } - return; + return context.CachedResponse; } + + return; } } @@ -239,7 +239,7 @@ internal async Task TryServeCachedResponseAsync(OutputCacheContext context foreach (var policy in policies) { - await policy.ServeFromCacheAsync(context); + await policy.ServeFromCacheAsync(context, context.HttpContext.RequestAborted); } context.IsCacheEntryFresh = true; @@ -341,10 +341,10 @@ internal void CreateCacheKey(OutputCacheContext context) return; } - var varyHeaders = context.CachedVaryByRules.Headers; - var varyQueryKeys = context.CachedVaryByRules.QueryKeys; - var varyByCustomKeys = context.CachedVaryByRules.VaryByCustom; - var varyByPrefix = context.CachedVaryByRules.VaryByPrefix; + var varyHeaders = context.CacheVaryByRules.Headers; + var varyQueryKeys = context.CacheVaryByRules.QueryKeys; + var varyByCustomKeys = context.CacheVaryByRules.VaryByCustom; + var varyByPrefix = context.CacheVaryByRules.VaryByPrefix; // Check if any vary rules exist if (!StringValues.IsNullOrEmpty(varyHeaders) || !StringValues.IsNullOrEmpty(varyQueryKeys) || !StringValues.IsNullOrEmpty(varyByPrefix) || varyByCustomKeys?.Count > 0) @@ -355,7 +355,7 @@ internal void CreateCacheKey(OutputCacheContext context) var normalizedVaryByCustom = GetOrderCasingNormalizedDictionary(varyByCustomKeys); // Update vary rules with normalized values - context.CachedVaryByRules = new CachedVaryByRules + context.CacheVaryByRules = new CacheVaryByRules { VaryByPrefix = varyByPrefix + normalizedVaryByCustom, Headers = normalizedVaryHeaders, diff --git a/src/Middleware/OutputCaching/src/OutputCachePolicyBuilder.cs b/src/Middleware/OutputCaching/src/OutputCachePolicyBuilder.cs index 8256174059d0..e2c2e59403bb 100644 --- a/src/Middleware/OutputCaching/src/OutputCachePolicyBuilder.cs +++ b/src/Middleware/OutputCaching/src/OutputCachePolicyBuilder.cs @@ -28,15 +28,20 @@ public OutputCachePolicyBuilder() _policies.Add(DefaultPolicy.Instance); } + internal OutputCachePolicyBuilder AddPolicy(IOutputCachePolicy policy) + { + _builtPolicy = null; + _policies.Add(policy); + return this; + } + /// /// Adds a dynamically resolved policy. /// /// The type of policy to add public OutputCachePolicyBuilder AddPolicy([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type policyType) { - _builtPolicy = null; - _policies.Add(new TypedPolicy(policyType)); - return this; + return AddPolicy(new TypedPolicy(policyType)); } /// @@ -87,9 +92,7 @@ public OutputCachePolicyBuilder VaryByQuery(params string[] queryKeys) { ArgumentNullException.ThrowIfNull(queryKeys); - _builtPolicy = null; - _policies.Add(new VaryByQueryPolicy(queryKeys)); - return this; + return AddPolicy(new VaryByQueryPolicy(queryKeys)); } /// @@ -100,9 +103,7 @@ public OutputCachePolicyBuilder VaryByHeader(params string[] headers) { ArgumentNullException.ThrowIfNull(headers); - _builtPolicy = null; - _policies.Add(new VaryByHeaderPolicy(headers)); - return this; + return AddPolicy(new VaryByHeaderPolicy(headers)); } /// @@ -113,9 +114,7 @@ public OutputCachePolicyBuilder VaryByValue(Func @@ -126,9 +125,7 @@ public OutputCachePolicyBuilder VaryByValue(Func @@ -139,9 +136,7 @@ public OutputCachePolicyBuilder VaryByValue(Func varyBy) { ArgumentNullException.ThrowIfNull(varyBy); - _builtPolicy = null; - _policies.Add(new VaryByValuePolicy(varyBy)); - return this; + return AddPolicy(new VaryByValuePolicy(varyBy)); } /// @@ -152,22 +147,7 @@ public OutputCachePolicyBuilder VaryByValue(Func - /// Adds a named policy. - /// - /// The name of the policy to add. - public OutputCachePolicyBuilder Policy(string profileName) - { - ArgumentNullException.ThrowIfNull(profileName); - - _builtPolicy = null; - _policies.Add(new ProfilePolicy(profileName)); - return this; + return AddPolicy(new VaryByValuePolicy(varyBy)); } /// @@ -178,9 +158,7 @@ public OutputCachePolicyBuilder Tag(params string[] tags) { ArgumentNullException.ThrowIfNull(tags); - _builtPolicy = null; - _policies.Add(new TagsPolicy(tags)); - return this; + return AddPolicy(new TagsPolicy(tags)); } /// @@ -189,9 +167,7 @@ public OutputCachePolicyBuilder Tag(params string[] tags) /// The expiration of the cached reponse. public OutputCachePolicyBuilder Expire(TimeSpan expiration) { - _builtPolicy = null; - _policies.Add(new ExpirationPolicy(expiration)); - return this; + return AddPolicy(new ExpirationPolicy(expiration)); } /// @@ -200,9 +176,7 @@ public OutputCachePolicyBuilder Expire(TimeSpan expiration) /// Whether the request should be locked. public OutputCachePolicyBuilder AllowLocking(bool lockResponse = true) { - _builtPolicy = null; - _policies.Add(lockResponse ? LockingPolicy.Enabled : LockingPolicy.Disabled); - return this; + return AddPolicy(lockResponse ? LockingPolicy.Enabled : LockingPolicy.Disabled); } /// @@ -228,10 +202,8 @@ public OutputCachePolicyBuilder Clear() /// public OutputCachePolicyBuilder NoCache() { - _builtPolicy = null; _policies.Clear(); - _policies.Add(EnableCachePolicy.Disabled); - return this; + return AddPolicy(EnableCachePolicy.Disabled); } /// diff --git a/src/Middleware/OutputCaching/src/Policies/CompositePolicy.cs b/src/Middleware/OutputCaching/src/Policies/CompositePolicy.cs index dff9efc9e583..b82b54386d5e 100644 --- a/src/Middleware/OutputCaching/src/Policies/CompositePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/CompositePolicy.cs @@ -14,35 +14,35 @@ internal sealed class CompositePolicy : IOutputCachePolicy /// Creates a new instance of /// /// The policies to include. - public CompositePolicy(params IOutputCachePolicy[] policies!!) + public CompositePolicy(params IOutputCachePolicy[] policies) { _policies = policies; } /// - async ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) + async ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken) { foreach (var policy in _policies) { - await policy.CacheRequestAsync(context); + await policy.CacheRequestAsync(context, cancellationToken); } } /// - async ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) + async ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellationToken) { foreach (var policy in _policies) { - await policy.ServeFromCacheAsync(context); + await policy.ServeFromCacheAsync(context, cancellationToken); } } /// - async ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) + async ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context, CancellationToken cancellationToken) { foreach (var policy in _policies) { - await policy.ServeResponseAsync(context); + await policy.ServeResponseAsync(context, cancellationToken); } } } diff --git a/src/Middleware/OutputCaching/src/Policies/DefaultPolicy.cs b/src/Middleware/OutputCaching/src/Policies/DefaultPolicy.cs index cbe78433579f..c17a700b0fae 100644 --- a/src/Middleware/OutputCaching/src/Policies/DefaultPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/DefaultPolicy.cs @@ -18,7 +18,7 @@ private DefaultPolicy() } /// - ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken) { var attemptOutputCaching = AttemptOutputCaching(context); context.EnableOutputCaching = true; @@ -27,19 +27,19 @@ ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) context.AllowLocking = true; // Vary by any query by default - context.CachedVaryByRules.QueryKeys = "*"; + context.CacheVaryByRules.QueryKeys = "*"; return ValueTask.CompletedTask; } /// - ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellationToken) { return ValueTask.CompletedTask; } /// - ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context, CancellationToken cancellationToken) { var response = context.HttpContext.Response; diff --git a/src/Middleware/OutputCaching/src/Policies/EnableCachePolicy.cs b/src/Middleware/OutputCaching/src/Policies/EnableCachePolicy.cs index 1894c8a5d0a4..3ebb8d4764d8 100644 --- a/src/Middleware/OutputCaching/src/Policies/EnableCachePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/EnableCachePolicy.cs @@ -16,7 +16,7 @@ private EnableCachePolicy() } /// - ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken) { context.EnableOutputCaching = this == Enabled; @@ -24,13 +24,13 @@ ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) } /// - ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context, CancellationToken cancellationToken) { return ValueTask.CompletedTask; } /// - ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellationToken) { return ValueTask.CompletedTask; } diff --git a/src/Middleware/OutputCaching/src/Policies/ExpirationPolicy.cs b/src/Middleware/OutputCaching/src/Policies/ExpirationPolicy.cs index 29589a4c1ed9..5a9f963f63da 100644 --- a/src/Middleware/OutputCaching/src/Policies/ExpirationPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/ExpirationPolicy.cs @@ -20,7 +20,7 @@ public ExpirationPolicy(TimeSpan expiration) } /// - ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken) { context.ResponseExpirationTimeSpan = _expiration; @@ -28,13 +28,13 @@ ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) } /// - ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellationToken) { return ValueTask.CompletedTask; } /// - ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context, CancellationToken cancellationToken) { return ValueTask.CompletedTask; } diff --git a/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs b/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs index c21dc5ecd9a1..504ac4e6a20a 100644 --- a/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/LockingPolicy.cs @@ -26,7 +26,7 @@ private LockingPolicy(bool lockResponse) public static readonly LockingPolicy Disabled = new(false); /// - ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken) { context.AllowLocking = _lockResponse; @@ -34,13 +34,13 @@ ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) } /// - ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellationToken) { return ValueTask.CompletedTask; } /// - ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context, CancellationToken cancellationToken) { return ValueTask.CompletedTask; } diff --git a/src/Middleware/OutputCaching/src/Policies/ProfilePolicy.cs b/src/Middleware/OutputCaching/src/Policies/NamedPolicy.cs similarity index 60% rename from src/Middleware/OutputCaching/src/Policies/ProfilePolicy.cs rename to src/Middleware/OutputCaching/src/Policies/NamedPolicy.cs index 2b154b547cb7..4967d8b1b3fc 100644 --- a/src/Middleware/OutputCaching/src/Policies/ProfilePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/NamedPolicy.cs @@ -4,23 +4,23 @@ namespace Microsoft.AspNetCore.OutputCaching; /// -/// A policy represented by a named profile. +/// A named policy. /// -internal sealed class ProfilePolicy : IOutputCachePolicy +internal sealed class NamedPolicy : IOutputCachePolicy { - private readonly string _profileName; + private readonly string _policyName; /// - /// Create a new instance. + /// Create a new instance. /// - /// The name of the profile. - public ProfilePolicy(string profileName) + /// The name of the profile. + public NamedPolicy(string policyName) { - _profileName = profileName; + _policyName = policyName; } /// - ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context, CancellationToken cancellationToken) { var policy = GetProfilePolicy(context); @@ -29,11 +29,11 @@ ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) return ValueTask.CompletedTask; } - return policy.ServeResponseAsync(context); + return policy.ServeResponseAsync(context, cancellationToken); } /// - ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellationToken) { var policy = GetProfilePolicy(context); @@ -42,11 +42,11 @@ ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) return ValueTask.CompletedTask; } - return policy.ServeFromCacheAsync(context); + return policy.ServeFromCacheAsync(context, cancellationToken); } /// - ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken) { var policy = GetProfilePolicy(context); @@ -55,14 +55,14 @@ ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) return ValueTask.CompletedTask; } - return policy.CacheRequestAsync(context); ; + return policy.CacheRequestAsync(context, cancellationToken); ; } internal IOutputCachePolicy? GetProfilePolicy(OutputCacheContext context) { var policies = context.Options.NamedPolicies; - return policies != null && policies.TryGetValue(_profileName, out var cacheProfile) + return policies != null && policies.TryGetValue(_policyName, out var cacheProfile) ? cacheProfile : null; } diff --git a/src/Middleware/OutputCaching/src/Policies/NoLookupPolicy.cs b/src/Middleware/OutputCaching/src/Policies/NoLookupPolicy.cs index c01db72338e7..d6fee5da0d9f 100644 --- a/src/Middleware/OutputCaching/src/Policies/NoLookupPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/NoLookupPolicy.cs @@ -15,7 +15,7 @@ private NoLookupPolicy() } /// - ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context, CancellationToken cancellationToken) { context.AllowCacheLookup = false; @@ -23,13 +23,13 @@ ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) } /// - ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellationToken) { return ValueTask.CompletedTask; } /// - ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken) { return ValueTask.CompletedTask; } diff --git a/src/Middleware/OutputCaching/src/Policies/NoStorePolicy.cs b/src/Middleware/OutputCaching/src/Policies/NoStorePolicy.cs index 11b7d420f892..11a22b6c5e75 100644 --- a/src/Middleware/OutputCaching/src/Policies/NoStorePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/NoStorePolicy.cs @@ -15,7 +15,7 @@ private NoStorePolicy() } /// - ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context, CancellationToken cancellationToken) { context.AllowCacheStorage = false; @@ -23,13 +23,13 @@ ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) } /// - ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellationToken) { return ValueTask.CompletedTask; } /// - ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken) { return ValueTask.CompletedTask; } diff --git a/src/Middleware/OutputCaching/src/Policies/OutputCacheConventionBuilderExtensions.cs b/src/Middleware/OutputCaching/src/Policies/OutputCacheConventionBuilderExtensions.cs index 3cdf507e0fc1..050fbeb84ed9 100644 --- a/src/Middleware/OutputCaching/src/Policies/OutputCacheConventionBuilderExtensions.cs +++ b/src/Middleware/OutputCaching/src/Policies/OutputCacheConventionBuilderExtensions.cs @@ -61,4 +61,21 @@ public static TBuilder CacheOutput(this TBuilder builder, Action + /// Marks an endpoint to be cached using a named policy. + /// + public static TBuilder CacheOutput(this TBuilder builder, string policyName) where TBuilder : IEndpointConventionBuilder + { + ArgumentNullException.ThrowIfNull(builder); + + var policy = new NamedPolicy(policyName); + + builder.Add(endpointBuilder => + { + endpointBuilder.Metadata.Add(policy); + }); + + return builder; + } } diff --git a/src/Middleware/OutputCaching/src/Policies/PredicatePolicy.cs b/src/Middleware/OutputCaching/src/Policies/PredicatePolicy.cs index 353d1ce223dc..d58c67fa57d8 100644 --- a/src/Middleware/OutputCaching/src/Policies/PredicatePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/PredicatePolicy.cs @@ -25,30 +25,30 @@ public PredicatePolicy(Func> asyncPredicate, } /// - ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken) { - return ExecuteAwaited(static (policy, context) => policy.CacheRequestAsync(context), _policy, context); + return ExecuteAwaited(static (policy, context, cancellationToken) => policy.CacheRequestAsync(context, cancellationToken), _policy, context, cancellationToken); } /// - ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellationToken) { - return ExecuteAwaited(static (policy, context) => policy.ServeFromCacheAsync(context), _policy, context); + return ExecuteAwaited(static (policy, context, cancellationToken) => policy.ServeFromCacheAsync(context, cancellationToken), _policy, context, cancellationToken); } /// - ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context, CancellationToken cancellationToken) { - return ExecuteAwaited(static (policy, context) => policy.ServeResponseAsync(context), _policy, context); + return ExecuteAwaited(static (policy, context, cancellationToken) => policy.ServeResponseAsync(context, cancellationToken), _policy, context, cancellationToken); } - private ValueTask ExecuteAwaited(Func action, IOutputCachePolicy policy, OutputCacheContext context) + private ValueTask ExecuteAwaited(Func action, IOutputCachePolicy policy, OutputCacheContext context, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(action); if (_predicate == null) { - return action(policy, context); + return action(policy, context, cancellationToken); } var task = _predicate(context); @@ -57,7 +57,7 @@ private ValueTask ExecuteAwaited(Func task) { if (await task) { - await action(policy, context); + await action(policy, context, cancellationToken); } } } diff --git a/src/Middleware/OutputCaching/src/Policies/TagsPolicy.cs b/src/Middleware/OutputCaching/src/Policies/TagsPolicy.cs index 622af389bdda..070e2a66b1e4 100644 --- a/src/Middleware/OutputCaching/src/Policies/TagsPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/TagsPolicy.cs @@ -20,7 +20,7 @@ public TagsPolicy(params string[] tags) } /// - ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken) { foreach (var tag in _tags) { @@ -31,13 +31,13 @@ ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) } /// - ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellationToken) { return ValueTask.CompletedTask; } /// - ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context, CancellationToken cancellationToken) { return ValueTask.CompletedTask; } diff --git a/src/Middleware/OutputCaching/src/Policies/TypedPolicy.cs b/src/Middleware/OutputCaching/src/Policies/TypedPolicy.cs index adfba1fef1a8..d01f60a24374 100644 --- a/src/Middleware/OutputCaching/src/Policies/TypedPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/TypedPolicy.cs @@ -33,20 +33,20 @@ public TypedPolicy([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Pu } /// - ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken) { - return CreatePolicy(context)?.CacheRequestAsync(context) ?? ValueTask.CompletedTask; + return CreatePolicy(context)?.CacheRequestAsync(context, cancellationToken) ?? ValueTask.CompletedTask; } /// - ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellationToken) { - return CreatePolicy(context)?.ServeFromCacheAsync(context) ?? ValueTask.CompletedTask; + return CreatePolicy(context)?.ServeFromCacheAsync(context, cancellationToken) ?? ValueTask.CompletedTask; } /// - ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context, CancellationToken cancellationToken) { - return CreatePolicy(context)?.ServeResponseAsync(context) ?? ValueTask.CompletedTask; + return CreatePolicy(context)?.ServeResponseAsync(context, cancellationToken) ?? ValueTask.CompletedTask; } } diff --git a/src/Middleware/OutputCaching/src/Policies/VaryByHeaderPolicy.cs b/src/Middleware/OutputCaching/src/Policies/VaryByHeaderPolicy.cs index 1827a9feae70..35ed3ca16a26 100644 --- a/src/Middleware/OutputCaching/src/Policies/VaryByHeaderPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/VaryByHeaderPolicy.cs @@ -40,28 +40,28 @@ public VaryByHeaderPolicy(params string[] headers) } /// - ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken) { // No vary by header? if (_headers.Count == 0) { - context.CachedVaryByRules.Headers = _headers; + context.CacheVaryByRules.Headers = _headers; return ValueTask.CompletedTask; } - context.CachedVaryByRules.Headers = StringValues.Concat(context.CachedVaryByRules.Headers, _headers); + context.CacheVaryByRules.Headers = StringValues.Concat(context.CacheVaryByRules.Headers, _headers); return ValueTask.CompletedTask; } /// - ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellationToken) { return ValueTask.CompletedTask; } /// - ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context, CancellationToken cancellationToken) { return ValueTask.CompletedTask; } diff --git a/src/Middleware/OutputCaching/src/Policies/VaryByQueryPolicy.cs b/src/Middleware/OutputCaching/src/Policies/VaryByQueryPolicy.cs index f6053a4eb35c..e415459644ba 100644 --- a/src/Middleware/OutputCaching/src/Policies/VaryByQueryPolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/VaryByQueryPolicy.cs @@ -37,35 +37,35 @@ public VaryByQueryPolicy(params string[] queryKeys) } /// - ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken) { // No vary by query? if (_queryKeys.Count == 0) { - context.CachedVaryByRules.QueryKeys = _queryKeys; + context.CacheVaryByRules.QueryKeys = _queryKeys; return ValueTask.CompletedTask; } // If the current key is "*" (default) replace it - if (context.CachedVaryByRules.QueryKeys.Count == 1 && string.Equals(context.CachedVaryByRules.QueryKeys[0], "*", StringComparison.Ordinal)) + if (context.CacheVaryByRules.QueryKeys.Count == 1 && string.Equals(context.CacheVaryByRules.QueryKeys[0], "*", StringComparison.Ordinal)) { - context.CachedVaryByRules.QueryKeys = _queryKeys; + context.CacheVaryByRules.QueryKeys = _queryKeys; return ValueTask.CompletedTask; } - context.CachedVaryByRules.QueryKeys = StringValues.Concat(context.CachedVaryByRules.QueryKeys, _queryKeys); + context.CacheVaryByRules.QueryKeys = StringValues.Concat(context.CacheVaryByRules.QueryKeys, _queryKeys); return ValueTask.CompletedTask; } /// - ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellationToken) { return ValueTask.CompletedTask; } /// - ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context, CancellationToken cancellationToken) { return ValueTask.CompletedTask; } diff --git a/src/Middleware/OutputCaching/src/Policies/VaryByValuePolicy.cs b/src/Middleware/OutputCaching/src/Policies/VaryByValuePolicy.cs index 58eb99e49420..5de5366307a6 100644 --- a/src/Middleware/OutputCaching/src/Policies/VaryByValuePolicy.cs +++ b/src/Middleware/OutputCaching/src/Policies/VaryByValuePolicy.cs @@ -10,8 +10,8 @@ namespace Microsoft.AspNetCore.OutputCaching; /// internal sealed class VaryByValuePolicy : IOutputCachePolicy { - private readonly Action? _varyBy; - private readonly Func? _varyByAsync; + private readonly Action? _varyBy; + private readonly Func? _varyByAsync; /// /// Creates a policy that doesn't vary the cached content based on values. @@ -61,21 +61,21 @@ public VaryByValuePolicy(Func - ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken) { - _varyBy?.Invoke(context.HttpContext, context.CachedVaryByRules); + _varyBy?.Invoke(context.HttpContext, context.CacheVaryByRules); - return _varyByAsync?.Invoke(context.HttpContext, context.CachedVaryByRules, context.HttpContext.RequestAborted) ?? ValueTask.CompletedTask; + return _varyByAsync?.Invoke(context.HttpContext, context.CacheVaryByRules, context.HttpContext.RequestAborted) ?? ValueTask.CompletedTask; } /// - ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellationToken) { return ValueTask.CompletedTask; } /// - ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context) + ValueTask IOutputCachePolicy.ServeResponseAsync(OutputCacheContext context, CancellationToken cancellationToken) { return ValueTask.CompletedTask; } diff --git a/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt b/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt index 1bd7e92e8e66..779e48f3530a 100644 --- a/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt +++ b/src/Middleware/OutputCaching/src/PublicAPI.Unshipped.txt @@ -1,19 +1,19 @@ #nullable enable Microsoft.AspNetCore.Builder.OutputCacheApplicationBuilderExtensions -Microsoft.AspNetCore.OutputCaching.CachedVaryByRules -Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.CachedVaryByRules() -> void -Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.Headers.get -> Microsoft.Extensions.Primitives.StringValues -Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.Headers.set -> void -Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.QueryKeys.get -> Microsoft.Extensions.Primitives.StringValues -Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.QueryKeys.set -> void -Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.VaryByCustom.get -> System.Collections.Generic.IDictionary! -Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.VaryByPrefix.get -> Microsoft.Extensions.Primitives.StringValues -Microsoft.AspNetCore.OutputCaching.CachedVaryByRules.VaryByPrefix.set -> void +Microsoft.AspNetCore.OutputCaching.CacheVaryByRules +Microsoft.AspNetCore.OutputCaching.CacheVaryByRules.CacheVaryByRules() -> void +Microsoft.AspNetCore.OutputCaching.CacheVaryByRules.Headers.get -> Microsoft.Extensions.Primitives.StringValues +Microsoft.AspNetCore.OutputCaching.CacheVaryByRules.Headers.set -> void +Microsoft.AspNetCore.OutputCaching.CacheVaryByRules.QueryKeys.get -> Microsoft.Extensions.Primitives.StringValues +Microsoft.AspNetCore.OutputCaching.CacheVaryByRules.QueryKeys.set -> void +Microsoft.AspNetCore.OutputCaching.CacheVaryByRules.VaryByCustom.get -> System.Collections.Generic.IDictionary! +Microsoft.AspNetCore.OutputCaching.CacheVaryByRules.VaryByPrefix.get -> Microsoft.Extensions.Primitives.StringValues +Microsoft.AspNetCore.OutputCaching.CacheVaryByRules.VaryByPrefix.set -> void Microsoft.AspNetCore.OutputCaching.IOutputCacheFeature Microsoft.AspNetCore.OutputCaching.IOutputCacheFeature.Context.get -> Microsoft.AspNetCore.OutputCaching.OutputCacheContext! -Microsoft.AspNetCore.OutputCaching.IOutputCachePolicy.CacheRequestAsync(Microsoft.AspNetCore.OutputCaching.OutputCacheContext! context) -> System.Threading.Tasks.ValueTask -Microsoft.AspNetCore.OutputCaching.IOutputCachePolicy.ServeFromCacheAsync(Microsoft.AspNetCore.OutputCaching.OutputCacheContext! context) -> System.Threading.Tasks.ValueTask -Microsoft.AspNetCore.OutputCaching.IOutputCachePolicy.ServeResponseAsync(Microsoft.AspNetCore.OutputCaching.OutputCacheContext! context) -> System.Threading.Tasks.ValueTask +Microsoft.AspNetCore.OutputCaching.IOutputCachePolicy.CacheRequestAsync(Microsoft.AspNetCore.OutputCaching.OutputCacheContext! context, System.Threading.CancellationToken cancellation) -> System.Threading.Tasks.ValueTask +Microsoft.AspNetCore.OutputCaching.IOutputCachePolicy.ServeFromCacheAsync(Microsoft.AspNetCore.OutputCaching.OutputCacheContext! context, System.Threading.CancellationToken cancellation) -> System.Threading.Tasks.ValueTask +Microsoft.AspNetCore.OutputCaching.IOutputCachePolicy.ServeResponseAsync(Microsoft.AspNetCore.OutputCaching.OutputCacheContext! context, System.Threading.CancellationToken cancellation) -> System.Threading.Tasks.ValueTask Microsoft.AspNetCore.OutputCaching.IOutputCacheStore Microsoft.AspNetCore.OutputCaching.IOutputCacheStore.EvictByTagAsync(string! tag, System.Threading.CancellationToken cancellationToken) -> System.Threading.Tasks.ValueTask Microsoft.AspNetCore.OutputCaching.IOutputCachePolicy @@ -22,8 +22,8 @@ Microsoft.AspNetCore.OutputCaching.IOutputCacheStore.SetAsync(string! key, byte[ Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.Duration.get -> int Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.Duration.init -> void -Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.NoCache.get -> bool -Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.NoCache.init -> void +Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.NoStore.get -> bool +Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.NoStore.init -> void Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.OutputCacheAttribute() -> void Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.PolicyName.get -> string? Microsoft.AspNetCore.OutputCaching.OutputCacheAttribute.PolicyName.init -> void @@ -45,7 +45,6 @@ Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Clear() -> Microsoft Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Expire(System.TimeSpan expiration) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.NoCache() -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.OutputCachePolicyBuilder() -> void -Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Policy(string! profileName) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.Tag(params string![]! tags) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByHeader(params string![]! headers) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder.VaryByQuery(params string![]! queryKeys) -> Microsoft.AspNetCore.OutputCaching.OutputCachePolicyBuilder! @@ -81,10 +80,11 @@ Microsoft.Extensions.DependencyInjection.OutputCacheServiceCollectionExtensions static Microsoft.AspNetCore.Builder.OutputCacheApplicationBuilderExtensions.UseOutputCache(this Microsoft.AspNetCore.Builder.IApplicationBuilder! app) -> Microsoft.AspNetCore.Builder.IApplicationBuilder! static Microsoft.Extensions.DependencyInjection.OutputCacheConventionBuilderExtensions.CacheOutput(this TBuilder builder) -> TBuilder static Microsoft.Extensions.DependencyInjection.OutputCacheConventionBuilderExtensions.CacheOutput(this TBuilder builder, Microsoft.AspNetCore.OutputCaching.IOutputCachePolicy! policy) -> TBuilder +static Microsoft.Extensions.DependencyInjection.OutputCacheConventionBuilderExtensions.CacheOutput(this TBuilder builder, string! policyName) -> TBuilder static Microsoft.Extensions.DependencyInjection.OutputCacheConventionBuilderExtensions.CacheOutput(this TBuilder builder, System.Action! policy) -> TBuilder static Microsoft.Extensions.DependencyInjection.OutputCacheServiceCollectionExtensions.AddOutputCache(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! static Microsoft.Extensions.DependencyInjection.OutputCacheServiceCollectionExtensions.AddOutputCache(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action! configureOptions) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! -~Microsoft.AspNetCore.OutputCaching.OutputCacheContext.CachedVaryByRules.get -> Microsoft.AspNetCore.OutputCaching.CachedVaryByRules -~Microsoft.AspNetCore.OutputCaching.OutputCacheContext.CachedVaryByRules.set -> void -~Microsoft.AspNetCore.OutputCaching.OutputCacheContext.HttpContext.get -> Microsoft.AspNetCore.Http.HttpContext -~Microsoft.AspNetCore.OutputCaching.OutputCacheContext.Tags.get -> System.Collections.Generic.HashSet +Microsoft.AspNetCore.OutputCaching.OutputCacheContext.CacheVaryByRules.get -> Microsoft.AspNetCore.OutputCaching.CacheVaryByRules! +Microsoft.AspNetCore.OutputCaching.OutputCacheContext.CacheVaryByRules.set -> void +Microsoft.AspNetCore.OutputCaching.OutputCacheContext.HttpContext.get -> Microsoft.AspNetCore.Http.HttpContext! +Microsoft.AspNetCore.OutputCaching.OutputCacheContext.Tags.get -> System.Collections.Generic.HashSet! diff --git a/src/Middleware/OutputCaching/src/Serialization/FormatterEntry.cs b/src/Middleware/OutputCaching/src/Serialization/FormatterEntry.cs index f3fc56f4bc63..f3aadfbadd6b 100644 --- a/src/Middleware/OutputCaching/src/Serialization/FormatterEntry.cs +++ b/src/Middleware/OutputCaching/src/Serialization/FormatterEntry.cs @@ -6,7 +6,7 @@ internal sealed class FormatterEntry { public DateTimeOffset Created { get; set; } public int StatusCode { get; set; } - public Dictionary Headers { get; set; } = new(); - public List Body { get; set; } = new(); - public string[]? Tags { get; set; } + public Dictionary Headers { get; set; } = default!; + public List Body { get; set; } = default!; + public string[] Tags { get; set; } = default!; } diff --git a/src/Middleware/OutputCaching/test/OutputCacheEntryFormatterTests.cs b/src/Middleware/OutputCaching/test/OutputCacheEntryFormatterTests.cs new file mode 100644 index 000000000000..e6610141b285 --- /dev/null +++ b/src/Middleware/OutputCaching/test/OutputCacheEntryFormatterTests.cs @@ -0,0 +1,66 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Net.Http; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.OutputCaching.Memory; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.Caching.Memory; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.OutputCaching.Tests; + +public class OutputCacheEntryFormatterTests +{ + [Fact] + public async Task StoreAndGet_StoresEmptyValues() + { + var store = new TestOutputCache(); + var key = "abc"; + var entry = new OutputCacheEntry() + { + Body = new CachedResponseBody(new List(), 0), + Headers = new HeaderDictionary(), + Tags = Array.Empty() + }; + + await OutputCacheEntryFormatter.StoreAsync(key, entry, TimeSpan.Zero, store, default); + + var result = await OutputCacheEntryFormatter.GetAsync(key, store, default); + + AssertEntriesAreSame(entry, result); + } + + [Fact] + public async Task StoreAndGet_StoresAllValues() + { + var store = new TestOutputCache(); + var key = "abc"; + var entry = new OutputCacheEntry() + { + Body = new CachedResponseBody(new List() { "lorem"u8, "ipsum"u8 }, 10), + Created = DateTimeOffset.UtcNow, + Headers = new HeaderDictionary { [HeaderNames.Accept] = "text/plain", [HeaderNames.AcceptCharset] = "utf8" }, + StatusCode = StatusCodes.Status201Created, + Tags = new[] { "tag1", "tag2" } + }; + + await OutputCacheEntryFormatter.StoreAsync(key, entry, TimeSpan.Zero, store, default); + + var result = await OutputCacheEntryFormatter.GetAsync(key, store, default); + + AssertEntriesAreSame(entry, result); + } + + private static void AssertEntriesAreSame(OutputCacheEntry expected, OutputCacheEntry actual) + { + Assert.NotNull(expected); + Assert.NotNull(actual); + Assert.Equal(expected.Tags, actual.Tags); + Assert.Equal(expected.Created, actual.Created); + Assert.Equal(expected.StatusCode, actual.StatusCode); + Assert.Equal(expected.Headers, actual.Headers); + Assert.Equal(expected.Body.Length, actual.Body.Length); + Assert.Equal(expected.Body.Segments, actual.Body.Segments); + } +} diff --git a/src/Middleware/OutputCaching/test/OutputCacheKeyProviderTests.cs b/src/Middleware/OutputCaching/test/OutputCacheKeyProviderTests.cs index 8b7a3d2c5bd6..ddeb485d73a9 100644 --- a/src/Middleware/OutputCaching/test/OutputCacheKeyProviderTests.cs +++ b/src/Middleware/OutputCaching/test/OutputCacheKeyProviderTests.cs @@ -59,7 +59,7 @@ public void OutputCachingKeyProvider_CreateStorageKey_VaryByRulesIsotNull() var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); var context = TestUtils.CreateTestContext(); - Assert.NotNull(context.CachedVaryByRules); + Assert.NotNull(context.CacheVaryByRules); } [Fact] @@ -67,12 +67,12 @@ public void OutputCachingKeyProvider_CreateStorageKey_ReturnsCachedVaryByGuid_If { var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); var context = TestUtils.CreateTestContext(); - context.CachedVaryByRules = new CachedVaryByRules() + context.CacheVaryByRules = new CacheVaryByRules() { VaryByPrefix = Guid.NewGuid().ToString("n"), }; - Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}C{KeyDelimiter}{context.CachedVaryByRules.VaryByPrefix}", cacheKeyProvider.CreateStorageKey(context)); + Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}C{KeyDelimiter}{context.CacheVaryByRules.VaryByPrefix}", cacheKeyProvider.CreateStorageKey(context)); } [Fact] @@ -82,7 +82,7 @@ public void OutputCachingKeyProvider_CreateStorageVaryKey_IncludesListedHeadersO var context = TestUtils.CreateTestContext(); context.HttpContext.Request.Headers["HeaderA"] = "ValueA"; context.HttpContext.Request.Headers["HeaderB"] = "ValueB"; - context.CachedVaryByRules = new CachedVaryByRules() + context.CacheVaryByRules = new CacheVaryByRules() { Headers = new string[] { "HeaderA", "HeaderC" } }; @@ -98,7 +98,7 @@ public void OutputCachingKeyProvider_CreateStorageVaryKey_HeaderValuesAreSorted( var context = TestUtils.CreateTestContext(); context.HttpContext.Request.Headers["HeaderA"] = "ValueB"; context.HttpContext.Request.Headers.Append("HeaderA", "ValueA"); - context.CachedVaryByRules = new CachedVaryByRules() + context.CacheVaryByRules = new CacheVaryByRules() { Headers = new string[] { "HeaderA", "HeaderC" } }; @@ -113,13 +113,13 @@ public void OutputCachingKeyProvider_CreateStorageVaryKey_IncludesListedQueryKey var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); var context = TestUtils.CreateTestContext(); context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueA&QueryB=ValueB"); - context.CachedVaryByRules = new CachedVaryByRules() + context.CacheVaryByRules = new CacheVaryByRules() { VaryByPrefix = Guid.NewGuid().ToString("n"), QueryKeys = new string[] { "QueryA", "QueryC" } }; - Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}C{KeyDelimiter}{context.CachedVaryByRules.VaryByPrefix}{KeyDelimiter}Q{KeyDelimiter}QueryA=ValueA{KeyDelimiter}QueryC=", + Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}C{KeyDelimiter}{context.CacheVaryByRules.VaryByPrefix}{KeyDelimiter}Q{KeyDelimiter}QueryA=ValueA{KeyDelimiter}QueryC=", cacheKeyProvider.CreateStorageKey(context)); } @@ -129,13 +129,13 @@ public void OutputCachingKeyProvider_CreateStorageVaryKey_IncludesQueryKeys_Quer var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); var context = TestUtils.CreateTestContext(); context.HttpContext.Request.QueryString = new QueryString("?queryA=ValueA&queryB=ValueB"); - context.CachedVaryByRules = new CachedVaryByRules() + context.CacheVaryByRules = new CacheVaryByRules() { VaryByPrefix = Guid.NewGuid().ToString("n"), QueryKeys = new string[] { "QueryA", "QueryC" } }; - Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}C{KeyDelimiter}{context.CachedVaryByRules.VaryByPrefix}{KeyDelimiter}Q{KeyDelimiter}QueryA=ValueA{KeyDelimiter}QueryC=", + Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}C{KeyDelimiter}{context.CacheVaryByRules.VaryByPrefix}{KeyDelimiter}Q{KeyDelimiter}QueryA=ValueA{KeyDelimiter}QueryC=", cacheKeyProvider.CreateStorageKey(context)); } @@ -145,7 +145,7 @@ public void OutputCachingKeyProvider_CreateStorageVaryKey_IncludesAllQueryKeysGi var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); var context = TestUtils.CreateTestContext(); context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueA&QueryB=ValueB"); - context.CachedVaryByRules = new CachedVaryByRules() + context.CacheVaryByRules = new CacheVaryByRules() { VaryByPrefix = Guid.NewGuid().ToString("n"), QueryKeys = new string[] { "*" } @@ -153,7 +153,7 @@ public void OutputCachingKeyProvider_CreateStorageVaryKey_IncludesAllQueryKeysGi // To support case insensitivity, all query keys are converted to upper case. // Explicit query keys uses the casing specified in the setting. - Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}C{KeyDelimiter}{context.CachedVaryByRules.VaryByPrefix}{KeyDelimiter}Q{KeyDelimiter}QUERYA=ValueA{KeyDelimiter}QUERYB=ValueB", + Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}C{KeyDelimiter}{context.CacheVaryByRules.VaryByPrefix}{KeyDelimiter}Q{KeyDelimiter}QUERYA=ValueA{KeyDelimiter}QUERYB=ValueB", cacheKeyProvider.CreateStorageKey(context)); } @@ -163,7 +163,7 @@ public void OutputCachingKeyProvider_CreateStorageVaryKey_QueryKeysValuesNotCons var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); var context = TestUtils.CreateTestContext(); context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueA&QueryA=ValueB"); - context.CachedVaryByRules = new CachedVaryByRules() + context.CacheVaryByRules = new CacheVaryByRules() { VaryByPrefix = Guid.NewGuid().ToString("n"), QueryKeys = new string[] { "*" } @@ -171,7 +171,7 @@ public void OutputCachingKeyProvider_CreateStorageVaryKey_QueryKeysValuesNotCons // To support case insensitivity, all query keys are converted to upper case. // Explicit query keys uses the casing specified in the setting. - Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}C{KeyDelimiter}{context.CachedVaryByRules.VaryByPrefix}{KeyDelimiter}Q{KeyDelimiter}QUERYA=ValueA{KeySubDelimiter}ValueB", + Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}C{KeyDelimiter}{context.CacheVaryByRules.VaryByPrefix}{KeyDelimiter}Q{KeyDelimiter}QUERYA=ValueA{KeySubDelimiter}ValueB", cacheKeyProvider.CreateStorageKey(context)); } @@ -181,7 +181,7 @@ public void OutputCachingKeyProvider_CreateStorageVaryKey_QueryKeysValuesAreSort var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); var context = TestUtils.CreateTestContext(); context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueB&QueryA=ValueA"); - context.CachedVaryByRules = new CachedVaryByRules() + context.CacheVaryByRules = new CacheVaryByRules() { VaryByPrefix = Guid.NewGuid().ToString("n"), QueryKeys = new string[] { "*" } @@ -189,7 +189,7 @@ public void OutputCachingKeyProvider_CreateStorageVaryKey_QueryKeysValuesAreSort // To support case insensitivity, all query keys are converted to upper case. // Explicit query keys uses the casing specified in the setting. - Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}C{KeyDelimiter}{context.CachedVaryByRules.VaryByPrefix}{KeyDelimiter}Q{KeyDelimiter}QUERYA=ValueA{KeySubDelimiter}ValueB", + Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}C{KeyDelimiter}{context.CacheVaryByRules.VaryByPrefix}{KeyDelimiter}Q{KeyDelimiter}QUERYA=ValueA{KeySubDelimiter}ValueB", cacheKeyProvider.CreateStorageKey(context)); } @@ -201,14 +201,14 @@ public void OutputCachingKeyProvider_CreateStorageVaryKey_IncludesListedHeadersA context.HttpContext.Request.Headers["HeaderA"] = "ValueA"; context.HttpContext.Request.Headers["HeaderB"] = "ValueB"; context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueA&QueryB=ValueB"); - context.CachedVaryByRules = new CachedVaryByRules() + context.CacheVaryByRules = new CacheVaryByRules() { VaryByPrefix = Guid.NewGuid().ToString("n"), Headers = new string[] { "HeaderA", "HeaderC" }, QueryKeys = new string[] { "QueryA", "QueryC" } }; - Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}C{KeyDelimiter}{context.CachedVaryByRules.VaryByPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC={KeyDelimiter}Q{KeyDelimiter}QueryA=ValueA{KeyDelimiter}QueryC=", + Assert.Equal($"{KeyDelimiter}{KeyDelimiter}{KeyDelimiter}C{KeyDelimiter}{context.CacheVaryByRules.VaryByPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC={KeyDelimiter}Q{KeyDelimiter}QueryA=ValueA{KeyDelimiter}QueryC=", cacheKeyProvider.CreateStorageKey(context)); } } diff --git a/src/Middleware/OutputCaching/test/OutputCacheMiddlewareTests.cs b/src/Middleware/OutputCaching/test/OutputCacheMiddlewareTests.cs index 0a71b50ea5f9..690031b2a78e 100644 --- a/src/Middleware/OutputCaching/test/OutputCacheMiddlewareTests.cs +++ b/src/Middleware/OutputCaching/test/OutputCacheMiddlewareTests.cs @@ -121,7 +121,8 @@ public async Task TryServeFromCacheAsync_CachedResponseFound_Serves304IfPossible await OutputCacheEntryFormatter.StoreAsync("BaseKey", new OutputCacheEntry() { - Body = new CachedResponseBody(new List(0), 0) + Body = new CachedResponseBody(new List(0), 0), + Headers = new() }, TimeSpan.Zero, cache, @@ -488,7 +489,7 @@ public void FinalizeCacheHeadersAsync_UpdateCachedVaryByRules_NullOrEmptyRules(S context.HttpContext.Response.Headers.Vary = vary; context.HttpContext.Features.Set(new OutputCacheFeature(context)); - context.CachedVaryByRules.QueryKeys = vary; + context.CacheVaryByRules.QueryKeys = vary; middleware.FinalizeCacheHeaders(context); @@ -579,7 +580,7 @@ public async Task FinalizeCacheBody_Cache_IfContentLengthMatches() await context.HttpContext.Response.WriteAsync(new string('0', 20)); - context.CachedResponse = new OutputCacheEntry(); + context.CachedResponse = new OutputCacheEntry { Headers = new() }; context.CacheKey = "BaseKey"; context.CachedResponseValidFor = TimeSpan.FromSeconds(10); @@ -639,7 +640,7 @@ public async Task FinalizeCacheBody_RequestHead_Cache_IfContentLengthPresent_And await context.HttpContext.Response.WriteAsync(new string('0', 10)); } - context.CachedResponse = new OutputCacheEntry(); + context.CachedResponse = new OutputCacheEntry { Headers = new() }; context.CacheKey = "BaseKey"; context.CachedResponseValidFor = TimeSpan.FromSeconds(10); diff --git a/src/Middleware/OutputCaching/test/OutputCachePoliciesTests.cs b/src/Middleware/OutputCaching/test/OutputCachePoliciesTests.cs index c84e5cb93cc8..53acf44c2acc 100644 --- a/src/Middleware/OutputCaching/test/OutputCachePoliciesTests.cs +++ b/src/Middleware/OutputCaching/test/OutputCachePoliciesTests.cs @@ -1,13 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Net.Http; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.OutputCaching.Memory; using Microsoft.AspNetCore.OutputCaching.Policies; -using Microsoft.AspNetCore.TestHost; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Net.Http.Headers; namespace Microsoft.AspNetCore.OutputCaching.Tests; @@ -19,7 +13,7 @@ public async Task DefaultCachePolicy_EnablesCache() IOutputCachePolicy policy = DefaultPolicy.Instance; var context = TestUtils.CreateUninitializedContext(); - await policy.CacheRequestAsync(context); + await policy.CacheRequestAsync(context, default); Assert.True(context.EnableOutputCaching); } @@ -30,7 +24,7 @@ public async Task DefaultCachePolicy_AllowsLocking() IOutputCachePolicy policy = DefaultPolicy.Instance; var context = TestUtils.CreateUninitializedContext(); - await policy.CacheRequestAsync(context); + await policy.CacheRequestAsync(context, default); Assert.True(context.AllowLocking); } @@ -41,9 +35,9 @@ public async Task DefaultCachePolicy_VariesByStar() IOutputCachePolicy policy = DefaultPolicy.Instance; var context = TestUtils.CreateUninitializedContext(); - await policy.CacheRequestAsync(context); + await policy.CacheRequestAsync(context, default); - Assert.Equal("*", context.CachedVaryByRules.QueryKeys); + Assert.Equal("*", context.CacheVaryByRules.QueryKeys); } [Fact] @@ -53,7 +47,7 @@ public async Task EnableCachePolicy_DisablesCache() var context = TestUtils.CreateUninitializedContext(); context.EnableOutputCaching = true; - await policy.CacheRequestAsync(context); + await policy.CacheRequestAsync(context, default); Assert.False(context.EnableOutputCaching); } @@ -65,7 +59,7 @@ public async Task ExpirationPolicy_SetsResponseExpirationTimeSpan() IOutputCachePolicy policy = new ExpirationPolicy(duration); var context = TestUtils.CreateUninitializedContext(); - await policy.CacheRequestAsync(context); + await policy.CacheRequestAsync(context, default); Assert.Equal(duration, context.ResponseExpirationTimeSpan); } @@ -76,7 +70,7 @@ public async Task LockingPolicy_EnablesLocking() IOutputCachePolicy policy = LockingPolicy.Enabled; var context = TestUtils.CreateUninitializedContext(); - await policy.CacheRequestAsync(context); + await policy.CacheRequestAsync(context, default); Assert.True(context.AllowLocking); } @@ -87,7 +81,7 @@ public async Task LockingPolicy_DisablesLocking() IOutputCachePolicy policy = LockingPolicy.Disabled; var context = TestUtils.CreateUninitializedContext(); - await policy.CacheRequestAsync(context); + await policy.CacheRequestAsync(context, default); Assert.False(context.AllowLocking); } @@ -98,7 +92,7 @@ public async Task NoLookupPolicy_DisablesLookup() IOutputCachePolicy policy = NoLookupPolicy.Instance; var context = TestUtils.CreateUninitializedContext(); - await policy.CacheRequestAsync(context); + await policy.CacheRequestAsync(context, default); Assert.False(context.AllowCacheLookup); } @@ -109,7 +103,7 @@ public async Task NoStorePolicy_DisablesStore() IOutputCachePolicy policy = NoStorePolicy.Instance; var context = TestUtils.CreateUninitializedContext(); - await policy.CacheRequestAsync(context); + await policy.CacheRequestAsync(context, default); Assert.False(context.AllowCacheStorage); } @@ -124,7 +118,7 @@ public async Task PredicatePolicy_Filters(bool filter, bool enabled, bool expect IOutputCachePolicy predicate = new PredicatePolicy(c => ValueTask.FromResult(filter), enabled ? EnableCachePolicy.Enabled : EnableCachePolicy.Disabled); var context = TestUtils.CreateUninitializedContext(); - await predicate.CacheRequestAsync(context); + await predicate.CacheRequestAsync(context, default); Assert.Equal(expected, context.EnableOutputCaching); } @@ -136,15 +130,15 @@ public async Task ProfilePolicy_UsesNamedProfile() context.Options.AddPolicy("enabled", EnableCachePolicy.Enabled); context.Options.AddPolicy("disabled", EnableCachePolicy.Disabled); - IOutputCachePolicy policy = new ProfilePolicy("enabled"); + IOutputCachePolicy policy = new NamedPolicy("enabled"); - await policy.CacheRequestAsync(context); + await policy.CacheRequestAsync(context, default); Assert.True(context.EnableOutputCaching); - policy = new ProfilePolicy("disabled"); + policy = new NamedPolicy("disabled"); - await policy.CacheRequestAsync(context); + await policy.CacheRequestAsync(context, default); Assert.False(context.EnableOutputCaching); } @@ -156,7 +150,7 @@ public async Task TagsPolicy_Tags() IOutputCachePolicy policy = new TagsPolicy("tag1", "tag2"); - await policy.CacheRequestAsync(context); + await policy.CacheRequestAsync(context, default); Assert.Contains("tag1", context.Tags); Assert.Contains("tag2", context.Tags); @@ -169,9 +163,9 @@ public async Task VaryByHeadersPolicy_IsEmpty() IOutputCachePolicy policy = new VaryByHeaderPolicy(); - await policy.CacheRequestAsync(context); + await policy.CacheRequestAsync(context, default); - Assert.Empty(context.CachedVaryByRules.Headers); + Assert.Empty(context.CacheVaryByRules.Headers); } [Fact] @@ -182,9 +176,9 @@ public async Task VaryByHeadersPolicy_AddsSingleHeader() IOutputCachePolicy policy = new VaryByHeaderPolicy(header); - await policy.CacheRequestAsync(context); + await policy.CacheRequestAsync(context, default); - Assert.Equal(header, context.CachedVaryByRules.Headers); + Assert.Equal(header, context.CacheVaryByRules.Headers); } [Fact] @@ -195,9 +189,9 @@ public async Task VaryByHeadersPolicy_AddsMultipleHeaders() IOutputCachePolicy policy = new VaryByHeaderPolicy(headers); - await policy.CacheRequestAsync(context); + await policy.CacheRequestAsync(context, default); - Assert.Equal(headers, context.CachedVaryByRules.Headers); + Assert.Equal(headers, context.CacheVaryByRules.Headers); } [Fact] @@ -207,9 +201,9 @@ public async Task VaryByQueryPolicy_IsEmpty() IOutputCachePolicy policy = new VaryByQueryPolicy(); - await policy.CacheRequestAsync(context); + await policy.CacheRequestAsync(context, default); - Assert.Empty(context.CachedVaryByRules.QueryKeys); + Assert.Empty(context.CacheVaryByRules.QueryKeys); } [Fact] @@ -220,9 +214,9 @@ public async Task VaryByQueryPolicy_AddsSingleHeader() IOutputCachePolicy policy = new VaryByQueryPolicy(query); - await policy.CacheRequestAsync(context); + await policy.CacheRequestAsync(context, default); - Assert.Equal(query, context.CachedVaryByRules.QueryKeys); + Assert.Equal(query, context.CacheVaryByRules.QueryKeys); } [Fact] @@ -233,9 +227,9 @@ public async Task VaryByQueryPolicy_AddsMultipleHeaders() IOutputCachePolicy policy = new VaryByQueryPolicy(queries); - await policy.CacheRequestAsync(context); + await policy.CacheRequestAsync(context, default); - Assert.Equal(queries, context.CachedVaryByRules.QueryKeys); + Assert.Equal(queries, context.CacheVaryByRules.QueryKeys); } [Fact] @@ -246,9 +240,9 @@ public async Task VaryByValuePolicy_SingleValue() IOutputCachePolicy policy = new VaryByValuePolicy(context => value); - await policy.CacheRequestAsync(context); + await policy.CacheRequestAsync(context, default); - Assert.Equal(value, context.CachedVaryByRules.VaryByPrefix); + Assert.Equal(value, context.CacheVaryByRules.VaryByPrefix); } [Fact] @@ -259,9 +253,9 @@ public async Task VaryByValuePolicy_SingleValueAsync() IOutputCachePolicy policy = new VaryByValuePolicy((context, token) => ValueTask.FromResult(value)); - await policy.CacheRequestAsync(context); + await policy.CacheRequestAsync(context, default); - Assert.Equal(value, context.CachedVaryByRules.VaryByPrefix); + Assert.Equal(value, context.CacheVaryByRules.VaryByPrefix); } [Fact] @@ -273,9 +267,9 @@ public async Task VaryByValuePolicy_KeyValuePair() IOutputCachePolicy policy = new VaryByValuePolicy(context => new KeyValuePair(key, value)); - await policy.CacheRequestAsync(context); + await policy.CacheRequestAsync(context, default); - Assert.Contains(new KeyValuePair(key, value), context.CachedVaryByRules.VaryByCustom); + Assert.Contains(new KeyValuePair(key, value), context.CacheVaryByRules.VaryByCustom); } [Fact] @@ -287,8 +281,8 @@ public async Task VaryByValuePolicy_KeyValuePairAsync() IOutputCachePolicy policy = new VaryByValuePolicy((context, token) => ValueTask.FromResult(new KeyValuePair(key, value))); - await policy.CacheRequestAsync(context); + await policy.CacheRequestAsync(context, default); - Assert.Contains(new KeyValuePair(key, value), context.CachedVaryByRules.VaryByCustom); + Assert.Contains(new KeyValuePair(key, value), context.CacheVaryByRules.VaryByCustom); } } diff --git a/src/Middleware/OutputCaching/test/OutputCachePolicyProviderTests.cs b/src/Middleware/OutputCaching/test/OutputCachePolicyProviderTests.cs index 64d8b62f8085..6238b09791aa 100644 --- a/src/Middleware/OutputCaching/test/OutputCachePolicyProviderTests.cs +++ b/src/Middleware/OutputCaching/test/OutputCachePolicyProviderTests.cs @@ -52,7 +52,7 @@ public async Task AttemptOutputCaching_CacheableMethods_IsAllowed(string method) foreach (var policy in policies) { - await policy.CacheRequestAsync(context); + await policy.CacheRequestAsync(context, default); } Assert.True(context.AllowCacheStorage); @@ -69,7 +69,7 @@ public async Task AttemptOutputCaching_UncacheableMethods_NotAllowed(string meth var policy = new OutputCachePolicyBuilder().Build(); context.HttpContext.Request.Method = method; - await policy.CacheRequestAsync(context); + await policy.CacheRequestAsync(context, default); Assert.False(context.AllowCacheLookup); Assert.False(context.AllowCacheStorage); @@ -88,7 +88,7 @@ public async Task AttemptResponseCaching_AuthorizationHeaders_NotAllowed() var policy = new OutputCachePolicyBuilder().Build(); - await policy.CacheRequestAsync(context); + await policy.CacheRequestAsync(context, default); Assert.False(context.AllowCacheStorage); Assert.False(context.AllowCacheLookup); @@ -110,7 +110,7 @@ public async Task AllowCacheStorage_NoStore_IsAllowed() }.ToString(); var policy = new OutputCachePolicyBuilder().Build(); - await policy.CacheRequestAsync(context); + await policy.CacheRequestAsync(context, default); Assert.True(context.AllowCacheStorage); Assert.Empty(sink.Writes); @@ -126,7 +126,7 @@ public async Task AllowCacheLookup_LegacyDirectives_OverridenByCacheControl() context.HttpContext.Request.Headers.CacheControl = "max-age=10"; var policy = new OutputCachePolicyBuilder().Build(); - await policy.CacheRequestAsync(context); + await policy.CacheRequestAsync(context, default); Assert.True(context.AllowCacheLookup); Assert.Empty(sink.Writes); @@ -139,7 +139,7 @@ public async Task IsResponseCacheable_NoPublic_IsAllowed() var context = TestUtils.CreateTestContext(sink); var policy = new OutputCachePolicyBuilder().Build(); - await policy.ServeResponseAsync(context); + await policy.ServeResponseAsync(context, default); Assert.True(context.AllowCacheStorage); Assert.True(context.AllowCacheLookup); @@ -157,7 +157,7 @@ public async Task IsResponseCacheable_Public_IsAllowed() }.ToString(); var policy = new OutputCachePolicyBuilder().Build(); - await policy.ServeResponseAsync(context); + await policy.ServeResponseAsync(context, default); Assert.True(context.AllowCacheStorage); Assert.True(context.AllowCacheLookup); @@ -175,7 +175,7 @@ public async Task IsResponseCacheable_NoCache_IsAllowed() }.ToString(); var policy = new OutputCachePolicyBuilder().Build(); - await policy.ServeResponseAsync(context); + await policy.ServeResponseAsync(context, default); Assert.True(context.AllowCacheStorage); Assert.True(context.AllowCacheLookup); @@ -193,7 +193,7 @@ public async Task IsResponseCacheable_ResponseNoStore_IsAllowed() }.ToString(); var policy = new OutputCachePolicyBuilder().Build(); - await policy.ServeResponseAsync(context); + await policy.ServeResponseAsync(context, default); Assert.True(context.AllowCacheStorage); Assert.True(context.AllowCacheLookup); @@ -208,7 +208,7 @@ public async Task IsResponseCacheable_SetCookieHeader_NotAllowed() context.HttpContext.Response.Headers.SetCookie = "cookieName=cookieValue"; var policy = new OutputCachePolicyBuilder().Build(); - await policy.ServeResponseAsync(context); + await policy.ServeResponseAsync(context, default); Assert.False(context.AllowCacheStorage); Assert.True(context.AllowCacheLookup); @@ -224,7 +224,7 @@ public async Task IsResponseCacheable_VaryHeaderByStar_IsAllowed() var context = TestUtils.CreateTestContext(sink); context.HttpContext.Response.Headers.Vary = "*"; var policy = new OutputCachePolicyBuilder().Build(); - await policy.ServeResponseAsync(context); + await policy.ServeResponseAsync(context, default); Assert.True(context.AllowCacheStorage); Assert.True(context.AllowCacheLookup); @@ -242,7 +242,7 @@ public async Task IsResponseCacheable_Private_IsAllowed() }.ToString(); var policy = new OutputCachePolicyBuilder().Build(); - await policy.ServeResponseAsync(context); + await policy.ServeResponseAsync(context, default); Assert.True(context.AllowCacheStorage); Assert.True(context.AllowCacheLookup); @@ -258,7 +258,7 @@ public async Task IsResponseCacheable_SuccessStatusCodes_IsAllowed(int statusCod context.HttpContext.Response.StatusCode = statusCode; var policy = new OutputCachePolicyBuilder().Build(); - await policy.ServeResponseAsync(context); + await policy.ServeResponseAsync(context, default); Assert.True(context.AllowCacheStorage); Assert.True(context.AllowCacheLookup); @@ -334,7 +334,7 @@ public async Task IsResponseCacheable_NonSuccessStatusCodes_NotAllowed(int statu context.HttpContext.Response.StatusCode = statusCode; var policy = new OutputCachePolicyBuilder().Build(); - await policy.ServeResponseAsync(context); + await policy.ServeResponseAsync(context, default); Assert.True(context.AllowCacheLookup); Assert.False(context.AllowCacheStorage); @@ -355,7 +355,7 @@ public async Task IsResponseCacheable_NoExpiryRequirements_IsAllowed() context.ResponseTime = DateTimeOffset.MaxValue; var policy = new OutputCachePolicyBuilder().Build(); - await policy.ServeResponseAsync(context); + await policy.ServeResponseAsync(context, default); Assert.True(context.AllowCacheStorage); Assert.True(context.AllowCacheLookup); @@ -378,7 +378,7 @@ public async Task IsResponseCacheable_MaxAgeOverridesExpiry_IsAllowed() context.ResponseTime = utcNow + TimeSpan.FromSeconds(9); var policy = new OutputCachePolicyBuilder().Build(); - await policy.ServeResponseAsync(context); + await policy.ServeResponseAsync(context, default); Assert.True(context.AllowCacheStorage); Assert.True(context.AllowCacheLookup); @@ -401,7 +401,7 @@ public async Task IsResponseCacheable_SharedMaxAgeOverridesMaxAge_IsAllowed() context.ResponseTime = utcNow + TimeSpan.FromSeconds(11); var policy = new OutputCachePolicyBuilder().Build(); - await policy.ServeResponseAsync(context); + await policy.ServeResponseAsync(context, default); Assert.True(context.AllowCacheStorage); Assert.True(context.AllowCacheLookup); diff --git a/src/Middleware/OutputCaching/test/TestUtils.cs b/src/Middleware/OutputCaching/test/TestUtils.cs index 99020db5d213..33ac6f56c869 100644 --- a/src/Middleware/OutputCaching/test/TestUtils.cs +++ b/src/Middleware/OutputCaching/test/TestUtils.cs @@ -355,19 +355,19 @@ internal class TestClock : ISystemClock internal class AllowTestPolicy : IOutputCachePolicy { - public ValueTask CacheRequestAsync(OutputCacheContext context) + public ValueTask CacheRequestAsync(OutputCacheContext context, CancellationToken cancellationToken) { context.AllowCacheLookup = true; context.AllowCacheStorage = true; return ValueTask.CompletedTask; } - public ValueTask ServeFromCacheAsync(OutputCacheContext context) + public ValueTask ServeFromCacheAsync(OutputCacheContext context, CancellationToken cancellationToken) { return ValueTask.CompletedTask; } - public ValueTask ServeResponseAsync(OutputCacheContext context) + public ValueTask ServeResponseAsync(OutputCacheContext context, CancellationToken cancellationToken) { return ValueTask.CompletedTask; } diff --git a/src/Mvc/Mvc.Core/src/Filters/OutputCacheFilter.cs b/src/Mvc/Mvc.Core/src/Filters/OutputCacheFilter.cs index 12e5ed96670c..08beb1bf295b 100644 --- a/src/Mvc/Mvc.Core/src/Filters/OutputCacheFilter.cs +++ b/src/Mvc/Mvc.Core/src/Filters/OutputCacheFilter.cs @@ -50,7 +50,7 @@ public void OnActionExecuted(ActionExecutedContext context) private static partial class Log { - [LoggerMessage(4, LogLevel.Debug, "Execution of filter {OverriddenFilter} is preempted by filter {OverridingFilter} which is the most effective filter implementing policy {FilterPolicy}.", EventName = "NotMostEffectiveFilter")] + [LoggerMessage(1, LogLevel.Debug, "Execution of filter {OverriddenFilter} is preempted by filter {OverridingFilter} which is the most effective filter implementing policy {FilterPolicy}.", EventName = "NotMostEffectiveFilter")] public static partial void NotMostEffectiveFilter(ILogger logger, Type overriddenFilter, Type overridingFilter, Type filterPolicy); } } From 0412a3f2b2cfd341590b200c01267dd7b43e2271 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Ros?= Date: Wed, 22 Jun 2022 16:50:14 -0700 Subject: [PATCH 48/48] Update submodule (#42357) --- src/submodules/googletest | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/submodules/googletest b/src/submodules/googletest index 5126f7166109..86add13493e5 160000 --- a/src/submodules/googletest +++ b/src/submodules/googletest @@ -1 +1 @@ -Subproject commit 5126f7166109666a9c0534021fb1a3038659494c +Subproject commit 86add13493e5c881d7e4ba77fb91c1f57752b3a4