├── .appveyor.yml ├── .gitattributes ├── .github └── ISSUE_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── .vsts-pipelines └── builds │ ├── ci-internal.yml │ └── ci-public.yml ├── CONTRIBUTING.md ├── Directory.Build.props ├── Directory.Build.targets ├── LICENSE.txt ├── NuGet.config ├── NuGetPackageVerifier.json ├── README.md ├── ResponseCaching.sln ├── build.cmd ├── build.sh ├── build ├── Key.snk ├── dependencies.props ├── repo.props └── sources.props ├── korebuild-lock.txt ├── korebuild.json ├── run.cmd ├── run.ps1 ├── run.sh ├── samples └── ResponseCachingSample │ ├── README.md │ ├── ResponseCachingSample.csproj │ └── Startup.cs ├── src ├── Directory.Build.props ├── Microsoft.AspNetCore.ResponseCaching.Abstractions │ ├── IResponseCachingFeature.cs │ ├── Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj │ └── baseline.netcore.json └── Microsoft.AspNetCore.ResponseCaching │ ├── Internal │ ├── CacheEntry │ │ ├── CacheEntryHelpers .cs │ │ ├── CachedResponse.cs │ │ └── CachedVaryByRules.cs │ ├── FastGuid.cs │ ├── ISystemClock.cs │ ├── Interfaces │ │ ├── IResponseCache.cs │ │ ├── IResponseCacheEntry.cs │ │ ├── IResponseCachingKeyProvider.cs │ │ └── IResponseCachingPolicyProvider.cs │ ├── LoggerExtensions.cs │ ├── MemoryCachedResponse.cs │ ├── MemoryResponseCache.cs │ ├── ResponseCachingContext.cs │ ├── ResponseCachingKeyProvider.cs │ ├── ResponseCachingPolicyProvider.cs │ ├── SendFileFeatureWrapper.cs │ ├── StringBuilderExtensions.cs │ └── SystemClock.cs │ ├── Microsoft.AspNetCore.ResponseCaching.csproj │ ├── Properties │ └── AssemblyInfo.cs │ ├── ResponseCachingExtensions.cs │ ├── ResponseCachingFeature.cs │ ├── ResponseCachingMiddleware.cs │ ├── ResponseCachingOptions.cs │ ├── ResponseCachingServicesExtensions.cs │ ├── Streams │ ├── ResponseCachingStream.cs │ ├── SegmentReadStream.cs │ ├── SegmentWriteStream.cs │ └── StreamUtilities.cs │ └── baseline.netcore.json ├── test ├── Directory.Build.props └── Microsoft.AspNetCore.ResponseCaching.Tests │ ├── Microsoft.AspNetCore.ResponseCaching.Tests.csproj │ ├── ResponseCachingFeatureTests.cs │ ├── ResponseCachingKeyProviderTests.cs │ ├── ResponseCachingMiddlewareTests.cs │ ├── ResponseCachingPolicyProviderTests.cs │ ├── ResponseCachingTests.cs │ ├── SegmentReadStreamTests.cs │ ├── SegmentWriteStreamTests.cs │ └── TestUtils.cs └── version.props /.appveyor.yml: -------------------------------------------------------------------------------- 1 | init: 2 | - git config --global core.autocrlf true 3 | branches: 4 | only: 5 | - master 6 | - /^release\/.*$/ 7 | - /^(.*\/)?ci-.*$/ 8 | build_script: 9 | - ps: .\run.ps1 default-build 10 | clone_depth: 1 11 | environment: 12 | global: 13 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 14 | DOTNET_CLI_TELEMETRY_OPTOUT: 1 15 | test: 'off' 16 | deploy: 'off' 17 | os: Visual Studio 2017 18 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.doc diff=astextplain 2 | *.DOC diff=astextplain 3 | *.docx diff=astextplain 4 | *.DOCX diff=astextplain 5 | *.dot diff=astextplain 6 | *.DOT diff=astextplain 7 | *.pdf diff=astextplain 8 | *.PDF diff=astextplain 9 | *.rtf diff=astextplain 10 | *.RTF diff=astextplain 11 | 12 | *.jpg binary 13 | *.png binary 14 | *.gif binary 15 | 16 | *.cs text=auto diff=csharp 17 | *.vb text=auto 18 | *.resx text=auto 19 | *.c text=auto 20 | *.cpp text=auto 21 | *.cxx text=auto 22 | *.h text=auto 23 | *.hxx text=auto 24 | *.py text=auto 25 | *.rb text=auto 26 | *.java text=auto 27 | *.html text=auto 28 | *.htm text=auto 29 | *.css text=auto 30 | *.scss text=auto 31 | *.sass text=auto 32 | *.less text=auto 33 | *.js text=auto 34 | *.lisp text=auto 35 | *.clj text=auto 36 | *.sql text=auto 37 | *.php text=auto 38 | *.lua text=auto 39 | *.m text=auto 40 | *.asm text=auto 41 | *.erl text=auto 42 | *.fs text=auto 43 | *.fsx text=auto 44 | *.hs text=auto 45 | 46 | *.csproj text=auto 47 | *.vbproj text=auto 48 | *.fsproj text=auto 49 | *.dbproj text=auto 50 | *.sln text=auto eol=crlf 51 | *.sh eol=lf 52 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | THIS ISSUE TRACKER IS CLOSED - please log new issues here: https://github.com/aspnet/Home/issues 2 | 3 | For information about this change, see https://github.com/aspnet/Announcements/issues/283 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | [Oo]bj/ 2 | [Bb]in/ 3 | TestResults/ 4 | .nuget/ 5 | _ReSharper.*/ 6 | packages/ 7 | artifacts/ 8 | PublishProfiles/ 9 | *.user 10 | *.suo 11 | *.cache 12 | *.docstates 13 | _ReSharper.* 14 | nuget.exe 15 | *net45.csproj 16 | *net451.csproj 17 | *k10.csproj 18 | *.psess 19 | *.vsp 20 | *.pidb 21 | *.userprefs 22 | *DS_Store 23 | *.ncrunchsolution 24 | *.*sdf 25 | *.ipch 26 | *.sln.ide 27 | project.lock.json 28 | /.vs/ 29 | .vscode/ 30 | .build/ 31 | .testPublish/ 32 | launchSettings.json 33 | global.json 34 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: csharp 2 | sudo: false 3 | dist: trusty 4 | env: 5 | global: 6 | - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 7 | - DOTNET_CLI_TELEMETRY_OPTOUT: 1 8 | mono: none 9 | os: 10 | - linux 11 | - osx 12 | osx_image: xcode8.2 13 | addons: 14 | apt: 15 | packages: 16 | - libunwind8 17 | branches: 18 | only: 19 | - master 20 | - /^release\/.*$/ 21 | - /^(.*\/)?ci-.*$/ 22 | before_install: 23 | - if test "$TRAVIS_OS_NAME" == "osx"; then brew update; brew install openssl; ln -s 24 | /usr/local/opt/openssl/lib/libcrypto.1.0.0.dylib /usr/local/lib/; ln -s /usr/local/opt/openssl/lib/libssl.1.0.0.dylib 25 | /usr/local/lib/; fi 26 | script: 27 | - ./build.sh 28 | -------------------------------------------------------------------------------- /.vsts-pipelines/builds/ci-internal.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | - master 3 | - release/* 4 | 5 | resources: 6 | repositories: 7 | - repository: buildtools 8 | type: git 9 | name: aspnet-BuildTools 10 | ref: refs/heads/master 11 | 12 | phases: 13 | - template: .vsts-pipelines/templates/project-ci.yml@buildtools 14 | -------------------------------------------------------------------------------- /.vsts-pipelines/builds/ci-public.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | - master 3 | - release/* 4 | 5 | # See https://github.com/aspnet/BuildTools 6 | resources: 7 | repositories: 8 | - repository: buildtools 9 | type: github 10 | endpoint: DotNet-Bot GitHub Connection 11 | name: aspnet/BuildTools 12 | ref: refs/heads/master 13 | 14 | phases: 15 | - template: .vsts-pipelines/templates/project-ci.yml@buildtools 16 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ====== 3 | 4 | Information on contributing to this repo is in the [Contributing Guide](https://github.com/aspnet/Home/blob/master/CONTRIBUTING.md) in the Home repo. 5 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 |  2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Microsoft ASP.NET Core 12 | https://github.com/aspnet/ResponseCaching 13 | git 14 | $(MSBuildThisFileDirectory) 15 | $(MSBuildThisFileDirectory)build\Key.snk 16 | true 17 | true 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | $(MicrosoftNETCoreApp20PackageVersion) 4 | $(MicrosoftNETCoreApp21PackageVersion) 5 | $(MicrosoftNETCoreApp22PackageVersion) 6 | $(NETStandardLibrary20PackageVersion) 7 | 8 | 99.9 9 | 10 | 11 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright (c) .NET Foundation and Contributors 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /NuGet.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /NuGetPackageVerifier.json: -------------------------------------------------------------------------------- 1 | { 2 | "Default": { 3 | "rules": [ 4 | "DefaultCompositeRule" 5 | ] 6 | } 7 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ASP.NET Core Response Caching [Archived] 2 | ======================================== 3 | 4 | **This GitHub project has been archived.** Ongoing development on this project can be found in . 5 | 6 | This repo hosts the ASP.NET Core middleware for response caching. 7 | 8 | This project is part of ASP.NET Core. You can find samples, documentation and getting started instructions for ASP.NET Core at the [AspNetCore](https://github.com/aspnet/AspNetCore) repo. 9 | -------------------------------------------------------------------------------- /ResponseCaching.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio 15 3 | VisualStudioVersion = 15.0.26730.10 4 | MinimumVisualStudioVersion = 15.0.26730.03 5 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{367AABAF-E03C-4491-A9A7-BDDE8903D1B4}" 6 | ProjectSection(SolutionItems) = preProject 7 | src\Directory.Build.props = src\Directory.Build.props 8 | EndProjectSection 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{C51DF5BD-B53D-4795-BC01-A9AB066BF286}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{89A50974-E9D4-4F87-ACF2-6A6005E64931}" 13 | ProjectSection(SolutionItems) = preProject 14 | test\Directory.Build.props = test\Directory.Build.props 15 | EndProjectSection 16 | EndProject 17 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ResponseCachingSample", "samples\ResponseCachingSample\ResponseCachingSample.csproj", "{1139BDEE-FA15-474D-8855-0AB91F23CF26}" 18 | EndProject 19 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.ResponseCaching.Tests", "test\Microsoft.AspNetCore.ResponseCaching.Tests\Microsoft.AspNetCore.ResponseCaching.Tests.csproj", "{151B2027-3936-44B9-A4A0-E1E5902125AB}" 20 | EndProject 21 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.ResponseCaching", "src\Microsoft.AspNetCore.ResponseCaching\Microsoft.AspNetCore.ResponseCaching.csproj", "{D1031270-DBD3-4F02-A3DC-3E7DADE8EBE6}" 22 | EndProject 23 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.ResponseCaching.Abstractions", "src\Microsoft.AspNetCore.ResponseCaching.Abstractions\Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj", "{2D1022E8-CBB6-478D-A420-CB888D0EF7B7}" 24 | EndProject 25 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{B984DDCF-0D61-44C4-9D30-2BC59EE6BD29}" 26 | ProjectSection(SolutionItems) = preProject 27 | Directory.Build.props = Directory.Build.props 28 | Directory.Build.targets = Directory.Build.targets 29 | EndProjectSection 30 | EndProject 31 | Global 32 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 33 | Debug|Any CPU = Debug|Any CPU 34 | Release|Any CPU = Release|Any CPU 35 | EndGlobalSection 36 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 37 | {1139BDEE-FA15-474D-8855-0AB91F23CF26}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {1139BDEE-FA15-474D-8855-0AB91F23CF26}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {1139BDEE-FA15-474D-8855-0AB91F23CF26}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {1139BDEE-FA15-474D-8855-0AB91F23CF26}.Release|Any CPU.Build.0 = Release|Any CPU 41 | {151B2027-3936-44B9-A4A0-E1E5902125AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {151B2027-3936-44B9-A4A0-E1E5902125AB}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | {151B2027-3936-44B9-A4A0-E1E5902125AB}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {151B2027-3936-44B9-A4A0-E1E5902125AB}.Release|Any CPU.Build.0 = Release|Any CPU 45 | {D1031270-DBD3-4F02-A3DC-3E7DADE8EBE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 46 | {D1031270-DBD3-4F02-A3DC-3E7DADE8EBE6}.Debug|Any CPU.Build.0 = Debug|Any CPU 47 | {D1031270-DBD3-4F02-A3DC-3E7DADE8EBE6}.Release|Any CPU.ActiveCfg = Release|Any CPU 48 | {D1031270-DBD3-4F02-A3DC-3E7DADE8EBE6}.Release|Any CPU.Build.0 = Release|Any CPU 49 | {2D1022E8-CBB6-478D-A420-CB888D0EF7B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 50 | {2D1022E8-CBB6-478D-A420-CB888D0EF7B7}.Debug|Any CPU.Build.0 = Debug|Any CPU 51 | {2D1022E8-CBB6-478D-A420-CB888D0EF7B7}.Release|Any CPU.ActiveCfg = Release|Any CPU 52 | {2D1022E8-CBB6-478D-A420-CB888D0EF7B7}.Release|Any CPU.Build.0 = Release|Any CPU 53 | EndGlobalSection 54 | GlobalSection(SolutionProperties) = preSolution 55 | HideSolutionNode = FALSE 56 | EndGlobalSection 57 | GlobalSection(NestedProjects) = preSolution 58 | {1139BDEE-FA15-474D-8855-0AB91F23CF26} = {C51DF5BD-B53D-4795-BC01-A9AB066BF286} 59 | {151B2027-3936-44B9-A4A0-E1E5902125AB} = {89A50974-E9D4-4F87-ACF2-6A6005E64931} 60 | {D1031270-DBD3-4F02-A3DC-3E7DADE8EBE6} = {367AABAF-E03C-4491-A9A7-BDDE8903D1B4} 61 | {2D1022E8-CBB6-478D-A420-CB888D0EF7B7} = {367AABAF-E03C-4491-A9A7-BDDE8903D1B4} 62 | EndGlobalSection 63 | GlobalSection(ExtensibilityGlobals) = postSolution 64 | SolutionGuid = {6F6B4994-06D7-4D35-B0F7-F60913AA8402} 65 | EndGlobalSection 66 | EndGlobal 67 | -------------------------------------------------------------------------------- /build.cmd: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | PowerShell -NoProfile -NoLogo -ExecutionPolicy unrestricted -Command "[System.Threading.Thread]::CurrentThread.CurrentCulture = ''; [System.Threading.Thread]::CurrentThread.CurrentUICulture = '';& '%~dp0run.ps1' default-build %*; exit $LASTEXITCODE" 3 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 5 | 6 | # Call "sync" between "chmod" and execution to prevent "text file busy" error in Docker (aufs) 7 | chmod +x "$DIR/run.sh"; sync 8 | "$DIR/run.sh" default-build "$@" 9 | -------------------------------------------------------------------------------- /build/Key.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aspnet/ResponseCaching/12395b4d1c03e49134704679f94af6f328a7e1d5/build/Key.snk -------------------------------------------------------------------------------- /build/dependencies.props: -------------------------------------------------------------------------------- 1 |  2 | 3 | $(MSBuildAllProjects);$(MSBuildThisFileFullPath) 4 | 5 | 6 | 3.0.0-alpha1-20181004.7 7 | 3.0.0-alpha1-10584 8 | 3.0.0-alpha1-10584 9 | 3.0.0-alpha1-10584 10 | 3.0.0-alpha1-10584 11 | 3.0.0-alpha1-10584 12 | 3.0.0-alpha1-10584 13 | 3.0.0-alpha1-10584 14 | 3.0.0-alpha1-10584 15 | 3.0.0-alpha1-10584 16 | 2.0.9 17 | 2.1.3 18 | 2.2.0-preview2-26905-02 19 | 15.6.1 20 | 2.0.3 21 | 2.3.1 22 | 2.4.0 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /build/repo.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Internal.AspNetCore.Universe.Lineup 7 | https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /build/sources.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | $(DotNetRestoreSources) 6 | 7 | $(RestoreSources); 8 | https://dotnet.myget.org/F/dotnet-core/api/v3/index.json; 9 | https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json; 10 | https://dotnet.myget.org/F/aspnetcore-tools/api/v3/index.json; 11 | 12 | 13 | $(RestoreSources); 14 | https://api.nuget.org/v3/index.json; 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /korebuild-lock.txt: -------------------------------------------------------------------------------- 1 | version:3.0.0-alpha1-20181004.7 2 | commithash:27fabdaf2b1d4753c3d2749581694ca65d78f7f2 3 | -------------------------------------------------------------------------------- /korebuild.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/aspnet/BuildTools/master/tools/korebuild.schema.json", 3 | "channel": "master" 4 | } 5 | -------------------------------------------------------------------------------- /run.cmd: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | PowerShell -NoProfile -NoLogo -ExecutionPolicy unrestricted -Command "[System.Threading.Thread]::CurrentThread.CurrentCulture = ''; [System.Threading.Thread]::CurrentThread.CurrentUICulture = '';& '%~dp0run.ps1' %*; exit $LASTEXITCODE" 3 | -------------------------------------------------------------------------------- /run.ps1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env powershell 2 | #requires -version 4 3 | 4 | <# 5 | .SYNOPSIS 6 | Executes KoreBuild commands. 7 | 8 | .DESCRIPTION 9 | Downloads korebuild if required. Then executes the KoreBuild command. To see available commands, execute with `-Command help`. 10 | 11 | .PARAMETER Command 12 | The KoreBuild command to run. 13 | 14 | .PARAMETER Path 15 | The folder to build. Defaults to the folder containing this script. 16 | 17 | .PARAMETER Channel 18 | The channel of KoreBuild to download. Overrides the value from the config file. 19 | 20 | .PARAMETER DotNetHome 21 | The directory where .NET Core tools will be stored. 22 | 23 | .PARAMETER ToolsSource 24 | The base url where build tools can be downloaded. Overrides the value from the config file. 25 | 26 | .PARAMETER Update 27 | Updates KoreBuild to the latest version even if a lock file is present. 28 | 29 | .PARAMETER Reinstall 30 | Re-installs KoreBuild 31 | 32 | .PARAMETER ConfigFile 33 | The path to the configuration file that stores values. Defaults to korebuild.json. 34 | 35 | .PARAMETER ToolsSourceSuffix 36 | The Suffix to append to the end of the ToolsSource. Useful for query strings in blob stores. 37 | 38 | .PARAMETER CI 39 | Sets up CI specific settings and variables. 40 | 41 | .PARAMETER Arguments 42 | Arguments to be passed to the command 43 | 44 | .NOTES 45 | This function will create a file $PSScriptRoot/korebuild-lock.txt. This lock file can be committed to source, but does not have to be. 46 | When the lockfile is not present, KoreBuild will create one using latest available version from $Channel. 47 | 48 | The $ConfigFile is expected to be an JSON file. It is optional, and the configuration values in it are optional as well. Any options set 49 | in the file are overridden by command line parameters. 50 | 51 | .EXAMPLE 52 | Example config file: 53 | ```json 54 | { 55 | "$schema": "https://raw.githubusercontent.com/aspnet/BuildTools/master/tools/korebuild.schema.json", 56 | "channel": "master", 57 | "toolsSource": "https://aspnetcore.blob.core.windows.net/buildtools" 58 | } 59 | ``` 60 | #> 61 | [CmdletBinding(PositionalBinding = $false)] 62 | param( 63 | [Parameter(Mandatory = $true, Position = 0)] 64 | [string]$Command, 65 | [string]$Path = $PSScriptRoot, 66 | [Alias('c')] 67 | [string]$Channel, 68 | [Alias('d')] 69 | [string]$DotNetHome, 70 | [Alias('s')] 71 | [string]$ToolsSource, 72 | [Alias('u')] 73 | [switch]$Update, 74 | [switch]$Reinstall, 75 | [string]$ToolsSourceSuffix, 76 | [string]$ConfigFile = $null, 77 | [switch]$CI, 78 | [Parameter(ValueFromRemainingArguments = $true)] 79 | [string[]]$Arguments 80 | ) 81 | 82 | Set-StrictMode -Version 2 83 | $ErrorActionPreference = 'Stop' 84 | 85 | # 86 | # Functions 87 | # 88 | 89 | function Get-KoreBuild { 90 | 91 | $lockFile = Join-Path $Path 'korebuild-lock.txt' 92 | 93 | if (!(Test-Path $lockFile) -or $Update) { 94 | Get-RemoteFile "$ToolsSource/korebuild/channels/$Channel/latest.txt" $lockFile $ToolsSourceSuffix 95 | } 96 | 97 | $version = Get-Content $lockFile | Where-Object { $_ -like 'version:*' } | Select-Object -first 1 98 | if (!$version) { 99 | Write-Error "Failed to parse version from $lockFile. Expected a line that begins with 'version:'" 100 | } 101 | $version = $version.TrimStart('version:').Trim() 102 | $korebuildPath = Join-Paths $DotNetHome ('buildtools', 'korebuild', $version) 103 | 104 | if ($Reinstall -and (Test-Path $korebuildPath)) { 105 | Remove-Item -Force -Recurse $korebuildPath 106 | } 107 | 108 | if (!(Test-Path $korebuildPath)) { 109 | Write-Host -ForegroundColor Magenta "Downloading KoreBuild $version" 110 | New-Item -ItemType Directory -Path $korebuildPath | Out-Null 111 | $remotePath = "$ToolsSource/korebuild/artifacts/$version/korebuild.$version.zip" 112 | 113 | try { 114 | $tmpfile = Join-Path ([IO.Path]::GetTempPath()) "KoreBuild-$([guid]::NewGuid()).zip" 115 | Get-RemoteFile $remotePath $tmpfile $ToolsSourceSuffix 116 | if (Get-Command -Name 'Microsoft.PowerShell.Archive\Expand-Archive' -ErrorAction Ignore) { 117 | # Use built-in commands where possible as they are cross-plat compatible 118 | Microsoft.PowerShell.Archive\Expand-Archive -Path $tmpfile -DestinationPath $korebuildPath 119 | } 120 | else { 121 | # Fallback to old approach for old installations of PowerShell 122 | Add-Type -AssemblyName System.IO.Compression.FileSystem 123 | [System.IO.Compression.ZipFile]::ExtractToDirectory($tmpfile, $korebuildPath) 124 | } 125 | } 126 | catch { 127 | Remove-Item -Recurse -Force $korebuildPath -ErrorAction Ignore 128 | throw 129 | } 130 | finally { 131 | Remove-Item $tmpfile -ErrorAction Ignore 132 | } 133 | } 134 | 135 | return $korebuildPath 136 | } 137 | 138 | function Join-Paths([string]$path, [string[]]$childPaths) { 139 | $childPaths | ForEach-Object { $path = Join-Path $path $_ } 140 | return $path 141 | } 142 | 143 | function Get-RemoteFile([string]$RemotePath, [string]$LocalPath, [string]$RemoteSuffix) { 144 | if ($RemotePath -notlike 'http*') { 145 | Copy-Item $RemotePath $LocalPath 146 | return 147 | } 148 | 149 | $retries = 10 150 | while ($retries -gt 0) { 151 | $retries -= 1 152 | try { 153 | Invoke-WebRequest -UseBasicParsing -Uri $($RemotePath + $RemoteSuffix) -OutFile $LocalPath 154 | return 155 | } 156 | catch { 157 | Write-Verbose "Request failed. $retries retries remaining" 158 | } 159 | } 160 | 161 | Write-Error "Download failed: '$RemotePath'." 162 | } 163 | 164 | # 165 | # Main 166 | # 167 | 168 | # Load configuration or set defaults 169 | 170 | $Path = Resolve-Path $Path 171 | if (!$ConfigFile) { $ConfigFile = Join-Path $Path 'korebuild.json' } 172 | 173 | if (Test-Path $ConfigFile) { 174 | try { 175 | $config = Get-Content -Raw -Encoding UTF8 -Path $ConfigFile | ConvertFrom-Json 176 | if ($config) { 177 | if (!($Channel) -and (Get-Member -Name 'channel' -InputObject $config)) { [string] $Channel = $config.channel } 178 | if (!($ToolsSource) -and (Get-Member -Name 'toolsSource' -InputObject $config)) { [string] $ToolsSource = $config.toolsSource} 179 | } 180 | } 181 | catch { 182 | Write-Host -ForegroundColor Red $Error[0] 183 | Write-Error "$ConfigFile contains invalid JSON." 184 | exit 1 185 | } 186 | } 187 | 188 | if (!$DotNetHome) { 189 | $DotNetHome = if ($env:DOTNET_HOME) { $env:DOTNET_HOME } ` 190 | elseif ($env:USERPROFILE) { Join-Path $env:USERPROFILE '.dotnet'} ` 191 | elseif ($env:HOME) {Join-Path $env:HOME '.dotnet'}` 192 | else { Join-Path $PSScriptRoot '.dotnet'} 193 | } 194 | 195 | if (!$Channel) { $Channel = 'master' } 196 | if (!$ToolsSource) { $ToolsSource = 'https://aspnetcore.blob.core.windows.net/buildtools' } 197 | 198 | # Execute 199 | 200 | $korebuildPath = Get-KoreBuild 201 | Import-Module -Force -Scope Local (Join-Path $korebuildPath 'KoreBuild.psd1') 202 | 203 | try { 204 | Set-KoreBuildSettings -ToolsSource $ToolsSource -DotNetHome $DotNetHome -RepoPath $Path -ConfigFile $ConfigFile -CI:$CI 205 | Invoke-KoreBuildCommand $Command @Arguments 206 | } 207 | finally { 208 | Remove-Module 'KoreBuild' -ErrorAction Ignore 209 | } 210 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | # 6 | # variables 7 | # 8 | 9 | RESET="\033[0m" 10 | RED="\033[0;31m" 11 | YELLOW="\033[0;33m" 12 | MAGENTA="\033[0;95m" 13 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 14 | [ -z "${DOTNET_HOME:-}" ] && DOTNET_HOME="$HOME/.dotnet" 15 | verbose=false 16 | update=false 17 | reinstall=false 18 | repo_path="$DIR" 19 | channel='' 20 | tools_source='' 21 | tools_source_suffix='' 22 | ci=false 23 | 24 | # 25 | # Functions 26 | # 27 | __usage() { 28 | echo "Usage: $(basename "${BASH_SOURCE[0]}") command [options] [[--] ...]" 29 | echo "" 30 | echo "Arguments:" 31 | echo " command The command to be run." 32 | echo " ... Arguments passed to the command. Variable number of arguments allowed." 33 | echo "" 34 | echo "Options:" 35 | echo " --verbose Show verbose output." 36 | echo " -c|--channel The channel of KoreBuild to download. Overrides the value from the config file.." 37 | echo " --config-file The path to the configuration file that stores values. Defaults to korebuild.json." 38 | echo " -d|--dotnet-home The directory where .NET Core tools will be stored. Defaults to '\$DOTNET_HOME' or '\$HOME/.dotnet." 39 | echo " --path The directory to build. Defaults to the directory containing the script." 40 | echo " -s|--tools-source|-ToolsSource The base url where build tools can be downloaded. Overrides the value from the config file." 41 | echo " --tools-source-suffix|-ToolsSourceSuffix The suffix to append to tools-source. Useful for query strings." 42 | echo " -u|--update Update to the latest KoreBuild even if the lock file is present." 43 | echo " --reinstall Reinstall KoreBuild." 44 | echo " --ci Apply CI specific settings and environment variables." 45 | echo "" 46 | echo "Description:" 47 | echo " This function will create a file \$DIR/korebuild-lock.txt. This lock file can be committed to source, but does not have to be." 48 | echo " When the lockfile is not present, KoreBuild will create one using latest available version from \$channel." 49 | 50 | if [[ "${1:-}" != '--no-exit' ]]; then 51 | exit 2 52 | fi 53 | } 54 | 55 | get_korebuild() { 56 | local version 57 | local lock_file="$repo_path/korebuild-lock.txt" 58 | if [ ! -f "$lock_file" ] || [ "$update" = true ]; then 59 | __get_remote_file "$tools_source/korebuild/channels/$channel/latest.txt" "$lock_file" "$tools_source_suffix" 60 | fi 61 | version="$(grep 'version:*' -m 1 "$lock_file")" 62 | if [[ "$version" == '' ]]; then 63 | __error "Failed to parse version from $lock_file. Expected a line that begins with 'version:'" 64 | return 1 65 | fi 66 | version="$(echo "${version#version:}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" 67 | local korebuild_path="$DOTNET_HOME/buildtools/korebuild/$version" 68 | 69 | if [ "$reinstall" = true ] && [ -d "$korebuild_path" ]; then 70 | rm -rf "$korebuild_path" 71 | fi 72 | 73 | { 74 | if [ ! -d "$korebuild_path" ]; then 75 | mkdir -p "$korebuild_path" 76 | local remote_path="$tools_source/korebuild/artifacts/$version/korebuild.$version.zip" 77 | tmpfile="$(mktemp)" 78 | echo -e "${MAGENTA}Downloading KoreBuild ${version}${RESET}" 79 | if __get_remote_file "$remote_path" "$tmpfile" "$tools_source_suffix"; then 80 | unzip -q -d "$korebuild_path" "$tmpfile" 81 | fi 82 | rm "$tmpfile" || true 83 | fi 84 | 85 | source "$korebuild_path/KoreBuild.sh" 86 | } || { 87 | if [ -d "$korebuild_path" ]; then 88 | echo "Cleaning up after failed installation" 89 | rm -rf "$korebuild_path" || true 90 | fi 91 | return 1 92 | } 93 | } 94 | 95 | __error() { 96 | echo -e "${RED}error: $*${RESET}" 1>&2 97 | } 98 | 99 | __warn() { 100 | echo -e "${YELLOW}warning: $*${RESET}" 101 | } 102 | 103 | __machine_has() { 104 | hash "$1" > /dev/null 2>&1 105 | return $? 106 | } 107 | 108 | __get_remote_file() { 109 | local remote_path=$1 110 | local local_path=$2 111 | local remote_path_suffix=$3 112 | 113 | if [[ "$remote_path" != 'http'* ]]; then 114 | cp "$remote_path" "$local_path" 115 | return 0 116 | fi 117 | 118 | local failed=false 119 | if __machine_has wget; then 120 | wget --tries 10 --quiet -O "$local_path" "${remote_path}${remote_path_suffix}" || failed=true 121 | else 122 | failed=true 123 | fi 124 | 125 | if [ "$failed" = true ] && __machine_has curl; then 126 | failed=false 127 | curl --retry 10 -sSL -f --create-dirs -o "$local_path" "${remote_path}${remote_path_suffix}" || failed=true 128 | fi 129 | 130 | if [ "$failed" = true ]; then 131 | __error "Download failed: $remote_path" 1>&2 132 | return 1 133 | fi 134 | } 135 | 136 | # 137 | # main 138 | # 139 | 140 | command="${1:-}" 141 | shift 142 | 143 | while [[ $# -gt 0 ]]; do 144 | case $1 in 145 | -\?|-h|--help) 146 | __usage --no-exit 147 | exit 0 148 | ;; 149 | -c|--channel|-Channel) 150 | shift 151 | channel="${1:-}" 152 | [ -z "$channel" ] && __usage 153 | ;; 154 | --config-file|-ConfigFile) 155 | shift 156 | config_file="${1:-}" 157 | [ -z "$config_file" ] && __usage 158 | if [ ! -f "$config_file" ]; then 159 | __error "Invalid value for --config-file. $config_file does not exist." 160 | exit 1 161 | fi 162 | ;; 163 | -d|--dotnet-home|-DotNetHome) 164 | shift 165 | DOTNET_HOME="${1:-}" 166 | [ -z "$DOTNET_HOME" ] && __usage 167 | ;; 168 | --path|-Path) 169 | shift 170 | repo_path="${1:-}" 171 | [ -z "$repo_path" ] && __usage 172 | ;; 173 | -s|--tools-source|-ToolsSource) 174 | shift 175 | tools_source="${1:-}" 176 | [ -z "$tools_source" ] && __usage 177 | ;; 178 | --tools-source-suffix|-ToolsSourceSuffix) 179 | shift 180 | tools_source_suffix="${1:-}" 181 | [ -z "$tools_source_suffix" ] && __usage 182 | ;; 183 | -u|--update|-Update) 184 | update=true 185 | ;; 186 | --reinstall|-[Rr]einstall) 187 | reinstall=true 188 | ;; 189 | --ci|-[Cc][Ii]) 190 | ci=true 191 | ;; 192 | --verbose|-Verbose) 193 | verbose=true 194 | ;; 195 | --) 196 | shift 197 | break 198 | ;; 199 | *) 200 | break 201 | ;; 202 | esac 203 | shift 204 | done 205 | 206 | if ! __machine_has unzip; then 207 | __error 'Missing required command: unzip' 208 | exit 1 209 | fi 210 | 211 | if ! __machine_has curl && ! __machine_has wget; then 212 | __error 'Missing required command. Either wget or curl is required.' 213 | exit 1 214 | fi 215 | 216 | [ -z "${config_file:-}" ] && config_file="$repo_path/korebuild.json" 217 | if [ -f "$config_file" ]; then 218 | if __machine_has jq ; then 219 | if jq '.' "$config_file" >/dev/null ; then 220 | config_channel="$(jq -r 'select(.channel!=null) | .channel' "$config_file")" 221 | config_tools_source="$(jq -r 'select(.toolsSource!=null) | .toolsSource' "$config_file")" 222 | else 223 | __error "$config_file contains invalid JSON." 224 | exit 1 225 | fi 226 | elif __machine_has python ; then 227 | if python -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'))" >/dev/null ; then 228 | config_channel="$(python -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'));print(obj['channel'] if 'channel' in obj else '')")" 229 | config_tools_source="$(python -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'));print(obj['toolsSource'] if 'toolsSource' in obj else '')")" 230 | else 231 | __error "$config_file contains invalid JSON." 232 | exit 1 233 | fi 234 | elif __machine_has python3 ; then 235 | if python3 -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'))" >/dev/null ; then 236 | config_channel="$(python3 -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'));print(obj['channel'] if 'channel' in obj else '')")" 237 | config_tools_source="$(python3 -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'));print(obj['toolsSource'] if 'toolsSource' in obj else '')")" 238 | else 239 | __error "$config_file contains invalid JSON." 240 | exit 1 241 | fi 242 | else 243 | __error 'Missing required command: jq or python. Could not parse the JSON file.' 244 | exit 1 245 | fi 246 | 247 | [ ! -z "${config_channel:-}" ] && channel="$config_channel" 248 | [ ! -z "${config_tools_source:-}" ] && tools_source="$config_tools_source" 249 | fi 250 | 251 | [ -z "$channel" ] && channel='master' 252 | [ -z "$tools_source" ] && tools_source='https://aspnetcore.blob.core.windows.net/buildtools' 253 | 254 | get_korebuild 255 | set_korebuildsettings "$tools_source" "$DOTNET_HOME" "$repo_path" "$config_file" "$ci" 256 | invoke_korebuild_command "$command" "$@" 257 | -------------------------------------------------------------------------------- /samples/ResponseCachingSample/README.md: -------------------------------------------------------------------------------- 1 | ASP.NET Core Response Caching Sample 2 | =================================== 3 | 4 | This sample illustrates the usage of ASP.NET Core response caching middleware. The application sends a `Hello World!` message and the current time along with a `Cache-Control` header to configure caching behavior. The application also sends a `Vary` header to configure the cache to serve the response only if the `Accept-Encoding` header of subsequent requests matches that from the original request. 5 | 6 | When running the sample, a response will be served from cache when possible and will be stored for up to 10 seconds. 7 | -------------------------------------------------------------------------------- /samples/ResponseCachingSample/ResponseCachingSample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.2 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /samples/ResponseCachingSample/Startup.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.IO; 6 | using Microsoft.AspNetCore.Builder; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.AspNetCore.Http; 9 | using Microsoft.Extensions.DependencyInjection; 10 | using Microsoft.Net.Http.Headers; 11 | 12 | namespace ResponseCachingSample 13 | { 14 | public class Startup 15 | { 16 | public void ConfigureServices(IServiceCollection services) 17 | { 18 | services.AddResponseCaching(); 19 | } 20 | 21 | public void Configure(IApplicationBuilder app) 22 | { 23 | app.UseResponseCaching(); 24 | app.Run(async (context) => 25 | { 26 | context.Response.GetTypedHeaders().CacheControl = new CacheControlHeaderValue() 27 | { 28 | Public = true, 29 | MaxAge = TimeSpan.FromSeconds(10) 30 | }; 31 | context.Response.Headers[HeaderNames.Vary] = new string[] { "Accept-Encoding" }; 32 | 33 | await context.Response.WriteAsync("Hello World! " + DateTime.UtcNow); 34 | }); 35 | } 36 | 37 | public static void Main(string[] args) 38 | { 39 | var host = new WebHostBuilder() 40 | .UseKestrel() 41 | .UseContentRoot(Directory.GetCurrentDirectory()) 42 | .UseIISIntegration() 43 | .UseStartup() 44 | .Build(); 45 | 46 | host.Run(); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.ResponseCaching.Abstractions/IResponseCachingFeature.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.AspNetCore.ResponseCaching 5 | { 6 | /// 7 | /// A feature for configuring additional response cache options on the HTTP response. 8 | /// 9 | public interface IResponseCachingFeature 10 | { 11 | /// 12 | /// Gets or sets the query keys used by the response cache middleware for calculating secondary vary keys. 13 | /// 14 | string[] VaryByQueryKeys { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.ResponseCaching.Abstractions/Microsoft.AspNetCore.ResponseCaching.Abstractions.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | ASP.NET Core response caching middleware abstractions and feature interface definitions. 5 | netstandard2.0 6 | true 7 | aspnetcore;cache;caching 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.ResponseCaching.Abstractions/baseline.netcore.json: -------------------------------------------------------------------------------- 1 | { 2 | "AssemblyIdentity": "Microsoft.AspNetCore.ResponseCaching.Abstractions, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", 3 | "Types": [ 4 | { 5 | "Name": "Microsoft.AspNetCore.ResponseCaching.IResponseCachingFeature", 6 | "Visibility": "Public", 7 | "Kind": "Interface", 8 | "Abstract": true, 9 | "ImplementedInterfaces": [], 10 | "Members": [ 11 | { 12 | "Kind": "Method", 13 | "Name": "get_VaryByQueryKeys", 14 | "Parameters": [], 15 | "ReturnType": "System.String[]", 16 | "GenericParameter": [] 17 | }, 18 | { 19 | "Kind": "Method", 20 | "Name": "set_VaryByQueryKeys", 21 | "Parameters": [ 22 | { 23 | "Name": "value", 24 | "Type": "System.String[]" 25 | } 26 | ], 27 | "ReturnType": "System.Void", 28 | "GenericParameter": [] 29 | } 30 | ], 31 | "GenericParameters": [] 32 | } 33 | ] 34 | } -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CacheEntryHelpers .cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using Microsoft.Extensions.Primitives; 5 | 6 | namespace Microsoft.AspNetCore.ResponseCaching.Internal 7 | { 8 | internal static class CacheEntryHelpers 9 | { 10 | 11 | internal static long EstimateCachedResponseSize(CachedResponse cachedResponse) 12 | { 13 | if (cachedResponse == null) 14 | { 15 | return 0L; 16 | } 17 | 18 | checked 19 | { 20 | // StatusCode 21 | long size = sizeof(int); 22 | 23 | // Headers 24 | if (cachedResponse.Headers != null) 25 | { 26 | foreach (var item in cachedResponse.Headers) 27 | { 28 | size += item.Key.Length * sizeof(char) + EstimateStringValuesSize(item.Value); 29 | } 30 | } 31 | 32 | // Body 33 | if (cachedResponse.Body != null) 34 | { 35 | size += cachedResponse.Body.Length; 36 | } 37 | 38 | return size; 39 | } 40 | } 41 | 42 | internal static long EstimateCachedVaryByRulesySize(CachedVaryByRules cachedVaryByRules) 43 | { 44 | if (cachedVaryByRules == null) 45 | { 46 | return 0L; 47 | } 48 | 49 | checked 50 | { 51 | var size = 0L; 52 | 53 | // VaryByKeyPrefix 54 | if (!string.IsNullOrEmpty(cachedVaryByRules.VaryByKeyPrefix)) 55 | { 56 | size = cachedVaryByRules.VaryByKeyPrefix.Length * sizeof(char); 57 | } 58 | 59 | // Headers 60 | size += EstimateStringValuesSize(cachedVaryByRules.Headers); 61 | 62 | // QueryKeys 63 | size += EstimateStringValuesSize(cachedVaryByRules.QueryKeys); 64 | 65 | return size; 66 | } 67 | } 68 | 69 | internal static long EstimateStringValuesSize(StringValues stringValues) 70 | { 71 | checked 72 | { 73 | var size = 0L; 74 | 75 | for (var i = 0; i < stringValues.Count; i++) 76 | { 77 | var stringValue = stringValues[i]; 78 | if (!string.IsNullOrEmpty(stringValue)) 79 | { 80 | size += stringValues[i].Length * sizeof(char); 81 | } 82 | } 83 | 84 | return size; 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CachedResponse.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.IO; 6 | using Microsoft.AspNetCore.Http; 7 | 8 | namespace Microsoft.AspNetCore.ResponseCaching.Internal 9 | { 10 | public class CachedResponse : IResponseCacheEntry 11 | { 12 | public DateTimeOffset Created { get; set; } 13 | 14 | public int StatusCode { get; set; } 15 | 16 | public IHeaderDictionary Headers { get; set; } 17 | 18 | public Stream Body { get; set; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.ResponseCaching/Internal/CacheEntry/CachedVaryByRules.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using Microsoft.Extensions.Primitives; 5 | 6 | namespace Microsoft.AspNetCore.ResponseCaching.Internal 7 | { 8 | public class CachedVaryByRules : IResponseCacheEntry 9 | { 10 | public string VaryByKeyPrefix { get; set; } 11 | 12 | public StringValues Headers { get; set; } 13 | 14 | public StringValues QueryKeys { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.ResponseCaching/Internal/FastGuid.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Threading; 6 | 7 | namespace Microsoft.AspNetCore.ResponseCaching.Internal 8 | { 9 | internal class FastGuid 10 | { 11 | // Base32 encoding - in ascii sort order for easy text based sorting 12 | private static readonly string _encode32Chars = "0123456789ABCDEFGHIJKLMNOPQRSTUV"; 13 | // Global ID 14 | private static long NextId; 15 | 16 | // Instance components 17 | private string _idString; 18 | internal long IdValue { get; private set; } 19 | 20 | internal string IdString 21 | { 22 | get 23 | { 24 | if (_idString == null) 25 | { 26 | _idString = GenerateGuidString(this); 27 | } 28 | return _idString; 29 | } 30 | } 31 | 32 | // Static constructor to initialize global components 33 | static FastGuid() 34 | { 35 | var guidBytes = Guid.NewGuid().ToByteArray(); 36 | 37 | // Use the first 4 bytes from the Guid to initialize global ID 38 | NextId = 39 | guidBytes[0] << 32 | 40 | guidBytes[1] << 40 | 41 | guidBytes[2] << 48 | 42 | guidBytes[3] << 56; 43 | } 44 | 45 | internal FastGuid(long id) 46 | { 47 | IdValue = id; 48 | } 49 | 50 | internal static FastGuid NewGuid() 51 | { 52 | return new FastGuid(Interlocked.Increment(ref NextId)); 53 | } 54 | 55 | private static unsafe string GenerateGuidString(FastGuid guid) 56 | { 57 | // stackalloc to allocate array on stack rather than heap 58 | char* charBuffer = stackalloc char[13]; 59 | 60 | // ID 61 | charBuffer[0] = _encode32Chars[(int)(guid.IdValue >> 60) & 31]; 62 | charBuffer[1] = _encode32Chars[(int)(guid.IdValue >> 55) & 31]; 63 | charBuffer[2] = _encode32Chars[(int)(guid.IdValue >> 50) & 31]; 64 | charBuffer[3] = _encode32Chars[(int)(guid.IdValue >> 45) & 31]; 65 | charBuffer[4] = _encode32Chars[(int)(guid.IdValue >> 40) & 31]; 66 | charBuffer[5] = _encode32Chars[(int)(guid.IdValue >> 35) & 31]; 67 | charBuffer[6] = _encode32Chars[(int)(guid.IdValue >> 30) & 31]; 68 | charBuffer[7] = _encode32Chars[(int)(guid.IdValue >> 25) & 31]; 69 | charBuffer[8] = _encode32Chars[(int)(guid.IdValue >> 20) & 31]; 70 | charBuffer[9] = _encode32Chars[(int)(guid.IdValue >> 15) & 31]; 71 | charBuffer[10] = _encode32Chars[(int)(guid.IdValue >> 10) & 31]; 72 | charBuffer[11] = _encode32Chars[(int)(guid.IdValue >> 5) & 31]; 73 | charBuffer[12] = _encode32Chars[(int)guid.IdValue & 31]; 74 | 75 | // string ctor overload that takes char* 76 | return new string(charBuffer, 0, 13); 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.ResponseCaching/Internal/ISystemClock.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | 6 | namespace Microsoft.AspNetCore.ResponseCaching.Internal 7 | { 8 | /// 9 | /// Abstracts the system clock to facilitate testing. 10 | /// 11 | internal interface ISystemClock 12 | { 13 | /// 14 | /// Retrieves the current system time in UTC. 15 | /// 16 | DateTimeOffset UtcNow { get; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCache.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Threading.Tasks; 6 | 7 | namespace Microsoft.AspNetCore.ResponseCaching.Internal 8 | { 9 | public interface IResponseCache 10 | { 11 | IResponseCacheEntry Get(string key); 12 | Task GetAsync(string key); 13 | 14 | void Set(string key, IResponseCacheEntry entry, TimeSpan validFor); 15 | Task SetAsync(string key, IResponseCacheEntry entry, TimeSpan validFor); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCacheEntry.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.AspNetCore.ResponseCaching.Internal 5 | { 6 | public interface IResponseCacheEntry 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCachingKeyProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | 6 | namespace Microsoft.AspNetCore.ResponseCaching.Internal 7 | { 8 | public interface IResponseCachingKeyProvider 9 | { 10 | /// 11 | /// Create a base key for a response cache entry. 12 | /// 13 | /// The . 14 | /// The created base key. 15 | string CreateBaseKey(ResponseCachingContext context); 16 | 17 | /// 18 | /// Create a vary key for storing cached responses. 19 | /// 20 | /// The . 21 | /// The created vary key. 22 | string CreateStorageVaryByKey(ResponseCachingContext context); 23 | 24 | /// 25 | /// Create one or more vary keys for looking up cached responses. 26 | /// 27 | /// The . 28 | /// An ordered containing the vary keys to try when looking up items. 29 | IEnumerable CreateLookupVaryByKeys(ResponseCachingContext context); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.ResponseCaching/Internal/Interfaces/IResponseCachingPolicyProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.AspNetCore.ResponseCaching.Internal 5 | { 6 | public interface IResponseCachingPolicyProvider 7 | { 8 | /// 9 | /// Determine whether the response caching logic should be attempted for the incoming HTTP request. 10 | /// 11 | /// The . 12 | /// true if response caching logic should be attempted; otherwise false. 13 | bool AttemptResponseCaching(ResponseCachingContext context); 14 | 15 | /// 16 | /// Determine whether a cache lookup is allowed for the incoming HTTP request. 17 | /// 18 | /// The . 19 | /// true if cache lookup for this request is allowed; otherwise false. 20 | bool AllowCacheLookup(ResponseCachingContext context); 21 | 22 | /// 23 | /// Determine whether storage of the response is allowed for the incoming HTTP request. 24 | /// 25 | /// The . 26 | /// true if storage of the response for this request is allowed; otherwise false. 27 | bool AllowCacheStorage(ResponseCachingContext context); 28 | 29 | /// 30 | /// Determine whether the response received by the middleware can be cached for future requests. 31 | /// 32 | /// The . 33 | /// true if the response is cacheable; otherwise false. 34 | bool IsResponseCacheable(ResponseCachingContext context); 35 | 36 | /// 37 | /// Determine whether the response retrieved from the response cache is fresh and can be served. 38 | /// 39 | /// The . 40 | /// true if the cached entry is fresh; otherwise false. 41 | bool IsCachedEntryFresh(ResponseCachingContext context); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.ResponseCaching/Internal/LoggerExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using Microsoft.Extensions.Logging; 6 | using Microsoft.Net.Http.Headers; 7 | 8 | namespace Microsoft.AspNetCore.ResponseCaching.Internal 9 | { 10 | /// 11 | /// Defines *all* the logger messages produced by response caching 12 | /// 13 | internal static class LoggerExtensions 14 | { 15 | private static Action _logRequestMethodNotCacheable; 16 | private static Action _logRequestWithAuthorizationNotCacheable; 17 | private static Action _logRequestWithNoCacheNotCacheable; 18 | private static Action _logRequestWithPragmaNoCacheNotCacheable; 19 | private static Action _logExpirationMinFreshAdded; 20 | private static Action _logExpirationSharedMaxAgeExceeded; 21 | private static Action _logExpirationMustRevalidate; 22 | private static Action _logExpirationMaxStaleSatisfied; 23 | private static Action _logExpirationMaxAgeExceeded; 24 | private static Action _logExpirationExpiresExceeded; 25 | private static Action _logResponseWithoutPublicNotCacheable; 26 | private static Action _logResponseWithNoStoreNotCacheable; 27 | private static Action _logResponseWithNoCacheNotCacheable; 28 | private static Action _logResponseWithSetCookieNotCacheable; 29 | private static Action _logResponseWithVaryStarNotCacheable; 30 | private static Action _logResponseWithPrivateNotCacheable; 31 | private static Action _logResponseWithUnsuccessfulStatusCodeNotCacheable; 32 | private static Action _logNotModifiedIfNoneMatchStar; 33 | private static Action _logNotModifiedIfNoneMatchMatched; 34 | private static Action _logNotModifiedIfModifiedSinceSatisfied; 35 | private static Action _logNotModifiedServed; 36 | private static Action _logCachedResponseServed; 37 | private static Action _logGatewayTimeoutServed; 38 | private static Action _logNoResponseServed; 39 | private static Action _logVaryByRulesUpdated; 40 | private static Action _logResponseCached; 41 | private static Action _logResponseNotCached; 42 | private static Action _logResponseContentLengthMismatchNotCached; 43 | private static Action _logExpirationInfiniteMaxStaleSatisfied; 44 | 45 | static LoggerExtensions() 46 | { 47 | _logRequestMethodNotCacheable = LoggerMessage.Define( 48 | logLevel: LogLevel.Debug, 49 | eventId: 1, 50 | formatString: "The request cannot be served from cache because it uses the HTTP method: {Method}."); 51 | _logRequestWithAuthorizationNotCacheable = LoggerMessage.Define( 52 | logLevel: LogLevel.Debug, 53 | eventId: 2, 54 | formatString: $"The request cannot be served from cache because it contains an '{HeaderNames.Authorization}' header."); 55 | _logRequestWithNoCacheNotCacheable = LoggerMessage.Define( 56 | logLevel: LogLevel.Debug, 57 | eventId: 3, 58 | formatString: "The request cannot be served from cache because it contains a 'no-cache' cache directive."); 59 | _logRequestWithPragmaNoCacheNotCacheable = LoggerMessage.Define( 60 | logLevel: LogLevel.Debug, 61 | eventId: 4, 62 | formatString: "The request cannot be served from cache because it contains a 'no-cache' pragma directive."); 63 | _logExpirationMinFreshAdded = LoggerMessage.Define( 64 | logLevel: LogLevel.Debug, 65 | eventId: 5, 66 | formatString: "Adding a minimum freshness requirement of {Duration} specified by the 'min-fresh' cache directive."); 67 | _logExpirationSharedMaxAgeExceeded = LoggerMessage.Define( 68 | logLevel: LogLevel.Debug, 69 | eventId: 6, 70 | formatString: "The age of the entry is {Age} and has exceeded the maximum age for shared caches of {SharedMaxAge} specified by the 's-maxage' cache directive."); 71 | _logExpirationMustRevalidate = LoggerMessage.Define( 72 | logLevel: LogLevel.Debug, 73 | eventId: 7, 74 | formatString: "The age of the entry is {Age} and has exceeded the maximum age of {MaxAge} specified by the 'max-age' cache directive. It must be revalidated because the 'must-revalidate' or 'proxy-revalidate' cache directive is specified."); 75 | _logExpirationMaxStaleSatisfied = LoggerMessage.Define( 76 | logLevel: LogLevel.Debug, 77 | eventId: 8, 78 | formatString: "The age of the entry is {Age} and has exceeded the maximum age of {MaxAge} specified by the 'max-age' cache directive. However, it satisfied the maximum stale allowance of {MaxStale} specified by the 'max-stale' cache directive."); 79 | _logExpirationMaxAgeExceeded = LoggerMessage.Define( 80 | logLevel: LogLevel.Debug, 81 | eventId: 9, 82 | formatString: "The age of the entry is {Age} and has exceeded the maximum age of {MaxAge} specified by the 'max-age' cache directive."); 83 | _logExpirationExpiresExceeded = LoggerMessage.Define( 84 | logLevel: LogLevel.Debug, 85 | eventId: 10, 86 | formatString: $"The response time of the entry is {{ResponseTime}} and has exceeded the expiry date of {{Expired}} specified by the '{HeaderNames.Expires}' header."); 87 | _logResponseWithoutPublicNotCacheable = LoggerMessage.Define( 88 | logLevel: LogLevel.Debug, 89 | eventId: 11, 90 | formatString: "Response is not cacheable because it does not contain the 'public' cache directive."); 91 | _logResponseWithNoStoreNotCacheable = LoggerMessage.Define( 92 | logLevel: LogLevel.Debug, 93 | eventId: 12, 94 | formatString: "Response is not cacheable because it or its corresponding request contains a 'no-store' cache directive."); 95 | _logResponseWithNoCacheNotCacheable = LoggerMessage.Define( 96 | logLevel: LogLevel.Debug, 97 | eventId: 13, 98 | formatString: "Response is not cacheable because it contains a 'no-cache' cache directive."); 99 | _logResponseWithSetCookieNotCacheable = LoggerMessage.Define( 100 | logLevel: LogLevel.Debug, 101 | eventId: 14, 102 | formatString: $"Response is not cacheable because it contains a '{HeaderNames.SetCookie}' header."); 103 | _logResponseWithVaryStarNotCacheable = LoggerMessage.Define( 104 | logLevel: LogLevel.Debug, 105 | eventId: 15, 106 | formatString: $"Response is not cacheable because it contains a '{HeaderNames.Vary}' header with a value of *."); 107 | _logResponseWithPrivateNotCacheable = LoggerMessage.Define( 108 | logLevel: LogLevel.Debug, 109 | eventId: 16, 110 | formatString: "Response is not cacheable because it contains the 'private' cache directive."); 111 | _logResponseWithUnsuccessfulStatusCodeNotCacheable = LoggerMessage.Define( 112 | logLevel: LogLevel.Debug, 113 | eventId: 17, 114 | formatString: "Response is not cacheable because its status code {StatusCode} does not indicate success."); 115 | _logNotModifiedIfNoneMatchStar = LoggerMessage.Define( 116 | logLevel: LogLevel.Debug, 117 | eventId: 18, 118 | formatString: $"The '{HeaderNames.IfNoneMatch}' header of the request contains a value of *."); 119 | _logNotModifiedIfNoneMatchMatched = LoggerMessage.Define( 120 | logLevel: LogLevel.Debug, 121 | eventId: 19, 122 | formatString: $"The ETag {{ETag}} in the '{HeaderNames.IfNoneMatch}' header matched the ETag of a cached entry."); 123 | _logNotModifiedIfModifiedSinceSatisfied = LoggerMessage.Define( 124 | logLevel: LogLevel.Debug, 125 | eventId: 20, 126 | formatString: $"The last modified date of {{LastModified}} is before the date {{IfModifiedSince}} specified in the '{HeaderNames.IfModifiedSince}' header."); 127 | _logNotModifiedServed = LoggerMessage.Define( 128 | logLevel: LogLevel.Information, 129 | eventId: 21, 130 | formatString: "The content requested has not been modified."); 131 | _logCachedResponseServed = LoggerMessage.Define( 132 | logLevel: LogLevel.Information, 133 | eventId: 22, 134 | formatString: "Serving response from cache."); 135 | _logGatewayTimeoutServed = LoggerMessage.Define( 136 | logLevel: LogLevel.Information, 137 | eventId: 23, 138 | formatString: "No cached response available for this request and the 'only-if-cached' cache directive was specified."); 139 | _logNoResponseServed = LoggerMessage.Define( 140 | logLevel: LogLevel.Information, 141 | eventId: 24, 142 | formatString: "No cached response available for this request."); 143 | _logVaryByRulesUpdated = LoggerMessage.Define( 144 | logLevel: LogLevel.Debug, 145 | eventId: 25, 146 | formatString: "Vary by rules were updated. Headers: {Headers}, Query keys: {QueryKeys}"); 147 | _logResponseCached = LoggerMessage.Define( 148 | logLevel: LogLevel.Information, 149 | eventId: 26, 150 | formatString: "The response has been cached."); 151 | _logResponseNotCached = LoggerMessage.Define( 152 | logLevel: LogLevel.Information, 153 | eventId: 27, 154 | formatString: "The response could not be cached for this request."); 155 | _logResponseContentLengthMismatchNotCached = LoggerMessage.Define( 156 | logLevel: LogLevel.Warning, 157 | eventId: 28, 158 | formatString: $"The response could not be cached for this request because the '{HeaderNames.ContentLength}' did not match the body length."); 159 | _logExpirationInfiniteMaxStaleSatisfied = LoggerMessage.Define( 160 | logLevel: LogLevel.Debug, 161 | eventId: 29, 162 | formatString: "The age of the entry is {Age} and has exceeded the maximum age of {MaxAge} specified by the 'max-age' cache directive. However, the 'max-stale' cache directive was specified without an assigned value and a stale response of any age is accepted."); 163 | } 164 | 165 | internal static void LogRequestMethodNotCacheable(this ILogger logger, string method) 166 | { 167 | _logRequestMethodNotCacheable(logger, method, null); 168 | } 169 | 170 | internal static void LogRequestWithAuthorizationNotCacheable(this ILogger logger) 171 | { 172 | _logRequestWithAuthorizationNotCacheable(logger, null); 173 | } 174 | 175 | internal static void LogRequestWithNoCacheNotCacheable(this ILogger logger) 176 | { 177 | _logRequestWithNoCacheNotCacheable(logger, null); 178 | } 179 | 180 | internal static void LogRequestWithPragmaNoCacheNotCacheable(this ILogger logger) 181 | { 182 | _logRequestWithPragmaNoCacheNotCacheable(logger, null); 183 | } 184 | 185 | internal static void LogExpirationMinFreshAdded(this ILogger logger, TimeSpan duration) 186 | { 187 | _logExpirationMinFreshAdded(logger, duration, null); 188 | } 189 | 190 | internal static void LogExpirationSharedMaxAgeExceeded(this ILogger logger, TimeSpan age, TimeSpan sharedMaxAge) 191 | { 192 | _logExpirationSharedMaxAgeExceeded(logger, age, sharedMaxAge, null); 193 | } 194 | 195 | internal static void LogExpirationMustRevalidate(this ILogger logger, TimeSpan age, TimeSpan maxAge) 196 | { 197 | _logExpirationMustRevalidate(logger, age, maxAge, null); 198 | } 199 | 200 | internal static void LogExpirationMaxStaleSatisfied(this ILogger logger, TimeSpan age, TimeSpan maxAge, TimeSpan maxStale) 201 | { 202 | _logExpirationMaxStaleSatisfied(logger, age, maxAge, maxStale, null); 203 | } 204 | 205 | internal static void LogExpirationMaxAgeExceeded(this ILogger logger, TimeSpan age, TimeSpan sharedMaxAge) 206 | { 207 | _logExpirationMaxAgeExceeded(logger, age, sharedMaxAge, null); 208 | } 209 | 210 | internal static void LogExpirationExpiresExceeded(this ILogger logger, DateTimeOffset responseTime, DateTimeOffset expires) 211 | { 212 | _logExpirationExpiresExceeded(logger, responseTime, expires, null); 213 | } 214 | 215 | internal static void LogResponseWithoutPublicNotCacheable(this ILogger logger) 216 | { 217 | _logResponseWithoutPublicNotCacheable(logger, null); 218 | } 219 | 220 | internal static void LogResponseWithNoStoreNotCacheable(this ILogger logger) 221 | { 222 | _logResponseWithNoStoreNotCacheable(logger, null); 223 | } 224 | 225 | internal static void LogResponseWithNoCacheNotCacheable(this ILogger logger) 226 | { 227 | _logResponseWithNoCacheNotCacheable(logger, null); 228 | } 229 | 230 | internal static void LogResponseWithSetCookieNotCacheable(this ILogger logger) 231 | { 232 | _logResponseWithSetCookieNotCacheable(logger, null); 233 | } 234 | 235 | internal static void LogResponseWithVaryStarNotCacheable(this ILogger logger) 236 | { 237 | _logResponseWithVaryStarNotCacheable(logger, null); 238 | } 239 | 240 | internal static void LogResponseWithPrivateNotCacheable(this ILogger logger) 241 | { 242 | _logResponseWithPrivateNotCacheable(logger, null); 243 | } 244 | 245 | internal static void LogResponseWithUnsuccessfulStatusCodeNotCacheable(this ILogger logger, int statusCode) 246 | { 247 | _logResponseWithUnsuccessfulStatusCodeNotCacheable(logger, statusCode, null); 248 | } 249 | 250 | internal static void LogNotModifiedIfNoneMatchStar(this ILogger logger) 251 | { 252 | _logNotModifiedIfNoneMatchStar(logger, null); 253 | } 254 | 255 | internal static void LogNotModifiedIfNoneMatchMatched(this ILogger logger, EntityTagHeaderValue etag) 256 | { 257 | _logNotModifiedIfNoneMatchMatched(logger, etag, null); 258 | } 259 | 260 | internal static void LogNotModifiedIfModifiedSinceSatisfied(this ILogger logger, DateTimeOffset lastModified, DateTimeOffset ifModifiedSince) 261 | { 262 | _logNotModifiedIfModifiedSinceSatisfied(logger, lastModified, ifModifiedSince, null); 263 | } 264 | 265 | internal static void LogNotModifiedServed(this ILogger logger) 266 | { 267 | _logNotModifiedServed(logger, null); 268 | } 269 | 270 | internal static void LogCachedResponseServed(this ILogger logger) 271 | { 272 | _logCachedResponseServed(logger, null); 273 | } 274 | 275 | internal static void LogGatewayTimeoutServed(this ILogger logger) 276 | { 277 | _logGatewayTimeoutServed(logger, null); 278 | } 279 | 280 | internal static void LogNoResponseServed(this ILogger logger) 281 | { 282 | _logNoResponseServed(logger, null); 283 | } 284 | 285 | internal static void LogVaryByRulesUpdated(this ILogger logger, string headers, string queryKeys) 286 | { 287 | _logVaryByRulesUpdated(logger, headers, queryKeys, null); 288 | } 289 | 290 | internal static void LogResponseCached(this ILogger logger) 291 | { 292 | _logResponseCached(logger, null); 293 | } 294 | 295 | internal static void LogResponseNotCached(this ILogger logger) 296 | { 297 | _logResponseNotCached(logger, null); 298 | } 299 | 300 | internal static void LogResponseContentLengthMismatchNotCached(this ILogger logger) 301 | { 302 | _logResponseContentLengthMismatchNotCached(logger, null); 303 | } 304 | 305 | internal static void LogExpirationInfiniteMaxStaleSatisfied(this ILogger logger, TimeSpan age, TimeSpan maxAge) 306 | { 307 | _logExpirationInfiniteMaxStaleSatisfied(logger, age, maxAge, null); 308 | } 309 | } 310 | } 311 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryCachedResponse.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using Microsoft.AspNetCore.Http; 7 | 8 | namespace Microsoft.AspNetCore.ResponseCaching.Internal 9 | { 10 | internal class MemoryCachedResponse 11 | { 12 | public DateTimeOffset Created { get; set; } 13 | 14 | public int StatusCode { get; set; } 15 | 16 | public IHeaderDictionary Headers { get; set; } = new HeaderDictionary(); 17 | 18 | public List BodySegments { get; set; } 19 | 20 | public long BodyLength { get; set; } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.ResponseCaching/Internal/MemoryResponseCache.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Threading.Tasks; 6 | using Microsoft.Extensions.Caching.Memory; 7 | 8 | namespace Microsoft.AspNetCore.ResponseCaching.Internal 9 | { 10 | public class MemoryResponseCache : IResponseCache 11 | { 12 | private readonly IMemoryCache _cache; 13 | 14 | public MemoryResponseCache(IMemoryCache cache) 15 | { 16 | if (cache == null) 17 | { 18 | throw new ArgumentNullException(nameof(cache)); 19 | } 20 | 21 | _cache = cache; 22 | } 23 | 24 | public IResponseCacheEntry Get(string key) 25 | { 26 | var entry = _cache.Get(key); 27 | 28 | var memoryCachedResponse = entry as MemoryCachedResponse; 29 | if (memoryCachedResponse != null) 30 | { 31 | return new CachedResponse 32 | { 33 | Created = memoryCachedResponse.Created, 34 | StatusCode = memoryCachedResponse.StatusCode, 35 | Headers = memoryCachedResponse.Headers, 36 | Body = new SegmentReadStream(memoryCachedResponse.BodySegments, memoryCachedResponse.BodyLength) 37 | }; 38 | } 39 | else 40 | { 41 | return entry as IResponseCacheEntry; 42 | } 43 | } 44 | 45 | public Task GetAsync(string key) 46 | { 47 | return Task.FromResult(Get(key)); 48 | } 49 | 50 | public void Set(string key, IResponseCacheEntry entry, TimeSpan validFor) 51 | { 52 | var cachedResponse = entry as CachedResponse; 53 | if (cachedResponse != null) 54 | { 55 | var segmentStream = new SegmentWriteStream(StreamUtilities.BodySegmentSize); 56 | cachedResponse.Body.CopyTo(segmentStream); 57 | 58 | _cache.Set( 59 | key, 60 | new MemoryCachedResponse 61 | { 62 | Created = cachedResponse.Created, 63 | StatusCode = cachedResponse.StatusCode, 64 | Headers = cachedResponse.Headers, 65 | BodySegments = segmentStream.GetSegments(), 66 | BodyLength = segmentStream.Length 67 | }, 68 | new MemoryCacheEntryOptions 69 | { 70 | AbsoluteExpirationRelativeToNow = validFor, 71 | Size = CacheEntryHelpers.EstimateCachedResponseSize(cachedResponse) 72 | }); 73 | } 74 | else 75 | { 76 | _cache.Set( 77 | key, 78 | entry, 79 | new MemoryCacheEntryOptions 80 | { 81 | AbsoluteExpirationRelativeToNow = validFor, 82 | Size = CacheEntryHelpers.EstimateCachedVaryByRulesySize(entry as CachedVaryByRules) 83 | }); 84 | } 85 | } 86 | 87 | public Task SetAsync(string key, IResponseCacheEntry entry, TimeSpan validFor) 88 | { 89 | Set(key, entry, validFor); 90 | return Task.CompletedTask; 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingContext.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.IO; 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.AspNetCore.Http.Features; 8 | using Microsoft.Extensions.Logging; 9 | using Microsoft.Net.Http.Headers; 10 | 11 | namespace Microsoft.AspNetCore.ResponseCaching.Internal 12 | { 13 | public class ResponseCachingContext 14 | { 15 | private DateTimeOffset? _responseDate; 16 | private bool _parsedResponseDate; 17 | private DateTimeOffset? _responseExpires; 18 | private bool _parsedResponseExpires; 19 | private TimeSpan? _responseSharedMaxAge; 20 | private bool _parsedResponseSharedMaxAge; 21 | private TimeSpan? _responseMaxAge; 22 | private bool _parsedResponseMaxAge; 23 | 24 | internal ResponseCachingContext(HttpContext httpContext, ILogger logger) 25 | { 26 | HttpContext = httpContext; 27 | Logger = logger; 28 | } 29 | 30 | public HttpContext HttpContext { get; } 31 | 32 | public DateTimeOffset? ResponseTime { get; internal set; } 33 | 34 | public TimeSpan? CachedEntryAge { get; internal set; } 35 | 36 | public CachedVaryByRules CachedVaryByRules { get; internal set; } 37 | 38 | internal ILogger Logger { get; } 39 | 40 | internal bool ShouldCacheResponse { get; set; } 41 | 42 | internal string BaseKey { get; set; } 43 | 44 | internal string StorageVaryKey { get; set; } 45 | 46 | internal TimeSpan CachedResponseValidFor { get; set; } 47 | 48 | internal CachedResponse CachedResponse { get; set; } 49 | 50 | internal bool ResponseStarted { get; set; } 51 | 52 | internal Stream OriginalResponseStream { get; set; } 53 | 54 | internal ResponseCachingStream ResponseCachingStream { get; set; } 55 | 56 | internal IHttpSendFileFeature OriginalSendFileFeature { get; set; } 57 | 58 | internal IHeaderDictionary CachedResponseHeaders { get; set; } 59 | 60 | internal DateTimeOffset? ResponseDate 61 | { 62 | get 63 | { 64 | if (!_parsedResponseDate) 65 | { 66 | _parsedResponseDate = true; 67 | DateTimeOffset date; 68 | if (HeaderUtilities.TryParseDate(HttpContext.Response.Headers[HeaderNames.Date].ToString(), out date)) 69 | { 70 | _responseDate = date; 71 | } 72 | else 73 | { 74 | _responseDate = null; 75 | } 76 | } 77 | return _responseDate; 78 | } 79 | set 80 | { 81 | // Don't reparse the response date again if it's explicitly set 82 | _parsedResponseDate = true; 83 | _responseDate = value; 84 | } 85 | } 86 | 87 | internal DateTimeOffset? ResponseExpires 88 | { 89 | get 90 | { 91 | if (!_parsedResponseExpires) 92 | { 93 | _parsedResponseExpires = true; 94 | DateTimeOffset expires; 95 | if (HeaderUtilities.TryParseDate(HttpContext.Response.Headers[HeaderNames.Expires].ToString(), out expires)) 96 | { 97 | _responseExpires = expires; 98 | } 99 | else 100 | { 101 | _responseExpires = null; 102 | } 103 | } 104 | return _responseExpires; 105 | } 106 | } 107 | 108 | internal TimeSpan? ResponseSharedMaxAge 109 | { 110 | get 111 | { 112 | if (!_parsedResponseSharedMaxAge) 113 | { 114 | _parsedResponseSharedMaxAge = true; 115 | HeaderUtilities.TryParseSeconds(HttpContext.Response.Headers[HeaderNames.CacheControl], CacheControlHeaderValue.SharedMaxAgeString, out _responseSharedMaxAge); 116 | } 117 | return _responseSharedMaxAge; 118 | } 119 | } 120 | 121 | internal TimeSpan? ResponseMaxAge 122 | { 123 | get 124 | { 125 | if (!_parsedResponseMaxAge) 126 | { 127 | _parsedResponseMaxAge = true; 128 | HeaderUtilities.TryParseSeconds(HttpContext.Response.Headers[HeaderNames.CacheControl], CacheControlHeaderValue.MaxAgeString, out _responseMaxAge); 129 | } 130 | return _responseMaxAge; 131 | } 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingKeyProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Text; 8 | using Microsoft.Extensions.ObjectPool; 9 | using Microsoft.Extensions.Options; 10 | using Microsoft.Extensions.Primitives; 11 | 12 | namespace Microsoft.AspNetCore.ResponseCaching.Internal 13 | { 14 | public class ResponseCachingKeyProvider : IResponseCachingKeyProvider 15 | { 16 | // Use the record separator for delimiting components of the cache key to avoid possible collisions 17 | private static readonly char KeyDelimiter = '\x1e'; 18 | // Use the unit separator for delimiting subcomponents of the cache key to avoid possible collisions 19 | private static readonly char KeySubDelimiter = '\x1f'; 20 | 21 | private readonly ObjectPool _builderPool; 22 | private readonly ResponseCachingOptions _options; 23 | 24 | public ResponseCachingKeyProvider(ObjectPoolProvider poolProvider, IOptions options) 25 | { 26 | if (poolProvider == null) 27 | { 28 | throw new ArgumentNullException(nameof(poolProvider)); 29 | } 30 | if (options == null) 31 | { 32 | throw new ArgumentNullException(nameof(options)); 33 | } 34 | 35 | _builderPool = poolProvider.CreateStringBuilderPool(); 36 | _options = options.Value; 37 | } 38 | 39 | public IEnumerable CreateLookupVaryByKeys(ResponseCachingContext context) 40 | { 41 | return new string[] { CreateStorageVaryByKey(context) }; 42 | } 43 | 44 | // GETSCHEMEHOST:PORT/PATHBASE/PATH 45 | public string CreateBaseKey(ResponseCachingContext context) 46 | { 47 | if (context == null) 48 | { 49 | throw new ArgumentNullException(nameof(context)); 50 | } 51 | 52 | var request = context.HttpContext.Request; 53 | var builder = _builderPool.Get(); 54 | 55 | try 56 | { 57 | builder 58 | .AppendUpperInvariant(request.Method) 59 | .Append(KeyDelimiter) 60 | .AppendUpperInvariant(request.Scheme) 61 | .Append(KeyDelimiter) 62 | .AppendUpperInvariant(request.Host.Value); 63 | 64 | if (_options.UseCaseSensitivePaths) 65 | { 66 | builder 67 | .Append(request.PathBase.Value) 68 | .Append(request.Path.Value); 69 | } 70 | else 71 | { 72 | builder 73 | .AppendUpperInvariant(request.PathBase.Value) 74 | .AppendUpperInvariant(request.Path.Value); 75 | } 76 | 77 | return builder.ToString(); 78 | } 79 | finally 80 | { 81 | _builderPool.Return(builder); 82 | } 83 | } 84 | 85 | // BaseKeyHHeaderName=HeaderValueQQueryName=QueryValue1QueryValue2 86 | public string CreateStorageVaryByKey(ResponseCachingContext context) 87 | { 88 | if (context == null) 89 | { 90 | throw new ArgumentNullException(nameof(context)); 91 | } 92 | 93 | var varyByRules = context.CachedVaryByRules; 94 | if (varyByRules == null) 95 | { 96 | throw new InvalidOperationException($"{nameof(CachedVaryByRules)} must not be null on the {nameof(ResponseCachingContext)}"); 97 | } 98 | 99 | if ((StringValues.IsNullOrEmpty(varyByRules.Headers) && StringValues.IsNullOrEmpty(varyByRules.QueryKeys))) 100 | { 101 | return varyByRules.VaryByKeyPrefix; 102 | } 103 | 104 | var request = context.HttpContext.Request; 105 | var builder = _builderPool.Get(); 106 | 107 | try 108 | { 109 | // Prepend with the Guid of the CachedVaryByRules 110 | builder.Append(varyByRules.VaryByKeyPrefix); 111 | 112 | // Vary by headers 113 | if (varyByRules?.Headers.Count > 0) 114 | { 115 | // Append a group separator for the header segment of the cache key 116 | builder.Append(KeyDelimiter) 117 | .Append('H'); 118 | 119 | for (var i = 0; i < varyByRules.Headers.Count; i++) 120 | { 121 | var header = varyByRules.Headers[i]; 122 | var headerValues = context.HttpContext.Request.Headers[header]; 123 | builder.Append(KeyDelimiter) 124 | .Append(header) 125 | .Append("="); 126 | 127 | var headerValuesArray = headerValues.ToArray(); 128 | Array.Sort(headerValuesArray, StringComparer.Ordinal); 129 | 130 | for (var j = 0; j < headerValuesArray.Length; j++) 131 | { 132 | builder.Append(headerValuesArray[j]); 133 | } 134 | } 135 | } 136 | 137 | // Vary by query keys 138 | if (varyByRules?.QueryKeys.Count > 0) 139 | { 140 | // Append a group separator for the query key segment of the cache key 141 | builder.Append(KeyDelimiter) 142 | .Append('Q'); 143 | 144 | if (varyByRules.QueryKeys.Count == 1 && string.Equals(varyByRules.QueryKeys[0], "*", StringComparison.Ordinal)) 145 | { 146 | // Vary by all available query keys 147 | var queryArray = context.HttpContext.Request.Query.ToArray(); 148 | // Query keys are aggregated case-insensitively whereas the query values are compared ordinally. 149 | Array.Sort(queryArray, QueryKeyComparer.OrdinalIgnoreCase); 150 | 151 | for (var i = 0; i < queryArray.Length; i++) 152 | { 153 | builder.Append(KeyDelimiter) 154 | .AppendUpperInvariant(queryArray[i].Key) 155 | .Append("="); 156 | 157 | var queryValueArray = queryArray[i].Value.ToArray(); 158 | Array.Sort(queryValueArray, StringComparer.Ordinal); 159 | 160 | for (var j = 0; j < queryValueArray.Length; j++) 161 | { 162 | if (j > 0) 163 | { 164 | builder.Append(KeySubDelimiter); 165 | } 166 | 167 | builder.Append(queryValueArray[j]); 168 | } 169 | } 170 | } 171 | else 172 | { 173 | for (var i = 0; i < varyByRules.QueryKeys.Count; i++) 174 | { 175 | var queryKey = varyByRules.QueryKeys[i]; 176 | var queryKeyValues = context.HttpContext.Request.Query[queryKey]; 177 | builder.Append(KeyDelimiter) 178 | .Append(queryKey) 179 | .Append("="); 180 | 181 | var queryValueArray = queryKeyValues.ToArray(); 182 | Array.Sort(queryValueArray, StringComparer.Ordinal); 183 | 184 | for (var j = 0; j < queryValueArray.Length; j++) 185 | { 186 | if (j > 0) 187 | { 188 | builder.Append(KeySubDelimiter); 189 | } 190 | 191 | builder.Append(queryValueArray[j]); 192 | } 193 | } 194 | } 195 | } 196 | 197 | return builder.ToString(); 198 | } 199 | finally 200 | { 201 | _builderPool.Return(builder); 202 | } 203 | } 204 | 205 | private class QueryKeyComparer : IComparer> 206 | { 207 | private StringComparer _stringComparer; 208 | 209 | public static QueryKeyComparer OrdinalIgnoreCase { get; } = new QueryKeyComparer(StringComparer.OrdinalIgnoreCase); 210 | 211 | public QueryKeyComparer(StringComparer stringComparer) 212 | { 213 | _stringComparer = stringComparer; 214 | } 215 | 216 | public int Compare(KeyValuePair x, KeyValuePair y) => _stringComparer.Compare(x.Key, y.Key); 217 | } 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.ResponseCaching/Internal/ResponseCachingPolicyProvider.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.Extensions.Primitives; 7 | using Microsoft.Net.Http.Headers; 8 | 9 | namespace Microsoft.AspNetCore.ResponseCaching.Internal 10 | { 11 | public class ResponseCachingPolicyProvider : IResponseCachingPolicyProvider 12 | { 13 | public virtual bool AttemptResponseCaching(ResponseCachingContext context) 14 | { 15 | var request = context.HttpContext.Request; 16 | 17 | // Verify the method 18 | if (!HttpMethods.IsGet(request.Method) && !HttpMethods.IsHead(request.Method)) 19 | { 20 | context.Logger.LogRequestMethodNotCacheable(request.Method); 21 | return false; 22 | } 23 | 24 | // Verify existence of authorization headers 25 | if (!StringValues.IsNullOrEmpty(request.Headers[HeaderNames.Authorization])) 26 | { 27 | context.Logger.LogRequestWithAuthorizationNotCacheable(); 28 | return false; 29 | } 30 | 31 | return true; 32 | } 33 | 34 | public virtual bool AllowCacheLookup(ResponseCachingContext context) 35 | { 36 | var request = context.HttpContext.Request; 37 | 38 | // Verify request cache-control parameters 39 | if (!StringValues.IsNullOrEmpty(request.Headers[HeaderNames.CacheControl])) 40 | { 41 | if (HeaderUtilities.ContainsCacheDirective(request.Headers[HeaderNames.CacheControl], CacheControlHeaderValue.NoCacheString)) 42 | { 43 | context.Logger.LogRequestWithNoCacheNotCacheable(); 44 | return false; 45 | } 46 | } 47 | else 48 | { 49 | // Support for legacy HTTP 1.0 cache directive 50 | var pragmaHeaderValues = request.Headers[HeaderNames.Pragma]; 51 | if (HeaderUtilities.ContainsCacheDirective(request.Headers[HeaderNames.Pragma], CacheControlHeaderValue.NoCacheString)) 52 | { 53 | context.Logger.LogRequestWithPragmaNoCacheNotCacheable(); 54 | return false; 55 | } 56 | } 57 | 58 | return true; 59 | } 60 | 61 | public virtual bool AllowCacheStorage(ResponseCachingContext context) 62 | { 63 | // Check request no-store 64 | return !HeaderUtilities.ContainsCacheDirective(context.HttpContext.Request.Headers[HeaderNames.CacheControl], CacheControlHeaderValue.NoStoreString); 65 | } 66 | 67 | public virtual bool IsResponseCacheable(ResponseCachingContext context) 68 | { 69 | var responseCacheControlHeader = context.HttpContext.Response.Headers[HeaderNames.CacheControl]; 70 | 71 | // Only cache pages explicitly marked with public 72 | if (!HeaderUtilities.ContainsCacheDirective(responseCacheControlHeader, CacheControlHeaderValue.PublicString)) 73 | { 74 | context.Logger.LogResponseWithoutPublicNotCacheable(); 75 | return false; 76 | } 77 | 78 | // Check response no-store 79 | if (HeaderUtilities.ContainsCacheDirective(responseCacheControlHeader, CacheControlHeaderValue.NoStoreString)) 80 | { 81 | context.Logger.LogResponseWithNoStoreNotCacheable(); 82 | return false; 83 | } 84 | 85 | // Check no-cache 86 | if (HeaderUtilities.ContainsCacheDirective(responseCacheControlHeader, CacheControlHeaderValue.NoCacheString)) 87 | { 88 | context.Logger.LogResponseWithNoCacheNotCacheable(); 89 | return false; 90 | } 91 | 92 | var response = context.HttpContext.Response; 93 | 94 | // Do not cache responses with Set-Cookie headers 95 | if (!StringValues.IsNullOrEmpty(response.Headers[HeaderNames.SetCookie])) 96 | { 97 | context.Logger.LogResponseWithSetCookieNotCacheable(); 98 | return false; 99 | } 100 | 101 | // Do not cache responses varying by * 102 | var varyHeader = response.Headers[HeaderNames.Vary]; 103 | if (varyHeader.Count == 1 && string.Equals(varyHeader, "*", StringComparison.OrdinalIgnoreCase)) 104 | { 105 | context.Logger.LogResponseWithVaryStarNotCacheable(); 106 | return false; 107 | } 108 | 109 | // Check private 110 | if (HeaderUtilities.ContainsCacheDirective(responseCacheControlHeader, CacheControlHeaderValue.PrivateString)) 111 | { 112 | context.Logger.LogResponseWithPrivateNotCacheable(); 113 | return false; 114 | } 115 | 116 | // Check response code 117 | if (response.StatusCode != StatusCodes.Status200OK) 118 | { 119 | context.Logger.LogResponseWithUnsuccessfulStatusCodeNotCacheable(response.StatusCode); 120 | return false; 121 | } 122 | 123 | // Check response freshness 124 | if (!context.ResponseDate.HasValue) 125 | { 126 | if (!context.ResponseSharedMaxAge.HasValue && 127 | !context.ResponseMaxAge.HasValue && 128 | context.ResponseTime.Value >= context.ResponseExpires) 129 | { 130 | context.Logger.LogExpirationExpiresExceeded(context.ResponseTime.Value, context.ResponseExpires.Value); 131 | return false; 132 | } 133 | } 134 | else 135 | { 136 | var age = context.ResponseTime.Value - context.ResponseDate.Value; 137 | 138 | // Validate shared max age 139 | if (age >= context.ResponseSharedMaxAge) 140 | { 141 | context.Logger.LogExpirationSharedMaxAgeExceeded(age, context.ResponseSharedMaxAge.Value); 142 | return false; 143 | } 144 | else if (!context.ResponseSharedMaxAge.HasValue) 145 | { 146 | // Validate max age 147 | if (age >= context.ResponseMaxAge) 148 | { 149 | context.Logger.LogExpirationMaxAgeExceeded(age, context.ResponseMaxAge.Value); 150 | return false; 151 | } 152 | else if (!context.ResponseMaxAge.HasValue) 153 | { 154 | // Validate expiration 155 | if (context.ResponseTime.Value >= context.ResponseExpires) 156 | { 157 | context.Logger.LogExpirationExpiresExceeded(context.ResponseTime.Value, context.ResponseExpires.Value); 158 | return false; 159 | } 160 | } 161 | } 162 | } 163 | 164 | return true; 165 | } 166 | 167 | public virtual bool IsCachedEntryFresh(ResponseCachingContext context) 168 | { 169 | var age = context.CachedEntryAge.Value; 170 | var cachedCacheControlHeaders = context.CachedResponseHeaders[HeaderNames.CacheControl]; 171 | var requestCacheControlHeaders = context.HttpContext.Request.Headers[HeaderNames.CacheControl]; 172 | 173 | // Add min-fresh requirements 174 | TimeSpan? minFresh; 175 | if (HeaderUtilities.TryParseSeconds(requestCacheControlHeaders, CacheControlHeaderValue.MinFreshString, out minFresh)) 176 | { 177 | age += minFresh.Value; 178 | context.Logger.LogExpirationMinFreshAdded(minFresh.Value); 179 | } 180 | 181 | // Validate shared max age, this overrides any max age settings for shared caches 182 | TimeSpan? cachedSharedMaxAge; 183 | HeaderUtilities.TryParseSeconds(cachedCacheControlHeaders, CacheControlHeaderValue.SharedMaxAgeString, out cachedSharedMaxAge); 184 | 185 | if (age >= cachedSharedMaxAge) 186 | { 187 | // shared max age implies must revalidate 188 | context.Logger.LogExpirationSharedMaxAgeExceeded(age, cachedSharedMaxAge.Value); 189 | return false; 190 | } 191 | else if (!cachedSharedMaxAge.HasValue) 192 | { 193 | TimeSpan? requestMaxAge; 194 | HeaderUtilities.TryParseSeconds(requestCacheControlHeaders, CacheControlHeaderValue.MaxAgeString, out requestMaxAge); 195 | 196 | TimeSpan? cachedMaxAge; 197 | HeaderUtilities.TryParseSeconds(cachedCacheControlHeaders, CacheControlHeaderValue.MaxAgeString, out cachedMaxAge); 198 | 199 | var lowestMaxAge = cachedMaxAge < requestMaxAge ? cachedMaxAge : requestMaxAge ?? cachedMaxAge; 200 | // Validate max age 201 | if (age >= lowestMaxAge) 202 | { 203 | // Must revalidate or proxy revalidate 204 | if (HeaderUtilities.ContainsCacheDirective(cachedCacheControlHeaders, CacheControlHeaderValue.MustRevalidateString) 205 | || HeaderUtilities.ContainsCacheDirective(cachedCacheControlHeaders, CacheControlHeaderValue.ProxyRevalidateString)) 206 | { 207 | context.Logger.LogExpirationMustRevalidate(age, lowestMaxAge.Value); 208 | return false; 209 | } 210 | 211 | TimeSpan? requestMaxStale; 212 | var maxStaleExist = HeaderUtilities.ContainsCacheDirective(requestCacheControlHeaders, CacheControlHeaderValue.MaxStaleString); 213 | HeaderUtilities.TryParseSeconds(requestCacheControlHeaders, CacheControlHeaderValue.MaxStaleString, out requestMaxStale); 214 | 215 | // Request allows stale values with no age limit 216 | if (maxStaleExist && !requestMaxStale.HasValue) 217 | { 218 | context.Logger.LogExpirationInfiniteMaxStaleSatisfied(age, lowestMaxAge.Value); 219 | return true; 220 | } 221 | 222 | // Request allows stale values with age limit 223 | if (requestMaxStale.HasValue && age - lowestMaxAge < requestMaxStale) 224 | { 225 | context.Logger.LogExpirationMaxStaleSatisfied(age, lowestMaxAge.Value, requestMaxStale.Value); 226 | return true; 227 | } 228 | 229 | context.Logger.LogExpirationMaxAgeExceeded(age, lowestMaxAge.Value); 230 | return false; 231 | } 232 | else if (!cachedMaxAge.HasValue && !requestMaxAge.HasValue) 233 | { 234 | // Validate expiration 235 | DateTimeOffset expires; 236 | if (HeaderUtilities.TryParseDate(context.CachedResponseHeaders[HeaderNames.Expires].ToString(), out expires) && 237 | context.ResponseTime.Value >= expires) 238 | { 239 | context.Logger.LogExpirationExpiresExceeded(context.ResponseTime.Value, expires); 240 | return false; 241 | } 242 | } 243 | } 244 | 245 | return true; 246 | } 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.ResponseCaching/Internal/SendFileFeatureWrapper.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Http.Features; 7 | 8 | namespace Microsoft.AspNetCore.ResponseCaching.Internal 9 | { 10 | internal class SendFileFeatureWrapper : IHttpSendFileFeature 11 | { 12 | private readonly IHttpSendFileFeature _originalSendFileFeature; 13 | private readonly ResponseCachingStream _responseCachingStream; 14 | 15 | public SendFileFeatureWrapper(IHttpSendFileFeature originalSendFileFeature, ResponseCachingStream responseCachingStream) 16 | { 17 | _originalSendFileFeature = originalSendFileFeature; 18 | _responseCachingStream = responseCachingStream; 19 | } 20 | 21 | // Flush and disable the buffer if anyone tries to call the SendFile feature. 22 | public Task SendFileAsync(string path, long offset, long? length, CancellationToken cancellation) 23 | { 24 | _responseCachingStream.DisableBuffering(); 25 | return _originalSendFileFeature.SendFileAsync(path, offset, length, cancellation); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.ResponseCaching/Internal/StringBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.Text; 5 | 6 | namespace Microsoft.AspNetCore.ResponseCaching.Internal 7 | { 8 | internal static class StringBuilderExtensions 9 | { 10 | internal static StringBuilder AppendUpperInvariant(this StringBuilder builder, string value) 11 | { 12 | if (!string.IsNullOrEmpty(value)) 13 | { 14 | builder.EnsureCapacity(builder.Length + value.Length); 15 | for (var i = 0; i < value.Length; i++) 16 | { 17 | builder.Append(char.ToUpperInvariant(value[i])); 18 | } 19 | } 20 | 21 | return builder; 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.ResponseCaching/Internal/SystemClock.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | 6 | namespace Microsoft.AspNetCore.ResponseCaching.Internal 7 | { 8 | /// 9 | /// Provides access to the normal system clock. 10 | /// 11 | internal class SystemClock : ISystemClock 12 | { 13 | /// 14 | /// Retrieves the current system time in UTC. 15 | /// 16 | public DateTimeOffset UtcNow 17 | { 18 | get 19 | { 20 | return DateTimeOffset.UtcNow; 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.ResponseCaching/Microsoft.AspNetCore.ResponseCaching.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | ASP.NET Core middleware for caching HTTP responses on the server. 5 | netstandard2.0 6 | $(NoWarn);CS1591 7 | true 8 | true 9 | aspnetcore;cache;caching 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.ResponseCaching/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.Runtime.CompilerServices; 5 | 6 | [assembly: InternalsVisibleTo("Microsoft.AspNetCore.ResponseCaching.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using Microsoft.AspNetCore.ResponseCaching; 6 | using Microsoft.Extensions.Options; 7 | 8 | namespace Microsoft.AspNetCore.Builder 9 | { 10 | public static class ResponseCachingExtensions 11 | { 12 | public static IApplicationBuilder UseResponseCaching(this IApplicationBuilder app) 13 | { 14 | if (app == null) 15 | { 16 | throw new ArgumentNullException(nameof(app)); 17 | } 18 | 19 | return app.UseMiddleware(); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingFeature.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | 6 | namespace Microsoft.AspNetCore.ResponseCaching 7 | { 8 | public class ResponseCachingFeature : IResponseCachingFeature 9 | { 10 | private string[] _varyByQueryKeys; 11 | 12 | public string[] VaryByQueryKeys 13 | { 14 | get 15 | { 16 | return _varyByQueryKeys; 17 | } 18 | set 19 | { 20 | if (value?.Length > 1) 21 | { 22 | for (var i = 0; i < value.Length; i++) 23 | { 24 | if (string.IsNullOrEmpty(value[i])) 25 | { 26 | throw new ArgumentException($"When {nameof(value)} contains more than one value, it cannot contain a null or empty value.", nameof(value)); 27 | } 28 | } 29 | } 30 | _varyByQueryKeys = value; 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingOptions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.ComponentModel; 5 | using Microsoft.AspNetCore.ResponseCaching.Internal; 6 | 7 | namespace Microsoft.AspNetCore.ResponseCaching 8 | { 9 | public class ResponseCachingOptions 10 | { 11 | /// 12 | /// The size limit for the response cache middleware in bytes. The default is set to 100 MB. 13 | /// 14 | public long SizeLimit { get; set; } = 100 * 1024 * 1024; 15 | 16 | /// 17 | /// The largest cacheable size for the response body in bytes. The default is set to 64 MB. 18 | /// 19 | public long MaximumBodySize { get; set; } = 64 * 1024 * 1024; 20 | 21 | /// 22 | /// true if request paths are case-sensitive; otherwise false. The default is to treat paths as case-insensitive. 23 | /// 24 | public bool UseCaseSensitivePaths { get; set; } = false; 25 | 26 | /// 27 | /// For testing purposes only. 28 | /// 29 | [EditorBrowsable(EditorBrowsableState.Never)] 30 | internal ISystemClock SystemClock { get; set; } = new SystemClock(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.ResponseCaching/ResponseCachingServicesExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using Microsoft.AspNetCore.ResponseCaching; 6 | using Microsoft.AspNetCore.ResponseCaching.Internal; 7 | using Microsoft.Extensions.DependencyInjection.Extensions; 8 | 9 | namespace Microsoft.Extensions.DependencyInjection 10 | { 11 | /// 12 | /// Extension methods for the ResponseCaching middleware. 13 | /// 14 | public static class ResponseCachingServicesExtensions 15 | { 16 | /// 17 | /// Add response caching services. 18 | /// 19 | /// The for adding services. 20 | /// 21 | public static IServiceCollection AddResponseCaching(this IServiceCollection services) 22 | { 23 | if (services == null) 24 | { 25 | throw new ArgumentNullException(nameof(services)); 26 | } 27 | 28 | services.TryAdd(ServiceDescriptor.Singleton()); 29 | services.TryAdd(ServiceDescriptor.Singleton()); 30 | 31 | return services; 32 | } 33 | 34 | /// 35 | /// Add response caching services and configure the related options. 36 | /// 37 | /// The for adding services. 38 | /// A delegate to configure the . 39 | /// 40 | public static IServiceCollection AddResponseCaching(this IServiceCollection services, Action configureOptions) 41 | { 42 | if (services == null) 43 | { 44 | throw new ArgumentNullException(nameof(services)); 45 | } 46 | if (configureOptions == null) 47 | { 48 | throw new ArgumentNullException(nameof(configureOptions)); 49 | } 50 | 51 | services.Configure(configureOptions); 52 | services.AddResponseCaching(); 53 | 54 | return services; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.ResponseCaching/Streams/ResponseCachingStream.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.IO; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace Microsoft.AspNetCore.ResponseCaching.Internal 10 | { 11 | internal class ResponseCachingStream : Stream 12 | { 13 | private readonly Stream _innerStream; 14 | private readonly long _maxBufferSize; 15 | private readonly int _segmentSize; 16 | private SegmentWriteStream _segmentWriteStream; 17 | private Action _startResponseCallback; 18 | private Func _startResponseCallbackAsync; 19 | 20 | internal ResponseCachingStream(Stream innerStream, long maxBufferSize, int segmentSize, Action startResponseCallback, Func startResponseCallbackAsync) 21 | { 22 | _innerStream = innerStream; 23 | _maxBufferSize = maxBufferSize; 24 | _segmentSize = segmentSize; 25 | _startResponseCallback = startResponseCallback; 26 | _startResponseCallbackAsync = startResponseCallbackAsync; 27 | _segmentWriteStream = new SegmentWriteStream(_segmentSize); 28 | } 29 | 30 | internal bool BufferingEnabled { get; private set; } = true; 31 | 32 | public override bool CanRead => _innerStream.CanRead; 33 | 34 | public override bool CanSeek => _innerStream.CanSeek; 35 | 36 | public override bool CanWrite => _innerStream.CanWrite; 37 | 38 | public override long Length => _innerStream.Length; 39 | 40 | public override long Position 41 | { 42 | get { return _innerStream.Position; } 43 | set 44 | { 45 | DisableBuffering(); 46 | _innerStream.Position = value; 47 | } 48 | } 49 | 50 | internal Stream GetBufferStream() 51 | { 52 | if (!BufferingEnabled) 53 | { 54 | throw new InvalidOperationException("Buffer stream cannot be retrieved since buffering is disabled."); 55 | } 56 | return new SegmentReadStream(_segmentWriteStream.GetSegments(), _segmentWriteStream.Length); 57 | } 58 | 59 | internal void DisableBuffering() 60 | { 61 | BufferingEnabled = false; 62 | _segmentWriteStream.Dispose(); 63 | } 64 | 65 | public override void SetLength(long value) 66 | { 67 | DisableBuffering(); 68 | _innerStream.SetLength(value); 69 | } 70 | 71 | public override long Seek(long offset, SeekOrigin origin) 72 | { 73 | DisableBuffering(); 74 | return _innerStream.Seek(offset, origin); 75 | } 76 | 77 | public override void Flush() 78 | { 79 | try 80 | { 81 | _startResponseCallback(); 82 | _innerStream.Flush(); 83 | } 84 | catch 85 | { 86 | DisableBuffering(); 87 | throw; 88 | } 89 | } 90 | 91 | public override async Task FlushAsync(CancellationToken cancellationToken) 92 | { 93 | try 94 | { 95 | await _startResponseCallbackAsync(); 96 | await _innerStream.FlushAsync(); 97 | } 98 | catch 99 | { 100 | DisableBuffering(); 101 | throw; 102 | } 103 | } 104 | 105 | // Underlying stream is write-only, no need to override other read related methods 106 | public override int Read(byte[] buffer, int offset, int count) 107 | => _innerStream.Read(buffer, offset, count); 108 | 109 | public override void Write(byte[] buffer, int offset, int count) 110 | { 111 | try 112 | { 113 | _startResponseCallback(); 114 | _innerStream.Write(buffer, offset, count); 115 | } 116 | catch 117 | { 118 | DisableBuffering(); 119 | throw; 120 | } 121 | 122 | if (BufferingEnabled) 123 | { 124 | if (_segmentWriteStream.Length + count > _maxBufferSize) 125 | { 126 | DisableBuffering(); 127 | } 128 | else 129 | { 130 | _segmentWriteStream.Write(buffer, offset, count); 131 | } 132 | } 133 | } 134 | 135 | public override async Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) 136 | { 137 | try 138 | { 139 | await _startResponseCallbackAsync(); 140 | await _innerStream.WriteAsync(buffer, offset, count, cancellationToken); 141 | } 142 | catch 143 | { 144 | DisableBuffering(); 145 | throw; 146 | } 147 | 148 | if (BufferingEnabled) 149 | { 150 | if (_segmentWriteStream.Length + count > _maxBufferSize) 151 | { 152 | DisableBuffering(); 153 | } 154 | else 155 | { 156 | await _segmentWriteStream.WriteAsync(buffer, offset, count, cancellationToken); 157 | } 158 | } 159 | } 160 | 161 | public override void WriteByte(byte value) 162 | { 163 | try 164 | { 165 | _innerStream.WriteByte(value); 166 | } 167 | catch 168 | { 169 | DisableBuffering(); 170 | throw; 171 | } 172 | 173 | if (BufferingEnabled) 174 | { 175 | if (_segmentWriteStream.Length + 1 > _maxBufferSize) 176 | { 177 | DisableBuffering(); 178 | } 179 | else 180 | { 181 | _segmentWriteStream.WriteByte(value); 182 | } 183 | } 184 | } 185 | 186 | public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) 187 | { 188 | return StreamUtilities.ToIAsyncResult(WriteAsync(buffer, offset, count), callback, state); 189 | } 190 | 191 | public override void EndWrite(IAsyncResult asyncResult) 192 | { 193 | if (asyncResult == null) 194 | { 195 | throw new ArgumentNullException(nameof(asyncResult)); 196 | } 197 | ((Task)asyncResult).GetAwaiter().GetResult(); 198 | } 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.ResponseCaching/Streams/SegmentReadStream.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace Microsoft.AspNetCore.ResponseCaching.Internal 11 | { 12 | internal class SegmentReadStream : Stream 13 | { 14 | private readonly List _segments; 15 | private readonly long _length; 16 | private int _segmentIndex; 17 | private int _segmentOffset; 18 | private long _position; 19 | 20 | internal SegmentReadStream(List segments, long length) 21 | { 22 | if (segments == null) 23 | { 24 | throw new ArgumentNullException(nameof(segments)); 25 | } 26 | 27 | _segments = segments; 28 | _length = length; 29 | } 30 | 31 | public override bool CanRead => true; 32 | 33 | public override bool CanSeek => true; 34 | 35 | public override bool CanWrite => false; 36 | 37 | public override long Length => _length; 38 | 39 | public override long Position 40 | { 41 | get 42 | { 43 | return _position; 44 | } 45 | set 46 | { 47 | // The stream only supports a full rewind. This will need an update if random access becomes a required feature. 48 | if (value != 0) 49 | { 50 | throw new ArgumentOutOfRangeException(nameof(value), value, $"{nameof(Position)} can only be set to 0."); 51 | } 52 | 53 | _position = 0; 54 | _segmentOffset = 0; 55 | _segmentIndex = 0; 56 | } 57 | } 58 | 59 | public override void Flush() 60 | { 61 | throw new NotSupportedException("The stream does not support writing."); 62 | } 63 | 64 | public override int Read(byte[] buffer, int offset, int count) 65 | { 66 | if (buffer == null) 67 | { 68 | throw new ArgumentNullException(nameof(buffer)); 69 | } 70 | if (offset < 0) 71 | { 72 | throw new ArgumentOutOfRangeException(nameof(offset), offset, "Non-negative number required."); 73 | } 74 | // Read of length 0 will return zero and indicate end of stream. 75 | if (count <= 0 ) 76 | { 77 | throw new ArgumentOutOfRangeException(nameof(count), count, "Positive number required."); 78 | } 79 | if (count > buffer.Length - offset) 80 | { 81 | 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."); 82 | } 83 | 84 | if (_segmentIndex == _segments.Count) 85 | { 86 | return 0; 87 | } 88 | 89 | var bytesRead = 0; 90 | while (count > 0) 91 | { 92 | if (_segmentOffset == _segments[_segmentIndex].Length) 93 | { 94 | // Move to the next segment 95 | _segmentIndex++; 96 | _segmentOffset = 0; 97 | 98 | if (_segmentIndex == _segments.Count) 99 | { 100 | break; 101 | } 102 | } 103 | 104 | // Read up to the end of the segment 105 | var segmentBytesRead = Math.Min(count, _segments[_segmentIndex].Length - _segmentOffset); 106 | Buffer.BlockCopy(_segments[_segmentIndex], _segmentOffset, buffer, offset, segmentBytesRead); 107 | bytesRead += segmentBytesRead; 108 | _segmentOffset += segmentBytesRead; 109 | _position += segmentBytesRead; 110 | offset += segmentBytesRead; 111 | count -= segmentBytesRead; 112 | } 113 | 114 | return bytesRead; 115 | } 116 | 117 | public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) 118 | { 119 | return Task.FromResult(Read(buffer, offset, count)); 120 | } 121 | 122 | public override int ReadByte() 123 | { 124 | if (Position == Length) 125 | { 126 | return -1; 127 | } 128 | 129 | if (_segmentOffset == _segments[_segmentIndex].Length) 130 | { 131 | // Move to the next segment 132 | _segmentIndex++; 133 | _segmentOffset = 0; 134 | } 135 | 136 | var byteRead = _segments[_segmentIndex][_segmentOffset]; 137 | _segmentOffset++; 138 | _position++; 139 | 140 | return byteRead; 141 | } 142 | 143 | public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state) 144 | { 145 | var tcs = new TaskCompletionSource(state); 146 | 147 | try 148 | { 149 | tcs.TrySetResult(Read(buffer, offset, count)); 150 | } 151 | catch (Exception ex) 152 | { 153 | tcs.TrySetException(ex); 154 | } 155 | 156 | if (callback != null) 157 | { 158 | // Offload callbacks to avoid stack dives on sync completions. 159 | var ignored = Task.Run(() => 160 | { 161 | try 162 | { 163 | callback(tcs.Task); 164 | } 165 | catch (Exception) 166 | { 167 | // Suppress exceptions on background threads. 168 | } 169 | }); 170 | } 171 | 172 | return tcs.Task; 173 | } 174 | 175 | public override int EndRead(IAsyncResult asyncResult) 176 | { 177 | if (asyncResult == null) 178 | { 179 | throw new ArgumentNullException(nameof(asyncResult)); 180 | } 181 | return ((Task)asyncResult).GetAwaiter().GetResult(); 182 | } 183 | 184 | public override long Seek(long offset, SeekOrigin origin) 185 | { 186 | // The stream only supports a full rewind. This will need an update if random access becomes a required feature. 187 | if (origin != SeekOrigin.Begin) 188 | { 189 | throw new ArgumentException(nameof(origin), $"{nameof(Seek)} can only be set to {nameof(SeekOrigin.Begin)}."); 190 | } 191 | if (offset != 0) 192 | { 193 | throw new ArgumentOutOfRangeException(nameof(offset), offset, $"{nameof(Seek)} can only be set to 0."); 194 | } 195 | 196 | Position = 0; 197 | return Position; 198 | } 199 | 200 | public override void SetLength(long value) 201 | { 202 | throw new NotSupportedException("The stream does not support writing."); 203 | } 204 | 205 | public override void Write(byte[] buffer, int offset, int count) 206 | { 207 | throw new NotSupportedException("The stream does not support writing."); 208 | } 209 | 210 | public override async Task CopyToAsync(Stream destination, int bufferSize, CancellationToken cancellationToken) 211 | { 212 | if (destination == null) 213 | { 214 | throw new ArgumentNullException(nameof(destination)); 215 | } 216 | if (!destination.CanWrite) 217 | { 218 | throw new NotSupportedException("The destination stream does not support writing."); 219 | } 220 | 221 | for (; _segmentIndex < _segments.Count; _segmentIndex++, _segmentOffset = 0) 222 | { 223 | cancellationToken.ThrowIfCancellationRequested(); 224 | var bytesCopied = _segments[_segmentIndex].Length - _segmentOffset; 225 | await destination.WriteAsync(_segments[_segmentIndex], _segmentOffset, bytesCopied, cancellationToken); 226 | _position += bytesCopied; 227 | } 228 | } 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.ResponseCaching/Streams/SegmentWriteStream.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace Microsoft.AspNetCore.ResponseCaching.Internal 11 | { 12 | internal class SegmentWriteStream : Stream 13 | { 14 | private readonly List _segments = new List(); 15 | private readonly MemoryStream _bufferStream = new MemoryStream(); 16 | private readonly int _segmentSize; 17 | private long _length; 18 | private bool _closed; 19 | private bool _disposed; 20 | 21 | internal SegmentWriteStream(int segmentSize) 22 | { 23 | if (segmentSize <= 0) 24 | { 25 | throw new ArgumentOutOfRangeException(nameof(segmentSize), segmentSize, $"{nameof(segmentSize)} must be greater than 0."); 26 | } 27 | 28 | _segmentSize = segmentSize; 29 | } 30 | 31 | // Extracting the buffered segments closes the stream for writing 32 | internal List GetSegments() 33 | { 34 | if (!_closed) 35 | { 36 | _closed = true; 37 | FinalizeSegments(); 38 | } 39 | return _segments; 40 | } 41 | 42 | public override bool CanRead => false; 43 | 44 | public override bool CanSeek => false; 45 | 46 | public override bool CanWrite => !_closed; 47 | 48 | public override long Length => _length; 49 | 50 | public override long Position 51 | { 52 | get 53 | { 54 | return _length; 55 | } 56 | set 57 | { 58 | throw new NotSupportedException("The stream does not support seeking."); 59 | } 60 | } 61 | 62 | private void DisposeMemoryStream() 63 | { 64 | // Clean up the memory stream 65 | _bufferStream.SetLength(0); 66 | _bufferStream.Capacity = 0; 67 | _bufferStream.Dispose(); 68 | } 69 | 70 | private void FinalizeSegments() 71 | { 72 | // Append any remaining segments 73 | if (_bufferStream.Length > 0) 74 | { 75 | // Add the last segment 76 | _segments.Add(_bufferStream.ToArray()); 77 | } 78 | 79 | DisposeMemoryStream(); 80 | } 81 | 82 | protected override void Dispose(bool disposing) 83 | { 84 | try 85 | { 86 | if (_disposed) 87 | { 88 | return; 89 | } 90 | 91 | if (disposing) 92 | { 93 | _segments.Clear(); 94 | DisposeMemoryStream(); 95 | } 96 | 97 | _disposed = true; 98 | _closed = true; 99 | } 100 | finally 101 | { 102 | base.Dispose(disposing); 103 | } 104 | } 105 | 106 | public override void Flush() 107 | { 108 | if (!CanWrite) 109 | { 110 | throw new ObjectDisposedException("The stream has been closed for writing."); 111 | } 112 | } 113 | 114 | public override int Read(byte[] buffer, int offset, int count) 115 | { 116 | throw new NotSupportedException("The stream does not support reading."); 117 | } 118 | 119 | public override long Seek(long offset, SeekOrigin origin) 120 | { 121 | throw new NotSupportedException("The stream does not support seeking."); 122 | } 123 | 124 | public override void SetLength(long value) 125 | { 126 | throw new NotSupportedException("The stream does not support seeking."); 127 | } 128 | 129 | public override void Write(byte[] buffer, int offset, int count) 130 | { 131 | if (buffer == null) 132 | { 133 | throw new ArgumentNullException(nameof(buffer)); 134 | } 135 | if (offset < 0) 136 | { 137 | throw new ArgumentOutOfRangeException(nameof(offset), offset, "Non-negative number required."); 138 | } 139 | if (count < 0) 140 | { 141 | throw new ArgumentOutOfRangeException(nameof(count), count, "Non-negative number required."); 142 | } 143 | if (count > buffer.Length - offset) 144 | { 145 | 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."); 146 | } 147 | if (!CanWrite) 148 | { 149 | throw new ObjectDisposedException("The stream has been closed for writing."); 150 | } 151 | 152 | while (count > 0) 153 | { 154 | if ((int)_bufferStream.Length == _segmentSize) 155 | { 156 | _segments.Add(_bufferStream.ToArray()); 157 | _bufferStream.SetLength(0); 158 | } 159 | 160 | var bytesWritten = Math.Min(count, _segmentSize - (int)_bufferStream.Length); 161 | 162 | _bufferStream.Write(buffer, offset, bytesWritten); 163 | count -= bytesWritten; 164 | offset += bytesWritten; 165 | _length += bytesWritten; 166 | } 167 | } 168 | 169 | public override Task WriteAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) 170 | { 171 | Write(buffer, offset, count); 172 | return Task.CompletedTask; 173 | } 174 | 175 | public override void WriteByte(byte value) 176 | { 177 | if (!CanWrite) 178 | { 179 | throw new ObjectDisposedException("The stream has been closed for writing."); 180 | } 181 | 182 | if ((int)_bufferStream.Length == _segmentSize) 183 | { 184 | _segments.Add(_bufferStream.ToArray()); 185 | _bufferStream.SetLength(0); 186 | } 187 | 188 | _bufferStream.WriteByte(value); 189 | _length++; 190 | } 191 | 192 | public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state) 193 | { 194 | return StreamUtilities.ToIAsyncResult(WriteAsync(buffer, offset, count), callback, state); 195 | } 196 | 197 | public override void EndWrite(IAsyncResult asyncResult) 198 | { 199 | if (asyncResult == null) 200 | { 201 | throw new ArgumentNullException(nameof(asyncResult)); 202 | } 203 | ((Task)asyncResult).GetAwaiter().GetResult(); 204 | } 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.ResponseCaching/Streams/StreamUtilities.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace Microsoft.AspNetCore.ResponseCaching.Internal 9 | { 10 | internal static class StreamUtilities 11 | { 12 | /// 13 | /// 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. 14 | /// 15 | // Internal for testing 16 | internal static int BodySegmentSize { get; set; } = 81920; 17 | 18 | internal static IAsyncResult ToIAsyncResult(Task task, AsyncCallback callback, object state) 19 | { 20 | var tcs = new TaskCompletionSource(state); 21 | task.ContinueWith(t => 22 | { 23 | if (t.IsFaulted) 24 | { 25 | tcs.TrySetException(t.Exception.InnerExceptions); 26 | } 27 | else if (t.IsCanceled) 28 | { 29 | tcs.TrySetCanceled(); 30 | } 31 | else 32 | { 33 | tcs.TrySetResult(0); 34 | } 35 | 36 | callback?.Invoke(tcs.Task); 37 | }, CancellationToken.None, TaskContinuationOptions.None, TaskScheduler.Default); 38 | return tcs.Task; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Microsoft.AspNetCore.ResponseCaching/baseline.netcore.json: -------------------------------------------------------------------------------- 1 | { 2 | "AssemblyIdentity": "Microsoft.AspNetCore.ResponseCaching, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", 3 | "Types": [ 4 | { 5 | "Name": "Microsoft.Extensions.DependencyInjection.ResponseCachingServicesExtensions", 6 | "Visibility": "Public", 7 | "Kind": "Class", 8 | "Abstract": true, 9 | "Static": true, 10 | "Sealed": true, 11 | "ImplementedInterfaces": [], 12 | "Members": [ 13 | { 14 | "Kind": "Method", 15 | "Name": "AddResponseCaching", 16 | "Parameters": [ 17 | { 18 | "Name": "services", 19 | "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection" 20 | } 21 | ], 22 | "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection", 23 | "Static": true, 24 | "Extension": true, 25 | "Visibility": "Public", 26 | "GenericParameter": [] 27 | }, 28 | { 29 | "Kind": "Method", 30 | "Name": "AddResponseCaching", 31 | "Parameters": [ 32 | { 33 | "Name": "services", 34 | "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection" 35 | }, 36 | { 37 | "Name": "configureOptions", 38 | "Type": "System.Action" 39 | } 40 | ], 41 | "ReturnType": "Microsoft.Extensions.DependencyInjection.IServiceCollection", 42 | "Static": true, 43 | "Extension": true, 44 | "Visibility": "Public", 45 | "GenericParameter": [] 46 | } 47 | ], 48 | "GenericParameters": [] 49 | }, 50 | { 51 | "Name": "Microsoft.AspNetCore.Builder.ResponseCachingExtensions", 52 | "Visibility": "Public", 53 | "Kind": "Class", 54 | "Abstract": true, 55 | "Static": true, 56 | "Sealed": true, 57 | "ImplementedInterfaces": [], 58 | "Members": [ 59 | { 60 | "Kind": "Method", 61 | "Name": "UseResponseCaching", 62 | "Parameters": [ 63 | { 64 | "Name": "app", 65 | "Type": "Microsoft.AspNetCore.Builder.IApplicationBuilder" 66 | } 67 | ], 68 | "ReturnType": "Microsoft.AspNetCore.Builder.IApplicationBuilder", 69 | "Static": true, 70 | "Extension": true, 71 | "Visibility": "Public", 72 | "GenericParameter": [] 73 | } 74 | ], 75 | "GenericParameters": [] 76 | }, 77 | { 78 | "Name": "Microsoft.AspNetCore.ResponseCaching.ResponseCachingFeature", 79 | "Visibility": "Public", 80 | "Kind": "Class", 81 | "ImplementedInterfaces": [ 82 | "Microsoft.AspNetCore.ResponseCaching.IResponseCachingFeature" 83 | ], 84 | "Members": [ 85 | { 86 | "Kind": "Method", 87 | "Name": "get_VaryByQueryKeys", 88 | "Parameters": [], 89 | "ReturnType": "System.String[]", 90 | "Sealed": true, 91 | "Virtual": true, 92 | "ImplementedInterface": "Microsoft.AspNetCore.ResponseCaching.IResponseCachingFeature", 93 | "Visibility": "Public", 94 | "GenericParameter": [] 95 | }, 96 | { 97 | "Kind": "Method", 98 | "Name": "set_VaryByQueryKeys", 99 | "Parameters": [ 100 | { 101 | "Name": "value", 102 | "Type": "System.String[]" 103 | } 104 | ], 105 | "ReturnType": "System.Void", 106 | "Sealed": true, 107 | "Virtual": true, 108 | "ImplementedInterface": "Microsoft.AspNetCore.ResponseCaching.IResponseCachingFeature", 109 | "Visibility": "Public", 110 | "GenericParameter": [] 111 | }, 112 | { 113 | "Kind": "Constructor", 114 | "Name": ".ctor", 115 | "Parameters": [], 116 | "Visibility": "Public", 117 | "GenericParameter": [] 118 | } 119 | ], 120 | "GenericParameters": [] 121 | }, 122 | { 123 | "Name": "Microsoft.AspNetCore.ResponseCaching.ResponseCachingMiddleware", 124 | "Visibility": "Public", 125 | "Kind": "Class", 126 | "ImplementedInterfaces": [], 127 | "Members": [ 128 | { 129 | "Kind": "Method", 130 | "Name": "Invoke", 131 | "Parameters": [ 132 | { 133 | "Name": "httpContext", 134 | "Type": "Microsoft.AspNetCore.Http.HttpContext" 135 | } 136 | ], 137 | "ReturnType": "System.Threading.Tasks.Task", 138 | "Visibility": "Public", 139 | "GenericParameter": [] 140 | }, 141 | { 142 | "Kind": "Constructor", 143 | "Name": ".ctor", 144 | "Parameters": [ 145 | { 146 | "Name": "next", 147 | "Type": "Microsoft.AspNetCore.Http.RequestDelegate" 148 | }, 149 | { 150 | "Name": "options", 151 | "Type": "Microsoft.Extensions.Options.IOptions" 152 | }, 153 | { 154 | "Name": "loggerFactory", 155 | "Type": "Microsoft.Extensions.Logging.ILoggerFactory" 156 | }, 157 | { 158 | "Name": "policyProvider", 159 | "Type": "Microsoft.AspNetCore.ResponseCaching.Internal.IResponseCachingPolicyProvider" 160 | }, 161 | { 162 | "Name": "keyProvider", 163 | "Type": "Microsoft.AspNetCore.ResponseCaching.Internal.IResponseCachingKeyProvider" 164 | } 165 | ], 166 | "Visibility": "Public", 167 | "GenericParameter": [] 168 | } 169 | ], 170 | "GenericParameters": [] 171 | }, 172 | { 173 | "Name": "Microsoft.AspNetCore.ResponseCaching.ResponseCachingOptions", 174 | "Visibility": "Public", 175 | "Kind": "Class", 176 | "ImplementedInterfaces": [], 177 | "Members": [ 178 | { 179 | "Kind": "Method", 180 | "Name": "get_SizeLimit", 181 | "Parameters": [], 182 | "ReturnType": "System.Int64", 183 | "Visibility": "Public", 184 | "GenericParameter": [] 185 | }, 186 | { 187 | "Kind": "Method", 188 | "Name": "set_SizeLimit", 189 | "Parameters": [ 190 | { 191 | "Name": "value", 192 | "Type": "System.Int64" 193 | } 194 | ], 195 | "ReturnType": "System.Void", 196 | "Visibility": "Public", 197 | "GenericParameter": [] 198 | }, 199 | { 200 | "Kind": "Method", 201 | "Name": "get_MaximumBodySize", 202 | "Parameters": [], 203 | "ReturnType": "System.Int64", 204 | "Visibility": "Public", 205 | "GenericParameter": [] 206 | }, 207 | { 208 | "Kind": "Method", 209 | "Name": "set_MaximumBodySize", 210 | "Parameters": [ 211 | { 212 | "Name": "value", 213 | "Type": "System.Int64" 214 | } 215 | ], 216 | "ReturnType": "System.Void", 217 | "Visibility": "Public", 218 | "GenericParameter": [] 219 | }, 220 | { 221 | "Kind": "Method", 222 | "Name": "get_UseCaseSensitivePaths", 223 | "Parameters": [], 224 | "ReturnType": "System.Boolean", 225 | "Visibility": "Public", 226 | "GenericParameter": [] 227 | }, 228 | { 229 | "Kind": "Method", 230 | "Name": "set_UseCaseSensitivePaths", 231 | "Parameters": [ 232 | { 233 | "Name": "value", 234 | "Type": "System.Boolean" 235 | } 236 | ], 237 | "ReturnType": "System.Void", 238 | "Visibility": "Public", 239 | "GenericParameter": [] 240 | }, 241 | { 242 | "Kind": "Constructor", 243 | "Name": ".ctor", 244 | "Parameters": [], 245 | "Visibility": "Public", 246 | "GenericParameter": [] 247 | } 248 | ], 249 | "GenericParameters": [] 250 | } 251 | ] 252 | } -------------------------------------------------------------------------------- /test/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | netcoreapp2.2 6 | $(DeveloperBuildTestTfms) 7 | 8 | $(StandardTestTfms);net461 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.ResponseCaching.Tests/Microsoft.AspNetCore.ResponseCaching.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | $(StandardTestTfms) 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingFeatureTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using Xunit; 6 | 7 | namespace Microsoft.AspNetCore.ResponseCaching.Tests 8 | { 9 | public class ResponseCachingFeatureTests 10 | { 11 | public static TheoryData ValidNullOrEmptyVaryRules 12 | { 13 | get 14 | { 15 | return new TheoryData 16 | { 17 | null, 18 | new string[0], 19 | new string[] { null }, 20 | new string[] { string.Empty } 21 | }; 22 | } 23 | } 24 | 25 | [Theory] 26 | [MemberData(nameof(ValidNullOrEmptyVaryRules))] 27 | public void VaryByQueryKeys_Set_ValidEmptyValues_Succeeds(string[] value) 28 | { 29 | // Does not throw 30 | new ResponseCachingFeature().VaryByQueryKeys = value; 31 | } 32 | 33 | public static TheoryData InvalidVaryRules 34 | { 35 | get 36 | { 37 | return new TheoryData 38 | { 39 | new string[] { null, null }, 40 | new string[] { null, string.Empty }, 41 | new string[] { string.Empty, null }, 42 | new string[] { string.Empty, "Valid" }, 43 | new string[] { "Valid", string.Empty }, 44 | new string[] { null, "Valid" }, 45 | new string[] { "Valid", null } 46 | }; 47 | } 48 | } 49 | 50 | 51 | [Theory] 52 | [MemberData(nameof(InvalidVaryRules))] 53 | public void VaryByQueryKeys_Set_InValidEmptyValues_Throws(string[] value) 54 | { 55 | // Throws 56 | Assert.Throws(() => new ResponseCachingFeature().VaryByQueryKeys = value); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.ResponseCaching.Tests/ResponseCachingKeyProviderTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.AspNetCore.ResponseCaching.Internal; 7 | using Xunit; 8 | 9 | namespace Microsoft.AspNetCore.ResponseCaching.Tests 10 | { 11 | public class ResponseCachingKeyProviderTests 12 | { 13 | private static readonly char KeyDelimiter = '\x1e'; 14 | private static readonly char KeySubDelimiter = '\x1f'; 15 | 16 | [Fact] 17 | public void ResponseCachingKeyProvider_CreateStorageBaseKey_IncludesOnlyNormalizedMethodSchemeHostPortAndPath() 18 | { 19 | var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); 20 | var context = TestUtils.CreateTestContext(); 21 | context.HttpContext.Request.Method = "head"; 22 | context.HttpContext.Request.Path = "/path/subpath"; 23 | context.HttpContext.Request.Scheme = "https"; 24 | context.HttpContext.Request.Host = new HostString("example.com", 80); 25 | context.HttpContext.Request.PathBase = "/pathBase"; 26 | context.HttpContext.Request.QueryString = new QueryString("?query.Key=a&query.Value=b"); 27 | 28 | Assert.Equal($"HEAD{KeyDelimiter}HTTPS{KeyDelimiter}EXAMPLE.COM:80/PATHBASE/PATH/SUBPATH", cacheKeyProvider.CreateBaseKey(context)); 29 | } 30 | 31 | [Fact] 32 | public void ResponseCachingKeyProvider_CreateStorageBaseKey_CaseInsensitivePath_NormalizesPath() 33 | { 34 | var cacheKeyProvider = TestUtils.CreateTestKeyProvider(new ResponseCachingOptions() 35 | { 36 | UseCaseSensitivePaths = false 37 | }); 38 | var context = TestUtils.CreateTestContext(); 39 | context.HttpContext.Request.Method = HttpMethods.Get; 40 | context.HttpContext.Request.Path = "/Path"; 41 | 42 | Assert.Equal($"{HttpMethods.Get}{KeyDelimiter}{KeyDelimiter}/PATH", cacheKeyProvider.CreateBaseKey(context)); 43 | } 44 | 45 | [Fact] 46 | public void ResponseCachingKeyProvider_CreateStorageBaseKey_CaseSensitivePath_PreservesPathCase() 47 | { 48 | var cacheKeyProvider = TestUtils.CreateTestKeyProvider(new ResponseCachingOptions() 49 | { 50 | UseCaseSensitivePaths = true 51 | }); 52 | var context = TestUtils.CreateTestContext(); 53 | context.HttpContext.Request.Method = HttpMethods.Get; 54 | context.HttpContext.Request.Path = "/Path"; 55 | 56 | Assert.Equal($"{HttpMethods.Get}{KeyDelimiter}{KeyDelimiter}/Path", cacheKeyProvider.CreateBaseKey(context)); 57 | } 58 | 59 | [Fact] 60 | public void ResponseCachingKeyProvider_CreateStorageVaryByKey_Throws_IfVaryByRulesIsNull() 61 | { 62 | var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); 63 | var context = TestUtils.CreateTestContext(); 64 | 65 | Assert.Throws(() => cacheKeyProvider.CreateStorageVaryByKey(context)); 66 | } 67 | 68 | [Fact] 69 | public void ResponseCachingKeyProvider_CreateStorageVaryKey_ReturnsCachedVaryByGuid_IfVaryByRulesIsEmpty() 70 | { 71 | var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); 72 | var context = TestUtils.CreateTestContext(); 73 | context.CachedVaryByRules = new CachedVaryByRules() 74 | { 75 | VaryByKeyPrefix = FastGuid.NewGuid().IdString 76 | }; 77 | 78 | Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}", cacheKeyProvider.CreateStorageVaryByKey(context)); 79 | } 80 | 81 | [Fact] 82 | public void ResponseCachingKeyProvider_CreateStorageVaryKey_IncludesListedHeadersOnly() 83 | { 84 | var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); 85 | var context = TestUtils.CreateTestContext(); 86 | context.HttpContext.Request.Headers["HeaderA"] = "ValueA"; 87 | context.HttpContext.Request.Headers["HeaderB"] = "ValueB"; 88 | context.CachedVaryByRules = new CachedVaryByRules() 89 | { 90 | Headers = new string[] { "HeaderA", "HeaderC" } 91 | }; 92 | 93 | Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC=", 94 | cacheKeyProvider.CreateStorageVaryByKey(context)); 95 | } 96 | 97 | [Fact] 98 | public void ResponseCachingKeyProvider_CreateStorageVaryKey_HeaderValuesAreSorted() 99 | { 100 | var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); 101 | var context = TestUtils.CreateTestContext(); 102 | context.HttpContext.Request.Headers["HeaderA"] = "ValueB"; 103 | context.HttpContext.Request.Headers.Append("HeaderA", "ValueA"); 104 | context.CachedVaryByRules = new CachedVaryByRules() 105 | { 106 | Headers = new string[] { "HeaderA", "HeaderC" } 107 | }; 108 | 109 | Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueAValueB{KeyDelimiter}HeaderC=", 110 | cacheKeyProvider.CreateStorageVaryByKey(context)); 111 | } 112 | 113 | [Fact] 114 | public void ResponseCachingKeyProvider_CreateStorageVaryKey_IncludesListedQueryKeysOnly() 115 | { 116 | var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); 117 | var context = TestUtils.CreateTestContext(); 118 | context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueA&QueryB=ValueB"); 119 | context.CachedVaryByRules = new CachedVaryByRules() 120 | { 121 | VaryByKeyPrefix = FastGuid.NewGuid().IdString, 122 | QueryKeys = new string[] { "QueryA", "QueryC" } 123 | }; 124 | 125 | Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}QueryA=ValueA{KeyDelimiter}QueryC=", 126 | cacheKeyProvider.CreateStorageVaryByKey(context)); 127 | } 128 | 129 | [Fact] 130 | public void ResponseCachingKeyProvider_CreateStorageVaryKey_IncludesQueryKeys_QueryKeyCaseInsensitive_UseQueryKeyCasing() 131 | { 132 | var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); 133 | var context = TestUtils.CreateTestContext(); 134 | context.HttpContext.Request.QueryString = new QueryString("?queryA=ValueA&queryB=ValueB"); 135 | context.CachedVaryByRules = new CachedVaryByRules() 136 | { 137 | VaryByKeyPrefix = FastGuid.NewGuid().IdString, 138 | QueryKeys = new string[] { "QueryA", "QueryC" } 139 | }; 140 | 141 | Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}QueryA=ValueA{KeyDelimiter}QueryC=", 142 | cacheKeyProvider.CreateStorageVaryByKey(context)); 143 | } 144 | 145 | [Fact] 146 | public void ResponseCachingKeyProvider_CreateStorageVaryKey_IncludesAllQueryKeysGivenAsterisk() 147 | { 148 | var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); 149 | var context = TestUtils.CreateTestContext(); 150 | context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueA&QueryB=ValueB"); 151 | context.CachedVaryByRules = new CachedVaryByRules() 152 | { 153 | VaryByKeyPrefix = FastGuid.NewGuid().IdString, 154 | QueryKeys = new string[] { "*" } 155 | }; 156 | 157 | // To support case insensitivity, all query keys are converted to upper case. 158 | // Explicit query keys uses the casing specified in the setting. 159 | Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}QUERYA=ValueA{KeyDelimiter}QUERYB=ValueB", 160 | cacheKeyProvider.CreateStorageVaryByKey(context)); 161 | } 162 | 163 | [Fact] 164 | public void ResponseCachingKeyProvider_CreateStorageVaryKey_QueryKeysValuesNotConsolidated() 165 | { 166 | var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); 167 | var context = TestUtils.CreateTestContext(); 168 | context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueA&QueryA=ValueB"); 169 | context.CachedVaryByRules = new CachedVaryByRules() 170 | { 171 | VaryByKeyPrefix = FastGuid.NewGuid().IdString, 172 | QueryKeys = new string[] { "*" } 173 | }; 174 | 175 | // To support case insensitivity, all query keys are converted to upper case. 176 | // Explicit query keys uses the casing specified in the setting. 177 | Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}QUERYA=ValueA{KeySubDelimiter}ValueB", 178 | cacheKeyProvider.CreateStorageVaryByKey(context)); 179 | } 180 | 181 | [Fact] 182 | public void ResponseCachingKeyProvider_CreateStorageVaryKey_QueryKeysValuesAreSorted() 183 | { 184 | var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); 185 | var context = TestUtils.CreateTestContext(); 186 | context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueB&QueryA=ValueA"); 187 | context.CachedVaryByRules = new CachedVaryByRules() 188 | { 189 | VaryByKeyPrefix = FastGuid.NewGuid().IdString, 190 | QueryKeys = new string[] { "*" } 191 | }; 192 | 193 | // To support case insensitivity, all query keys are converted to upper case. 194 | // Explicit query keys uses the casing specified in the setting. 195 | Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}Q{KeyDelimiter}QUERYA=ValueA{KeySubDelimiter}ValueB", 196 | cacheKeyProvider.CreateStorageVaryByKey(context)); 197 | } 198 | 199 | [Fact] 200 | public void ResponseCachingKeyProvider_CreateStorageVaryKey_IncludesListedHeadersAndQueryKeys() 201 | { 202 | var cacheKeyProvider = TestUtils.CreateTestKeyProvider(); 203 | var context = TestUtils.CreateTestContext(); 204 | context.HttpContext.Request.Headers["HeaderA"] = "ValueA"; 205 | context.HttpContext.Request.Headers["HeaderB"] = "ValueB"; 206 | context.HttpContext.Request.QueryString = new QueryString("?QueryA=ValueA&QueryB=ValueB"); 207 | context.CachedVaryByRules = new CachedVaryByRules() 208 | { 209 | VaryByKeyPrefix = FastGuid.NewGuid().IdString, 210 | Headers = new string[] { "HeaderA", "HeaderC" }, 211 | QueryKeys = new string[] { "QueryA", "QueryC" } 212 | }; 213 | 214 | Assert.Equal($"{context.CachedVaryByRules.VaryByKeyPrefix}{KeyDelimiter}H{KeyDelimiter}HeaderA=ValueA{KeyDelimiter}HeaderC={KeyDelimiter}Q{KeyDelimiter}QueryA=ValueA{KeyDelimiter}QueryC=", 215 | cacheKeyProvider.CreateStorageVaryByKey(context)); 216 | } 217 | } 218 | } 219 | -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.ResponseCaching.Tests/SegmentReadStreamTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.IO; 7 | using System.Linq; 8 | using Microsoft.AspNetCore.ResponseCaching.Internal; 9 | using Xunit; 10 | 11 | namespace Microsoft.AspNetCore.ResponseCaching.Tests 12 | { 13 | public class SegmentReadStreamTests 14 | { 15 | public class TestStreamInitInfo 16 | { 17 | internal List Segments { get; set; } 18 | internal int SegmentSize { get; set; } 19 | internal long Length { get; set; } 20 | } 21 | 22 | public static TheoryData TestStreams 23 | { 24 | get 25 | { 26 | return new TheoryData 27 | { 28 | // Partial Segment 29 | new TestStreamInitInfo() 30 | { 31 | Segments = new List(new[] 32 | { 33 | new byte[] { 0, 1, 2, 3, 4 }, 34 | new byte[] { 5, 6, 7, 8, 9 }, 35 | new byte[] { 10, 11, 12 }, 36 | }), 37 | SegmentSize = 5, 38 | Length = 13 39 | }, 40 | // Full Segments 41 | new TestStreamInitInfo() 42 | { 43 | Segments = new List(new[] 44 | { 45 | new byte[] { 0, 1, 2, 3, 4 }, 46 | new byte[] { 5, 6, 7, 8, 9 }, 47 | new byte[] { 10, 11, 12, 13, 14 }, 48 | }), 49 | SegmentSize = 5, 50 | Length = 15 51 | } 52 | }; 53 | } 54 | } 55 | 56 | [Fact] 57 | public void SegmentReadStream_NullSegments_Throws() 58 | { 59 | Assert.Throws(() => new SegmentReadStream(null, 0)); 60 | } 61 | 62 | [Fact] 63 | public void Position_ResetToZero_Succeeds() 64 | { 65 | var stream = new SegmentReadStream(new List(), 0); 66 | 67 | // This should not throw 68 | stream.Position = 0; 69 | } 70 | 71 | [Theory] 72 | [InlineData(1)] 73 | [InlineData(-1)] 74 | [InlineData(100)] 75 | [InlineData(long.MaxValue)] 76 | [InlineData(long.MinValue)] 77 | public void Position_SetToNonZero_Throws(long position) 78 | { 79 | var stream = new SegmentReadStream(new List(new[] { new byte[100] }), 100); 80 | 81 | Assert.Throws(() => stream.Position = position); 82 | } 83 | 84 | [Fact] 85 | public void WriteOperations_Throws() 86 | { 87 | var stream = new SegmentReadStream(new List(), 0); 88 | 89 | 90 | Assert.Throws(() => stream.Flush()); 91 | Assert.Throws(() => stream.Write(new byte[1], 0, 0)); 92 | } 93 | 94 | [Fact] 95 | public void SetLength_Throws() 96 | { 97 | var stream = new SegmentReadStream(new List(), 0); 98 | 99 | Assert.Throws(() => stream.SetLength(0)); 100 | } 101 | 102 | [Theory] 103 | [InlineData(SeekOrigin.Current)] 104 | [InlineData(SeekOrigin.End)] 105 | public void Seek_NotBegin_Throws(SeekOrigin origin) 106 | { 107 | var stream = new SegmentReadStream(new List(), 0); 108 | 109 | Assert.Throws(() => stream.Seek(0, origin)); 110 | } 111 | 112 | [Theory] 113 | [InlineData(1)] 114 | [InlineData(-1)] 115 | [InlineData(100)] 116 | [InlineData(long.MaxValue)] 117 | [InlineData(long.MinValue)] 118 | public void Seek_NotZero_Throws(long offset) 119 | { 120 | var stream = new SegmentReadStream(new List(), 0); 121 | 122 | Assert.Throws(() => stream.Seek(offset, SeekOrigin.Begin)); 123 | } 124 | 125 | [Theory] 126 | [MemberData(nameof(TestStreams))] 127 | public void ReadByte_CanReadAllBytes(TestStreamInitInfo info) 128 | { 129 | var stream = new SegmentReadStream(info.Segments, info.Length); 130 | 131 | for (var i = 0; i < stream.Length; i++) 132 | { 133 | Assert.Equal(i, stream.Position); 134 | Assert.Equal(i, stream.ReadByte()); 135 | } 136 | Assert.Equal(stream.Length, stream.Position); 137 | Assert.Equal(-1, stream.ReadByte()); 138 | Assert.Equal(stream.Length, stream.Position); 139 | } 140 | 141 | [Theory] 142 | [MemberData(nameof(TestStreams))] 143 | public void Read_CountLessThanSegmentSize_CanReadAllBytes(TestStreamInitInfo info) 144 | { 145 | var stream = new SegmentReadStream(info.Segments, info.Length); 146 | var count = info.SegmentSize - 1; 147 | 148 | for (var i = 0; i < stream.Length; i+=count) 149 | { 150 | var output = new byte[count]; 151 | var expectedOutput = new byte[count]; 152 | var expectedBytesRead = Math.Min(count, stream.Length - i); 153 | for (var j = 0; j < expectedBytesRead; j++) 154 | { 155 | expectedOutput[j] = (byte)(i + j); 156 | } 157 | Assert.Equal(i, stream.Position); 158 | Assert.Equal(expectedBytesRead, stream.Read(output, 0, count)); 159 | Assert.True(expectedOutput.SequenceEqual(output)); 160 | } 161 | Assert.Equal(stream.Length, stream.Position); 162 | Assert.Equal(0, stream.Read(new byte[count], 0, count)); 163 | Assert.Equal(stream.Length, stream.Position); 164 | } 165 | 166 | [Theory] 167 | [MemberData(nameof(TestStreams))] 168 | public void Read_CountEqualSegmentSize_CanReadAllBytes(TestStreamInitInfo info) 169 | { 170 | var stream = new SegmentReadStream(info.Segments, info.Length); 171 | var count = info.SegmentSize; 172 | 173 | for (var i = 0; i < stream.Length; i += count) 174 | { 175 | var output = new byte[count]; 176 | var expectedOutput = new byte[count]; 177 | var expectedBytesRead = Math.Min(count, stream.Length - i); 178 | for (var j = 0; j < expectedBytesRead; j++) 179 | { 180 | expectedOutput[j] = (byte)(i + j); 181 | } 182 | Assert.Equal(i, stream.Position); 183 | Assert.Equal(expectedBytesRead, stream.Read(output, 0, count)); 184 | Assert.True(expectedOutput.SequenceEqual(output)); 185 | } 186 | Assert.Equal(stream.Length, stream.Position); 187 | Assert.Equal(0, stream.Read(new byte[count], 0, count)); 188 | Assert.Equal(stream.Length, stream.Position); 189 | } 190 | 191 | [Theory] 192 | [MemberData(nameof(TestStreams))] 193 | public void Read_CountGreaterThanSegmentSize_CanReadAllBytes(TestStreamInitInfo info) 194 | { 195 | var stream = new SegmentReadStream(info.Segments, info.Length); 196 | var count = info.SegmentSize + 1; 197 | 198 | for (var i = 0; i < stream.Length; i += count) 199 | { 200 | var output = new byte[count]; 201 | var expectedOutput = new byte[count]; 202 | var expectedBytesRead = Math.Min(count, stream.Length - i); 203 | for (var j = 0; j < expectedBytesRead; j++) 204 | { 205 | expectedOutput[j] = (byte)(i + j); 206 | } 207 | Assert.Equal(i, stream.Position); 208 | Assert.Equal(expectedBytesRead, stream.Read(output, 0, count)); 209 | Assert.True(expectedOutput.SequenceEqual(output)); 210 | } 211 | Assert.Equal(stream.Length, stream.Position); 212 | Assert.Equal(0, stream.Read(new byte[count], 0, count)); 213 | Assert.Equal(stream.Length, stream.Position); 214 | } 215 | 216 | [Theory] 217 | [MemberData(nameof(TestStreams))] 218 | public void CopyToAsync_CopiesAllBytes(TestStreamInitInfo info) 219 | { 220 | var stream = new SegmentReadStream(info.Segments, info.Length); 221 | var writeStream = new SegmentWriteStream(info.SegmentSize); 222 | 223 | stream.CopyTo(writeStream); 224 | 225 | Assert.Equal(stream.Length, stream.Position); 226 | Assert.Equal(stream.Length, writeStream.Length); 227 | var writeSegments = writeStream.GetSegments(); 228 | for (var i = 0; i < info.Segments.Count; i++) 229 | { 230 | Assert.True(writeSegments[i].SequenceEqual(info.Segments[i])); 231 | } 232 | } 233 | 234 | [Theory] 235 | [MemberData(nameof(TestStreams))] 236 | public void CopyToAsync_CopiesFromCurrentPosition(TestStreamInitInfo info) 237 | { 238 | var skippedBytes = info.SegmentSize; 239 | var writeStream = new SegmentWriteStream((int)info.Length); 240 | var stream = new SegmentReadStream(info.Segments, info.Length); 241 | stream.Read(new byte[skippedBytes], 0, skippedBytes); 242 | 243 | stream.CopyTo(writeStream); 244 | 245 | Assert.Equal(stream.Length, stream.Position); 246 | Assert.Equal(stream.Length - skippedBytes, writeStream.Length); 247 | var writeSegments = writeStream.GetSegments(); 248 | 249 | for (var i = skippedBytes; i < info.Length; i++) 250 | { 251 | Assert.Equal(info.Segments[i / info.SegmentSize][i % info.SegmentSize], writeSegments[0][i - skippedBytes]); 252 | } 253 | } 254 | 255 | [Theory] 256 | [MemberData(nameof(TestStreams))] 257 | public void CopyToAsync_CopiesFromStart_AfterReset(TestStreamInitInfo info) 258 | { 259 | var skippedBytes = info.SegmentSize; 260 | var writeStream = new SegmentWriteStream(info.SegmentSize); 261 | var stream = new SegmentReadStream(info.Segments, info.Length); 262 | stream.Read(new byte[skippedBytes], 0, skippedBytes); 263 | 264 | stream.CopyTo(writeStream); 265 | 266 | // Assert bytes read from current location to the end 267 | Assert.Equal(stream.Length, stream.Position); 268 | Assert.Equal(stream.Length - skippedBytes, writeStream.Length); 269 | 270 | // Reset 271 | stream.Position = 0; 272 | writeStream = new SegmentWriteStream(info.SegmentSize); 273 | 274 | stream.CopyTo(writeStream); 275 | 276 | Assert.Equal(stream.Length, stream.Position); 277 | Assert.Equal(stream.Length, writeStream.Length); 278 | var writeSegments = writeStream.GetSegments(); 279 | for (var i = 0; i < info.Segments.Count; i++) 280 | { 281 | Assert.True(writeSegments[i].SequenceEqual(info.Segments[i])); 282 | } 283 | } 284 | } 285 | } 286 | -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.ResponseCaching.Tests/SegmentWriteStreamTests.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.IO; 6 | using System.Linq; 7 | using Microsoft.AspNetCore.ResponseCaching.Internal; 8 | using Xunit; 9 | 10 | namespace Microsoft.AspNetCore.ResponseCaching.Tests 11 | { 12 | public class SegmentWriteStreamTests 13 | { 14 | private static byte[] WriteData = new byte[] 15 | { 16 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14 17 | }; 18 | 19 | [Theory] 20 | [InlineData(0)] 21 | [InlineData(-1)] 22 | public void SegmentWriteStream_InvalidSegmentSize_Throws(int segmentSize) 23 | { 24 | Assert.Throws(() => new SegmentWriteStream(segmentSize)); 25 | } 26 | 27 | [Fact] 28 | public void ReadAndSeekOperations_Throws() 29 | { 30 | var stream = new SegmentWriteStream(1); 31 | 32 | Assert.Throws(() => stream.Read(new byte[1], 0, 0)); 33 | Assert.Throws(() => stream.Position = 0); 34 | Assert.Throws(() => stream.Seek(0, SeekOrigin.Begin)); 35 | } 36 | 37 | [Fact] 38 | public void GetSegments_ExtractionDisablesWriting() 39 | { 40 | var stream = new SegmentWriteStream(1); 41 | 42 | Assert.True(stream.CanWrite); 43 | Assert.Empty(stream.GetSegments()); 44 | Assert.False(stream.CanWrite); 45 | } 46 | 47 | [Theory] 48 | [InlineData(4)] 49 | [InlineData(5)] 50 | [InlineData(6)] 51 | public void WriteByte_CanWriteAllBytes(int segmentSize) 52 | { 53 | var stream = new SegmentWriteStream(segmentSize); 54 | 55 | foreach (var datum in WriteData) 56 | { 57 | stream.WriteByte(datum); 58 | } 59 | var segments = stream.GetSegments(); 60 | 61 | Assert.Equal(WriteData.Length, stream.Length); 62 | Assert.Equal((WriteData.Length + segmentSize - 1)/ segmentSize, segments.Count); 63 | 64 | for (var i = 0; i < WriteData.Length; i += segmentSize) 65 | { 66 | var expectedSegmentSize = Math.Min(segmentSize, WriteData.Length - i); 67 | var expectedSegment = new byte[expectedSegmentSize]; 68 | for (int j = 0; j < expectedSegmentSize; j++) 69 | { 70 | expectedSegment[j] = (byte)(i + j); 71 | } 72 | var segment = segments[i / segmentSize]; 73 | 74 | Assert.Equal(expectedSegmentSize, segment.Length); 75 | Assert.True(expectedSegment.SequenceEqual(segment)); 76 | } 77 | } 78 | 79 | [Theory] 80 | [InlineData(4)] 81 | [InlineData(5)] 82 | [InlineData(6)] 83 | public void Write_CanWriteAllBytes(int writeSize) 84 | { 85 | var segmentSize = 5; 86 | var stream = new SegmentWriteStream(segmentSize); 87 | 88 | 89 | for (var i = 0; i < WriteData.Length; i += writeSize) 90 | { 91 | stream.Write(WriteData, i, Math.Min(writeSize, WriteData.Length - i)); 92 | } 93 | var segments = stream.GetSegments(); 94 | 95 | Assert.Equal(WriteData.Length, stream.Length); 96 | Assert.Equal((WriteData.Length + segmentSize - 1) / segmentSize, segments.Count); 97 | 98 | for (var i = 0; i < WriteData.Length; i += segmentSize) 99 | { 100 | var expectedSegmentSize = Math.Min(segmentSize, WriteData.Length - i); 101 | var expectedSegment = new byte[expectedSegmentSize]; 102 | for (int j = 0; j < expectedSegmentSize; j++) 103 | { 104 | expectedSegment[j] = (byte)(i + j); 105 | } 106 | var segment = segments[i / segmentSize]; 107 | 108 | Assert.Equal(expectedSegmentSize, segment.Length); 109 | Assert.True(expectedSegment.SequenceEqual(segment)); 110 | } 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /test/Microsoft.AspNetCore.ResponseCaching.Tests/TestUtils.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Net.Http; 8 | using System.Text; 9 | using System.Threading; 10 | using System.Threading.Tasks; 11 | using Microsoft.AspNetCore.Builder; 12 | using Microsoft.AspNetCore.Hosting; 13 | using Microsoft.AspNetCore.Http; 14 | using Microsoft.AspNetCore.Http.Features; 15 | using Microsoft.AspNetCore.ResponseCaching.Internal; 16 | using Microsoft.Extensions.DependencyInjection; 17 | using Microsoft.Extensions.Logging; 18 | using Microsoft.Extensions.Logging.Abstractions; 19 | using Microsoft.Extensions.Logging.Testing; 20 | using Microsoft.Extensions.ObjectPool; 21 | using Microsoft.Extensions.Options; 22 | using Microsoft.Extensions.Primitives; 23 | using Microsoft.Net.Http.Headers; 24 | using Xunit; 25 | using ISystemClock = Microsoft.AspNetCore.ResponseCaching.Internal.ISystemClock; 26 | 27 | namespace Microsoft.AspNetCore.ResponseCaching.Tests 28 | { 29 | internal class TestUtils 30 | { 31 | static TestUtils() 32 | { 33 | // Force sharding in tests 34 | StreamUtilities.BodySegmentSize = 10; 35 | } 36 | 37 | private static bool TestRequestDelegate(HttpContext context, string guid) 38 | { 39 | var headers = context.Response.GetTypedHeaders(); 40 | 41 | var expires = context.Request.Query["Expires"]; 42 | if (!string.IsNullOrEmpty(expires)) 43 | { 44 | headers.Expires = DateTimeOffset.Now.AddSeconds(int.Parse(expires)); 45 | } 46 | 47 | if (headers.CacheControl == null) 48 | { 49 | headers.CacheControl = new CacheControlHeaderValue 50 | { 51 | Public = true, 52 | MaxAge = string.IsNullOrEmpty(expires) ? TimeSpan.FromSeconds(10) : (TimeSpan?)null 53 | }; 54 | } 55 | else 56 | { 57 | headers.CacheControl.Public = true; 58 | headers.CacheControl.MaxAge = string.IsNullOrEmpty(expires) ? TimeSpan.FromSeconds(10) : (TimeSpan?)null; 59 | } 60 | headers.Date = DateTimeOffset.UtcNow; 61 | headers.Headers["X-Value"] = guid; 62 | 63 | if (context.Request.Method != "HEAD") 64 | { 65 | return true; 66 | } 67 | return false; 68 | } 69 | 70 | internal static async Task TestRequestDelegateWriteAsync(HttpContext context) 71 | { 72 | var uniqueId = Guid.NewGuid().ToString(); 73 | if (TestRequestDelegate(context, uniqueId)) 74 | { 75 | await context.Response.WriteAsync(uniqueId); 76 | } 77 | } 78 | 79 | internal static Task TestRequestDelegateWrite(HttpContext context) 80 | { 81 | var uniqueId = Guid.NewGuid().ToString(); 82 | if (TestRequestDelegate(context, uniqueId)) 83 | { 84 | context.Response.Write(uniqueId); 85 | } 86 | return Task.CompletedTask; 87 | } 88 | 89 | internal static IResponseCachingKeyProvider CreateTestKeyProvider() 90 | { 91 | return CreateTestKeyProvider(new ResponseCachingOptions()); 92 | } 93 | 94 | internal static IResponseCachingKeyProvider CreateTestKeyProvider(ResponseCachingOptions options) 95 | { 96 | return new ResponseCachingKeyProvider(new DefaultObjectPoolProvider(), Options.Create(options)); 97 | } 98 | 99 | internal static IEnumerable CreateBuildersWithResponseCaching( 100 | Action configureDelegate = null, 101 | ResponseCachingOptions options = null, 102 | Action contextAction = null) 103 | { 104 | return CreateBuildersWithResponseCaching(configureDelegate, options, new RequestDelegate[] 105 | { 106 | context => 107 | { 108 | contextAction?.Invoke(context); 109 | return TestRequestDelegateWrite(context); 110 | }, 111 | context => 112 | { 113 | contextAction?.Invoke(context); 114 | return TestRequestDelegateWriteAsync(context); 115 | }, 116 | }); 117 | } 118 | 119 | private static IEnumerable CreateBuildersWithResponseCaching( 120 | Action configureDelegate = null, 121 | ResponseCachingOptions options = null, 122 | IEnumerable requestDelegates = null) 123 | { 124 | if (configureDelegate == null) 125 | { 126 | configureDelegate = app => { }; 127 | } 128 | if (requestDelegates == null) 129 | { 130 | requestDelegates = new RequestDelegate[] 131 | { 132 | TestRequestDelegateWriteAsync, 133 | TestRequestDelegateWrite 134 | }; 135 | } 136 | 137 | foreach (var requestDelegate in requestDelegates) 138 | { 139 | // Test with in memory ResponseCache 140 | yield return new WebHostBuilder() 141 | .ConfigureServices(services => 142 | { 143 | services.AddResponseCaching(responseCachingOptions => 144 | { 145 | if (options != null) 146 | { 147 | responseCachingOptions.MaximumBodySize = options.MaximumBodySize; 148 | responseCachingOptions.UseCaseSensitivePaths = options.UseCaseSensitivePaths; 149 | responseCachingOptions.SystemClock = options.SystemClock; 150 | } 151 | }); 152 | }) 153 | .Configure(app => 154 | { 155 | configureDelegate(app); 156 | app.UseResponseCaching(); 157 | app.Run(requestDelegate); 158 | }); 159 | } 160 | } 161 | 162 | internal static ResponseCachingMiddleware CreateTestMiddleware( 163 | RequestDelegate next = null, 164 | IResponseCache cache = null, 165 | ResponseCachingOptions options = null, 166 | TestSink testSink = null, 167 | IResponseCachingKeyProvider keyProvider = null, 168 | IResponseCachingPolicyProvider policyProvider = null) 169 | { 170 | if (next == null) 171 | { 172 | next = httpContext => Task.CompletedTask; 173 | } 174 | if (cache == null) 175 | { 176 | cache = new TestResponseCache(); 177 | } 178 | if (options == null) 179 | { 180 | options = new ResponseCachingOptions(); 181 | } 182 | if (keyProvider == null) 183 | { 184 | keyProvider = new ResponseCachingKeyProvider(new DefaultObjectPoolProvider(), Options.Create(options)); 185 | } 186 | if (policyProvider == null) 187 | { 188 | policyProvider = new TestResponseCachingPolicyProvider(); 189 | } 190 | 191 | return new ResponseCachingMiddleware( 192 | next, 193 | Options.Create(options), 194 | testSink == null ? (ILoggerFactory)NullLoggerFactory.Instance : new TestLoggerFactory(testSink, true), 195 | policyProvider, 196 | cache, 197 | keyProvider); 198 | } 199 | 200 | internal static ResponseCachingContext CreateTestContext() 201 | { 202 | return new ResponseCachingContext(new DefaultHttpContext(), NullLogger.Instance) 203 | { 204 | ResponseTime = DateTimeOffset.UtcNow 205 | }; 206 | } 207 | 208 | internal static ResponseCachingContext CreateTestContext(ITestSink testSink) 209 | { 210 | return new ResponseCachingContext(new DefaultHttpContext(), new TestLogger("ResponseCachingTests", testSink, true)) 211 | { 212 | ResponseTime = DateTimeOffset.UtcNow 213 | }; 214 | } 215 | 216 | internal static void AssertLoggedMessages(IEnumerable messages, params LoggedMessage[] expectedMessages) 217 | { 218 | var messageList = messages.ToList(); 219 | Assert.Equal(messageList.Count, expectedMessages.Length); 220 | 221 | for (var i = 0; i < messageList.Count; i++) 222 | { 223 | Assert.Equal(expectedMessages[i].EventId, messageList[i].EventId); 224 | Assert.Equal(expectedMessages[i].LogLevel, messageList[i].LogLevel); 225 | } 226 | } 227 | 228 | public static HttpRequestMessage CreateRequest(string method, string requestUri) 229 | { 230 | return new HttpRequestMessage(new HttpMethod(method), requestUri); 231 | } 232 | } 233 | 234 | internal static class HttpResponseWritingExtensions 235 | { 236 | internal static void Write(this HttpResponse response, string text) 237 | { 238 | if (response == null) 239 | { 240 | throw new ArgumentNullException(nameof(response)); 241 | } 242 | 243 | if (text == null) 244 | { 245 | throw new ArgumentNullException(nameof(text)); 246 | } 247 | 248 | byte[] data = Encoding.UTF8.GetBytes(text); 249 | response.Body.Write(data, 0, data.Length); 250 | } 251 | } 252 | 253 | internal class LoggedMessage 254 | { 255 | internal static LoggedMessage RequestMethodNotCacheable => new LoggedMessage(1, LogLevel.Debug); 256 | internal static LoggedMessage RequestWithAuthorizationNotCacheable => new LoggedMessage(2, LogLevel.Debug); 257 | internal static LoggedMessage RequestWithNoCacheNotCacheable => new LoggedMessage(3, LogLevel.Debug); 258 | internal static LoggedMessage RequestWithPragmaNoCacheNotCacheable => new LoggedMessage(4, LogLevel.Debug); 259 | internal static LoggedMessage ExpirationMinFreshAdded => new LoggedMessage(5, LogLevel.Debug); 260 | internal static LoggedMessage ExpirationSharedMaxAgeExceeded => new LoggedMessage(6, LogLevel.Debug); 261 | internal static LoggedMessage ExpirationMustRevalidate => new LoggedMessage(7, LogLevel.Debug); 262 | internal static LoggedMessage ExpirationMaxStaleSatisfied => new LoggedMessage(8, LogLevel.Debug); 263 | internal static LoggedMessage ExpirationMaxAgeExceeded => new LoggedMessage(9, LogLevel.Debug); 264 | internal static LoggedMessage ExpirationExpiresExceeded => new LoggedMessage(10, LogLevel.Debug); 265 | internal static LoggedMessage ResponseWithoutPublicNotCacheable => new LoggedMessage(11, LogLevel.Debug); 266 | internal static LoggedMessage ResponseWithNoStoreNotCacheable => new LoggedMessage(12, LogLevel.Debug); 267 | internal static LoggedMessage ResponseWithNoCacheNotCacheable => new LoggedMessage(13, LogLevel.Debug); 268 | internal static LoggedMessage ResponseWithSetCookieNotCacheable => new LoggedMessage(14, LogLevel.Debug); 269 | internal static LoggedMessage ResponseWithVaryStarNotCacheable => new LoggedMessage(15, LogLevel.Debug); 270 | internal static LoggedMessage ResponseWithPrivateNotCacheable => new LoggedMessage(16, LogLevel.Debug); 271 | internal static LoggedMessage ResponseWithUnsuccessfulStatusCodeNotCacheable => new LoggedMessage(17, LogLevel.Debug); 272 | internal static LoggedMessage NotModifiedIfNoneMatchStar => new LoggedMessage(18, LogLevel.Debug); 273 | internal static LoggedMessage NotModifiedIfNoneMatchMatched => new LoggedMessage(19, LogLevel.Debug); 274 | internal static LoggedMessage NotModifiedIfModifiedSinceSatisfied => new LoggedMessage(20, LogLevel.Debug); 275 | internal static LoggedMessage NotModifiedServed => new LoggedMessage(21, LogLevel.Information); 276 | internal static LoggedMessage CachedResponseServed => new LoggedMessage(22, LogLevel.Information); 277 | internal static LoggedMessage GatewayTimeoutServed => new LoggedMessage(23, LogLevel.Information); 278 | internal static LoggedMessage NoResponseServed => new LoggedMessage(24, LogLevel.Information); 279 | internal static LoggedMessage VaryByRulesUpdated => new LoggedMessage(25, LogLevel.Debug); 280 | internal static LoggedMessage ResponseCached => new LoggedMessage(26, LogLevel.Information); 281 | internal static LoggedMessage ResponseNotCached => new LoggedMessage(27, LogLevel.Information); 282 | internal static LoggedMessage ResponseContentLengthMismatchNotCached => new LoggedMessage(28, LogLevel.Warning); 283 | internal static LoggedMessage ExpirationInfiniteMaxStaleSatisfied => new LoggedMessage(29, LogLevel.Debug); 284 | 285 | private LoggedMessage(int evenId, LogLevel logLevel) 286 | { 287 | EventId = evenId; 288 | LogLevel = logLevel; 289 | } 290 | 291 | internal int EventId { get; } 292 | internal LogLevel LogLevel { get; } 293 | } 294 | 295 | internal class DummySendFileFeature : IHttpSendFileFeature 296 | { 297 | public Task SendFileAsync(string path, long offset, long? count, CancellationToken cancellation) 298 | { 299 | return Task.CompletedTask; 300 | } 301 | } 302 | 303 | internal class TestResponseCachingPolicyProvider : IResponseCachingPolicyProvider 304 | { 305 | public bool AllowCacheLookupValue { get; set; } = false; 306 | public bool AllowCacheStorageValue { get; set; } = false; 307 | public bool AttemptResponseCachingValue { get; set; } = false; 308 | public bool IsCachedEntryFreshValue { get; set; } = true; 309 | public bool IsResponseCacheableValue { get; set; } = true; 310 | 311 | public bool AllowCacheLookup(ResponseCachingContext context) => AllowCacheLookupValue; 312 | 313 | public bool AllowCacheStorage(ResponseCachingContext context) => AllowCacheStorageValue; 314 | 315 | public bool AttemptResponseCaching(ResponseCachingContext context) => AttemptResponseCachingValue; 316 | 317 | public bool IsCachedEntryFresh(ResponseCachingContext context) => IsCachedEntryFreshValue; 318 | 319 | public bool IsResponseCacheable(ResponseCachingContext context) => IsResponseCacheableValue; 320 | } 321 | 322 | internal class TestResponseCachingKeyProvider : IResponseCachingKeyProvider 323 | { 324 | private readonly string _baseKey; 325 | private readonly StringValues _varyKey; 326 | 327 | public TestResponseCachingKeyProvider(string lookupBaseKey = null, StringValues? lookupVaryKey = null) 328 | { 329 | _baseKey = lookupBaseKey; 330 | if (lookupVaryKey.HasValue) 331 | { 332 | _varyKey = lookupVaryKey.Value; 333 | } 334 | } 335 | 336 | public IEnumerable CreateLookupVaryByKeys(ResponseCachingContext context) 337 | { 338 | foreach (var varyKey in _varyKey) 339 | { 340 | yield return _baseKey + varyKey; 341 | } 342 | } 343 | 344 | public string CreateBaseKey(ResponseCachingContext context) 345 | { 346 | return _baseKey; 347 | } 348 | 349 | public string CreateStorageVaryByKey(ResponseCachingContext context) 350 | { 351 | throw new NotImplementedException(); 352 | } 353 | } 354 | 355 | internal class TestResponseCache : IResponseCache 356 | { 357 | private readonly IDictionary _storage = new Dictionary(); 358 | public int GetCount { get; private set; } 359 | public int SetCount { get; private set; } 360 | 361 | public IResponseCacheEntry Get(string key) 362 | { 363 | GetCount++; 364 | try 365 | { 366 | return _storage[key]; 367 | } 368 | catch 369 | { 370 | return null; 371 | } 372 | } 373 | 374 | public Task GetAsync(string key) 375 | { 376 | return Task.FromResult(Get(key)); 377 | } 378 | 379 | public void Set(string key, IResponseCacheEntry entry, TimeSpan validFor) 380 | { 381 | SetCount++; 382 | _storage[key] = entry; 383 | } 384 | 385 | public Task SetAsync(string key, IResponseCacheEntry entry, TimeSpan validFor) 386 | { 387 | Set(key, entry, validFor); 388 | return Task.CompletedTask; 389 | } 390 | } 391 | 392 | internal class TestClock : ISystemClock 393 | { 394 | public DateTimeOffset UtcNow { get; set; } 395 | } 396 | } 397 | -------------------------------------------------------------------------------- /version.props: -------------------------------------------------------------------------------- 1 |  2 | 3 | 3.0.0 4 | dev 5 | 6 | 7 | --------------------------------------------------------------------------------