├── .editorconfig ├── .gitattributes ├── .github └── workflows │ └── PublishNugetPackage.yml ├── .gitignore ├── CodeMaid.config ├── Cuture.AspNetCore.ResponseCaching.sln ├── Directory.Build.props ├── LICENSE ├── flow_of_execution.md ├── package.props ├── readme.md ├── src ├── Cuture.AspNetCore.ResponseCaching.StackExchange.Redis │ ├── Cuture.AspNetCore.ResponseCaching.StackExchange.Redis.csproj │ ├── RedisResponseCacheOptions.cs │ ├── ResponseCaches │ │ └── RedisResponseCache.cs │ └── ResponseCachingServicesExtensions.cs └── Cuture.AspNetCore.ResponseCaching │ ├── Attributes │ ├── CacheKeyGeneratorAttribute.cs │ ├── CacheModelKeyParserAttribute.cs │ ├── ExecutingLockAttribute.cs │ ├── HotDataCacheAttribute.cs │ ├── QuickSet │ │ ├── CacheByClaimAttribute.cs │ │ ├── CacheByFormAttribute.cs │ │ ├── CacheByFullUrlAttribute.cs │ │ ├── CacheByHeaderAttribute.cs │ │ ├── CacheByModelAttribute.cs │ │ ├── CacheByPathAttribute.cs │ │ └── CacheByQueryAttribute.cs │ ├── ResponseCacheableAttribute.cs │ ├── ResponseCachingAttribute.cs │ └── ResponseDumpCapacityAttribute.cs │ ├── CacheKey │ ├── Builders │ │ ├── CacheKeyBuilder.cs │ │ ├── ClaimsCacheKeyBuilder.cs │ │ ├── FormKeysCacheKeyBuilder.cs │ │ ├── ModelCacheKeyBuilder.cs │ │ ├── QueryKeysCacheKeyBuilder.cs │ │ └── RequestHeadersCacheKeyBuilder.cs │ ├── Generators │ │ ├── DefaultCacheKeyGenerator.cs │ │ ├── DefiniteCacheKeyGenerator.cs │ │ ├── FullPathAndQueryCacheKeyGenerator.cs │ │ ├── ICacheKeyGenerator.cs │ │ ├── IEndpointSetter.cs │ │ └── RequestPathCacheKeyGenerator.cs │ └── ICacheKeyable.cs │ ├── CacheKeyAccessor.cs │ ├── Checks.cs │ ├── Context │ ├── DefaultModelKeyParser.cs │ ├── DefaultResponseCacheDeterminer.cs │ ├── DefaultResponseDumpStreamFactory.cs │ ├── IModelKeyParser.cs │ ├── IResponseCacheDeterminer.cs │ ├── IResponseDumpStreamFactory.cs │ └── ResponseCachingContext.cs │ ├── Cuture.AspNetCore.ResponseCaching.csproj │ ├── DefaultEndpointAccessor.cs │ ├── DefaultResponseCachingFilterBuilder.cs │ ├── Diagnostics │ ├── CachingDiagnostics.cs │ ├── CachingDiagnosticsAccessor.cs │ ├── DiagnosticLogger.cs │ ├── DiagnosticLoggerSubscriber.cs │ ├── DiagnosticLoggerSubscriberDisposerAccessor.cs │ └── EventData.cs │ ├── Enums │ ├── CacheKeyStrictMode.cs │ ├── CacheMode.cs │ ├── CacheStoreLocation.cs │ ├── ExecutingLockMode.cs │ └── FilterType.cs │ ├── Exceptions │ ├── CacheVaryKeyNotFoundException.cs │ ├── RequestFormNotFoundException.cs │ └── ResponseCachingException.cs │ ├── ExecutingLock │ ├── ExclusiveExecutingLockLifecycleExecutor.cs │ ├── ExecutingLockLifecycleExecutorBase.cs │ ├── Lock │ │ ├── ExclusiveExecutingLock.cs │ │ ├── ExecutingLockBase.cs │ │ ├── IExecutingLock.cs │ │ └── SharedExecutingLock.cs │ ├── Pool │ │ ├── ExecutingLockPool.cs │ │ ├── IExecutingLockPool.cs │ │ └── SingleLockExecutingLockPool.cs │ └── SharedExecutingLockLifecycleExecutor.cs │ ├── Extensions │ └── InterceptorOptionsExtensions.cs │ ├── Filters │ ├── CacheFilterBase.cs │ ├── DefaultActionCacheFilter.cs │ ├── DefaultLockedActionCacheFilter.cs │ ├── DefaultLockedResourceCacheFilter.cs │ ├── DefaultResourceCacheFilter.cs │ └── EmptyFilterMetadata.cs │ ├── IEndpointAccessor.cs │ ├── IOrdered.cs │ ├── IResponseCachingFilterBuilder.cs │ ├── Interceptors │ ├── CacheHitStampInterceptor.cs │ ├── InterceptorAggregator.cs │ ├── InterceptorOptions.cs │ └── Interface │ │ ├── ICacheStoringInterceptor.cs │ │ ├── IResponseCachingInterceptor.cs │ │ └── IResponseWritingInterceptor.cs │ ├── Internal │ ├── ActionPathCache.cs │ ├── Caching │ │ └── Memory │ │ │ ├── BoundedMemoryCache.cs │ │ │ ├── BoundedMemoryCacheEntry.cs │ │ │ ├── DefaultBoundedMemoryCache.cs │ │ │ ├── IBoundedMemoryCache.cs │ │ │ ├── IBoundedMemoryCacheExtensions.cs │ │ │ ├── IBoundedMemoryCachePolicy.cs │ │ │ └── LRUMemoryCache │ │ │ ├── LRUMemoryCachePolicy.cs │ │ │ ├── LRUSpecializedLinkedList.cs │ │ │ └── LRUSpecializedLinkedListNode.cs │ ├── DefaultExecutionLockTimeoutFallback.cs │ ├── EndpointMetadataCollectionExtensions.cs │ ├── HttpRequestExtensions.cs │ ├── LocalCachedPayload.cs │ ├── ObjectPool │ │ ├── BoundedObjectPool.cs │ │ ├── BoundedObjectPoolOptions.cs │ │ ├── DefaultBoundedObjectPool.cs │ │ ├── DefaultObjectLifecycleExecutor.cs │ │ ├── DefaultPoolReductionPolicy.cs │ │ ├── IBoundedObjectPool.cs │ │ ├── INakedBoundedObjectPool.cs │ │ ├── IObjectLifecycleExecutor.cs │ │ ├── IObjectOwner.cs │ │ ├── IPoolReductionPolicy.cs │ │ ├── IRecyclePool.cs │ │ └── ObjectOwner.cs │ ├── PooledReadOnlyCharSpan.cs │ ├── QueryStringOrderUtil.cs │ ├── ResponseDumpContext.cs │ ├── SinglePassSemaphoreLifecycleExecutor.cs │ └── StringExtensions.cs │ ├── Metadatas │ ├── CachePatterns │ │ ├── IResponseCachePatternMetadata.cs │ │ ├── IResponseClaimCachePatternMetadata.cs │ │ ├── IResponseFormCachePatternMetadata.cs │ │ ├── IResponseHeaderCachePatternMetadata.cs │ │ ├── IResponseModelCachePatternMetadata.cs │ │ └── IResponseQueryCachePatternMetadata.cs │ ├── ICacheKeyGeneratorMetadata.cs │ ├── ICacheKeyStrictModeMetadata.cs │ ├── ICacheModeMetadata.cs │ ├── ICacheModelKeyParserMetadata.cs │ ├── ICacheStoreLocationMetadata.cs │ ├── IDumpStreamInitialCapacityMetadata.cs │ ├── IExecutingLockMetadata.cs │ ├── IHotDataCacheMetadata.cs │ ├── IMaxCacheableResponseLengthMetadata.cs │ ├── IResponseCachingMetadata.cs │ └── IResponseDurationMetadata.cs │ ├── ResponseCaches │ ├── DefaultMemoryResponseCache.cs │ ├── HotDataCache │ │ ├── DefaultHotDataCacheProvider.cs │ │ ├── IHotDataCache.cs │ │ ├── IHotDataCacheBuilder.cs │ │ ├── IHotDataCacheProvider.cs │ │ └── LRUHotDataCache.cs │ ├── HotDataCachePolicy.cs │ ├── IDistributedResponseCache.cs │ ├── IMemoryResponseCache.cs │ ├── IResponseCache.cs │ ├── ResponseCacheEntry.cs │ ├── ResponseCacheEntryOptions.cs │ └── ResponseCacheHotDataCacheWrapper.cs │ ├── ResponseCachingConstants.cs │ ├── ResponseCachingExecutingLockOptions.cs │ ├── ResponseCachingOptions.cs │ ├── ResponseCachingServiceBuilder.cs │ └── ResponseCachingServicesExtensions.cs └── test ├── ResponseCaching.Test.WebHost ├── AuthorizeMixedAttribute.cs ├── Controllers │ ├── CacheByAllMixedController.cs │ ├── CacheByCustomCacheKeyGeneratorController.cs │ ├── CacheByCustomModelKeyParserController.cs │ ├── CacheByFormController.cs │ ├── CacheByFullQueryController.cs │ ├── CacheByFullUrlController.cs │ ├── CacheByFullUrlKeyAccessController.cs │ ├── CacheByHeaderController.cs │ ├── CacheByModelController.cs │ ├── CacheByModelKeyAccessController.cs │ ├── CacheByPathController.cs │ ├── CacheByPathWithCustomInterceptorController.cs │ ├── CacheByQueryController.cs │ ├── CacheByQueryWithAuthorizeController.cs │ ├── ExecuteLockTimeoutController.cs │ ├── HotDataCacheController.cs │ ├── LoginController.cs │ ├── MaxCacheableResponseLengthController.cs │ ├── TestControllerBase.cs │ └── WeatherForecastController.cs ├── Dtos │ └── PageResultRequestDto.cs ├── Models │ └── WeatherForecast.cs ├── Properties │ └── launchSettings.json ├── ResponseCaching.Test.WebHost.csproj ├── Startup.cs ├── Test │ ├── TestCachingProcessInterceptor.cs │ ├── TestCustomCacheKeyGenerator.cs │ ├── TestCustomModelKeyParser.cs │ └── TestDataGenerator.cs ├── TestWebHost.cs └── appsettings.json └── ResponseCaching.Test ├── Base ├── AuthenticationRequiredTestBase.cs ├── TestCutureHttpMessageInvokerPool.cs └── WebServerHostedTestBase.cs ├── HotDataCacheTests ├── CountDistributedResponseCache.cs └── LRUHotDataCacheTest.cs ├── Interceptors ├── CacheHitStampHeaderTest.cs ├── CacheKeyAccessorTest.cs └── InterceptorAggregatorTest.cs ├── QueryStringOrderUtilTest.cs ├── RequestTests ├── CacheByAllMixedTest.cs ├── CacheByCustomModelKeyParserTest.cs ├── CacheByCustomTest.cs ├── CacheByFormTest.cs ├── CacheByFullQueryTest.cs ├── CacheByFullUrlTest.cs ├── CacheByHeaderTest.cs ├── CacheByModelTest.cs ├── CacheByPathTest.cs ├── CacheByQueryTest.cs ├── CacheByQueryWithCookieAuthorizeTest.cs ├── CacheByQueryWithJWTAuthorizeTest.cs ├── CustomCachingProcessInterceptorTest.cs ├── ExecuteLockTimeoutTest.cs ├── MaxCacheableResponseLengthTest.cs ├── NoneCachingTest.cs └── RequestTestBase.cs ├── ResponseCaches ├── MemoryResponseCacheTest.cs ├── RedisResponseCacheTest.cs └── ResponseCacheTest.cs ├── ResponseCaching.Test.csproj └── TestUtil.cs /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/workflows/PublishNugetPackage.yml: -------------------------------------------------------------------------------- 1 | name: Publish Nuget Package 2 | 3 | on: 4 | release: 5 | types: [released,prereleased] 6 | branches: [ master ] 7 | 8 | jobs: 9 | publish-with-build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v3 15 | - name: Setup .NET Core SDK 16 | uses: actions/setup-dotnet@v3 17 | with: 18 | dotnet-version: '8.0.x' 19 | - name: build - main 20 | run: dotnet build -c Release ./src/Cuture.AspNetCore.ResponseCaching/Cuture.AspNetCore.ResponseCaching.csproj 21 | - name: build - redis 22 | run: dotnet build -c Release ./src/Cuture.AspNetCore.ResponseCaching.StackExchange.Redis/Cuture.AspNetCore.ResponseCaching.StackExchange.Redis.csproj 23 | - name: setup test redis 24 | uses: zhulik/redis-action@1.1.0 25 | - name: run test 26 | run: dotnet user-secrets set 'ResponseCache_Test_Redis' '127.0.0.1:6379,allowAdmin=true' -p ./test/ResponseCaching.Test.WebHost/ResponseCaching.Test.WebHost.csproj && dotnet test -c Release 27 | - name: pack 28 | run: dotnet pack -c Release -o ./output --include-symbols 29 | - name: push package 30 | shell: pwsh 31 | working-directory: ./output 32 | run: Get-ChildItem -File -Filter '*.nupkg' | ForEach-Object { dotnet nuget push $_ -k ${{secrets.NUGET_KEY}} -s https://api.nuget.org/v3/index.json --no-service-endpoint --skip-duplicate; dotnet nuget push $_ -k ${{secrets.NUGET_GITHUB_KEY}} -s https://nuget.pkg.github.com/StratosBlue/index.json --no-service-endpoint --skip-duplicate; } 33 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 |  2 | 3 | net6.0;net7.0;net8.0 4 | 5 | enable 6 | enable 7 | 8 | latest 9 | 10 | 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 StratosBlue 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.props: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | true 5 | 6 | 7 | 8 | 9 | 2.2.0 10 | 11 | 12 | The `asp.net core` server-side caching component implemented based on `ResourceFilter` and `ActionFilter`; 基于`ResourceFilter`和`ActionFilter`实现的`asp.net core`服务端缓存组件 13 | 14 | Cuture.AspNetCore.ResponseCaching 15 | Stratos 16 | MIT 17 | https://github.com/StratosBlue/Cuture.AspNetCore.ResponseCaching 18 | 19 | git 20 | $(PackageProjectUrl) 21 | 22 | aspnetcore cache caching responsecache responsecaching response-cache response-caching redis 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | true 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching.StackExchange.Redis/Cuture.AspNetCore.ResponseCaching.StackExchange.Redis.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Cuture.AspNetCore.ResponseCaching 6 | 7 | $(Description)。此包为使用 StackExchange.Redis 的 ResponseCache 实现。 8 | 9 | 10 | 11 | $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching.StackExchange.Redis/RedisResponseCacheOptions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | 3 | using StackExchange.Redis; 4 | 5 | namespace Cuture.AspNetCore.ResponseCaching; 6 | 7 | /// 8 | /// redis响应缓存选项 9 | /// 10 | public class RedisResponseCacheOptions : IOptions 11 | { 12 | #region Public 属性 13 | 14 | /// 15 | /// 缓存Key前缀 16 | /// 17 | public string? CacheKeyPrefix { get; set; } 18 | 19 | /// 20 | /// 连接配置 21 | /// 22 | public string? Configuration { get; set; } 23 | 24 | /// 25 | /// 26 | /// 27 | public IConnectionMultiplexer? ConnectionMultiplexer { get; set; } 28 | 29 | /// 30 | /// 31 | /// 32 | public RedisResponseCacheOptions Value => this; 33 | 34 | #endregion Public 属性 35 | } 36 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Attributes/CacheKeyGeneratorAttribute.cs: -------------------------------------------------------------------------------- 1 | using Cuture.AspNetCore.ResponseCaching; 2 | using Cuture.AspNetCore.ResponseCaching.CacheKey.Generators; 3 | using Cuture.AspNetCore.ResponseCaching.Metadatas; 4 | 5 | using Microsoft.AspNetCore.Http; 6 | 7 | namespace Microsoft.AspNetCore.Mvc; 8 | 9 | /// 10 | /// 指定缓存Key生成器 11 | /// 12 | /// 13 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] 14 | public class CacheKeyGeneratorAttribute : Attribute, ICacheKeyGeneratorMetadata 15 | { 16 | #region Public 属性 17 | 18 | /// 19 | public Type CacheKeyGeneratorType { get; } 20 | 21 | /// 22 | public FilterType FilterType { get; } 23 | 24 | #endregion Public 属性 25 | 26 | #region Public 构造函数 27 | 28 | /// 29 | /// 30 | /// 31 | /// 32 | /// 需要实现 接口 33 | /// 34 | /// 需要 Action 的 时, 实现 接口 35 | /// 36 | /// 37 | public CacheKeyGeneratorAttribute(Type type, FilterType filterType) 38 | { 39 | ArgumentNullException.ThrowIfNull(type); 40 | 41 | CacheKeyGeneratorType = Checks.ThrowIfNotICacheKeyGenerator(type); 42 | FilterType = filterType; 43 | } 44 | 45 | #endregion Public 构造函数 46 | } 47 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Attributes/CacheModelKeyParserAttribute.cs: -------------------------------------------------------------------------------- 1 | using Cuture.AspNetCore.ResponseCaching; 2 | using Cuture.AspNetCore.ResponseCaching.Metadatas; 3 | 4 | namespace Microsoft.AspNetCore.Mvc; 5 | 6 | /// 7 | /// 指定Model的Key解析器类型 8 | /// 9 | /// 10 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] 11 | public class CacheModelKeyParserAttribute : Attribute, ICacheModelKeyParserMetadata 12 | { 13 | #region Public 属性 14 | 15 | /// 16 | public Type ModelKeyParserType { get; } 17 | 18 | #endregion Public 属性 19 | 20 | #region Public 构造函数 21 | 22 | /// 23 | /// 24 | /// 25 | /// 26 | /// Model的Key解析器类型 27 | /// 28 | /// 需要实现 接口 29 | /// 30 | public CacheModelKeyParserAttribute(Type type) 31 | { 32 | ArgumentNullException.ThrowIfNull(type); 33 | 34 | ModelKeyParserType = Checks.ThrowIfNotIModelKeyParser(type); 35 | } 36 | 37 | #endregion Public 构造函数 38 | } 39 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Attributes/ExecutingLockAttribute.cs: -------------------------------------------------------------------------------- 1 | using Cuture.AspNetCore.ResponseCaching; 2 | using Cuture.AspNetCore.ResponseCaching.Metadatas; 3 | 4 | namespace Microsoft.AspNetCore.Mvc; 5 | 6 | /// 7 | /// 指定缓存通行模式(设置执行action的并发控制) 8 | /// Note: 9 | /// * 越细粒度的控制会带来相对更多的性能消耗 10 | /// * 虽然已经尽可能的实现了并发控制,仍然最好不要依赖此功能实现具体业务 11 | /// 12 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] 13 | public class ExecutingLockAttribute : Attribute, IExecutingLockMetadata 14 | { 15 | #region Public 属性 16 | 17 | /// 18 | public int? LockMillisecondsTimeout { get; } = null; 19 | 20 | /// 21 | public ExecutingLockMode LockMode { get; } = ExecutingLockMode.Default; 22 | 23 | /// 24 | public ExecutionLockTimeoutFallbackDelegate? OnExecutionLockTimeout { get; set; } 25 | 26 | #endregion Public 属性 27 | 28 | #region Public 构造函数 29 | 30 | /// 31 | public ExecutingLockAttribute(ExecutingLockMode lockMode) 32 | { 33 | LockMode = Checks.ThrowIfExecutingLockModeIsNone(lockMode); 34 | } 35 | 36 | /// 37 | /// 38 | /// 39 | /// 锁定模式 40 | /// 锁定的等待超时时间 41 | public ExecutingLockAttribute(ExecutingLockMode lockMode, int lockMillisecondsTimeout) : this(lockMode) 42 | { 43 | LockMillisecondsTimeout = Checks.ThrowIfLockMillisecondsTimeoutInvalid(lockMillisecondsTimeout); 44 | } 45 | 46 | #endregion Public 构造函数 47 | } 48 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Attributes/HotDataCacheAttribute.cs: -------------------------------------------------------------------------------- 1 | using Cuture.AspNetCore.ResponseCaching.Metadatas; 2 | using Cuture.AspNetCore.ResponseCaching.ResponseCaches; 3 | 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace Microsoft.AspNetCore.Mvc; 7 | 8 | /// 9 | /// 本地热数据缓存 10 | /// 11 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] 12 | public class HotDataCacheAttribute : Attribute, IHotDataCacheBuilder, IHotDataCacheMetadata 13 | { 14 | #region Public 属性 15 | 16 | /// 17 | public HotDataCachePolicy CachePolicy { get; } = HotDataCachePolicy.Default; 18 | 19 | /// 20 | public int Capacity { get; } 21 | 22 | /// 23 | public string? HotDataCacheName { get; } 24 | 25 | /// 26 | public HotDataCacheAttribute(int capacity) 27 | { 28 | Capacity = capacity; 29 | } 30 | 31 | /// 32 | /// 33 | /// 34 | /// 缓存热数据的数量 35 | /// 缓存替换策略 36 | public HotDataCacheAttribute(int capacity, HotDataCachePolicy cachePolicy) 37 | { 38 | Capacity = capacity; 39 | CachePolicy = cachePolicy; 40 | } 41 | 42 | #endregion Public 属性 43 | 44 | #region Public 方法 45 | 46 | /// 47 | public IHotDataCache Build(IServiceProvider serviceProvider, IHotDataCacheMetadata metadata) 48 | { 49 | var hotDataCacheProvider = serviceProvider.GetRequiredService(); 50 | return hotDataCacheProvider.Get(serviceProvider, 51 | metadata.HotDataCacheName ?? string.Empty, 52 | metadata.CachePolicy, 53 | metadata.Capacity); 54 | } 55 | 56 | #endregion Public 方法 57 | } 58 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Attributes/QuickSet/CacheByClaimAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.AspNetCore.Mvc; 2 | 3 | /// 4 | /// 依据Claim声明进行响应缓存 5 | /// 6 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] 7 | public class CacheByClaimAttribute : ResponseCachingAttribute 8 | { 9 | #region Public 构造函数 10 | 11 | /// 12 | /// 依据Claim声明进行响应缓存 13 | /// 14 | /// 缓存时长(秒) 15 | /// 依据的具体ClaimType 16 | public CacheByClaimAttribute(int duration, params string[] claimTypes) : base(duration, CacheMode.Custom) 17 | { 18 | if (claimTypes is null || claimTypes.Length == 0) 19 | { 20 | throw new ArgumentOutOfRangeException(nameof(claimTypes)); 21 | } 22 | VaryByClaims = claimTypes; 23 | } 24 | 25 | /// 26 | /// 依据Claim声明进行响应缓存 27 | /// 28 | /// 缓存时长(秒) 29 | /// 缓存存储位置 30 | /// 依据的具体ClaimType 31 | public CacheByClaimAttribute(int duration, CacheStoreLocation storeLocation, params string[] claimTypes) : this(duration, claimTypes) 32 | { 33 | StoreLocation = storeLocation; 34 | } 35 | 36 | #endregion Public 构造函数 37 | } 38 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Attributes/QuickSet/CacheByFormAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.AspNetCore.Mvc; 2 | 3 | /// 4 | /// 根据form表单键进行响应缓存 5 | /// 6 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] 7 | public class CacheByFormAttribute : ResponseCachingAttribute 8 | { 9 | #region Public 构造函数 10 | 11 | /// 12 | /// 根据form表单键进行响应缓存 13 | /// 14 | /// 缓存时长(秒) 15 | /// 依据的具体表单键 16 | public CacheByFormAttribute(int duration, params string[] formKeys) : base(duration, CacheMode.Custom) 17 | { 18 | if (formKeys is null || formKeys.Length == 0) 19 | { 20 | throw new ArgumentOutOfRangeException(nameof(formKeys)); 21 | } 22 | VaryByFormKeys = formKeys; 23 | } 24 | 25 | /// 26 | /// 根据form表单键进行响应缓存 27 | /// 28 | /// 缓存时长(秒) 29 | /// 缓存存储位置 30 | /// 依据的具体表单键 31 | public CacheByFormAttribute(int duration, CacheStoreLocation storeLocation, params string[] formKeys) : this(duration, formKeys) 32 | { 33 | StoreLocation = storeLocation; 34 | } 35 | 36 | #endregion Public 构造函数 37 | } 38 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Attributes/QuickSet/CacheByFullUrlAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.AspNetCore.Mvc; 2 | 3 | /// 4 | /// 根据完整的请求url进行响应缓存(包含所有查询键) 5 | /// 6 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] 7 | public class CacheByFullUrlAttribute : ResponseCachingAttribute 8 | { 9 | #region Public 构造函数 10 | 11 | /// 12 | /// 根据完整的请求url进行响应缓存(包含所有查询键) 13 | /// 14 | /// 缓存时长(秒) 15 | public CacheByFullUrlAttribute(int duration) : base(duration, CacheMode.FullPathAndQuery) 16 | { 17 | } 18 | 19 | /// 20 | /// 根据完整的请求url进行响应缓存(包含所有查询键) 21 | /// 22 | /// 缓存时长(秒) 23 | /// 缓存存储位置 24 | public CacheByFullUrlAttribute(int duration, CacheStoreLocation storeLocation) : this(duration) 25 | { 26 | StoreLocation = storeLocation; 27 | } 28 | 29 | #endregion Public 构造函数 30 | } 31 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Attributes/QuickSet/CacheByHeaderAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.AspNetCore.Mvc; 2 | 3 | /// 4 | /// 根据请求头进行响应缓存 5 | /// 6 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] 7 | public class CacheByHeaderAttribute : ResponseCachingAttribute 8 | { 9 | #region Public 构造函数 10 | 11 | /// 12 | /// 根据请求头进行响应缓存 13 | /// 14 | /// 缓存时长(秒) 15 | /// 依据的请求头 16 | public CacheByHeaderAttribute(int duration, params string[] headers) : base(duration, CacheMode.Custom) 17 | { 18 | if (headers is null || headers.Length == 0) 19 | { 20 | throw new ArgumentOutOfRangeException(nameof(headers)); 21 | } 22 | VaryByHeaders = headers; 23 | } 24 | 25 | /// 26 | /// 根据请求头进行响应缓存 27 | /// 28 | /// 缓存时长(秒) 29 | /// 缓存存储位置 30 | /// 依据的请求头 31 | public CacheByHeaderAttribute(int duration, CacheStoreLocation storeLocation, params string[] headers) : this(duration, headers) 32 | { 33 | StoreLocation = storeLocation; 34 | } 35 | 36 | #endregion Public 构造函数 37 | } 38 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Attributes/QuickSet/CacheByModelAttribute.cs: -------------------------------------------------------------------------------- 1 | using Cuture.AspNetCore.ResponseCaching.Metadatas; 2 | 3 | namespace Microsoft.AspNetCore.Mvc; 4 | 5 | /// 6 | /// 根据Model进行响应缓存 7 | /// 8 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] 9 | public class CacheByModelAttribute : ResponseCachingAttribute 10 | { 11 | #region Public 构造函数 12 | 13 | /// 14 | public CacheByModelAttribute(int duration, params string[] modelNames) : base(duration, CacheMode.Custom) 15 | { 16 | if (modelNames is null) 17 | { 18 | throw new ArgumentNullException(nameof(modelNames)); 19 | } 20 | VaryByModels = modelNames; 21 | } 22 | 23 | /// 24 | /// 根据Model进行响应缓存 25 | /// 26 | /// 具体细节参照 27 | /// 28 | /// 缓存时长(秒) 29 | /// 缓存存储位置 30 | /// 依据的model名称,不进行设置时为使用所有model进行生成 31 | public CacheByModelAttribute(int duration, CacheStoreLocation storeLocation, params string[] modelNames) : this(duration, modelNames) 32 | { 33 | StoreLocation = storeLocation; 34 | } 35 | 36 | #endregion Public 构造函数 37 | } 38 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Attributes/QuickSet/CacheByPathAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.AspNetCore.Mvc; 2 | 3 | /// 4 | /// 根据请求路径进行响应缓存(不包含查询) 5 | /// 6 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] 7 | public class CacheByPathAttribute : ResponseCachingAttribute 8 | { 9 | #region Public 构造函数 10 | 11 | /// 12 | /// 根据请求路径进行响应缓存(不包含查询) 13 | /// 14 | /// 缓存时长(秒) 15 | public CacheByPathAttribute(int duration) : base(duration, CacheMode.PathUniqueness) 16 | { 17 | } 18 | 19 | /// 20 | /// 根据请求路径进行响应缓存(不包含查询) 21 | /// 22 | /// 缓存时长(秒) 23 | /// 缓存存储位置 24 | public CacheByPathAttribute(int duration, CacheStoreLocation storeLocation) : this(duration) 25 | { 26 | StoreLocation = storeLocation; 27 | } 28 | 29 | #endregion Public 构造函数 30 | } 31 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Attributes/QuickSet/CacheByQueryAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.AspNetCore.Mvc; 2 | 3 | /// 4 | /// 根据查询键进行响应缓存 5 | /// 6 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] 7 | public class CacheByQueryAttribute : ResponseCachingAttribute 8 | { 9 | #region Public 构造函数 10 | 11 | /// 12 | /// 根据查询键进行响应缓存 13 | /// 14 | /// 缓存时长(秒) 15 | /// 依据的具体查询键 16 | public CacheByQueryAttribute(int duration, params string[] queryKeys) : base(duration, CacheMode.Custom) 17 | { 18 | if (queryKeys is null) 19 | { 20 | throw new ArgumentOutOfRangeException(nameof(queryKeys)); 21 | } 22 | VaryByQueryKeys = queryKeys; 23 | } 24 | 25 | /// 26 | /// 根据查询键进行响应缓存 27 | /// 28 | /// 缓存时长(秒) 29 | /// 缓存存储位置 30 | /// 依据的具体查询键 31 | public CacheByQueryAttribute(int duration, CacheStoreLocation storeLocation, params string[] queryKeys) : this(duration, queryKeys) 32 | { 33 | StoreLocation = storeLocation; 34 | } 35 | 36 | #endregion Public 构造函数 37 | } 38 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Attributes/ResponseCacheableAttribute.cs: -------------------------------------------------------------------------------- 1 | using Cuture.AspNetCore.ResponseCaching; 2 | 3 | using Microsoft.AspNetCore.Mvc.Filters; 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace Microsoft.AspNetCore.Mvc; 7 | 8 | /// 9 | /// 表示标记的Action响应内容是可缓存的 10 | /// 从 DI 容器中获取 并构造对应的响应缓存 11 | /// 12 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = false)] 13 | public class ResponseCacheableAttribute : Attribute, IFilterFactory, IOrderedFilter 14 | { 15 | #region Private 字段 16 | 17 | private SpinLock _createInstanceLock = new(false); 18 | 19 | private IFilterMetadata? _filterMetadata; 20 | 21 | #endregion Private 字段 22 | 23 | #region Public 属性 24 | 25 | /// 26 | public bool IsReusable => true; 27 | 28 | /// 29 | public int Order { get; set; } 30 | 31 | #endregion Public 属性 32 | 33 | #region Public 方法 34 | 35 | /// 36 | public IFilterMetadata CreateInstance(IServiceProvider serviceProvider) 37 | { 38 | var locked = false; 39 | try 40 | { 41 | _createInstanceLock.Enter(ref locked); 42 | return _filterMetadata ??= CreateFilter(serviceProvider); 43 | } 44 | finally 45 | { 46 | if (locked) 47 | { 48 | _createInstanceLock.Exit(false); 49 | } 50 | } 51 | } 52 | 53 | #endregion Public 方法 54 | 55 | #region Protected 方法 56 | 57 | /// 58 | /// 创建Filter(线程安全) 59 | /// 60 | /// 61 | /// 62 | protected virtual IFilterMetadata CreateFilter(IServiceProvider serviceProvider) 63 | { 64 | var filterBuilder = serviceProvider.GetRequiredService(); 65 | 66 | return filterBuilder.CreateFilter(serviceProvider); 67 | } 68 | 69 | #endregion Protected 方法 70 | } 71 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Attributes/ResponseDumpCapacityAttribute.cs: -------------------------------------------------------------------------------- 1 | using Cuture.AspNetCore.ResponseCaching; 2 | using Cuture.AspNetCore.ResponseCaching.Metadatas; 3 | 4 | namespace Microsoft.AspNetCore.Mvc; 5 | 6 | /// 7 | /// 指定Dump响应时的初始化大小 8 | /// 9 | /// 10 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] 11 | public class ResponseDumpCapacityAttribute : Attribute, IDumpStreamInitialCapacityMetadata 12 | { 13 | #region Public 属性 14 | 15 | /// 16 | public int Capacity { get; } 17 | 18 | #endregion Public 属性 19 | 20 | #region Public 构造函数 21 | 22 | /// 23 | /// 24 | /// 25 | /// 初始化大小 26 | public ResponseDumpCapacityAttribute(int capacity) 27 | { 28 | Capacity = Checks.ThrowIfDumpStreamInitialCapacityTooSmall(capacity, nameof(capacity)); 29 | } 30 | 31 | #endregion Public 构造函数 32 | } 33 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/CacheKey/Builders/CacheKeyBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.AspNetCore.Mvc.Filters; 5 | 6 | namespace Cuture.AspNetCore.ResponseCaching.CacheKey.Builders; 7 | 8 | /// 9 | /// 缓存键构建器 10 | /// 11 | public abstract class CacheKeyBuilder 12 | { 13 | #region const 14 | 15 | /// 16 | /// 连接Key字符 17 | /// 18 | public const char CombineChar = ResponseCachingConstants.CombineChar; 19 | 20 | #endregion const 21 | 22 | #region Private 字段 23 | 24 | private readonly CacheKeyBuilder? _innerBuilder; 25 | 26 | #endregion Private 字段 27 | 28 | #region Public 属性 29 | 30 | /// 31 | /// 严格模式 32 | /// 33 | public CacheKeyStrictMode StrictMode { get; } 34 | 35 | #endregion Public 属性 36 | 37 | #region Public 构造函数 38 | 39 | /// 40 | /// 缓存键构建器 41 | /// 42 | /// 43 | /// 44 | public CacheKeyBuilder(CacheKeyBuilder? innerBuilder, CacheKeyStrictMode strictMode) 45 | { 46 | _innerBuilder = innerBuilder; 47 | StrictMode = strictMode; 48 | } 49 | 50 | #endregion Public 构造函数 51 | 52 | #region Public 方法 53 | 54 | /// 55 | /// 构建Key 56 | /// 57 | /// Filter上下文 58 | /// 59 | /// 60 | public virtual ValueTask BuildAsync(FilterContext filterContext, StringBuilder keyBuilder) 61 | { 62 | return _innerBuilder is null 63 | ? new(keyBuilder.ToString()) 64 | : _innerBuilder.BuildAsync(filterContext, keyBuilder); 65 | } 66 | 67 | #endregion Public 方法 68 | 69 | #region Protected 方法 70 | 71 | /// 72 | /// 处理未找到key的情况 73 | /// 74 | /// 75 | /// 76 | protected bool ProcessKeyNotFound(string notFoundKeyName) 77 | { 78 | return StrictMode switch 79 | { 80 | CacheKeyStrictMode.Ignore => true, 81 | CacheKeyStrictMode.Strict => false, 82 | CacheKeyStrictMode.StrictWithException => throw new CacheVaryKeyNotFoundException(notFoundKeyName), 83 | _ => throw new ArgumentException($"Unhandleable CacheKeyStrictMode: {StrictMode}"), 84 | }; 85 | } 86 | 87 | #endregion Protected 方法 88 | } 89 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/CacheKey/Builders/ClaimsCacheKeyBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Security.Claims; 2 | using System.Text; 3 | 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.AspNetCore.Mvc.Filters; 6 | 7 | namespace Cuture.AspNetCore.ResponseCaching.CacheKey.Builders; 8 | 9 | /// 10 | /// Claims缓存键构建器 11 | /// 12 | public class ClaimsCacheKeyBuilder : CacheKeyBuilder 13 | { 14 | #region Private 字段 15 | 16 | /// 17 | /// ClaimType列表 18 | /// 19 | private readonly string[] _claimTypes; 20 | 21 | #endregion Private 字段 22 | 23 | #region Public 构造函数 24 | 25 | /// 26 | /// Claims缓存键构建器 27 | /// 28 | /// 29 | /// 30 | /// ClaimType列表 31 | public ClaimsCacheKeyBuilder(CacheKeyBuilder? innerBuilder, CacheKeyStrictMode strictMode, IEnumerable claimTypes) : base(innerBuilder, strictMode) 32 | { 33 | _claimTypes = claimTypes?.ToLowerArray() ?? throw new ArgumentNullException(nameof(claimTypes)); 34 | } 35 | 36 | #endregion Public 构造函数 37 | 38 | #region Public 方法 39 | 40 | /// 41 | public override ValueTask BuildAsync(FilterContext filterContext, StringBuilder keyBuilder) 42 | { 43 | keyBuilder.Append(CombineChar); 44 | 45 | foreach (var claimType in _claimTypes) 46 | { 47 | if (filterContext.HttpContext.User.FindFirst(claimType) is Claim claim) 48 | { 49 | keyBuilder.Append($"{claimType}={claim.Value}&"); 50 | } 51 | else 52 | { 53 | if (!ProcessKeyNotFound(claimType)) 54 | { 55 | return default; 56 | } 57 | } 58 | } 59 | return base.BuildAsync(filterContext, keyBuilder.TrimEndAnd()); 60 | } 61 | 62 | #endregion Public 方法 63 | } 64 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/CacheKey/Builders/RequestHeadersCacheKeyBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.AspNetCore.Mvc.Filters; 5 | 6 | namespace Cuture.AspNetCore.ResponseCaching.CacheKey.Builders; 7 | 8 | /// 9 | /// 请求头缓存键构建器 10 | /// 11 | public class RequestHeadersCacheKeyBuilder : CacheKeyBuilder 12 | { 13 | #region Private 字段 14 | 15 | /// 16 | /// 请求头列表 17 | /// 18 | private readonly string[] _headers; 19 | 20 | #endregion Private 字段 21 | 22 | #region Public 构造函数 23 | 24 | /// 25 | /// 请求头缓存键构建器 26 | /// 27 | /// 28 | /// 29 | /// 30 | public RequestHeadersCacheKeyBuilder(CacheKeyBuilder? innerBuilder, CacheKeyStrictMode strictMode, IEnumerable headers) : base(innerBuilder, strictMode) 31 | { 32 | _headers = headers?.ToLowerArray() ?? throw new ArgumentNullException(nameof(headers)); 33 | } 34 | 35 | #endregion Public 构造函数 36 | 37 | #region Public 方法 38 | 39 | /// 40 | public override ValueTask BuildAsync(FilterContext filterContext, StringBuilder keyBuilder) 41 | { 42 | keyBuilder.Append(CombineChar); 43 | 44 | var headers = filterContext.HttpContext.Request.Headers; 45 | 46 | foreach (var header in _headers) 47 | { 48 | if (headers.TryGetValue(header, out var value)) 49 | { 50 | keyBuilder.Append($"{header}={value}&"); 51 | } 52 | else 53 | { 54 | if (!ProcessKeyNotFound(header)) 55 | { 56 | return default; 57 | } 58 | } 59 | } 60 | return base.BuildAsync(filterContext, keyBuilder.TrimEndAnd()); 61 | } 62 | 63 | #endregion Public 方法 64 | } 65 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/CacheKey/Generators/DefiniteCacheKeyGenerator.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.Filters; 2 | 3 | namespace Cuture.AspNetCore.ResponseCaching.CacheKey.Generators; 4 | 5 | /// 6 | /// 确定值缓存键生成器 7 | /// 8 | public class DefiniteCacheKeyGenerator : ICacheKeyGenerator 9 | { 10 | #region Private 字段 11 | 12 | private readonly string _key; 13 | 14 | #endregion Private 字段 15 | 16 | #region Public 构造函数 17 | 18 | /// 19 | /// 确定值缓存键生成器 20 | /// 21 | /// 22 | public DefiniteCacheKeyGenerator(string key) 23 | { 24 | if (string.IsNullOrWhiteSpace(key)) 25 | { 26 | throw new ArgumentNullException(nameof(key)); 27 | } 28 | _key = key; 29 | } 30 | 31 | #endregion Public 构造函数 32 | 33 | #region Public 方法 34 | 35 | /// 36 | public ValueTask GenerateKeyAsync(FilterContext filterContext) => new(_key); 37 | 38 | #endregion Public 方法 39 | } 40 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/CacheKey/Generators/FullPathAndQueryCacheKeyGenerator.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | using Cuture.AspNetCore.ResponseCaching.Internal; 3 | using Microsoft.AspNetCore.Mvc.Filters; 4 | 5 | namespace Cuture.AspNetCore.ResponseCaching.CacheKey.Generators; 6 | 7 | /// 8 | /// 完整请求路径和查询字符串缓存键生成器 9 | /// 10 | public class FullPathAndQueryCacheKeyGenerator : ICacheKeyGenerator 11 | { 12 | #region Private 字段 13 | 14 | private readonly ActionPathCache _actionPathCache = new(); 15 | 16 | #endregion Private 字段 17 | 18 | #region Public 方法 19 | 20 | /// 21 | public ValueTask GenerateKeyAsync(FilterContext filterContext) 22 | { 23 | var method = filterContext.HttpContext.Request.NormalizeMethodNameAsKeyPrefix(); 24 | 25 | using var pathValue = _actionPathCache.GetPath(filterContext); 26 | 27 | var queryString = filterContext.HttpContext.Request.QueryString; 28 | if (!queryString.HasValue) 29 | { 30 | return new(new string(method) + pathValue.ToString()); 31 | } 32 | 33 | char[]? buffer = null; 34 | try 35 | { 36 | var path = pathValue.Value; 37 | buffer = ArrayPool.Shared.Rent(method.Length + path.Length + queryString.Value!.Length + 1); 38 | 39 | var span = buffer.AsSpan(); 40 | method.CopyTo(span); 41 | span = span.Slice(method.Length); 42 | 43 | path.CopyTo(span); 44 | span = span.Slice(path.Length); 45 | 46 | span[0] = ResponseCachingConstants.CombineChar; 47 | 48 | var length = QueryStringOrderUtil.Order(queryString, span.Slice(1)); 49 | 50 | return new(new string(value: buffer, startIndex: 0, length: method.Length + path.Length + length)); 51 | } 52 | finally 53 | { 54 | if (buffer is not null) 55 | { 56 | ArrayPool.Shared.Return(buffer); 57 | } 58 | } 59 | } 60 | 61 | #endregion Public 方法 62 | } 63 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/CacheKey/Generators/ICacheKeyGenerator.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.Filters; 2 | 3 | namespace Cuture.AspNetCore.ResponseCaching.CacheKey.Generators; 4 | 5 | /// 6 | /// 缓存Key生成器 7 | /// 8 | public interface ICacheKeyGenerator 9 | { 10 | #region Public 方法 11 | 12 | /// 13 | /// 生成缓存Key 14 | /// 15 | /// 当类型为 时,类型为 16 | /// 当类型为 时,类型为 17 | /// 18 | /// 19 | ValueTask GenerateKeyAsync(FilterContext filterContext); 20 | 21 | #endregion Public 方法 22 | } 23 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/CacheKey/Generators/IEndpointSetter.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | 3 | namespace Cuture.AspNetCore.ResponseCaching.CacheKey.Generators; 4 | 5 | /// 6 | /// 设定接口 7 | /// 8 | public interface IEndpointSetter 9 | { 10 | #region Public 方法 11 | 12 | /// 13 | /// 设置 14 | /// 15 | /// 16 | void SetEndpoint(Endpoint endpoint); 17 | 18 | #endregion Public 方法 19 | } 20 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/CacheKey/Generators/RequestPathCacheKeyGenerator.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | using Cuture.AspNetCore.ResponseCaching.Internal; 3 | using Microsoft.AspNetCore.Mvc.Filters; 4 | 5 | namespace Cuture.AspNetCore.ResponseCaching.CacheKey.Generators; 6 | 7 | /// 8 | /// 请求路径缓存键生成器 9 | /// 10 | public class RequestPathCacheKeyGenerator : ICacheKeyGenerator 11 | { 12 | #region Private 字段 13 | 14 | private readonly ActionPathCache _actionPathCache = new(); 15 | 16 | #endregion Private 字段 17 | 18 | #region Public 方法 19 | 20 | /// 21 | public ValueTask GenerateKeyAsync(FilterContext filterContext) 22 | { 23 | var method = filterContext.HttpContext.Request.NormalizeMethodNameAsKeyPrefix(); 24 | 25 | char[]? buffer = null; 26 | try 27 | { 28 | using var pathValue = _actionPathCache.GetPath(filterContext); 29 | var path = pathValue.Value; 30 | var length = method.Length + path.Length; 31 | buffer = ArrayPool.Shared.Rent(length); 32 | method.CopyTo(buffer); 33 | path.CopyTo(buffer.AsSpan(method.Length)); 34 | return new(new string(buffer, 0, length)); 35 | } 36 | finally 37 | { 38 | if (buffer is not null) 39 | { 40 | ArrayPool.Shared.Return(buffer); 41 | } 42 | } 43 | } 44 | 45 | #endregion Public 方法 46 | } 47 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/CacheKey/ICacheKeyable.cs: -------------------------------------------------------------------------------- 1 | namespace Cuture.AspNetCore.ResponseCaching; 2 | 3 | /// 4 | /// 可以转换为缓存Key 5 | /// 6 | public interface ICacheKeyable 7 | { 8 | #region Public 方法 9 | 10 | /// 11 | /// 作为缓存Key 12 | /// 13 | /// 14 | string AsCacheKey(); 15 | 16 | #endregion Public 方法 17 | } 18 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/CacheKeyAccessor.cs: -------------------------------------------------------------------------------- 1 | namespace Cuture.AspNetCore.ResponseCaching; 2 | 3 | /// 4 | /// 缓存Key访问器 5 | /// 6 | public interface ICacheKeyAccessor 7 | { 8 | #region Public 属性 9 | 10 | /// 11 | /// 缓存key 12 | /// 13 | string? Key { get; } 14 | 15 | #endregion Public 属性 16 | } 17 | 18 | internal sealed class CacheKeyAccessor : ICacheKeyAccessor 19 | { 20 | #region Private 字段 21 | 22 | private static readonly AsyncLocal s_asyncLocalCacheKey = new(); 23 | 24 | #endregion Private 字段 25 | 26 | #region Public 属性 27 | 28 | public string? Key { get => s_asyncLocalCacheKey.Value; set => s_asyncLocalCacheKey.Value = value; } 29 | 30 | #endregion Public 属性 31 | } 32 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Context/DefaultModelKeyParser.cs: -------------------------------------------------------------------------------- 1 | namespace Cuture.AspNetCore.ResponseCaching; 2 | 3 | internal class DefaultModelKeyParser : IModelKeyParser 4 | { 5 | #region Public 方法 6 | 7 | public string? Parse(in T? model) 8 | { 9 | if (model is ICacheKeyable keyable) 10 | { 11 | return keyable.AsCacheKey(); 12 | } 13 | return model?.ToString(); 14 | } 15 | 16 | #endregion Public 方法 17 | } 18 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Context/DefaultResponseCacheDeterminer.cs: -------------------------------------------------------------------------------- 1 | using Cuture.AspNetCore.ResponseCaching.ResponseCaches; 2 | 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.AspNetCore.Mvc.Filters; 5 | 6 | namespace Cuture.AspNetCore.ResponseCaching; 7 | 8 | /// 9 | /// 默认响应缓存确定器 10 | /// 11 | internal class DefaultResponseCacheDeterminer : IResponseCacheDeterminer 12 | { 13 | #region Public 方法 14 | 15 | /// 16 | public bool CanCaching(ResourceExecutedContext context, ResponseCacheEntry cacheEntry) 17 | => context.HttpContext.Response.StatusCode == StatusCodes.Status200OK; 18 | 19 | /// 20 | public bool CanCaching(ActionExecutedContext context) => context.Result != null; 21 | 22 | #endregion Public 方法 23 | } 24 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Context/DefaultResponseDumpStreamFactory.cs: -------------------------------------------------------------------------------- 1 | namespace Cuture.AspNetCore.ResponseCaching; 2 | 3 | /// 4 | /// 默认 5 | /// 6 | internal class DefaultResponseDumpStreamFactory : IResponseDumpStreamFactory 7 | { 8 | #region Public 方法 9 | 10 | /// 11 | public MemoryStream Create(int initialCapacity) => new(initialCapacity); 12 | 13 | #endregion Public 方法 14 | } 15 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Context/IModelKeyParser.cs: -------------------------------------------------------------------------------- 1 | namespace Cuture.AspNetCore.ResponseCaching; 2 | 3 | /// 4 | /// Model缓存key解析器 5 | /// 6 | public interface IModelKeyParser 7 | { 8 | #region Public 方法 9 | 10 | /// 11 | /// 解析为key 12 | /// 13 | /// 14 | /// 解析后等价的key字符串 15 | string? Parse(in T? model); 16 | 17 | #endregion Public 方法 18 | } 19 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Context/IResponseCacheDeterminer.cs: -------------------------------------------------------------------------------- 1 | using Cuture.AspNetCore.ResponseCaching.ResponseCaches; 2 | 3 | using Microsoft.AspNetCore.Mvc.Filters; 4 | 5 | namespace Cuture.AspNetCore.ResponseCaching; 6 | 7 | /// 8 | /// 响应缓存确定器 9 | /// 10 | public interface IResponseCacheDeterminer 11 | { 12 | #region Public 方法 13 | 14 | /// 15 | /// 是否可以缓存此次请求 16 | /// 17 | /// 默认仅在使用 类型的过滤器 () 时生效 18 | /// 19 | /// 20 | /// 21 | /// 22 | bool CanCaching(ResourceExecutedContext context, ResponseCacheEntry cacheEntry); 23 | 24 | /// 25 | /// 是否可以缓存此次请求 26 | /// 27 | /// 仅在使用 类型的过滤器 () 时生效 28 | /// 29 | /// 30 | /// 31 | bool CanCaching(ActionExecutedContext context); 32 | 33 | #endregion Public 方法 34 | } 35 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Context/IResponseDumpStreamFactory.cs: -------------------------------------------------------------------------------- 1 | namespace Cuture.AspNetCore.ResponseCaching; 2 | 3 | /// 4 | /// 创建用于转储响应的 工厂 5 | /// 6 | public interface IResponseDumpStreamFactory 7 | { 8 | #region Public 方法 9 | 10 | /// 11 | /// 创建转储 12 | /// 13 | /// 14 | /// 15 | MemoryStream Create(int initialCapacity); 16 | 17 | #endregion Public 方法 18 | } 19 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Cuture.AspNetCore.ResponseCaching.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | true 6 | 7 | readme.md 8 | 9 | 10 | 11 | true 12 | true 13 | snupkg 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | <_Parameter1>ResponseCaching.Test 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/DefaultEndpointAccessor.cs: -------------------------------------------------------------------------------- 1 | using Cuture.AspNetCore.ResponseCaching.Internal; 2 | 3 | using Microsoft.AspNetCore.Http; 4 | 5 | namespace Cuture.AspNetCore.ResponseCaching; 6 | 7 | /// 8 | internal class DefaultEndpointAccessor : IEndpointAccessor 9 | { 10 | #region Public 属性 11 | 12 | /// 13 | public Endpoint Endpoint { get; } 14 | 15 | /// 16 | public EndpointMetadataCollection Metadatas => Endpoint.Metadata; 17 | 18 | #endregion Public 属性 19 | 20 | #region Public 构造函数 21 | 22 | public DefaultEndpointAccessor(IHttpContextAccessor httpContextAccessor) 23 | { 24 | Endpoint = httpContextAccessor?.HttpContext?.GetEndpoint() ?? throw new ResponseCachingException("Cannot access Endpoint by IHttpContextAccessor."); 25 | } 26 | 27 | #endregion Public 构造函数 28 | 29 | #region Public 方法 30 | 31 | /// 32 | public TMetadata? GetMetadata() where TMetadata : class => Endpoint.Metadata.GetMetadata(); 33 | 34 | /// 35 | public TMetadata GetRequiredMetadata() where TMetadata : class => Endpoint.Metadata.RequiredMetadata(); 36 | 37 | /// 38 | public override string ToString() => Endpoint.ToString() ?? string.Empty; 39 | 40 | #endregion Public 方法 41 | } 42 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Diagnostics/CachingDiagnosticsAccessor.cs: -------------------------------------------------------------------------------- 1 | namespace Cuture.AspNetCore.ResponseCaching.Diagnostics; 2 | 3 | /// 4 | /// 缓存诊断访问器 5 | /// 6 | public sealed class CachingDiagnosticsAccessor 7 | { 8 | #region Public 属性 9 | 10 | /// 11 | public CachingDiagnostics CachingDiagnostics { get; } 12 | 13 | #endregion Public 属性 14 | 15 | #region Public 构造函数 16 | 17 | /// 18 | public CachingDiagnosticsAccessor(CachingDiagnostics cachingDiagnostics) 19 | { 20 | CachingDiagnostics = cachingDiagnostics ?? throw new ArgumentNullException(nameof(cachingDiagnostics)); 21 | } 22 | 23 | #endregion Public 构造函数 24 | } 25 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Diagnostics/DiagnosticLoggerSubscriber.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | using System.Diagnostics; 3 | 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace Cuture.AspNetCore.ResponseCaching.Diagnostics; 7 | 8 | /// 9 | /// 诊断信息日志订阅器 10 | /// 11 | public sealed class DiagnosticLoggerSubscriber : IObserver 12 | { 13 | #region Private 字段 14 | 15 | private readonly WeakReference _diagnosticLogger; 16 | 17 | private readonly ConcurrentBag _disposables = new(); 18 | 19 | #endregion Private 字段 20 | 21 | #region Public 构造函数 22 | 23 | /// 24 | public DiagnosticLoggerSubscriber(IServiceProvider serviceProvider) 25 | { 26 | _diagnosticLogger = new WeakReference(serviceProvider.GetRequiredService()); 27 | } 28 | 29 | #endregion Public 构造函数 30 | 31 | #region Public 方法 32 | 33 | /// 34 | public void OnCompleted() 35 | { 36 | foreach (var disposable in _disposables) 37 | { 38 | disposable.Dispose(); 39 | } 40 | } 41 | 42 | /// 43 | public void OnError(Exception error) 44 | { 45 | } 46 | 47 | /// 48 | public void OnNext(DiagnosticListener value) 49 | { 50 | if (value.Name.StartsWith(ResponseCachingEventData.DiagnosticName, StringComparison.OrdinalIgnoreCase) 51 | && _diagnosticLogger.TryGetTarget(out var diagnosticLogger)) 52 | { 53 | _disposables.Add(value.Subscribe(diagnosticLogger!)); 54 | } 55 | } 56 | 57 | #endregion Public 方法 58 | } 59 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Diagnostics/DiagnosticLoggerSubscriberDisposerAccessor.cs: -------------------------------------------------------------------------------- 1 | namespace Cuture.AspNetCore.ResponseCaching.Diagnostics; 2 | 3 | /// 4 | /// DiagnosticLogger的订阅释放器访问器 5 | /// 6 | public sealed class DiagnosticLoggerSubscriberDisposerAccessor : IDisposable 7 | { 8 | #region Private 字段 9 | 10 | private IDisposable? _disposable; 11 | 12 | private bool _isDisposed = false; 13 | 14 | #endregion Private 字段 15 | 16 | #region Public 属性 17 | 18 | /// 19 | /// 释放订阅对象 20 | /// 21 | public IDisposable? Disposable 22 | { 23 | get => _disposable; 24 | set 25 | { 26 | if (_isDisposed) 27 | { 28 | throw new ObjectDisposedException(nameof(DiagnosticLoggerSubscriberDisposerAccessor)); 29 | } 30 | _disposable?.Dispose(); 31 | _disposable = value; 32 | } 33 | } 34 | 35 | #endregion Public 属性 36 | 37 | #region Private 析构函数 38 | 39 | /// 40 | /// 41 | /// 42 | ~DiagnosticLoggerSubscriberDisposerAccessor() 43 | { 44 | Dispose(); 45 | } 46 | 47 | #endregion Private 析构函数 48 | 49 | #region Public 方法 50 | 51 | /// 52 | public void Dispose() 53 | { 54 | if (!_isDisposed) 55 | { 56 | _isDisposed = true; 57 | if (_disposable is not null) 58 | { 59 | try 60 | { 61 | _disposable.Dispose(); 62 | } 63 | catch { } 64 | } 65 | GC.SuppressFinalize(this); 66 | } 67 | } 68 | 69 | #endregion Public 方法 70 | } 71 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Enums/CacheKeyStrictMode.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.AspNetCore.Mvc; 2 | 3 | /// 4 | /// 缓存key的严格模式 5 | /// 6 | public enum CacheKeyStrictMode 7 | { 8 | /// 9 | /// 默认 10 | /// 11 | Default = 0, 12 | 13 | /// 14 | /// 忽略不存在的Key 15 | /// 16 | Ignore = 1, 17 | 18 | /// 19 | /// 严格模式,key不存在时不进行缓存 20 | /// 21 | Strict = 2, 22 | 23 | /// 24 | /// 严格模式,并在key不存在时抛出异常 25 | /// 26 | StrictWithException = 10, 27 | } 28 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Enums/CacheMode.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.AspNetCore.Mvc; 2 | 3 | /// 4 | /// 缓存模式 5 | /// 6 | public enum CacheMode 7 | { 8 | /// 9 | /// 默认 10 | /// 11 | Default = FullPathAndQuery, 12 | 13 | /// 14 | /// 完整的请求路径和查询键 15 | /// 16 | FullPathAndQuery = 1, 17 | 18 | /// 19 | /// 自定义 20 | /// 21 | Custom = 2, 22 | 23 | /// 24 | /// 请求路径唯一缓存(不包含查询) 25 | /// 26 | PathUniqueness = 3, 27 | } 28 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Enums/CacheStoreLocation.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.AspNetCore.Mvc; 2 | 3 | /// 4 | /// 缓存存储位置 5 | /// 6 | public enum CacheStoreLocation 7 | { 8 | /// 9 | /// 默认(使用全局设置) 10 | /// 11 | Default, 12 | 13 | /// 14 | /// 分布式缓存 15 | /// 16 | Distributed, 17 | 18 | /// 19 | /// 内存 20 | /// 21 | Memory, 22 | } 23 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Enums/ExecutingLockMode.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.AspNetCore.Mvc; 2 | 3 | /// 4 | /// 执行锁定模式 5 | /// 6 | public enum ExecutingLockMode 7 | { 8 | /// 9 | /// 默认 10 | /// 11 | Default = 0, 12 | 13 | /// 14 | /// 没有控制,并发访问时可能穿过缓存 15 | /// 16 | None = 1, 17 | 18 | /// 19 | /// 根据Action放行单个请求 20 | /// 21 | ActionSingle = 2, 22 | 23 | /// 24 | /// 根据缓存键放行单个请求(可能会有很多的同步对象,慎重评估并使用此选项) 25 | /// 26 | CacheKeySingle = 3, 27 | } 28 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Enums/FilterType.cs: -------------------------------------------------------------------------------- 1 | namespace Cuture.AspNetCore.ResponseCaching; 2 | 3 | /// 4 | /// 过滤器类型 5 | /// 6 | public enum FilterType 7 | { 8 | /// 9 | /// ResourceFilter (在模型绑定前) 10 | /// 11 | Resource, 12 | 13 | /// 14 | /// ActionFilter (在模型绑定后) 15 | /// 16 | Action 17 | } 18 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Exceptions/CacheVaryKeyNotFoundException.cs: -------------------------------------------------------------------------------- 1 | namespace Cuture.AspNetCore.ResponseCaching; 2 | 3 | /// 4 | /// 缓存依据的键没有找到 5 | /// 6 | [Serializable] 7 | public class CacheVaryKeyNotFoundException : ArgumentNullException 8 | { 9 | #region Public 构造函数 10 | 11 | /// 12 | public CacheVaryKeyNotFoundException(string keyName) : base($"Cache VaryKey Not Found: {keyName}") 13 | { 14 | } 15 | 16 | /// 17 | public CacheVaryKeyNotFoundException() 18 | { 19 | } 20 | 21 | /// 22 | public CacheVaryKeyNotFoundException(string message, Exception innerException) : base(message, innerException) 23 | { 24 | } 25 | 26 | #endregion Public 构造函数 27 | } 28 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Exceptions/RequestFormNotFoundException.cs: -------------------------------------------------------------------------------- 1 | namespace Cuture.AspNetCore.ResponseCaching; 2 | 3 | /// 4 | /// 没有找到表单 5 | /// 6 | [Serializable] 7 | public class RequestFormNotFoundException : ArgumentNullException 8 | { 9 | #region Public 构造函数 10 | 11 | /// 12 | public RequestFormNotFoundException(string message) : base(message) 13 | { 14 | } 15 | 16 | /// 17 | public RequestFormNotFoundException(string message, Exception innerException) : base(message, innerException) 18 | { 19 | } 20 | 21 | /// 22 | public RequestFormNotFoundException() 23 | { 24 | } 25 | 26 | #endregion Public 构造函数 27 | } 28 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Exceptions/ResponseCachingException.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.Serialization; 2 | 3 | namespace Cuture.AspNetCore.ResponseCaching; 4 | 5 | /// 6 | /// 7 | /// 8 | [Serializable] 9 | public class ResponseCachingException : Exception 10 | { 11 | #region Public 构造函数 12 | 13 | /// 14 | public ResponseCachingException() 15 | { 16 | } 17 | 18 | /// 19 | public ResponseCachingException(string message) : base(message) 20 | { 21 | } 22 | 23 | /// 24 | public ResponseCachingException(string message, Exception innerException) : base(message, innerException) 25 | { 26 | } 27 | 28 | #endregion Public 构造函数 29 | 30 | #region Protected 构造函数 31 | 32 | /// 33 | [Obsolete("see https://github.com/dotnet/docs/issues/34893")] 34 | protected ResponseCachingException(SerializationInfo info, StreamingContext context) : base(info, context) 35 | { 36 | } 37 | 38 | #endregion Protected 构造函数 39 | } 40 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/ExecutingLock/ExclusiveExecutingLockLifecycleExecutor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.ObjectPool; 2 | 3 | namespace Cuture.AspNetCore.ResponseCaching.ExecutingLock; 4 | 5 | internal sealed class ExclusiveExecutingLockLifecycleExecutor 6 | : ExecutingLockLifecycleExecutorBase> 7 | where TCachePayload : class 8 | { 9 | #region Public 构造函数 10 | 11 | public ExclusiveExecutingLockLifecycleExecutor(INakedBoundedObjectPool semaphorePool) : base(semaphorePool) 12 | { 13 | } 14 | 15 | #endregion Public 构造函数 16 | 17 | #region Public 方法 18 | 19 | protected override ExclusiveExecutingLock Create(SemaphoreSlim semaphore) 20 | => new(semaphore); 21 | 22 | #endregion Public 方法 23 | } 24 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/ExecutingLock/ExecutingLockLifecycleExecutorBase.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.ObjectPool; 2 | 3 | namespace Cuture.AspNetCore.ResponseCaching.ExecutingLock; 4 | 5 | internal abstract class ExecutingLockLifecycleExecutorBase 6 | : IObjectLifecycleExecutor 7 | where TExecutingLock : IExecutingLock 8 | where TCachePayload : class 9 | { 10 | #region Private 字段 11 | 12 | private readonly INakedBoundedObjectPool _semaphorePool; 13 | 14 | #endregion Private 字段 15 | 16 | #region Public 构造函数 17 | 18 | public ExecutingLockLifecycleExecutorBase(INakedBoundedObjectPool semaphorePool) 19 | { 20 | _semaphorePool = semaphorePool ?? throw new ArgumentNullException(nameof(semaphorePool)); 21 | } 22 | 23 | #endregion Public 构造函数 24 | 25 | #region Public 方法 26 | 27 | public TExecutingLock? Create() 28 | { 29 | var semaphore = _semaphorePool.Rent(); 30 | if (semaphore is not null) 31 | { 32 | return Create(semaphore); 33 | } 34 | return default; 35 | } 36 | 37 | public void Destroy(TExecutingLock item) 38 | { 39 | if (item is ExecutingLockBase executingLockBase 40 | && executingLockBase.Semaphore is not null) 41 | { 42 | _semaphorePool.Return(executingLockBase.Semaphore); 43 | executingLockBase.Semaphore = null!; 44 | } 45 | } 46 | 47 | public bool Reset(TExecutingLock item) 48 | { 49 | if (item is ExecutingLockBase executingLockBase) 50 | { 51 | if (executingLockBase.ReferenceCount > 0) 52 | { 53 | throw new InvalidOperationException($"{nameof(ExecutingLockBase)} in use."); 54 | } 55 | while (executingLockBase.Semaphore.CurrentCount < 1) 56 | { 57 | executingLockBase.Semaphore.Release(); 58 | } 59 | Interlocked.Exchange(ref executingLockBase.ReferenceCount, 0); 60 | executingLockBase.SetLocalCache(string.Empty, null, 0L); 61 | return true; 62 | } 63 | return false; 64 | } 65 | 66 | #endregion Public 方法 67 | 68 | #region Protected 方法 69 | 70 | protected abstract TExecutingLock Create(SemaphoreSlim semaphore); 71 | 72 | #endregion Protected 方法 73 | } 74 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/ExecutingLock/Lock/ExclusiveExecutingLock.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Diagnostics.CodeAnalysis; 3 | 4 | namespace Cuture.AspNetCore.ResponseCaching; 5 | 6 | internal sealed class ExclusiveExecutingLock 7 | : ExecutingLockBase 8 | where TCachePayload : class 9 | { 10 | #region Private 字段 11 | 12 | /// 13 | /// 本地缓存数据 14 | /// 15 | private volatile TCachePayload? _localCachedPayload; 16 | 17 | /// 18 | /// 本地缓存数据过期时间 19 | /// 20 | private long _localCacheExpire; 21 | 22 | #endregion Private 字段 23 | 24 | #region Internal 构造函数 25 | 26 | internal ExclusiveExecutingLock(SemaphoreSlim semaphore) : base(semaphore) 27 | { 28 | } 29 | 30 | #endregion Internal 构造函数 31 | 32 | #region Public 方法 33 | 34 | /// 35 | public override void SetLocalCache(string key, TCachePayload? payload, long expireMilliseconds) 36 | { 37 | Debug.WriteLine("{0} - SetLocalCache {1} {2} {3}", nameof(ExclusiveExecutingLock), key, expireMilliseconds, payload); 38 | _localCachedPayload = payload; 39 | _localCacheExpire = expireMilliseconds; 40 | } 41 | 42 | /// 43 | public override bool TryGetLocalCache(string key, long checkMilliseconds, [NotNullWhen(true)] out TCachePayload? cachedPayload) 44 | { 45 | Debug.WriteLine("{0} - TryGetLocalCache {1} {2}", nameof(ExclusiveExecutingLock), key, checkMilliseconds); 46 | if (checkMilliseconds <= _localCacheExpire 47 | && _localCachedPayload is not null) 48 | { 49 | cachedPayload = _localCachedPayload; 50 | return true; 51 | } 52 | 53 | cachedPayload = default; 54 | 55 | return false; 56 | } 57 | 58 | #endregion Public 方法 59 | } 60 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/ExecutingLock/Lock/ExecutingLockBase.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Diagnostics.CodeAnalysis; 3 | 4 | namespace Cuture.AspNetCore.ResponseCaching; 5 | 6 | [DebuggerDisplay("ReferenceCount = {ReferenceCount} , LockKey = {LockKey}")] 7 | internal abstract class ExecutingLockBase 8 | : IExecutingLock 9 | where TCachePayload : class 10 | { 11 | #region Public 字段 12 | 13 | /// 14 | /// 引用计数 15 | /// 16 | public int ReferenceCount = 0; 17 | 18 | public SemaphoreSlim Semaphore; 19 | 20 | /// 21 | /// 锁的唯一标识 22 | /// 23 | public string? LockKey { get; set; } 24 | 25 | #endregion Public 字段 26 | 27 | #region Internal 构造函数 28 | 29 | internal ExecutingLockBase(SemaphoreSlim semaphore) 30 | { 31 | Semaphore = semaphore; 32 | } 33 | 34 | #endregion Internal 构造函数 35 | 36 | #region Public 方法 37 | 38 | /// 39 | public int Release() 40 | { 41 | return Semaphore.Release(); 42 | } 43 | 44 | /// 45 | public abstract void SetLocalCache(string key, TCachePayload? payload, long expireMilliseconds); 46 | 47 | /// 48 | public abstract bool TryGetLocalCache(string key, long checkMilliseconds, [NotNullWhen(true)] out TCachePayload? cachedPayload); 49 | 50 | /// 51 | public Task WaitAsync(int millisecondsTimeout, CancellationToken cancellationToken) 52 | { 53 | return Semaphore.WaitAsync(millisecondsTimeout, cancellationToken); 54 | } 55 | 56 | #endregion Public 方法 57 | } 58 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/ExecutingLock/Lock/IExecutingLock.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | namespace Cuture.AspNetCore.ResponseCaching; 4 | 5 | /// 6 | /// 执行锁 7 | /// 8 | /// 执行锁的缓存数据 9 | public interface IExecutingLock where TCachePayload : class 10 | { 11 | #region Public 方法 12 | 13 | /// 14 | int Release(); 15 | 16 | /// 17 | /// 设置本地缓存 18 | /// 19 | /// key 20 | /// 缓存数据(传递 null 时,为清除缓存,此时 key 应当传递 ) 21 | /// 过期时间(Unix 毫秒时间戳) 22 | void SetLocalCache(string key, TCachePayload? payload, long expireMilliseconds); 23 | 24 | /// 25 | /// 尝试获取本地缓存 26 | /// 27 | /// key 28 | /// 检查时间,用于与缓存有效时间进行对比(Unix 毫秒时间戳) 29 | /// 缓存数据 30 | /// 是否获取到缓存 31 | bool TryGetLocalCache(string key, long checkMilliseconds, [NotNullWhen(true)] out TCachePayload? cachedPayload); 32 | 33 | /// 34 | Task WaitAsync(int millisecondsTimeout, CancellationToken cancellationToken); 35 | 36 | #endregion Public 方法 37 | } 38 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/ExecutingLock/Lock/SharedExecutingLock.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.Diagnostics.CodeAnalysis; 3 | 4 | using Cuture.AspNetCore.ResponseCaching.Internal; 5 | 6 | using Microsoft.Extensions.Caching.Memory; 7 | 8 | namespace Cuture.AspNetCore.ResponseCaching; 9 | 10 | internal sealed class SharedExecutingLock 11 | : ExecutingLockBase 12 | where TCachePayload : class 13 | { 14 | #region Private 字段 15 | 16 | private readonly IMemoryCache _memoryCache; 17 | 18 | private string _key = string.Empty; 19 | 20 | #endregion Private 字段 21 | 22 | #region Public 构造函数 23 | 24 | public SharedExecutingLock(SemaphoreSlim semaphore, IMemoryCache memoryCache) : base(semaphore) 25 | { 26 | _memoryCache = memoryCache ?? throw new ArgumentNullException(nameof(memoryCache)); 27 | } 28 | 29 | #endregion Public 构造函数 30 | 31 | #region Public 方法 32 | 33 | /// 34 | public override void SetLocalCache(string key, TCachePayload? payload, long expireMilliseconds) 35 | { 36 | Debug.WriteLine("{0} - SetLocalCache {1} {2} {3}", nameof(SharedExecutingLock), key, expireMilliseconds, payload); 37 | // HACK 此处使用私有变量 _key 存放当前的缓存key,在清除缓存时使用 _key 进行清除,逻辑上进行清除缓存时当前 lock 已经没有引用,理论上没有问题。。。 38 | if (payload is null) 39 | { 40 | _memoryCache.Remove(_key); 41 | } 42 | else 43 | { 44 | var localCachedPayload = new LocalCachedPayload 45 | { 46 | Payload = payload, 47 | ExpireTime = expireMilliseconds 48 | }; 49 | _memoryCache.Set(key, localCachedPayload); 50 | } 51 | _key = key; 52 | } 53 | 54 | /// 55 | public override bool TryGetLocalCache(string key, long checkMilliseconds, [NotNullWhen(true)] out TCachePayload? cachedPayload) 56 | { 57 | Debug.WriteLine("{0} - TryGetLocalCache {1} {2}", nameof(ExclusiveExecutingLock), key, checkMilliseconds); 58 | if (_memoryCache.TryGetValue>(key, out var localCachedPayload) 59 | && localCachedPayload.ExpireTime >= checkMilliseconds 60 | && localCachedPayload.Payload is not null) 61 | { 62 | cachedPayload = localCachedPayload.Payload; 63 | return true; 64 | } 65 | cachedPayload = null; 66 | return false; 67 | } 68 | 69 | #endregion Public 方法 70 | } 71 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/ExecutingLock/Pool/IExecutingLockPool.cs: -------------------------------------------------------------------------------- 1 | namespace Cuture.AspNetCore.ResponseCaching; 2 | 3 | /// 4 | /// 池 5 | /// 6 | public interface IExecutingLockPool 7 | : IDisposable 8 | where TCachePayload : class 9 | { 10 | #region Public 方法 11 | 12 | /// 13 | /// 通过 获取一个对应的 14 | /// 15 | /// 16 | /// 对应的,如果池已用尽,则返回 null 17 | IExecutingLock? GetLock(string lockKey); 18 | 19 | /// 20 | /// 将一个 归还给池 21 | /// 22 | /// 23 | void Return(IExecutingLock item); 24 | 25 | #endregion Public 方法 26 | } 27 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/ExecutingLock/SharedExecutingLockLifecycleExecutor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Caching.Memory; 2 | using Microsoft.Extensions.ObjectPool; 3 | using Microsoft.Extensions.Options; 4 | 5 | namespace Cuture.AspNetCore.ResponseCaching.ExecutingLock; 6 | 7 | internal sealed class SharedExecutingLockLifecycleExecutor 8 | : ExecutingLockLifecycleExecutorBase> 9 | where TCachePayload : class 10 | { 11 | #region Private 字段 12 | 13 | private readonly IMemoryCache _memoryCache; 14 | 15 | #endregion Private 字段 16 | 17 | #region Public 构造函数 18 | 19 | public SharedExecutingLockLifecycleExecutor(INakedBoundedObjectPool semaphorePool, IOptions options) : base(semaphorePool) 20 | { 21 | _memoryCache = options.Value.LockedExecutionLocalResultCache; 22 | } 23 | 24 | #endregion Public 构造函数 25 | 26 | #region Public 方法 27 | 28 | protected override SharedExecutingLock Create(SemaphoreSlim semaphore) 29 | => new(semaphore, _memoryCache); 30 | 31 | #endregion Public 方法 32 | } 33 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Extensions/InterceptorOptionsExtensions.cs: -------------------------------------------------------------------------------- 1 | using Cuture.AspNetCore.ResponseCaching.Interceptors; 2 | 3 | namespace Cuture.AspNetCore.ResponseCaching; 4 | 5 | /// 6 | /// 拓展 7 | /// 8 | public static class InterceptorOptionsExtensions 9 | { 10 | #region Public 方法 11 | 12 | /// 13 | /// 添加拦截器 14 | /// 15 | /// 16 | /// 17 | /// 18 | /// 19 | public static InterceptorOptions AddInterceptor(this InterceptorOptions options, TInterceptor interceptor) where TInterceptor : IResponseCachingInterceptor 20 | { 21 | options.CachingProcessInterceptors.Add(interceptor); 22 | return options; 23 | } 24 | 25 | /// 26 | /// 添加从 中获取的拦截器 27 | /// 28 | /// 29 | /// 30 | /// 31 | public static InterceptorOptions AddServiceInterceptor(this InterceptorOptions options) where TInterceptor : IResponseCachingInterceptor 32 | { 33 | options.CachingProcessInterceptorTypes.Add(typeof(TInterceptor)); 34 | return options; 35 | } 36 | 37 | #endregion Public 方法 38 | } 39 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Filters/EmptyFilterMetadata.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.Filters; 2 | 3 | namespace Cuture.AspNetCore.ResponseCaching.Filters; 4 | 5 | /// 6 | /// 空过滤器 7 | /// 8 | internal class EmptyFilterMetadata : IFilterMetadata 9 | { 10 | #region Public 属性 11 | 12 | /// 13 | /// 静态实例 14 | /// 15 | public static EmptyFilterMetadata Instance { get; } = new(); 16 | 17 | #endregion Public 属性 18 | } 19 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/IEndpointAccessor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | 3 | namespace Cuture.AspNetCore.ResponseCaching; 4 | 5 | /// 6 | /// 动态构建 Filter 时 访问器 7 | /// 8 | public interface IEndpointAccessor 9 | { 10 | #region Public 属性 11 | 12 | /// 13 | Endpoint Endpoint { get; } 14 | 15 | /// 16 | EndpointMetadataCollection Metadatas { get; } 17 | 18 | #endregion Public 属性 19 | 20 | #region Public 方法 21 | 22 | /// 23 | /// 尝试获取 24 | /// 25 | /// 26 | /// 27 | TMetadata? GetMetadata() where TMetadata : class; 28 | 29 | /// 30 | /// 获取 ,如不存在,则抛出异常 31 | /// 32 | /// 33 | /// 34 | TMetadata GetRequiredMetadata() where TMetadata : class; 35 | 36 | #endregion Public 方法 37 | } 38 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/IOrdered.cs: -------------------------------------------------------------------------------- 1 | namespace Cuture.AspNetCore.ResponseCaching; 2 | 3 | /// 4 | /// 有序的 5 | /// 6 | public interface IOrdered 7 | { 8 | #region Public 属性 9 | 10 | /// 11 | /// 排序 12 | /// 13 | int Order => 1000; 14 | 15 | #endregion Public 属性 16 | } 17 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/IResponseCachingFilterBuilder.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.Filters; 2 | 3 | namespace Cuture.AspNetCore.ResponseCaching; 4 | 5 | /// 6 | /// 响应缓存 构建器 7 | /// 8 | public interface IResponseCachingFilterBuilder 9 | { 10 | #region Public 方法 11 | 12 | /// 13 | /// 创建 14 | /// 15 | /// 作用域为 触发构建Filter的请求 的 16 | /// 17 | IFilterMetadata CreateFilter(IServiceProvider serviceProvider); 18 | 19 | #endregion Public 方法 20 | } 21 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Interceptors/CacheHitStampInterceptor.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | using Cuture.AspNetCore.ResponseCaching.ResponseCaches; 4 | 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.Extensions.Primitives; 7 | 8 | namespace Cuture.AspNetCore.ResponseCaching.Interceptors; 9 | 10 | /// 11 | /// 缓存处理拦截器 - 缓存命中标记响应头 12 | /// 13 | internal class CacheHitStampInterceptor : IResponseWritingInterceptor 14 | { 15 | #region Private 字段 16 | 17 | private readonly string _headerKey; 18 | 19 | private readonly StringValues _headerValue; 20 | 21 | #endregion Private 字段 22 | 23 | #region Public 构造函数 24 | 25 | /// 26 | /// 缓存处理拦截器 - 缓存命中标记响应头 27 | /// 28 | /// 29 | /// 30 | public CacheHitStampInterceptor(string headerKey, StringValues headerValue) 31 | { 32 | if (string.IsNullOrEmpty(headerKey)) 33 | { 34 | throw new ArgumentException($"“{nameof(headerKey)}”不能是 Null 或为空", nameof(headerKey)); 35 | } 36 | 37 | if (string.IsNullOrEmpty(headerValue)) 38 | { 39 | throw new ArgumentException($"“{nameof(headerValue)}”不能是 Null 或为空", nameof(headerValue)); 40 | } 41 | 42 | _headerKey = headerKey; 43 | _headerValue = headerValue; 44 | } 45 | 46 | #endregion Public 构造函数 47 | 48 | #region Public 方法 49 | 50 | /// 51 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 52 | public Task OnResponseWritingAsync(ActionContext actionContext, 53 | ResponseCacheEntry entry, 54 | OnResponseWritingDelegate next) 55 | { 56 | actionContext.HttpContext.Response.Headers[_headerKey] = _headerValue; 57 | return next(actionContext, entry); 58 | } 59 | 60 | #endregion Public 方法 61 | } 62 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Interceptors/InterceptorOptions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Options; 2 | 3 | namespace Cuture.AspNetCore.ResponseCaching.Interceptors; 4 | 5 | /// 6 | /// 默认拦截器配置 7 | /// 8 | public class InterceptorOptions : IOptions 9 | { 10 | #region Public 属性 11 | 12 | /// 13 | /// 拦截器实例集合 14 | /// 15 | public List CachingProcessInterceptors { get; } = new(); 16 | 17 | /// 18 | /// 需要从 中获取的拦截器类型 19 | /// 20 | public List CachingProcessInterceptorTypes { get; } = new(); 21 | 22 | /// 23 | public InterceptorOptions Value => this; 24 | 25 | #endregion Public 属性 26 | } 27 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Interceptors/Interface/ICacheStoringInterceptor.cs: -------------------------------------------------------------------------------- 1 | using Cuture.AspNetCore.ResponseCaching.ResponseCaches; 2 | 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | namespace Cuture.AspNetCore.ResponseCaching.Interceptors; 6 | 7 | /// 8 | /// - 缓存存储 9 | /// 10 | public interface ICacheStoringInterceptor : IResponseCachingInterceptor 11 | { 12 | #region Public 方法 13 | 14 | /// 15 | /// 缓存存储时拦截 16 | /// 17 | /// Http请求方法的上下文 18 | /// 缓存key 19 | /// 缓存项 20 | /// 后续进行缓存的方法委托 21 | /// 进行内存缓存,以用于立即响应的 22 | Task OnCacheStoringAsync(ActionContext actionContext, string key, ResponseCacheEntry entry, OnCacheStoringDelegate next); 23 | 24 | #endregion Public 方法 25 | } 26 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Interceptors/Interface/IResponseCachingInterceptor.cs: -------------------------------------------------------------------------------- 1 | namespace Cuture.AspNetCore.ResponseCaching.Interceptors; 2 | 3 | /// 4 | /// 响应缓存拦截器 5 | /// 6 | public interface IResponseCachingInterceptor : IOrdered 7 | { 8 | } 9 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Interceptors/Interface/IResponseWritingInterceptor.cs: -------------------------------------------------------------------------------- 1 | using Cuture.AspNetCore.ResponseCaching.ResponseCaches; 2 | 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | namespace Cuture.AspNetCore.ResponseCaching.Interceptors; 6 | 7 | /// 8 | /// - 缓存写入响应 9 | /// 10 | public interface IResponseWritingInterceptor : IResponseCachingInterceptor 11 | { 12 | #region Public 方法 13 | 14 | /// 15 | /// 缓存写入响应时拦截 16 | /// 17 | /// Http请求方法的上下文 18 | /// 缓存项 19 | /// 后续进行将缓存写入响应的方法委托 20 | /// 是否已写入响应 21 | Task OnResponseWritingAsync(ActionContext actionContext, ResponseCacheEntry entry, OnResponseWritingDelegate next); 22 | 23 | #endregion Public 方法 24 | } 25 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Internal/ActionPathCache.cs: -------------------------------------------------------------------------------- 1 | using System.Buffers; 2 | using System.Collections.Immutable; 3 | using System.Diagnostics; 4 | using System.Runtime.CompilerServices; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.AspNetCore.Mvc.Abstractions; 7 | 8 | namespace Cuture.AspNetCore.ResponseCaching.Internal; 9 | 10 | /// 11 | /// 对应的 请求路径缓存 12 | /// 13 | internal sealed class ActionPathCache 14 | { 15 | #region Private 字段 16 | 17 | private ImmutableDictionary _actionPathCache = ImmutableDictionary.Create(StringComparer.Ordinal); 18 | 19 | #endregion Private 字段 20 | 21 | #region Public 方法 22 | 23 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 24 | public PooledReadOnlyCharSpan GetPath(ActionContext actionContext) 25 | { 26 | var id = actionContext.ActionDescriptor.Id; 27 | var result = _actionPathCache.TryGetValue(id, out var cachedValue) 28 | ? cachedValue is null 29 | ? GetRequestPath(actionContext) 30 | : new PooledReadOnlyCharSpan(null, cachedValue) 31 | : GetAndCacheRequestPath(actionContext, id); 32 | 33 | Debug.Assert(string.Equals(actionContext.HttpContext.Request.Path.ToString(), result.ToString(), StringComparison.OrdinalIgnoreCase)); 34 | 35 | return result; 36 | } 37 | 38 | #endregion Public 方法 39 | 40 | #region Private 方法 41 | 42 | private static PooledReadOnlyCharSpan GetRequestPath(ActionContext actionContext) 43 | { 44 | var path = actionContext.HttpContext.Request.Path.Value.AsSpan().TrimEnd('/'); 45 | var buffer = ArrayPool.Shared.Rent(path.Length); 46 | return new(buffer, buffer.AsSpan(0, path.ToLowerInvariant(buffer))); 47 | } 48 | 49 | private PooledReadOnlyCharSpan GetAndCacheRequestPath(ActionContext actionContext, string id) 50 | { 51 | var pathValue = GetRequestPath(actionContext); 52 | var routeTemplate = actionContext.ActionDescriptor.AttributeRouteInfo?.Template; 53 | 54 | //路由模板或非ApiController缓存null,每次获取当次请求的path 55 | if (string.IsNullOrEmpty(routeTemplate) 56 | || routeTemplate.Contains('{')) 57 | { 58 | _actionPathCache = _actionPathCache.SetItem(id, null); 59 | } 60 | else 61 | { 62 | _actionPathCache = _actionPathCache.SetItem(id, pathValue.Value.ToArray()); 63 | } 64 | return pathValue; 65 | } 66 | 67 | #endregion Private 方法 68 | } 69 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Internal/Caching/Memory/BoundedMemoryCache.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.Extensions.Caching.Memory; 2 | 3 | /// 4 | /// 有限大小的内存缓存 5 | /// 6 | internal static class BoundedMemoryCache 7 | { 8 | #region Public 方法 9 | 10 | /// 11 | /// 使用指定策略,创建一个内存缓存 12 | /// 13 | /// 14 | /// 15 | /// 16 | /// 17 | public static IBoundedMemoryCache Create(IBoundedMemoryCachePolicy cachePolicy) where TValue : class 18 | { 19 | return new DefaultBoundedMemoryCache(cachePolicy); 20 | } 21 | 22 | /// 23 | /// 使用 LRU 算法,创建一个最大容量为 的内存缓存 24 | /// 25 | /// 26 | /// 27 | /// 最大容量 28 | /// 29 | public static IBoundedMemoryCache CreateLRU(int capacity) where TKey : notnull where TValue : class 30 | { 31 | return Create(new LRUMemoryCachePolicy(capacity)); 32 | } 33 | 34 | #endregion Public 方法 35 | } 36 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Internal/Caching/Memory/BoundedMemoryCacheEntry.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.Extensions.Caching.Memory; 2 | 3 | /// 4 | /// 缓存项被移除时的回调 5 | /// 6 | /// 7 | /// 8 | /// 9 | /// 10 | internal delegate void CacheEntryRemovingCallback(TKey key, TValue value) where TValue : class; 11 | 12 | /// 13 | /// 有限大小的内存缓存项 14 | /// 15 | /// 16 | /// 17 | internal readonly struct BoundedMemoryCacheEntry where TValue : class 18 | { 19 | #region Public 字段 20 | 21 | /// 22 | /// 移除缓存项时的回调 23 | /// 24 | public readonly CacheEntryRemovingCallback? EntryRemovingCallback; 25 | 26 | /// 27 | /// 缓存Key 28 | /// 29 | public readonly TKey Key; 30 | 31 | /// 32 | /// 缓存值 33 | /// 34 | public readonly TValue Value; 35 | 36 | #endregion Public 字段 37 | 38 | #region Public 构造函数 39 | 40 | /// 41 | /// 42 | /// 43 | /// 缓存Key 44 | /// 缓存值 45 | /// 移除缓存项时的回调 46 | public BoundedMemoryCacheEntry(TKey key, TValue value, CacheEntryRemovingCallback? entryRemovingCallback) 47 | { 48 | Key = key; 49 | Value = value ?? throw new ArgumentNullException(nameof(value)); 50 | EntryRemovingCallback = entryRemovingCallback; 51 | } 52 | 53 | #endregion Public 构造函数 54 | } 55 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Internal/Caching/Memory/DefaultBoundedMemoryCache.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.Extensions.Caching.Memory; 2 | 3 | /// 4 | internal class DefaultBoundedMemoryCache : IBoundedMemoryCache where TValue : class 5 | { 6 | #region Private 字段 7 | 8 | private readonly IBoundedMemoryCachePolicy _cachePolicy; 9 | 10 | #endregion Private 字段 11 | 12 | #region Public 构造函数 13 | 14 | /// 15 | public DefaultBoundedMemoryCache(IBoundedMemoryCachePolicy cachePolicy) 16 | { 17 | _cachePolicy = cachePolicy ?? throw new ArgumentNullException(nameof(cachePolicy)); 18 | } 19 | 20 | #endregion Public 构造函数 21 | 22 | #region Public 方法 23 | 24 | /// 25 | public void Add(in BoundedMemoryCacheEntry cacheEntry) => _cachePolicy.Add(cacheEntry); 26 | 27 | /// 28 | public bool Remove(TKey key) => _cachePolicy.Remove(key); 29 | 30 | /// 31 | public bool TryGet(TKey key, out TValue? item) => _cachePolicy.TryGet(key, out item); 32 | 33 | #endregion Public 方法 34 | } 35 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Internal/Caching/Memory/IBoundedMemoryCache.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.Extensions.Caching.Memory; 2 | 3 | /// 4 | /// 有限大小的内存缓存 5 | /// 6 | /// 缓存键 7 | /// 缓存值 8 | internal interface IBoundedMemoryCache where TValue : class 9 | { 10 | #region Public 方法 11 | 12 | /// 13 | /// 添加缓存项 14 | /// 15 | /// 16 | void Add(in BoundedMemoryCacheEntry cacheEntry); 17 | 18 | /// 19 | /// 移除缓存 20 | /// 21 | /// 22 | /// 23 | bool Remove(TKey key); 24 | 25 | /// 26 | /// 尝试获取缓存值 27 | /// 28 | /// 29 | /// 30 | /// 31 | bool TryGet(TKey key, out TValue? item); 32 | 33 | #endregion Public 方法 34 | } 35 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Internal/Caching/Memory/IBoundedMemoryCacheExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.Extensions.Caching.Memory; 2 | 3 | /// 4 | /// 5 | /// 6 | internal static class IBoundedMemoryCacheExtensions 7 | { 8 | #region Public 方法 9 | 10 | /// 11 | /// 添加缓存项 12 | /// 13 | /// 14 | /// 15 | /// 16 | /// 17 | /// 18 | public static void Add(this IBoundedMemoryCache memoryCache, TKey key, TValue value) where TValue : class 19 | { 20 | memoryCache.Add(new(key, value, null)); 21 | } 22 | 23 | /// 24 | /// 添加缓存项 25 | /// 26 | /// 27 | /// 28 | /// 29 | /// 30 | /// 31 | /// 32 | public static void Add(this IBoundedMemoryCache memoryCache, TKey key, TValue value, CacheEntryRemovingCallback entryRemovingCallback) where TValue : class 33 | { 34 | memoryCache.Add(new(key, value, entryRemovingCallback)); 35 | } 36 | 37 | #endregion Public 方法 38 | } 39 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Internal/Caching/Memory/IBoundedMemoryCachePolicy.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.Extensions.Caching.Memory; 2 | 3 | /// 4 | /// 有限大小内存缓存策略 5 | /// 6 | /// 缓存键 7 | /// 缓存值 8 | internal interface IBoundedMemoryCachePolicy where TValue : class 9 | { 10 | #region Public 方法 11 | 12 | /// 13 | /// 添加缓存项 14 | /// 15 | /// 16 | void Add(in BoundedMemoryCacheEntry cacheEntry); 17 | 18 | /// 19 | /// 移除缓存 20 | /// 21 | /// 22 | /// 23 | bool Remove(TKey key); 24 | 25 | /// 26 | /// 尝试获取缓存值 27 | /// 28 | /// 29 | /// 30 | /// 31 | bool TryGet(TKey key, out TValue? item); 32 | 33 | #endregion Public 方法 34 | } 35 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Internal/Caching/Memory/LRUMemoryCache/LRUSpecializedLinkedListNode.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace Microsoft.Extensions.Caching.Memory; 4 | 5 | /// 6 | /// 内部使用的针对 LRU 的 LinkedListNode 7 | /// 8 | /// * 不会对参数进行任何合法检查 9 | /// 10 | /// 11 | [DebuggerDisplay("Current: {Value} , Previous: {Previous.Value} , Next: {Next.Value}")] 12 | internal class LRUSpecializedLinkedListNode 13 | { 14 | #region Public 字段 15 | 16 | public LRUSpecializedLinkedListNode? Next; 17 | public LRUSpecializedLinkedListNode? Previous; 18 | public TValue Value; 19 | 20 | #endregion Public 字段 21 | 22 | #region Public 构造函数 23 | 24 | /// 25 | [DebuggerStepThrough] 26 | public LRUSpecializedLinkedListNode(in TValue value) 27 | { 28 | Value = value; 29 | } 30 | 31 | #endregion Public 构造函数 32 | } 33 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Internal/DefaultExecutionLockTimeoutFallback.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.AspNetCore.Mvc.Filters; 3 | 4 | namespace Cuture.AspNetCore.ResponseCaching.Internal; 5 | 6 | internal static class DefaultExecutionLockTimeoutFallback 7 | { 8 | #region Public 方法 9 | 10 | public static Task SetStatus429(string cacheKey, FilterContext filterContext, Func next) 11 | { 12 | filterContext.HttpContext.Response.StatusCode = StatusCodes.Status429TooManyRequests; 13 | return Task.CompletedTask; 14 | } 15 | 16 | #endregion Public 方法 17 | } 18 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Internal/EndpointMetadataCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | 3 | namespace Cuture.AspNetCore.ResponseCaching.Internal; 4 | 5 | internal static class EndpointMetadataCollectionExtensions 6 | { 7 | #region Public 方法 8 | 9 | public static T RequiredMetadata(this EndpointMetadataCollection metadatas) where T : class 10 | { 11 | return metadatas.GetMetadata() ?? throw new ResponseCachingException($"Metadata - {typeof(T)} is required."); 12 | } 13 | 14 | #endregion Public 方法 15 | } 16 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Internal/HttpRequestExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | 3 | namespace Cuture.AspNetCore.ResponseCaching.Internal; 4 | 5 | internal static class HttpRequestExtensions 6 | { 7 | #region Public 方法 8 | 9 | public static ReadOnlySpan NormalizeMethodNameAsKeyPrefix(this HttpRequest httpRequest) 10 | { 11 | const char CombineChar = ResponseCachingConstants.CombineChar; 12 | 13 | return httpRequest.Method switch 14 | { 15 | "GET" => $"get{CombineChar}", 16 | "POST" => $"post{CombineChar}", 17 | "PUT" => $"put{CombineChar}", 18 | "DELETE" => $"delete{CombineChar}", 19 | "PATCH" => $"patch{CombineChar}", 20 | "OPTIONS" => $"options{CombineChar}", 21 | "HEAD" => $"head{CombineChar}", 22 | "CONNECT" => $"connect{CombineChar}", 23 | "TRACE" => $"trace{CombineChar}", 24 | _ => $"{httpRequest.Method.ToLowerInvariant()}{CombineChar}", 25 | }; 26 | } 27 | 28 | #endregion Public 方法 29 | } 30 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Internal/LocalCachedPayload.cs: -------------------------------------------------------------------------------- 1 | namespace Cuture.AspNetCore.ResponseCaching.Internal; 2 | 3 | /// 4 | /// 本地缓存的响应数据 5 | /// 6 | /// 7 | internal struct LocalCachedPayload 8 | { 9 | #region Public 字段 10 | 11 | public long ExpireTime; 12 | 13 | public TPayload Payload; 14 | 15 | #endregion Public 字段 16 | } 17 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Internal/ObjectPool/BoundedObjectPool.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.Extensions.ObjectPool; 2 | 3 | /// 4 | /// 有限大小的对象池 5 | /// 6 | internal static class BoundedObjectPool 7 | { 8 | #region Public 方法 9 | 10 | /// 11 | /// 使用 创建一个有限大小的对象池 12 | /// 13 | /// 14 | /// 最大对象数量 15 | /// 最小保留对象数量 16 | /// 回收间隔(秒) 17 | /// 18 | public static IBoundedObjectPool Create(int maximumPooled, int minimumRetained, int recycleIntervalSeconds) where T : new() 19 | { 20 | return Create(maximumPooled, minimumRetained, TimeSpan.FromSeconds(recycleIntervalSeconds)); 21 | } 22 | 23 | /// 24 | /// 使用 创建一个有限大小的对象池 25 | /// 26 | /// 27 | /// 最大对象数量 28 | /// 最小保留对象数量 29 | /// 回收间隔 30 | /// 31 | public static IBoundedObjectPool Create(int maximumPooled, int minimumRetained, TimeSpan recycleInterval) where T : new() 32 | { 33 | var options = new BoundedObjectPoolOptions 34 | { 35 | MaximumPooled = maximumPooled, 36 | MinimumRetained = minimumRetained, 37 | RecycleInterval = recycleInterval, 38 | }; 39 | return Create(options); 40 | } 41 | 42 | /// 43 | /// 使用 创建一个有限大小的对象池 44 | /// 45 | /// 46 | /// 47 | /// 48 | public static IBoundedObjectPool Create(BoundedObjectPoolOptions options) where T : new() 49 | { 50 | return Create(new DefaultObjectLifecycleExecutor(), options); 51 | } 52 | 53 | /// 54 | /// 创建一个有限大小的对象池 55 | /// 56 | /// 57 | /// 58 | /// 59 | /// 60 | public static IBoundedObjectPool Create(IObjectLifecycleExecutor objectLifecycleExecutor, BoundedObjectPoolOptions options) 61 | { 62 | return new DefaultBoundedObjectPool(objectLifecycleExecutor, options); 63 | } 64 | 65 | #endregion Public 方法 66 | } 67 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Internal/ObjectPool/BoundedObjectPoolOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.Extensions.ObjectPool; 2 | 3 | /// 4 | /// 有限大小的对象池配置项 5 | /// 6 | internal class BoundedObjectPoolOptions 7 | { 8 | #region Public 属性 9 | 10 | /// 11 | /// 最大池对象数量 12 | /// 13 | public int MaximumPooled { get; set; } 14 | 15 | /// 16 | /// 回收时,池中保留的最小对象数量 17 | /// 18 | public int MinimumRetained { get; set; } 19 | 20 | /// 21 | public IPoolReductionPolicy? PoolReductionPolicy { get; set; } 22 | 23 | /// 24 | /// 自动回收对象的检查间隔 25 | /// 26 | public TimeSpan RecycleInterval { get; set; } = TimeSpan.FromSeconds(120); 27 | 28 | #endregion Public 属性 29 | } 30 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Internal/ObjectPool/DefaultObjectLifecycleExecutor.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.Extensions.ObjectPool; 2 | 3 | /// 4 | /// 默认对象生命周期执行器 5 | /// 6 | /// 7 | internal sealed class DefaultObjectLifecycleExecutor : IObjectLifecycleExecutor where T : new() 8 | { 9 | #region Public 方法 10 | 11 | /// 12 | public T? Create() => new(); 13 | 14 | /// 15 | public void Destroy(T item) 16 | { 17 | if (item is IDisposable disposable) 18 | { 19 | disposable.Dispose(); 20 | } 21 | else if (item is IAsyncDisposable asyncDisposable) 22 | { 23 | _ = asyncDisposable.DisposeAsync(); 24 | } 25 | } 26 | 27 | /// 28 | public bool Reset(T item) => true; 29 | 30 | #endregion Public 方法 31 | } 32 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Internal/ObjectPool/DefaultPoolReductionPolicy.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.Extensions.ObjectPool; 2 | 3 | /// 4 | /// 默认池缩减策略 5 | /// 6 | internal class DefaultPoolReductionPolicy : IPoolReductionPolicy 7 | { 8 | #region Public 方法 9 | 10 | /// 11 | public int NextSize(int currentPoolSize, int poolMinimumRetained) 12 | { 13 | var reserved = currentPoolSize - (currentPoolSize / 4); 14 | return reserved > poolMinimumRetained 15 | ? reserved 16 | : poolMinimumRetained; 17 | } 18 | 19 | #endregion Public 方法 20 | } 21 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Internal/ObjectPool/IBoundedObjectPool.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.Extensions.ObjectPool; 2 | 3 | /// 4 | /// 有界的对象池 5 | /// 6 | /// 7 | internal interface IBoundedObjectPool : IDisposable 8 | { 9 | #region Public 方法 10 | 11 | /// 12 | /// 当前可用对象数量 13 | /// 14 | int AvailableCount { get; } 15 | 16 | /// 17 | /// 池大小 18 | /// 19 | int PoolSize { get; } 20 | 21 | /// 22 | /// 从池中取一个对象 23 | /// 24 | /// 当没有可用对象时,返回 null 25 | /// 26 | /// 27 | IObjectOwner? Rent(); 28 | 29 | #endregion Public 方法 30 | } 31 | 32 | /// 33 | /// 直接借用对象的 34 | /// 35 | /// 36 | internal interface IDirectBoundedObjectPool : IDisposable 37 | { 38 | #region Public 方法 39 | 40 | /// 41 | /// 从池中取一个对象 42 | /// 43 | /// 当没有可用对象时,返回 null 44 | /// 45 | /// 46 | T? Rent(); 47 | 48 | #endregion Public 方法 49 | } 50 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Internal/ObjectPool/INakedBoundedObjectPool.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.Extensions.ObjectPool; 2 | 3 | /// 4 | /// 可直接借用、归还的对象池,不使用 5 | /// 6 | /// 7 | internal interface INakedBoundedObjectPool : IRecyclePool, IDirectBoundedObjectPool 8 | { 9 | } 10 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Internal/ObjectPool/IObjectLifecycleExecutor.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.Extensions.ObjectPool; 2 | 3 | /// 4 | /// 对象生命周期执行器 5 | /// 6 | /// 7 | internal interface IObjectLifecycleExecutor 8 | { 9 | #region Public 方法 10 | 11 | /// 12 | /// 创建对象 13 | /// 14 | /// 15 | T? Create(); 16 | 17 | /// 18 | /// 销毁对象 19 | /// 20 | /// 21 | void Destroy(T item); 22 | 23 | /// 24 | /// 重置对象 25 | /// 26 | /// 27 | /// 是否重置成功 28 | bool Reset(T item); 29 | 30 | #endregion Public 方法 31 | } 32 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Internal/ObjectPool/IObjectOwner.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.Extensions.ObjectPool; 2 | 3 | /// 4 | /// 对象所有者 5 | /// 6 | /// 7 | internal interface IObjectOwner : IDisposable 8 | { 9 | #region Public 属性 10 | 11 | /// 12 | /// 对象 13 | /// 14 | public T Item { get; } 15 | 16 | #endregion Public 属性 17 | } 18 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Internal/ObjectPool/IPoolReductionPolicy.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.Extensions.ObjectPool; 2 | 3 | /// 4 | /// 池缩减策略 5 | /// 6 | internal interface IPoolReductionPolicy 7 | { 8 | #region Public 属性 9 | 10 | /// 11 | /// 默认测策略 12 | /// 13 | public static IPoolReductionPolicy Default { get; } = new DefaultPoolReductionPolicy(); 14 | 15 | #endregion Public 属性 16 | 17 | #region Public 方法 18 | 19 | /// 20 | /// 计算缩减对象池后,池保留对象的数量 21 | /// 22 | /// 当前对象池大小 23 | /// 对象池应该保留的对象数量 24 | /// 池应该缩减到的大小 25 | int NextSize(int currentPoolSize, int poolMinimumRetained); 26 | 27 | #endregion Public 方法 28 | } 29 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Internal/ObjectPool/IRecyclePool.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.Extensions.ObjectPool; 2 | 3 | /// 4 | /// 回收池 5 | /// 6 | /// 7 | internal interface IRecyclePool 8 | { 9 | #region Public 方法 10 | 11 | /// 12 | /// 将对象还回对象池 13 | /// 14 | /// 15 | void Return(T item); 16 | 17 | #endregion Public 方法 18 | } 19 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Internal/ObjectPool/ObjectOwner.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace Microsoft.Extensions.ObjectPool; 4 | 5 | /// 6 | /// 对象所有者 7 | /// 8 | /// 9 | internal sealed class ObjectOwner : IObjectOwner 10 | { 11 | #region Private 字段 12 | 13 | private readonly IRecyclePool _recyclePool; 14 | private T _item; 15 | 16 | #endregion Private 字段 17 | 18 | #region Public 属性 19 | 20 | /// 21 | public T Item { get => _item ?? throw new ObjectDisposedException(nameof(IObjectOwner)); } 22 | 23 | #endregion Public 属性 24 | 25 | #region Public 构造函数 26 | 27 | public ObjectOwner(IRecyclePool recyclePool, T item) 28 | { 29 | _recyclePool = recyclePool ?? throw new ArgumentNullException(nameof(recyclePool)); 30 | _item = item; 31 | } 32 | 33 | #endregion Public 构造函数 34 | 35 | #region Private 析构函数 36 | 37 | ~ObjectOwner() 38 | { 39 | DoDispose(); 40 | } 41 | 42 | #endregion Private 析构函数 43 | 44 | #region Public 方法 45 | 46 | /// 47 | public void Dispose() 48 | { 49 | DoDispose(); 50 | GC.SuppressFinalize(this); 51 | } 52 | 53 | #endregion Public 方法 54 | 55 | #region Private 方法 56 | 57 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 58 | private void DoDispose() 59 | { 60 | var item = _item; 61 | if (item != null) 62 | { 63 | _item = default!; 64 | _recyclePool.Return(item); 65 | } 66 | } 67 | 68 | #endregion Private 方法 69 | } 70 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Internal/PooledReadOnlyCharSpan.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System.Buffers; 3 | 4 | namespace Cuture.AspNetCore.ResponseCaching.Internal; 5 | 6 | internal ref struct PooledReadOnlyCharSpan 7 | { 8 | #region Private 字段 9 | 10 | private readonly char[]? _pooledValue; 11 | 12 | #endregion Private 字段 13 | 14 | #region Public 属性 15 | 16 | public ReadOnlySpan Value { get; } 17 | 18 | #endregion Public 属性 19 | 20 | #region Public 构造函数 21 | 22 | public PooledReadOnlyCharSpan(char[]? pooledValue, ReadOnlySpan value) 23 | { 24 | _pooledValue = pooledValue; 25 | Value = value; 26 | } 27 | 28 | #endregion Public 构造函数 29 | 30 | #region Public 方法 31 | 32 | public void Dispose() 33 | { 34 | if (_pooledValue is not null) 35 | { 36 | //Hack 重复调用检查? 37 | ArrayPool.Shared.Return(_pooledValue, false); 38 | } 39 | } 40 | 41 | /// 42 | /// 转换为字符串 43 | /// 44 | /// 45 | public override string ToString() 46 | { 47 | return new string(Value); 48 | } 49 | 50 | #endregion Public 方法 51 | } 52 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Internal/QueryStringOrderUtil.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | 3 | namespace Cuture.AspNetCore.ResponseCaching.Internal; 4 | 5 | /// 6 | /// QueryString排序工具 7 | /// 8 | internal static class QueryStringOrderUtil 9 | { 10 | #region Public 方法 11 | 12 | public static int Order(in QueryString queryString, Span destination) 13 | { 14 | if (queryString.HasValue) 15 | { 16 | if (destination.Length < queryString.Value!.Length) 17 | { 18 | throw new ArgumentException($"\"{nameof(destination)}\" not enough to fill data."); 19 | } 20 | 21 | //TODO 使用 span 优化 22 | var items = queryString.Value.TrimStart('?') 23 | .Split('&', StringSplitOptions.RemoveEmptyEntries) 24 | .OrderBy(m => m); 25 | 26 | var length = 0; 27 | foreach (var item in items) 28 | { 29 | var span = item.AsSpan(); 30 | span.CopyTo(destination); 31 | destination[span.Length] = '&'; 32 | destination = destination.Slice(span.Length + 1); 33 | length += span.Length + 1; 34 | } 35 | return length; 36 | } 37 | return 0; 38 | } 39 | 40 | #endregion Public 方法 41 | } 42 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Internal/ResponseDumpContext.cs: -------------------------------------------------------------------------------- 1 | namespace Cuture.AspNetCore.ResponseCaching.Internal; 2 | 3 | internal class ResponseDumpContext 4 | { 5 | #region Public 字段 6 | 7 | /// 8 | /// DumpStream 9 | /// 10 | public MemoryStream DumpStream { get; } 11 | 12 | /// 13 | /// Key 14 | /// 15 | public string Key { get; } 16 | 17 | /// 18 | /// 原始流 19 | /// 20 | public Stream OriginalStream { get; } 21 | 22 | #endregion Public 字段 23 | 24 | #region Public 构造函数 25 | 26 | public ResponseDumpContext(string key, MemoryStream dumpStream, Stream originalStream) 27 | { 28 | Key = key; 29 | DumpStream = dumpStream; 30 | OriginalStream = originalStream; 31 | } 32 | 33 | #endregion Public 构造函数 34 | } 35 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Internal/SinglePassSemaphoreLifecycleExecutor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.ObjectPool; 2 | 3 | namespace Cuture.AspNetCore.ResponseCaching.Internal; 4 | 5 | internal class SinglePassSemaphoreLifecycleExecutor : IObjectLifecycleExecutor 6 | { 7 | #region Public 方法 8 | 9 | public SemaphoreSlim Create() => new(1, 1); 10 | 11 | public void Destroy(SemaphoreSlim item) => item.Dispose(); 12 | 13 | public bool Reset(SemaphoreSlim item) 14 | { 15 | while (item.CurrentCount < 1) 16 | { 17 | item.Release(); 18 | } 19 | return true; 20 | } 21 | 22 | #endregion Public 方法 23 | } 24 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Internal/StringExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using Cuture.AspNetCore.ResponseCaching.Internal; 3 | 4 | namespace System; 5 | 6 | internal static class StringExtensions 7 | { 8 | #region Public 方法 9 | 10 | /// 11 | /// 添加并释放 12 | /// 13 | /// 14 | /// 15 | /// 16 | public static StringBuilder Append(this StringBuilder builder, PooledReadOnlyCharSpan pooledReadOnlySpan) 17 | { 18 | using (pooledReadOnlySpan) 19 | { 20 | builder.Append(pooledReadOnlySpan.Value); 21 | } 22 | return builder; 23 | } 24 | 25 | /// 26 | /// 转化为小写字符串 27 | /// 28 | /// 29 | /// 30 | public static string[] ToLowerArray(this IEnumerable values) => values.Select(m => m.ToLowerInvariant()).ToArray(); 31 | 32 | /// 33 | /// 移除尾部的 逻辑与 符号 34 | /// 35 | /// 36 | /// 37 | public static StringBuilder TrimEndAnd(this StringBuilder builder) 38 | { 39 | if (builder[^1] == '&') 40 | { 41 | builder.Length--; 42 | } 43 | return builder; 44 | } 45 | 46 | #endregion Public 方法 47 | } 48 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Metadatas/CachePatterns/IResponseCachePatternMetadata.cs: -------------------------------------------------------------------------------- 1 | namespace Cuture.AspNetCore.ResponseCaching.Metadatas; 2 | 3 | /// 4 | /// - 缓存创建模式 5 | /// 6 | public interface IResponseCachePatternMetadata : IResponseCachingMetadata 7 | { 8 | } 9 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Metadatas/CachePatterns/IResponseClaimCachePatternMetadata.cs: -------------------------------------------------------------------------------- 1 | namespace Cuture.AspNetCore.ResponseCaching.Metadatas; 2 | 3 | /// 4 | /// - 基于 Claim 的缓存 5 | /// 6 | public interface IResponseClaimCachePatternMetadata : IResponseCachePatternMetadata 7 | { 8 | #region Public 属性 9 | 10 | /// 11 | /// 创建缓存时依据的 Claim 类型 12 | /// 13 | string[]? VaryByClaims { get; } 14 | 15 | #endregion Public 属性 16 | } 17 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Metadatas/CachePatterns/IResponseFormCachePatternMetadata.cs: -------------------------------------------------------------------------------- 1 | namespace Cuture.AspNetCore.ResponseCaching.Metadatas; 2 | 3 | /// 4 | /// - 基于 Form 的缓存 5 | /// 6 | public interface IResponseFormCachePatternMetadata : IResponseCachePatternMetadata 7 | { 8 | #region Public 属性 9 | 10 | /// 11 | /// 创建缓存时依据的 Form 键 12 | /// 13 | string[]? VaryByFormKeys { get; } 14 | 15 | #endregion Public 属性 16 | } 17 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Metadatas/CachePatterns/IResponseHeaderCachePatternMetadata.cs: -------------------------------------------------------------------------------- 1 | namespace Cuture.AspNetCore.ResponseCaching.Metadatas; 2 | 3 | /// 4 | /// - 基于 Header 的缓存 5 | /// 6 | public interface IResponseHeaderCachePatternMetadata : IResponseCachePatternMetadata 7 | { 8 | #region Public 属性 9 | 10 | /// 11 | /// 创建缓存时依据的 Header 键 12 | /// 13 | string[]? VaryByHeaders { get; } 14 | 15 | #endregion Public 属性 16 | } 17 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Metadatas/CachePatterns/IResponseModelCachePatternMetadata.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.Filters; 2 | 3 | namespace Cuture.AspNetCore.ResponseCaching.Metadatas; 4 | 5 | /// 6 | /// - 基于 Model 的缓存 7 | /// 8 | public interface IResponseModelCachePatternMetadata : IResponseCachePatternMetadata 9 | { 10 | #region Public 属性 11 | 12 | /// 13 | /// 创建缓存时依据的 Model 参数名 14 | /// 15 | /// Note: 16 | /// 17 | /// * 以下为使用默认实现时的情况 18 | /// 19 | /// * 使用空数组时为使用所有 Model 进行生成Key 20 | /// 21 | /// * 使用的Filter将会从 转变为 22 | /// 23 | /// * 由于内部的实现问题, 的设置在某些情况下可能无法严格限制所有请求 24 | /// 25 | /// * 生成Key时,如果没有指定 , 26 | /// 则检查Model是否实现 接口,如果Model未实现 接口, 27 | /// 则调用Model的 方法生成Key 28 | /// 29 | string[]? VaryByModels { get; } 30 | 31 | #endregion Public 属性 32 | } 33 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Metadatas/CachePatterns/IResponseQueryCachePatternMetadata.cs: -------------------------------------------------------------------------------- 1 | namespace Cuture.AspNetCore.ResponseCaching.Metadatas; 2 | 3 | /// 4 | /// - 基于 Query 的缓存 5 | /// 6 | public interface IResponseQueryCachePatternMetadata : IResponseCachePatternMetadata 7 | { 8 | #region Public 属性 9 | 10 | /// 11 | /// 创建缓存时依据的 Query 键 12 | /// 13 | string[]? VaryByQueryKeys { get; } 14 | 15 | #endregion Public 属性 16 | } 17 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Metadatas/ICacheKeyGeneratorMetadata.cs: -------------------------------------------------------------------------------- 1 | using Cuture.AspNetCore.ResponseCaching.CacheKey.Generators; 2 | 3 | namespace Cuture.AspNetCore.ResponseCaching.Metadatas; 4 | 5 | /// 6 | /// - 用于生成缓存Key的 实现类型 7 | /// 8 | public interface ICacheKeyGeneratorMetadata : IResponseCachingMetadata 9 | { 10 | #region Public 属性 11 | 12 | /// 13 | /// 用于生成缓存Key的 实现类型 14 | /// 15 | Type CacheKeyGeneratorType { get; } 16 | 17 | /// 18 | /// 对应的过滤器类型 19 | /// 20 | FilterType FilterType { get; } 21 | 22 | #endregion Public 属性 23 | } 24 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Metadatas/ICacheKeyStrictModeMetadata.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace Cuture.AspNetCore.ResponseCaching.Metadatas; 4 | 5 | /// 6 | /// - 缓存键严格模式 7 | /// 8 | public interface ICacheKeyStrictModeMetadata : IResponseCachingMetadata 9 | { 10 | #region Public 属性 11 | 12 | /// 13 | /// 缓存键严格模式(指定键找不到时的处理方式,为 时,使用全局配置) 14 | /// 15 | CacheKeyStrictMode StrictMode { get; } 16 | 17 | #endregion Public 属性 18 | } 19 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Metadatas/ICacheModeMetadata.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace Cuture.AspNetCore.ResponseCaching.Metadatas; 4 | 5 | /// 6 | /// - 缓存模式 7 | /// 8 | public interface ICacheModeMetadata : IResponseCachingMetadata 9 | { 10 | #region Public 属性 11 | 12 | /// 13 | /// 缓存模式(设置依据什么内容进行缓存) 14 | /// 15 | CacheMode Mode { get; } 16 | 17 | #endregion Public 属性 18 | } 19 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Metadatas/ICacheModelKeyParserMetadata.cs: -------------------------------------------------------------------------------- 1 | namespace Cuture.AspNetCore.ResponseCaching.Metadatas; 2 | 3 | /// 4 | /// - 用于生成Model的缓存Key的 实现类型 5 | /// 6 | public interface ICacheModelKeyParserMetadata : IResponseCachingMetadata 7 | { 8 | #region Public 属性 9 | 10 | /// 11 | /// 用于生成Model的缓存Key的 实现类型 12 | /// 13 | Type ModelKeyParserType { get; } 14 | 15 | #endregion Public 属性 16 | } 17 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Metadatas/ICacheStoreLocationMetadata.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace Cuture.AspNetCore.ResponseCaching.Metadatas; 4 | 5 | /// 6 | /// - 缓存数据存储位置 7 | /// 8 | public interface ICacheStoreLocationMetadata : IResponseCachingMetadata 9 | { 10 | #region Public 属性 11 | 12 | /// 13 | /// 缓存数据存储位置(为 时,使用全局配置) 14 | /// 15 | CacheStoreLocation StoreLocation { get; } 16 | 17 | #endregion Public 属性 18 | } 19 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Metadatas/IDumpStreamInitialCapacityMetadata.cs: -------------------------------------------------------------------------------- 1 | namespace Cuture.AspNetCore.ResponseCaching.Metadatas; 2 | 3 | /// 4 | /// - Dump响应的Stream初始容量 5 | /// 6 | public interface IDumpStreamInitialCapacityMetadata : IResponseCachingMetadata 7 | { 8 | #region Public 属性 9 | 10 | /// 11 | /// Dump响应的Stream初始容量 12 | /// 13 | int Capacity { get; } 14 | 15 | #endregion Public 属性 16 | } 17 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Metadatas/IExecutingLockMetadata.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace Cuture.AspNetCore.ResponseCaching.Metadatas; 4 | 5 | /// 6 | /// - 执行时的锁定模式 7 | /// 8 | public interface IExecutingLockMetadata : IResponseCachingMetadata 9 | { 10 | #region Public 属性 11 | 12 | /// 13 | /// 锁定的超时时间(毫秒) 14 | /// null 或大于 -1 的任意值 15 | /// null 表示使用默认值 16 | /// (-1) 为无限等待 17 | /// 18 | int? LockMillisecondsTimeout { get; } 19 | 20 | /// 21 | ExecutingLockMode LockMode { get; } 22 | 23 | /// 24 | ExecutionLockTimeoutFallbackDelegate? OnExecutionLockTimeout { get; } 25 | 26 | #endregion Public 属性 27 | } 28 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Metadatas/IHotDataCacheMetadata.cs: -------------------------------------------------------------------------------- 1 | using Cuture.AspNetCore.ResponseCaching.ResponseCaches; 2 | 3 | namespace Cuture.AspNetCore.ResponseCaching.Metadatas; 4 | 5 | /// 6 | /// - 信息 7 | /// 8 | public interface IHotDataCacheMetadata : IResponseCachingMetadata 9 | { 10 | #region Public 属性 11 | 12 | /// 13 | /// 热点数据缓存策略 14 | /// 15 | HotDataCachePolicy CachePolicy { get; } 16 | 17 | /// 18 | /// 缓存热数据的数量 19 | /// 20 | int Capacity { get; } 21 | 22 | /// 23 | /// 热点数据缓存容器名称 24 | /// 25 | string? HotDataCacheName { get; } 26 | 27 | #endregion Public 属性 28 | } 29 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Metadatas/IMaxCacheableResponseLengthMetadata.cs: -------------------------------------------------------------------------------- 1 | namespace Cuture.AspNetCore.ResponseCaching.Metadatas; 2 | 3 | /// 4 | /// - 最大可缓存响应长度 5 | /// 6 | public interface IMaxCacheableResponseLengthMetadata : IResponseCachingMetadata 7 | { 8 | #region Public 属性 9 | 10 | /// 11 | /// 最大可缓存响应长度 (为 null 时,使用全局配置) 12 | /// 13 | int? MaxCacheableResponseLength { get; } 14 | 15 | #endregion Public 属性 16 | } 17 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Metadatas/IResponseCachingMetadata.cs: -------------------------------------------------------------------------------- 1 | namespace Cuture.AspNetCore.ResponseCaching.Metadatas; 2 | 3 | /// 4 | /// 响应缓存元数据 5 | /// 6 | public interface IResponseCachingMetadata 7 | { 8 | } 9 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/Metadatas/IResponseDurationMetadata.cs: -------------------------------------------------------------------------------- 1 | namespace Cuture.AspNetCore.ResponseCaching.Metadatas; 2 | 3 | /// 4 | /// - 缓存时长 5 | /// 6 | public interface IResponseDurationMetadata : IResponseCachingMetadata 7 | { 8 | #region Public 属性 9 | 10 | /// 11 | /// 缓存时长(单位:秒) 12 | /// 13 | int Duration { get; } 14 | 15 | #endregion Public 属性 16 | } 17 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/ResponseCaches/DefaultMemoryResponseCache.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Caching.Memory; 2 | using Microsoft.Extensions.Logging; 3 | 4 | namespace Cuture.AspNetCore.ResponseCaching.ResponseCaches; 5 | 6 | /// 7 | /// 默认的基于内存的响应缓存 8 | /// 9 | public sealed class DefaultMemoryResponseCache : IMemoryResponseCache, IDisposable 10 | { 11 | #region Private 字段 12 | 13 | private readonly MemoryCache _memoryCache; 14 | 15 | #endregion Private 字段 16 | 17 | #region Public 构造函数 18 | 19 | /// 20 | /// 默认的基于内存的响应缓存 21 | /// 22 | /// 23 | public DefaultMemoryResponseCache(ILoggerFactory loggerFactory) 24 | { 25 | _memoryCache = new MemoryCache(new MemoryCacheOptions(), loggerFactory); 26 | } 27 | 28 | #endregion Public 构造函数 29 | 30 | #region Public 方法 31 | 32 | /// 33 | public void Dispose() 34 | { 35 | _memoryCache.Dispose(); 36 | } 37 | 38 | /// 39 | public Task GetAsync(string key, CancellationToken cancellationToken) 40 | { 41 | _memoryCache.TryGetValue(key, out var cacheEntry); 42 | return Task.FromResult(cacheEntry)!; 43 | } 44 | 45 | /// 46 | public Task RemoveAsync(string key, CancellationToken cancellationToken = default) 47 | { 48 | _memoryCache.Remove(key); 49 | return Task.FromResult(null); 50 | } 51 | 52 | /// 53 | public Task SetAsync(string key, ResponseCacheEntry entry, CancellationToken cancellationToken) 54 | { 55 | _memoryCache.Set(key, entry, entry.GetAbsoluteExpirationDateTimeOffset()); 56 | return Task.CompletedTask; 57 | } 58 | 59 | #endregion Public 方法 60 | } 61 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/ResponseCaches/HotDataCache/DefaultHotDataCacheProvider.cs: -------------------------------------------------------------------------------- 1 | namespace Cuture.AspNetCore.ResponseCaching.ResponseCaches; 2 | 3 | /// 4 | /// 热点数据缓存提供器 5 | /// 6 | internal class DefaultHotDataCacheProvider : IHotDataCacheProvider 7 | { 8 | #region Private 字段 9 | 10 | private readonly Dictionary _caches = new(); 11 | 12 | private bool _disposedValue; 13 | 14 | #endregion Private 字段 15 | 16 | #region Public 方法 17 | 18 | public void Dispose() 19 | { 20 | if (!_disposedValue) 21 | { 22 | _disposedValue = true; 23 | 24 | IHotDataCache[] caches; 25 | 26 | lock (_caches) 27 | { 28 | caches = _caches.Values.ToArray(); 29 | _caches.Clear(); 30 | } 31 | 32 | if (caches.Length > 0) 33 | { 34 | Task.Run(() => 35 | { 36 | foreach (var item in caches) 37 | { 38 | item.Dispose(); 39 | } 40 | }); 41 | } 42 | } 43 | } 44 | 45 | /// 46 | public IHotDataCache Get(IServiceProvider serviceProvider, string name, HotDataCachePolicy policy, int capacity) 47 | { 48 | name = $"{name}_{policy}_{capacity}"; 49 | 50 | lock (_caches) 51 | { 52 | CheckDisposed(); 53 | 54 | if (_caches.TryGetValue(name, out var cache)) 55 | { 56 | return cache; 57 | } 58 | cache = policy switch 59 | { 60 | HotDataCachePolicy.Default => new LRUHotDataCache(capacity), 61 | HotDataCachePolicy.LRU => new LRUHotDataCache(capacity), 62 | _ => throw new ResponseCachingException($"UnSupport HotDataCachePolicy - {policy}."), 63 | }; 64 | _caches.Add(name, cache); 65 | return cache; 66 | } 67 | } 68 | 69 | #endregion Public 方法 70 | 71 | #region Private 方法 72 | 73 | private void CheckDisposed() 74 | { 75 | if (_disposedValue) 76 | { 77 | throw new ObjectDisposedException(nameof(IHotDataCacheProvider)); 78 | } 79 | } 80 | 81 | #endregion Private 方法 82 | } 83 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/ResponseCaches/HotDataCache/IHotDataCache.cs: -------------------------------------------------------------------------------- 1 | namespace Cuture.AspNetCore.ResponseCaching.ResponseCaches; 2 | 3 | /// 4 | /// 热点数据缓存 5 | /// 6 | public interface IHotDataCache : IDisposable 7 | { 8 | #region Public 方法 9 | 10 | /// 11 | /// 获取缓存 12 | /// 13 | /// 14 | /// 15 | ResponseCacheEntry? Get(string key); 16 | 17 | /// 18 | /// 移除缓存 19 | /// 20 | /// 21 | /// 22 | bool? Remove(string key); 23 | 24 | /// 25 | /// 设置缓存 26 | /// 27 | /// 28 | /// 29 | /// 30 | void Set(string key, ResponseCacheEntry entry); 31 | 32 | #endregion Public 方法 33 | } 34 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/ResponseCaches/HotDataCache/IHotDataCacheBuilder.cs: -------------------------------------------------------------------------------- 1 | using Cuture.AspNetCore.ResponseCaching.Metadatas; 2 | 3 | namespace Cuture.AspNetCore.ResponseCaching.ResponseCaches; 4 | 5 | /// 6 | /// 热点数据缓存提供器 7 | /// 8 | public interface IHotDataCacheBuilder 9 | { 10 | #region Public 方法 11 | 12 | /// 13 | /// 获取热点数据缓存 14 | /// 15 | /// 16 | /// 热点数据缓存信息 17 | /// 18 | IHotDataCache Build(IServiceProvider serviceProvider, IHotDataCacheMetadata metadata); 19 | 20 | #endregion Public 方法 21 | } 22 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/ResponseCaches/HotDataCache/IHotDataCacheProvider.cs: -------------------------------------------------------------------------------- 1 | namespace Cuture.AspNetCore.ResponseCaching.ResponseCaches; 2 | 3 | /// 4 | /// 热点数据缓存提供器 5 | /// 6 | public interface IHotDataCacheProvider : IDisposable 7 | { 8 | #region Public 方法 9 | 10 | /// 11 | /// 获取热点数据缓存 12 | /// 13 | /// 根据 名称-策略-容量 可共享 14 | /// 15 | /// 16 | /// 名称 17 | /// 策略 18 | /// 容量 19 | /// 20 | IHotDataCache Get(IServiceProvider serviceProvider, string name, HotDataCachePolicy policy, int capacity); 21 | 22 | #endregion Public 方法 23 | } 24 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/ResponseCaches/HotDataCache/LRUHotDataCache.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Caching.Memory; 2 | 3 | namespace Cuture.AspNetCore.ResponseCaching.ResponseCaches; 4 | 5 | /// 6 | /// LRU算法的热点数据缓存 7 | /// 8 | public class LRUHotDataCache : IHotDataCache 9 | { 10 | #region Private 字段 11 | 12 | private readonly IBoundedMemoryCache _boundedMemoryCache; 13 | 14 | #endregion Private 字段 15 | 16 | #region Public 构造函数 17 | 18 | /// 19 | /// 20 | /// 21 | /// 容量 22 | public LRUHotDataCache(int capacity) 23 | { 24 | _boundedMemoryCache = BoundedMemoryCache.CreateLRU(capacity); 25 | } 26 | 27 | #endregion Public 构造函数 28 | 29 | #region Public 方法 30 | 31 | /// 32 | public void Dispose() 33 | { 34 | GC.SuppressFinalize(this); 35 | } 36 | 37 | /// 38 | public ResponseCacheEntry? Get(string key) 39 | { 40 | if (_boundedMemoryCache.TryGet(key, out var result)) 41 | { 42 | if (result!.IsExpired()) 43 | { 44 | _boundedMemoryCache.Remove(key); 45 | return null; 46 | } 47 | return result; 48 | } 49 | return null; 50 | } 51 | 52 | /// 53 | public bool? Remove(string key) 54 | { 55 | return _boundedMemoryCache.Remove(key); 56 | } 57 | 58 | /// 59 | public void Set(string key, ResponseCacheEntry entry) 60 | { 61 | _boundedMemoryCache.Add(key, entry); 62 | } 63 | 64 | #endregion Public 方法 65 | } 66 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/ResponseCaches/HotDataCachePolicy.cs: -------------------------------------------------------------------------------- 1 | namespace Cuture.AspNetCore.ResponseCaching.ResponseCaches; 2 | 3 | /// 4 | /// 热数据缓存策略 5 | /// 6 | public enum HotDataCachePolicy 7 | { 8 | /// 9 | /// 默认(当前只支持LRU) 10 | /// 11 | Default = 0, 12 | 13 | /// 14 | /// LRU 15 | /// 16 | LRU = 1, 17 | } 18 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/ResponseCaches/IDistributedResponseCache.cs: -------------------------------------------------------------------------------- 1 | namespace Cuture.AspNetCore.ResponseCaching.ResponseCaches; 2 | 3 | /// 4 | /// 分布式响应缓存 5 | /// 6 | public interface IDistributedResponseCache : IResponseCache 7 | { 8 | } 9 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/ResponseCaches/IMemoryResponseCache.cs: -------------------------------------------------------------------------------- 1 | namespace Cuture.AspNetCore.ResponseCaching.ResponseCaches; 2 | 3 | /// 4 | /// 基于内存的响应缓存 5 | /// 6 | public interface IMemoryResponseCache : IResponseCache 7 | { 8 | } 9 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/ResponseCaches/IResponseCache.cs: -------------------------------------------------------------------------------- 1 | namespace Cuture.AspNetCore.ResponseCaching.ResponseCaches; 2 | 3 | /// 4 | /// 响应缓存 5 | /// 6 | public interface IResponseCache 7 | { 8 | #region Public 方法 9 | 10 | /// 11 | /// 获取缓存 12 | /// 13 | /// 14 | /// 15 | /// 16 | Task GetAsync(string key, CancellationToken cancellationToken = default); 17 | 18 | /// 19 | /// 移除缓存 20 | /// 21 | /// 22 | /// 23 | /// 是否移除成功 24 | Task RemoveAsync(string key, CancellationToken cancellationToken = default); 25 | 26 | /// 27 | /// 设置缓存 28 | /// 29 | /// 30 | /// 31 | /// 32 | /// 33 | Task SetAsync(string key, ResponseCacheEntry entry, CancellationToken cancellationToken = default); 34 | 35 | #endregion Public 方法 36 | } 37 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/ResponseCaches/ResponseCacheEntryOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Cuture.AspNetCore.ResponseCaching.ResponseCaches; 2 | 3 | /// 4 | /// 响应缓存选项 5 | /// 6 | public class ResponseCacheEntryOptions 7 | { 8 | #region Public 属性 9 | 10 | /// 11 | /// 缓存时长(秒) 12 | /// 13 | public int Duration { get; set; } 14 | 15 | #endregion Public 属性 16 | } 17 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/ResponseCaches/ResponseCacheHotDataCacheWrapper.cs: -------------------------------------------------------------------------------- 1 | namespace Cuture.AspNetCore.ResponseCaching.ResponseCaches; 2 | 3 | /// 4 | /// 响应缓存的热数据缓存包装器 5 | /// 6 | public sealed class ResponseCacheHotDataCacheWrapper : IDistributedResponseCache, IDisposable 7 | { 8 | #region Private 字段 9 | 10 | private readonly IDistributedResponseCache _distributedCache; 11 | 12 | private readonly IHotDataCache _hotDataCache; 13 | 14 | #endregion Private 字段 15 | 16 | #region Public 构造函数 17 | 18 | /// 19 | public ResponseCacheHotDataCacheWrapper(IDistributedResponseCache distributedCache, IHotDataCache hotDataCache) 20 | { 21 | _distributedCache = distributedCache ?? throw new ArgumentNullException(nameof(distributedCache)); 22 | _hotDataCache = hotDataCache ?? throw new ArgumentNullException(nameof(hotDataCache)); 23 | } 24 | 25 | #endregion Public 构造函数 26 | 27 | #region Public 方法 28 | 29 | /// 30 | public void Dispose() 31 | { 32 | _hotDataCache.Dispose(); 33 | } 34 | 35 | /// 36 | public async Task GetAsync(string key, CancellationToken cancellationToken) 37 | { 38 | //HACK 此处未加锁,并发访问会穿透本地缓存 39 | var cacheEntry = _hotDataCache.Get(key); 40 | if (cacheEntry is not null 41 | && !cacheEntry.IsExpired()) 42 | { 43 | return cacheEntry; 44 | } 45 | cacheEntry = await _distributedCache.GetAsync(key, cancellationToken); 46 | if (cacheEntry is not null) 47 | { 48 | _hotDataCache.Set(key, cacheEntry); 49 | } 50 | return cacheEntry; 51 | } 52 | 53 | /// 54 | public Task RemoveAsync(string key, CancellationToken cancellationToken = default) 55 | { 56 | return Task.FromResult(_hotDataCache.Remove(key)); 57 | } 58 | 59 | /// 60 | public Task SetAsync(string key, ResponseCacheEntry entry, CancellationToken cancellationToken) 61 | { 62 | _hotDataCache.Set(key, entry); 63 | return _distributedCache.SetAsync(key, entry, cancellationToken); 64 | } 65 | 66 | #endregion Public 方法 67 | } 68 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/ResponseCachingConstants.cs: -------------------------------------------------------------------------------- 1 | namespace Cuture.AspNetCore.ResponseCaching; 2 | 3 | /// 4 | /// 缓存相关常量 5 | /// 6 | public static class ResponseCachingConstants 7 | { 8 | #region const 9 | 10 | /// 11 | /// 默认字符串连接字符 12 | /// 13 | public const char CombineChar = ':'; 14 | 15 | /// 16 | /// 默认dump时memorystream初始容量 17 | /// 18 | public const int DefaultDumpCapacity = 1024; 19 | 20 | /// 21 | /// 默认锁定等待超时时间(毫秒) 22 | /// 23 | public const int DefaultLockMillisecondsTimeout = 10_000; 24 | 25 | /// 26 | /// 默认最大可缓存响应长度 - 512k 27 | /// 28 | public const int DefaultMaxCacheableResponseLength = 512 * 1024; 29 | 30 | /// 31 | /// 默认缓存Key的最大长度 32 | /// 33 | public const int DefaultMaxCacheKeyLength = 1024; 34 | 35 | /// 36 | /// 默认最大可缓存响应长度的最小值 - 128byte 37 | /// 38 | public const int DefaultMinMaxCacheableResponseLength = 128; 39 | 40 | /// 41 | /// 最小缓存可用毫秒数 42 | /// 43 | public const int MinCacheAvailableMilliseconds = MinCacheAvailableSeconds * 1000; 44 | 45 | /// 46 | /// 最小缓存可用秒数 47 | /// 48 | public const int MinCacheAvailableSeconds = 1; 49 | 50 | /// 51 | /// 在Request.Items的Key 52 | /// 53 | public const string ResponseCachingExecutingLockKey = "__ResponseCaching.ExecutingLock"; 54 | 55 | /// 56 | /// ResponseDumpContext 在Request.Items的Key 57 | /// 58 | public const string ResponseCachingResponseDumpContextKey = "__ResponseCaching.ResponseDumpContext"; 59 | 60 | #endregion const 61 | } 62 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseCaching/ResponseCachingServiceBuilder.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace Cuture.AspNetCore.ResponseCaching; 4 | 5 | /// 6 | /// ResponseCaching服务构建类 7 | /// 8 | public class ResponseCachingServiceBuilder 9 | { 10 | #region Public 属性 11 | 12 | /// 13 | public IServiceCollection Services { get; } 14 | 15 | #endregion Public 属性 16 | 17 | #region Public 构造函数 18 | 19 | /// 20 | public ResponseCachingServiceBuilder(IServiceCollection services) 21 | { 22 | Services = services ?? throw new ArgumentNullException(nameof(services)); 23 | } 24 | 25 | #endregion Public 构造函数 26 | } 27 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test.WebHost/AuthorizeMixedAttribute.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authentication.Cookies; 2 | using Microsoft.AspNetCore.Authentication.JwtBearer; 3 | using Microsoft.AspNetCore.Authorization; 4 | 5 | namespace ResponseCaching.Test.WebHost; 6 | 7 | public class AuthorizeMixedAttribute : AuthorizeAttribute 8 | { 9 | public AuthorizeMixedAttribute() 10 | { 11 | AuthenticationSchemes = $"{CookieAuthenticationDefaults.AuthenticationScheme},{JwtBearerDefaults.AuthenticationScheme}"; 12 | } 13 | 14 | public AuthorizeMixedAttribute(string policy) : base(policy) 15 | { 16 | AuthenticationSchemes = $"{CookieAuthenticationDefaults.AuthenticationScheme},{JwtBearerDefaults.AuthenticationScheme}"; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test.WebHost/Controllers/CacheByAllMixedController.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | using ResponseCaching.Test.WebHost.Dtos; 6 | using ResponseCaching.Test.WebHost.Models; 7 | using ResponseCaching.Test.WebHost.Test; 8 | 9 | namespace ResponseCaching.Test.WebHost.Controllers; 10 | 11 | public class CacheByAllMixedController : TestControllerBase 12 | { 13 | #region Private 字段 14 | 15 | private readonly ILogger _logger; 16 | 17 | #endregion Private 字段 18 | 19 | #region Public 构造函数 20 | 21 | public CacheByAllMixedController(ILogger logger) 22 | { 23 | _logger = logger; 24 | } 25 | 26 | #endregion Public 构造函数 27 | 28 | #region Public 方法 29 | 30 | [HttpPost] 31 | [AuthorizeMixed] 32 | [ResponseCaching(Duration, 33 | Mode = CacheMode.Custom, 34 | VaryByClaims = new[] { "id", "sid" }, 35 | VaryByHeaders = new[] { "page", "pageSize" }, 36 | VaryByQueryKeys = new[] { "page", "pageSize" }, 37 | VaryByModels = new[] { "input" })] 38 | [ExecutingLock(ExecutingLockMode.CacheKeySingle)] 39 | public IEnumerable Post([Required][FromQuery] int page, [Required][FromQuery] int pageSize, [FromBody] PageResultRequestDto input) 40 | { 41 | _logger.LogInformation("{0} - {1}", page, pageSize); 42 | return TestDataGenerator.GetData((page - 1) * pageSize, pageSize); 43 | } 44 | 45 | [HttpPost] 46 | [AuthorizeMixed] 47 | [ResponseCaching(Duration, 48 | Mode = CacheMode.Custom, 49 | VaryByClaims = new[] { "id", "sid" }, 50 | VaryByHeaders = new[] { "page", "pageSize" }, 51 | VaryByQueryKeys = new[] { "page", "pageSize" }, 52 | VaryByModels = new[] { "input" })] 53 | [ExecutingLock(ExecutingLockMode.CacheKeySingle)] 54 | [Route("{Value1}/{Value2}")] 55 | public IEnumerable PostWithPath([Required][FromQuery] int page, [Required][FromQuery] int pageSize, [FromBody] PageResultRequestDto input) 56 | { 57 | _logger.LogInformation("{0} - {1}", page, pageSize); 58 | return TestDataGenerator.GetData((page - 1) * pageSize, pageSize); 59 | } 60 | 61 | #endregion Public 方法 62 | } 63 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test.WebHost/Controllers/CacheByCustomCacheKeyGeneratorController.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | using Cuture.AspNetCore.ResponseCaching; 4 | 5 | using Microsoft.AspNetCore.Mvc; 6 | 7 | using ResponseCaching.Test.WebHost.Models; 8 | using ResponseCaching.Test.WebHost.Test; 9 | 10 | namespace ResponseCaching.Test.WebHost.Controllers; 11 | 12 | public class CacheByCustomCacheKeyGeneratorController : TestControllerBase 13 | { 14 | #region Private 字段 15 | 16 | private readonly ILogger _logger; 17 | 18 | #endregion Private 字段 19 | 20 | #region Public 构造函数 21 | 22 | public CacheByCustomCacheKeyGeneratorController(ILogger logger) 23 | { 24 | _logger = logger; 25 | } 26 | 27 | #endregion Public 构造函数 28 | 29 | #region Public 方法 30 | 31 | [HttpGet] 32 | [ResponseCaching(Duration)] 33 | [ExecutingLock(ExecutingLockMode.CacheKeySingle)] 34 | [Description("cache_key_definite")] 35 | [CacheKeyGenerator(typeof(TestCustomCacheKeyGenerator), FilterType.Resource)] 36 | public IEnumerable Get() 37 | { 38 | return TestDataGenerator.GetData(0, 5); 39 | } 40 | 41 | #endregion Public 方法 42 | } 43 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test.WebHost/Controllers/CacheByCustomModelKeyParserController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | using ResponseCaching.Test.WebHost.Dtos; 4 | using ResponseCaching.Test.WebHost.Models; 5 | using ResponseCaching.Test.WebHost.Test; 6 | 7 | namespace ResponseCaching.Test.WebHost.Controllers; 8 | 9 | public class CacheByCustomModelKeyParserController : TestControllerBase 10 | { 11 | #region Private 字段 12 | 13 | private readonly ILogger _logger; 14 | 15 | #endregion Private 字段 16 | 17 | #region Public 构造函数 18 | 19 | public CacheByCustomModelKeyParserController(ILogger logger) 20 | { 21 | _logger = logger; 22 | } 23 | 24 | #endregion Public 构造函数 25 | 26 | #region Public 方法 27 | 28 | [HttpPost] 29 | [CacheByModel(Duration)] 30 | [CacheModelKeyParser(typeof(TestCustomModelKeyParser))] 31 | [ExecutingLock(ExecutingLockMode.CacheKeySingle)] 32 | public IEnumerable Post(PageResultRequestDto input) 33 | { 34 | int page = input.Page; 35 | int pageSize = input.PageSize; 36 | _logger.LogInformation("{0} - {1}", page, pageSize); 37 | return TestDataGenerator.GetData(0, 5); 38 | } 39 | 40 | #endregion Public 方法 41 | } 42 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test.WebHost/Controllers/CacheByFormController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | using ResponseCaching.Test.WebHost.Models; 4 | using ResponseCaching.Test.WebHost.Test; 5 | 6 | namespace ResponseCaching.Test.WebHost.Controllers; 7 | 8 | public class CacheByFormController : TestControllerBase 9 | { 10 | #region Private 字段 11 | 12 | private readonly ILogger _logger; 13 | 14 | #endregion Private 字段 15 | 16 | #region Public 构造函数 17 | 18 | public CacheByFormController(ILogger logger) 19 | { 20 | _logger = logger; 21 | } 22 | 23 | #endregion Public 构造函数 24 | 25 | #region Public 方法 26 | 27 | [HttpPost] 28 | [CacheByForm(Duration, "page", "pageSize")] 29 | [ExecutingLock(ExecutingLockMode.CacheKeySingle)] 30 | public IEnumerable Post() 31 | { 32 | int page = int.Parse(Request.Form["page"]); 33 | int pageSize = int.Parse(Request.Form["pageSize"]); 34 | _logger.LogInformation("{0} - {1}", page, pageSize); 35 | return TestDataGenerator.GetData((page - 1) * pageSize, pageSize); 36 | } 37 | 38 | #endregion Public 方法 39 | } 40 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test.WebHost/Controllers/CacheByFullQueryController.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | using ResponseCaching.Test.WebHost.Models; 6 | using ResponseCaching.Test.WebHost.Test; 7 | 8 | namespace ResponseCaching.Test.WebHost.Controllers; 9 | 10 | public class CacheByFullQueryController : TestControllerBase 11 | { 12 | #region Private 字段 13 | 14 | private readonly ILogger _logger; 15 | 16 | #endregion Private 字段 17 | 18 | #region Public 构造函数 19 | 20 | public CacheByFullQueryController(ILogger logger) 21 | { 22 | _logger = logger; 23 | } 24 | 25 | #endregion Public 构造函数 26 | 27 | #region Public 方法 28 | 29 | [HttpGet] 30 | [CacheByQuery(Duration)] 31 | [ExecutingLock(ExecutingLockMode.CacheKeySingle)] 32 | public IEnumerable Get([Required] int page, [Required] int pageSize) 33 | { 34 | _logger.LogInformation("{0} - {1}", page, pageSize); 35 | return TestDataGenerator.GetData((page - 1) * pageSize, pageSize); 36 | } 37 | 38 | #endregion Public 方法 39 | } 40 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test.WebHost/Controllers/CacheByFullUrlController.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | using ResponseCaching.Test.WebHost.Models; 6 | using ResponseCaching.Test.WebHost.Test; 7 | 8 | namespace ResponseCaching.Test.WebHost.Controllers; 9 | 10 | public class CacheByFullUrlController : TestControllerBase 11 | { 12 | #region Private 字段 13 | 14 | private readonly ILogger _logger; 15 | 16 | #endregion Private 字段 17 | 18 | #region Public 构造函数 19 | 20 | public CacheByFullUrlController(ILogger logger) 21 | { 22 | _logger = logger; 23 | } 24 | 25 | #endregion Public 构造函数 26 | 27 | #region Public 方法 28 | 29 | [HttpGet] 30 | [CacheByFullUrl(Duration)] 31 | [ExecutingLock(ExecutingLockMode.ActionSingle)] 32 | [ResponseDumpCapacity(128)] 33 | public IEnumerable Get([Required] int page, [Required] int pageSize) 34 | { 35 | _logger.LogInformation("{0} - {1}", page, pageSize); 36 | return TestDataGenerator.GetData((page - 1) * pageSize, pageSize); 37 | } 38 | 39 | #endregion Public 方法 40 | } 41 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test.WebHost/Controllers/CacheByFullUrlKeyAccessController.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using Cuture.AspNetCore.ResponseCaching; 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | namespace ResponseCaching.Test.WebHost.Controllers; 6 | 7 | public class CacheByFullUrlKeyAccessController : TestControllerBase 8 | { 9 | #region Private 字段 10 | 11 | private readonly ICacheKeyAccessor _cacheKeyAccessor; 12 | 13 | private readonly ILogger _logger; 14 | 15 | #endregion Private 字段 16 | 17 | #region Public 构造函数 18 | 19 | public CacheByFullUrlKeyAccessController(ILogger logger, ICacheKeyAccessor cacheKeyAccessor) 20 | { 21 | _logger = logger; 22 | _cacheKeyAccessor = cacheKeyAccessor; 23 | } 24 | 25 | #endregion Public 构造函数 26 | 27 | #region Public 方法 28 | 29 | [HttpGet] 30 | [CacheByFullUrl(Duration)] 31 | [ExecutingLock(ExecutingLockMode.ActionSingle)] 32 | [ResponseDumpCapacity(128)] 33 | public string Get([Required] int page, [Required] int pageSize) 34 | { 35 | return _cacheKeyAccessor.Key!; 36 | } 37 | 38 | #endregion Public 方法 39 | } 40 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test.WebHost/Controllers/CacheByHeaderController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | using ResponseCaching.Test.WebHost.Models; 4 | using ResponseCaching.Test.WebHost.Test; 5 | 6 | namespace ResponseCaching.Test.WebHost.Controllers; 7 | 8 | public class CacheByHeaderController : TestControllerBase 9 | { 10 | #region Private 字段 11 | 12 | private readonly ILogger _logger; 13 | 14 | #endregion Private 字段 15 | 16 | #region Public 构造函数 17 | 18 | public CacheByHeaderController(ILogger logger) 19 | { 20 | _logger = logger; 21 | } 22 | 23 | #endregion Public 构造函数 24 | 25 | #region Public 方法 26 | 27 | [HttpGet] 28 | [CacheByHeader(Duration, 29 | "page", "pageSize")] 30 | [ExecutingLock(ExecutingLockMode.CacheKeySingle)] 31 | public IEnumerable Get() 32 | { 33 | int page = int.Parse(Request.Headers["page"]); 34 | int pageSize = int.Parse(Request.Headers["pageSize"]); 35 | _logger.LogInformation("{0} - {1}", page, pageSize); 36 | return TestDataGenerator.GetData((page - 1) * pageSize, pageSize); 37 | } 38 | 39 | #endregion Public 方法 40 | } 41 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test.WebHost/Controllers/CacheByModelController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | using ResponseCaching.Test.WebHost.Dtos; 4 | using ResponseCaching.Test.WebHost.Models; 5 | using ResponseCaching.Test.WebHost.Test; 6 | 7 | namespace ResponseCaching.Test.WebHost.Controllers; 8 | 9 | public class CacheByModelController : TestControllerBase 10 | { 11 | #region Private 字段 12 | 13 | private readonly ILogger _logger; 14 | 15 | #endregion Private 字段 16 | 17 | #region Public 构造函数 18 | 19 | public CacheByModelController(ILogger logger) 20 | { 21 | _logger = logger; 22 | } 23 | 24 | #endregion Public 构造函数 25 | 26 | #region Public 方法 27 | 28 | [HttpPost] 29 | [CacheByModel(Duration, "input")] 30 | [ExecutingLock(ExecutingLockMode.ActionSingle)] 31 | public IEnumerable Post(PageResultRequestDto input) 32 | { 33 | int page = input.Page; 34 | int pageSize = input.PageSize; 35 | _logger.LogInformation("{0} - {1}", page, pageSize); 36 | return TestDataGenerator.GetData((page - 1) * pageSize, pageSize); 37 | } 38 | 39 | #endregion Public 方法 40 | } 41 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test.WebHost/Controllers/CacheByModelKeyAccessController.cs: -------------------------------------------------------------------------------- 1 | using Cuture.AspNetCore.ResponseCaching; 2 | using Microsoft.AspNetCore.Mvc; 3 | 4 | using ResponseCaching.Test.WebHost.Dtos; 5 | 6 | namespace ResponseCaching.Test.WebHost.Controllers; 7 | 8 | public class CacheByModelKeyAccessController : TestControllerBase 9 | { 10 | #region Private 字段 11 | 12 | private readonly ICacheKeyAccessor _cacheKeyAccessor; 13 | 14 | private readonly ILogger _logger; 15 | 16 | #endregion Private 字段 17 | 18 | #region Public 构造函数 19 | 20 | public CacheByModelKeyAccessController(ILogger logger, ICacheKeyAccessor cacheKeyAccessor) 21 | { 22 | _logger = logger; 23 | _cacheKeyAccessor = cacheKeyAccessor; 24 | } 25 | 26 | #endregion Public 构造函数 27 | 28 | #region Public 方法 29 | 30 | [HttpPost] 31 | [CacheByModel(Duration, "input")] 32 | [ExecutingLock(ExecutingLockMode.ActionSingle)] 33 | public string Post(PageResultRequestDto input) 34 | { 35 | return _cacheKeyAccessor.Key!; 36 | } 37 | 38 | #endregion Public 方法 39 | } 40 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test.WebHost/Controllers/CacheByPathController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | using ResponseCaching.Test.WebHost.Models; 4 | using ResponseCaching.Test.WebHost.Test; 5 | 6 | namespace ResponseCaching.Test.WebHost.Controllers; 7 | 8 | public class CacheByPathController : TestControllerBase 9 | { 10 | #region Private 字段 11 | 12 | private readonly ILogger _logger; 13 | 14 | #endregion Private 字段 15 | 16 | #region Public 构造函数 17 | 18 | public CacheByPathController(ILogger logger) 19 | { 20 | _logger = logger; 21 | } 22 | 23 | #endregion Public 构造函数 24 | 25 | #region Public 方法 26 | 27 | [HttpGet] 28 | [CacheByPath(Duration)] 29 | [ExecutingLock(ExecutingLockMode.CacheKeySingle)] 30 | public IEnumerable Get() 31 | { 32 | _logger.LogInformation("{0}", "path-cache Get"); 33 | return TestDataGenerator.GetData(1, 5); 34 | } 35 | 36 | [HttpPost] 37 | [CacheByPath(Duration)] 38 | [ExecutingLock(ExecutingLockMode.CacheKeySingle)] 39 | public IEnumerable Post() 40 | { 41 | _logger.LogInformation("{0}", "path-cache Post"); 42 | return TestDataGenerator.GetData(1, 5); 43 | } 44 | 45 | #region Route 46 | 47 | [HttpGet] 48 | [CacheByPath(Duration)] 49 | [ExecutingLock(ExecutingLockMode.CacheKeySingle)] 50 | [Route("/R1/{Value1}")] 51 | public IEnumerable AbsoluteRoute1() 52 | { 53 | _logger.LogInformation("{0}", "path-cache AbsoluteRoute1"); 54 | return TestDataGenerator.GetData(1, 5); 55 | } 56 | 57 | [HttpGet] 58 | [CacheByPath(Duration)] 59 | [ExecutingLock(ExecutingLockMode.CacheKeySingle)] 60 | [Route("R1/{Value1}")] 61 | public IEnumerable RelativeRoute1() 62 | { 63 | _logger.LogInformation("{0}", "path-cache RelativeRoute1"); 64 | return TestDataGenerator.GetData(1, 5); 65 | } 66 | 67 | #endregion Route 68 | 69 | #endregion Public 方法 70 | } 71 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test.WebHost/Controllers/CacheByPathWithCustomInterceptorController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | using ResponseCaching.Test.WebHost.Models; 4 | using ResponseCaching.Test.WebHost.Test; 5 | 6 | namespace ResponseCaching.Test.WebHost.Controllers; 7 | 8 | public class CacheByPathWithCustomInterceptorController : TestControllerBase 9 | { 10 | #region Private 字段 11 | 12 | private readonly ILogger _logger; 13 | 14 | #endregion Private 字段 15 | 16 | #region Public 构造函数 17 | 18 | public CacheByPathWithCustomInterceptorController(ILogger logger) 19 | { 20 | _logger = logger; 21 | } 22 | 23 | #endregion Public 构造函数 24 | 25 | #region Public 方法 26 | 27 | [HttpGet] 28 | [HttpPost] 29 | [CacheByPath(Duration)] 30 | [ExecutingLock(ExecutingLockMode.ActionSingle)] 31 | [TestCachingProcessInterceptor] 32 | public IEnumerable Get() 33 | { 34 | return TestDataGenerator.GetData(1, 5); 35 | } 36 | 37 | #endregion Public 方法 38 | } 39 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test.WebHost/Controllers/CacheByQueryController.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | using ResponseCaching.Test.WebHost.Models; 6 | using ResponseCaching.Test.WebHost.Test; 7 | 8 | namespace ResponseCaching.Test.WebHost.Controllers; 9 | 10 | public class CacheByQueryController : TestControllerBase 11 | { 12 | #region Private 字段 13 | 14 | private readonly ILogger _logger; 15 | 16 | #endregion Private 字段 17 | 18 | #region Public 构造函数 19 | 20 | public CacheByQueryController(ILogger logger) 21 | { 22 | _logger = logger; 23 | } 24 | 25 | #endregion Public 构造函数 26 | 27 | #region Public 方法 28 | 29 | [HttpGet] 30 | [CacheByQuery(Duration, "page", "pageSize")] 31 | [ExecutingLock(ExecutingLockMode.CacheKeySingle)] 32 | public IEnumerable Get([Required] int page, [Required] int pageSize) 33 | { 34 | _logger.LogInformation("{0} - {1}", page, pageSize); 35 | return TestDataGenerator.GetData((page - 1) * pageSize, pageSize); 36 | } 37 | 38 | #endregion Public 方法 39 | } 40 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test.WebHost/Controllers/CacheByQueryWithAuthorizeController.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | using ResponseCaching.Test.WebHost.Models; 6 | using ResponseCaching.Test.WebHost.Test; 7 | 8 | namespace ResponseCaching.Test.WebHost.Controllers; 9 | 10 | public class CacheByQueryWithAuthorizeController : TestControllerBase 11 | { 12 | #region Private 字段 13 | 14 | private readonly ILogger _logger; 15 | 16 | #endregion Private 字段 17 | 18 | #region Public 构造函数 19 | 20 | public CacheByQueryWithAuthorizeController(ILogger logger) 21 | { 22 | _logger = logger; 23 | } 24 | 25 | #endregion Public 构造函数 26 | 27 | #region Public 方法 28 | 29 | [HttpGet] 30 | [AuthorizeMixed] 31 | [CacheByQuery(Duration, 32 | "page", "pageSize")] 33 | [ExecutingLock(ExecutingLockMode.CacheKeySingle)] 34 | public IEnumerable Get([Required] int page, [Required] int pageSize) 35 | { 36 | _logger.LogInformation("{0} - {1}", page, pageSize); 37 | return TestDataGenerator.GetData((page - 1) * pageSize, pageSize); 38 | } 39 | 40 | #endregion Public 方法 41 | } 42 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test.WebHost/Controllers/ExecuteLockTimeoutController.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | using Cuture.AspNetCore.ResponseCaching; 4 | 5 | using Microsoft.AspNetCore.Mvc; 6 | 7 | using ResponseCaching.Test.WebHost.Models; 8 | using ResponseCaching.Test.WebHost.Test; 9 | 10 | namespace ResponseCaching.Test.WebHost.Controllers; 11 | 12 | public class ExecuteLockTimeoutController : TestControllerBase 13 | { 14 | #region Private 字段 15 | 16 | private readonly ILogger _logger; 17 | 18 | #endregion Private 字段 19 | 20 | #region Public 构造函数 21 | 22 | public ExecuteLockTimeoutController(ILogger logger) 23 | { 24 | _logger = logger; 25 | } 26 | 27 | #endregion Public 构造函数 28 | 29 | #region Public 方法 30 | 31 | [HttpGet] 32 | [CacheByModel(Duration)] 33 | [ExecutingLock(ExecutingLockMode.ActionSingle, 500)] 34 | public async Task> ActionFilterAsync([Required][FromQuery] Input input) 35 | { 36 | _logger.LogInformation("Wait {0} - {1}", nameof(ActionFilterAsync), input.WaitMilliseconds); 37 | await Task.Delay(input.WaitMilliseconds); 38 | return TestDataGenerator.GetData(1, 5); 39 | } 40 | 41 | [HttpGet] 42 | [ResponseCaching(Duration, Mode = CacheMode.PathUniqueness)] 43 | [ExecutingLock(ExecutingLockMode.ActionSingle, 500)] 44 | public async Task> ResourceFilterAsync([Required][FromQuery] int waitMilliseconds) 45 | { 46 | _logger.LogInformation("Wait {0} - {1}", nameof(ResourceFilterAsync), waitMilliseconds); 47 | await Task.Delay(waitMilliseconds); 48 | return TestDataGenerator.GetData(1, 5); 49 | } 50 | 51 | #endregion Public 方法 52 | 53 | #region Public 类 54 | 55 | public class Input : ICacheKeyable 56 | { 57 | #region Public 属性 58 | 59 | public int WaitMilliseconds { get; set; } 60 | 61 | #endregion Public 属性 62 | 63 | #region Public 方法 64 | 65 | public string AsCacheKey() => string.Empty; 66 | 67 | #endregion Public 方法 68 | } 69 | 70 | #endregion Public 类 71 | } 72 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test.WebHost/Controllers/HotDataCacheController.cs: -------------------------------------------------------------------------------- 1 | using Cuture.AspNetCore.ResponseCaching.ResponseCaches; 2 | 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | namespace ResponseCaching.Test.WebHost.Controllers; 6 | 7 | public class HotDataCacheController : TestControllerBase 8 | { 9 | #region Public 方法 10 | 11 | [HttpGet] 12 | [HotDataCache(50, HotDataCachePolicy.LRU)] 13 | [ResponseCaching(3, 14 | Mode = CacheMode.FullPathAndQuery, 15 | StoreLocation = CacheStoreLocation.Distributed)] 16 | [ExecutingLock(ExecutingLockMode.CacheKeySingle)] 17 | public string Get(string input) 18 | { 19 | return "Inputed:" + input; 20 | } 21 | 22 | #endregion Public 方法 23 | } 24 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test.WebHost/Controllers/LoginController.cs: -------------------------------------------------------------------------------- 1 | using System.IdentityModel.Tokens.Jwt; 2 | using System.Security.Claims; 3 | using System.Text; 4 | 5 | using IdentityModel; 6 | 7 | using Microsoft.AspNetCore.Authentication; 8 | using Microsoft.AspNetCore.Authentication.Cookies; 9 | using Microsoft.AspNetCore.Mvc; 10 | using Microsoft.IdentityModel.Tokens; 11 | 12 | namespace ResponseCaching.Test.WebHost.Controllers; 13 | 14 | [ApiController] 15 | [Route("[controller]/[action]")] 16 | public class LoginController : ControllerBase 17 | { 18 | #region Private 字段 19 | 20 | private readonly ILogger _logger; 21 | 22 | #endregion Private 字段 23 | 24 | #region Public 构造函数 25 | 26 | public LoginController(ILogger logger) 27 | { 28 | _logger = logger; 29 | } 30 | 31 | #endregion Public 构造函数 32 | 33 | #region Public 方法 34 | 35 | [HttpGet] 36 | public async Task CookieAsync([FromQuery] string uid) 37 | { 38 | var claimsIdentity = new ClaimsIdentity(new[] 39 | { 40 | new Claim(JwtClaimTypes.Id,uid), 41 | new Claim(JwtClaimTypes.SessionId,new string(uid.Reverse().ToArray())) 42 | }, CookieAuthenticationDefaults.AuthenticationScheme); 43 | var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); 44 | await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, claimsPrincipal); 45 | } 46 | 47 | [HttpGet] 48 | public string Jwt([FromQuery] string uid) 49 | { 50 | var claims = new[] 51 | { 52 | new Claim(JwtClaimTypes.Id,uid), 53 | new Claim(JwtClaimTypes.SessionId,new string(uid.Reverse().ToArray())) 54 | }; 55 | var key = new SymmetricSecurityKey(Encoding.ASCII.GetBytes("123456789123456789_123456789123456789")); 56 | var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); 57 | var jwtToken = new JwtSecurityToken("Issuer", "Audience", claims, expires: DateTime.Now.AddMinutes(600), signingCredentials: credentials); 58 | 59 | var token = new JwtSecurityTokenHandler().WriteToken(jwtToken); 60 | 61 | return token; 62 | } 63 | 64 | #endregion Public 方法 65 | } 66 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test.WebHost/Controllers/MaxCacheableResponseLengthController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | using ResponseCaching.Test.WebHost.Models; 4 | using ResponseCaching.Test.WebHost.Test; 5 | 6 | namespace ResponseCaching.Test.WebHost.Controllers; 7 | 8 | public class MaxCacheableResponseLengthController : TestControllerBase 9 | { 10 | #region Private 字段 11 | 12 | private readonly ILogger _logger; 13 | 14 | #endregion Private 字段 15 | 16 | #region Public 构造函数 17 | 18 | public MaxCacheableResponseLengthController(ILogger logger) 19 | { 20 | _logger = logger; 21 | } 22 | 23 | #endregion Public 构造函数 24 | 25 | #region Public 方法 26 | 27 | [HttpGet] 28 | [ActionName("by-action-filter")] 29 | [CacheByModel(Duration, "count", MaxCacheableResponseLength = 256)] 30 | [ExecutingLock(ExecutingLockMode.CacheKeySingle)] 31 | public IEnumerable ByActionFilter(int count) 32 | { 33 | _logger.LogInformation(count.ToString()); 34 | return TestDataGenerator.GetData(1, count); 35 | } 36 | 37 | [HttpGet] 38 | [ActionName("by-resource-filter")] 39 | [CacheByQuery(Duration, "count", MaxCacheableResponseLength = 256)] 40 | [ExecutingLock(ExecutingLockMode.CacheKeySingle)] 41 | public IEnumerable ByResourceFilter(int count) 42 | { 43 | _logger.LogInformation(count.ToString()); 44 | return TestDataGenerator.GetData(1, count); 45 | } 46 | 47 | #endregion Public 方法 48 | } 49 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test.WebHost/Controllers/TestControllerBase.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace ResponseCaching.Test.WebHost.Controllers; 4 | 5 | [ApiController] 6 | [Route("[controller]/[action]")] 7 | public abstract class TestControllerBase : ControllerBase 8 | { 9 | #region Protected 字段 10 | 11 | protected const int Duration = 10; 12 | 13 | #endregion Protected 字段 14 | } 15 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test.WebHost/Controllers/WeatherForecastController.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | using ResponseCaching.Test.WebHost.Models; 6 | using ResponseCaching.Test.WebHost.Test; 7 | 8 | namespace ResponseCaching.Test.WebHost.Controllers; 9 | 10 | public class WeatherForecastController : TestControllerBase 11 | { 12 | #region Private 字段 13 | 14 | private readonly ILogger _logger; 15 | 16 | #endregion Private 字段 17 | 18 | #region Public 构造函数 19 | 20 | public WeatherForecastController(ILogger logger) 21 | { 22 | _logger = logger; 23 | } 24 | 25 | #endregion Public 构造函数 26 | 27 | #region Public 方法 28 | 29 | [HttpGet] 30 | public IEnumerable Get([Required] int page, [Required] int pageSize) 31 | { 32 | _logger.LogInformation("{0} - {1}", page, pageSize); 33 | return TestDataGenerator.GetData((page - 1) * pageSize, pageSize); 34 | } 35 | 36 | #endregion Public 方法 37 | } 38 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test.WebHost/Dtos/PageResultRequestDto.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | using Cuture.AspNetCore.ResponseCaching; 4 | 5 | namespace ResponseCaching.Test.WebHost.Dtos; 6 | 7 | public class PageResultRequestDto : ICacheKeyable 8 | { 9 | [Range(1, 200)] 10 | public int Page { get; set; } 11 | 12 | [Range(2, 100)] 13 | public int PageSize { get; set; } 14 | 15 | public string AsCacheKey() => $"{Page}_{PageSize}"; 16 | } 17 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test.WebHost/Models/WeatherForecast.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | namespace ResponseCaching.Test.WebHost.Models; 4 | 5 | #pragma warning disable CS0660 // 类型定义运算符 == 或运算符 !=,但不重写 Object.Equals(object o) 6 | public class WeatherForecast : IEquatable 7 | #pragma warning restore CS0660 // 类型定义运算符 == 或运算符 !=,但不重写 Object.Equals(object o) 8 | { 9 | public DateTime Date { get; set; } 10 | 11 | public int TemperatureC { get; set; } 12 | 13 | public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); 14 | 15 | public string Summary { get; set; } 16 | 17 | public bool Equals([AllowNull] WeatherForecast other) 18 | { 19 | return other != null 20 | & other.Date == Date 21 | & other.Summary == Summary 22 | & other.TemperatureC == TemperatureC; 23 | } 24 | 25 | public override int GetHashCode() 26 | { 27 | return HashCode.Combine(Date, TemperatureC, Summary); 28 | } 29 | 30 | public static bool operator ==(WeatherForecast left, WeatherForecast right) 31 | { 32 | return EqualityComparer.Default.Equals(left, right); 33 | } 34 | 35 | public static bool operator !=(WeatherForecast left, WeatherForecast right) 36 | { 37 | return !(left == right); 38 | } 39 | 40 | public override string ToString() 41 | { 42 | return $"{Date},{Summary},{TemperatureC}"; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test.WebHost/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "ResponseCaching.Test.WebHost": { 5 | "commandName": "Project", 6 | "launchBrowser": false, 7 | "launchUrl": "weatherforecast", 8 | "applicationUrl": "http://0.0.0.0:5000", 9 | "environmentVariables": { 10 | "ASPNETCORE_ENVIRONMENT": "Development" 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test.WebHost/ResponseCaching.Test.WebHost.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | false 5 | 6 | false 7 | 8 | false 9 | 59ec6951-9d94-4c2c-ba9c-ead1c523ce5d 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test.WebHost/Test/TestCachingProcessInterceptor.cs: -------------------------------------------------------------------------------- 1 | using Cuture.AspNetCore.ResponseCaching.Interceptors; 2 | using Cuture.AspNetCore.ResponseCaching.ResponseCaches; 3 | 4 | using Microsoft.AspNetCore.Mvc; 5 | 6 | namespace ResponseCaching.Test.WebHost.Test; 7 | 8 | public class TestCachingProcessInterceptor : Attribute, ICacheStoringInterceptor 9 | { 10 | #region Public 方法 11 | 12 | public Task OnCacheStoringAsync(ActionContext actionContext, string key, ResponseCacheEntry entry, OnCacheStoringDelegate next) 13 | { 14 | return Task.FromResult(null); 15 | } 16 | 17 | #endregion Public 方法 18 | } 19 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test.WebHost/Test/TestCustomCacheKeyGenerator.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using Cuture.AspNetCore.ResponseCaching.CacheKey.Generators; 3 | 4 | using Microsoft.AspNetCore.Mvc.Filters; 5 | 6 | namespace ResponseCaching.Test.WebHost.Test; 7 | 8 | public class TestCustomCacheKeyGenerator : ICacheKeyGenerator 9 | { 10 | public ValueTask GenerateKeyAsync(FilterContext filterContext) 11 | { 12 | var description = filterContext.ActionDescriptor.EndpointMetadata.First(m => m is DescriptionAttribute) as DescriptionAttribute; 13 | return new ValueTask(description.Description); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test.WebHost/Test/TestCustomModelKeyParser.cs: -------------------------------------------------------------------------------- 1 | using Cuture.AspNetCore.ResponseCaching; 2 | 3 | using ResponseCaching.Test.WebHost.Dtos; 4 | 5 | namespace ResponseCaching.Test.WebHost.Test; 6 | 7 | public class TestCustomModelKeyParser : IModelKeyParser 8 | { 9 | #region Public 方法 10 | 11 | public string? Parse(in T? model) 12 | { 13 | if (model is PageResultRequestDto requestDto) 14 | { 15 | Console.WriteLine($"{nameof(TestCustomModelKeyParser)} for PageResultRequestDto - {requestDto.AsCacheKey()}"); 16 | return "constant-key"; 17 | } 18 | else 19 | { 20 | Console.WriteLine($"{nameof(TestCustomModelKeyParser)} for {model}"); 21 | } 22 | return null; 23 | } 24 | 25 | #endregion Public 方法 26 | } 27 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test.WebHost/Test/TestDataGenerator.cs: -------------------------------------------------------------------------------- 1 | using ResponseCaching.Test.WebHost.Models; 2 | 3 | namespace ResponseCaching.Test.WebHost.Test; 4 | 5 | public static class TestDataGenerator 6 | { 7 | public const int Count = 25; 8 | 9 | private static readonly string[] s_summaries = new[] 10 | { 11 | "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" 12 | }; 13 | 14 | public static IEnumerable GetData(int count) 15 | { 16 | return GetData(0, count); 17 | } 18 | 19 | public static IEnumerable GetData(int skip = 0, int count = Count) 20 | { 21 | var random = SharedRandom.Shared; 22 | return Enumerable.Range(skip + 1, count).Select(index => new WeatherForecast 23 | { 24 | Date = DateTime.Now.AddDays(index), 25 | TemperatureC = random.Next(-20, 55), 26 | Summary = s_summaries[random.Next(s_summaries.Length)] 27 | }).ToArray(); 28 | } 29 | 30 | private static class SharedRandom 31 | { 32 | private static readonly ThreadLocal s_random = new(() => new(), false); 33 | 34 | public static Random Shared => s_random.Value; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test.WebHost/TestWebHost.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.TestHost; 2 | 3 | namespace ResponseCaching.Test.WebHost; 4 | 5 | public class TestWebHost 6 | { 7 | public static bool IsTest { get; set; } = false; 8 | 9 | public static void Main(string[] args) 10 | { 11 | CreateHostBuilder(args).Build().Run(); 12 | } 13 | 14 | public static IHostBuilder CreateHostBuilder(string[] args) => 15 | Host.CreateDefaultBuilder(args) 16 | .ConfigureAppConfiguration(configure => 17 | { 18 | configure.AddUserSecrets(); 19 | }) 20 | .ConfigureLogging(builder => 21 | { 22 | builder.ClearProviders(); 23 | builder.AddConsole(options => 24 | { 25 | options.TimestampFormat = "yyyy-MM-dd HH:mm:ss.fff "; 26 | }); 27 | }) 28 | .ConfigureWebHostDefaults(webBuilder => 29 | { 30 | if (IsTest) 31 | { 32 | webBuilder.UseTestServer(); 33 | } 34 | webBuilder.UseStartup(); 35 | }); 36 | } 37 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test.WebHost/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | //日志太多会影响执行时间,测试无法通过 5 | "Default": "Warning" 6 | } 7 | }, 8 | "AllowedHosts": "*", 9 | //"ResponseCache_Test_Redis": "127.0.0.1:6379,allowAdmin=true", 10 | "Caching": { 11 | "ResponseCaching": { 12 | "Enable": true, 13 | "CacheKeyPrefix": "Test_Cache_Key", 14 | "DefaultCacheStoreLocation": "Memory" 15 | //"DefaultCacheStoreLocation": "Distributed" 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /test/ResponseCaching.Test/Base/AuthenticationRequiredTestBase.cs: -------------------------------------------------------------------------------- 1 | using Cuture.Http; 2 | using Cuture.Http.Util; 3 | 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | 6 | namespace ResponseCaching.Test.Base; 7 | 8 | [TestClass] 9 | public abstract class AuthenticationRequiredTestBase : WebServerHostedTestBase 10 | { 11 | protected abstract Task LoginAsync(string account); 12 | } 13 | 14 | [TestClass] 15 | public abstract class JwtAuthenticationRequiredTestBase : AuthenticationRequiredTestBase 16 | { 17 | protected override async Task LoginAsync(string account) 18 | { 19 | var token = await $"{BaseUrl}/login/jwt?uid={account}".CreateHttpRequest().GetAsStringAsync(); 20 | 21 | return token; 22 | } 23 | } 24 | 25 | [TestClass] 26 | public abstract class CookieAuthenticationRequiredTestBase : AuthenticationRequiredTestBase 27 | { 28 | protected override async Task LoginAsync(string account) 29 | { 30 | var result = await $"{BaseUrl}/login/cookie?uid={account}".CreateHttpRequest().TryGetAsStringAsync(); 31 | 32 | return CookieUtility.Clean(result.ResponseMessage.GetCookie()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test/Base/TestCutureHttpMessageInvokerPool.cs: -------------------------------------------------------------------------------- 1 | using Cuture.Http; 2 | 3 | namespace ResponseCaching.Test.Base; 4 | 5 | internal class TestCutureHttpMessageInvokerPool : IHttpMessageInvokerPool 6 | { 7 | #region Private 字段 8 | 9 | private readonly HttpClient _testHttpClient; 10 | 11 | private readonly HttpMessageInvokerOwner _testHttpClientOwner; 12 | 13 | #endregion Private 字段 14 | 15 | #region Public 构造函数 16 | 17 | public TestCutureHttpMessageInvokerPool(HttpClient testHttpClient) 18 | { 19 | _testHttpClient = testHttpClient ?? throw new ArgumentNullException(nameof(testHttpClient)); 20 | _testHttpClientOwner = new(_testHttpClient); 21 | } 22 | 23 | #endregion Public 构造函数 24 | 25 | #region Public 方法 26 | 27 | public void Dispose() 28 | { 29 | } 30 | 31 | public IOwner Rent(IHttpRequest request) => _testHttpClientOwner; 32 | 33 | #endregion Public 方法 34 | 35 | #region Private 类 36 | 37 | private record class HttpMessageInvokerOwner(HttpMessageInvoker Value) : IOwner 38 | { 39 | #region Public 方法 40 | 41 | public void Dispose() { } 42 | 43 | #endregion Public 方法 44 | } 45 | 46 | #endregion Private 类 47 | } 48 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test/HotDataCacheTests/CountDistributedResponseCache.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Concurrent; 2 | 3 | using Cuture.AspNetCore.ResponseCaching.ResponseCaches; 4 | 5 | namespace ResponseCaching.Test.RequestTests; 6 | 7 | public class CountDistributedResponseCache : IDistributedResponseCache 8 | { 9 | #region Private 字段 10 | 11 | private readonly ConcurrentDictionary _caches = new(); 12 | 13 | private readonly Dictionary _count = new(); 14 | 15 | #endregion Private 字段 16 | 17 | #region Public 方法 18 | 19 | public int Count(string key) 20 | { 21 | if (string.IsNullOrEmpty(key)) 22 | { 23 | lock (_count) 24 | { 25 | return _count.Values.Sum(); 26 | } 27 | } 28 | lock (_count) 29 | { 30 | if (_count.ContainsKey(key)) 31 | { 32 | return _count[key]; 33 | } 34 | } 35 | return 0; 36 | } 37 | 38 | public Task GetAsync(string key, CancellationToken cancellationToken) 39 | { 40 | if (_caches.TryGetValue(key, out var cacheEntry)) 41 | { 42 | if (!cacheEntry.IsExpired()) 43 | { 44 | lock (_count) 45 | { 46 | if (_count.ContainsKey(key)) 47 | { 48 | _count[key] += 1; 49 | } 50 | else 51 | { 52 | _count[key] = 1; 53 | } 54 | } 55 | return Task.FromResult(cacheEntry); 56 | } 57 | } 58 | return Task.FromResult((ResponseCacheEntry)null); 59 | } 60 | 61 | public string[] GetKeys() => _count.Keys.ToArray(); 62 | 63 | public Task RemoveAsync(string key, CancellationToken cancellationToken = default) 64 | { 65 | return Task.FromResult(_caches.Remove(key, out _)); 66 | } 67 | 68 | public Task SetAsync(string key, ResponseCacheEntry entry, CancellationToken cancellationToken) 69 | { 70 | _caches.AddOrUpdate(key, entry, (_, _) => entry); 71 | return Task.CompletedTask; 72 | } 73 | 74 | #endregion Public 方法 75 | } 76 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test/RequestTests/CacheByCustomModelKeyParserTest.cs: -------------------------------------------------------------------------------- 1 | using Cuture.Http; 2 | 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | 5 | using ResponseCaching.Test.WebHost.Dtos; 6 | using ResponseCaching.Test.WebHost.Models; 7 | 8 | namespace ResponseCaching.Test.RequestTests; 9 | 10 | [TestClass] 11 | public class CacheByCustomModelKeyParserTest : RequestTestBase 12 | { 13 | #region Public 方法 14 | 15 | [TestMethod] 16 | public async Task Should_Equals_With_Multi_Different_Request() 17 | { 18 | var funcs = new Func>>[] { 19 | () => $"{BaseUrl}/CacheByCustomModelKeyParser/Post".CreateHttpRequest().UsePost().WithJsonContent(new PageResultRequestDto(){ Page = 1,PageSize = 5 }).TryGetAsObjectAsync(), 20 | () => $"{BaseUrl}/CacheByCustomModelKeyParser/Post".CreateHttpRequest().UsePost().WithJsonContent(new PageResultRequestDto(){ Page = 1,PageSize = 6 }).TryGetAsObjectAsync(), 21 | () => $"{BaseUrl}/CacheByCustomModelKeyParser/Post".CreateHttpRequest().UsePost().WithJsonContent(new PageResultRequestDto(){ Page = 2,PageSize = 4 }).TryGetAsObjectAsync(), 22 | () => $"{BaseUrl}/CacheByCustomModelKeyParser/Post".CreateHttpRequest().UsePost().WithJsonContent(new PageResultRequestDto(){ Page = 2,PageSize = 6 }).TryGetAsObjectAsync(), 23 | () => $"{BaseUrl}/CacheByCustomModelKeyParser/Post".CreateHttpRequest().UsePost().WithJsonContent(new PageResultRequestDto(){ Page = 3,PageSize = 3 }).TryGetAsObjectAsync(), 24 | }; 25 | await ExecuteAsync(funcs, true, true, 3); 26 | } 27 | 28 | #endregion Public 方法 29 | } 30 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test/RequestTests/CacheByCustomTest.cs: -------------------------------------------------------------------------------- 1 | using Cuture.Http; 2 | 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | 5 | using ResponseCaching.Test.WebHost.Models; 6 | 7 | namespace ResponseCaching.Test.RequestTests; 8 | 9 | [TestClass] 10 | public class CacheByCustomTest : RequestTestBase 11 | { 12 | #region Public 方法 13 | 14 | [TestMethod] 15 | public async Task Should_Equals_With_Different_QueryKey() 16 | { 17 | var funcs = new Func>>[] { 18 | () => $"{BaseUrl}/CacheByCustomCacheKeyGenerator/Get?page=1&pageSize=5".CreateHttpRequest().TryGetAsObjectAsync(), 19 | () => $"{BaseUrl}/CacheByCustomCacheKeyGenerator/Get?page=1&pageSize=6".CreateHttpRequest().TryGetAsObjectAsync(), 20 | () => $"{BaseUrl}/CacheByCustomCacheKeyGenerator/Get?page=2&pageSize=4".CreateHttpRequest().TryGetAsObjectAsync(), 21 | () => $"{BaseUrl}/CacheByCustomCacheKeyGenerator/Get?page=2&pageSize=6".CreateHttpRequest().TryGetAsObjectAsync(), 22 | () => $"{BaseUrl}/CacheByCustomCacheKeyGenerator/Get?page=3&pageSize=3".CreateHttpRequest().TryGetAsObjectAsync(), 23 | }; 24 | await ExecuteAsync(funcs, true, true, 3); 25 | } 26 | 27 | #endregion Public 方法 28 | } 29 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test/RequestTests/CacheByFormTest.cs: -------------------------------------------------------------------------------- 1 | using Cuture.Http; 2 | 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | 5 | using ResponseCaching.Test.WebHost.Models; 6 | 7 | namespace ResponseCaching.Test.RequestTests; 8 | 9 | [TestClass] 10 | public class CacheByFormTest : RequestTestBase 11 | { 12 | #region Public 方法 13 | 14 | [TestMethod] 15 | public async Task Should_Different_With_Different_FormData() 16 | { 17 | var funcs = new Func>>[] { 18 | () => $"{BaseUrl}/CacheByForm/Post".CreateHttpRequest().UsePost().WithFormContent("page=1&pageSize=5").TryGetAsObjectAsync(), 19 | () => $"{BaseUrl}/CacheByForm/Post".CreateHttpRequest().UsePost().WithFormContent("page=1&pageSize=6").TryGetAsObjectAsync(), 20 | () => $"{BaseUrl}/CacheByForm/Post".CreateHttpRequest().UsePost().WithFormContent("page=2&pageSize=4").TryGetAsObjectAsync(), 21 | () => $"{BaseUrl}/CacheByForm/Post".CreateHttpRequest().UsePost().WithFormContent("page=2&pageSize=6").TryGetAsObjectAsync(), 22 | () => $"{BaseUrl}/CacheByForm/Post".CreateHttpRequest().UsePost().WithFormContent("page=3&pageSize=3").TryGetAsObjectAsync(), 23 | }; 24 | await ExecuteAsync(funcs, true, false, 3); 25 | } 26 | 27 | #endregion Public 方法 28 | } 29 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test/RequestTests/CacheByFullQueryTest.cs: -------------------------------------------------------------------------------- 1 | using Cuture.Http; 2 | 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | 5 | using ResponseCaching.Test.WebHost.Models; 6 | 7 | namespace ResponseCaching.Test.RequestTests; 8 | 9 | [TestClass] 10 | public class CacheByFullQueryTest : RequestTestBase 11 | { 12 | #region Public 方法 13 | 14 | [TestMethod] 15 | public async Task Should_Different_With_Different_QueryKeys() 16 | { 17 | var funcs = new Func>>[] { 18 | () => $"{BaseUrl}/CacheByFullQuery/Get?page=1&pageSize=3".CreateHttpRequest().TryGetAsObjectAsync(), 19 | () => $"{BaseUrl}/CacheByFullQuery/Get?page=1&pageSize=3&t".CreateHttpRequest().TryGetAsObjectAsync(), 20 | () => $"{BaseUrl}/CacheByFullQuery/Get?page=1&pageSize=3&=t".CreateHttpRequest().TryGetAsObjectAsync(), 21 | () => $"{BaseUrl}/CacheByFullQuery/Get?page=1&pageSize=3&=0".CreateHttpRequest().TryGetAsObjectAsync(), 22 | () => $"{BaseUrl}/CacheByFullQuery/Get?page=1&pageSize=3&t=0".CreateHttpRequest().TryGetAsObjectAsync(), 23 | }; 24 | await ExecuteAsync(funcs, true, false, 3); 25 | } 26 | 27 | [TestMethod] 28 | public async Task Should_Equals_With_Different_QueryKey_Order() 29 | { 30 | var funcs = new Func>>[] { 31 | () => $"{BaseUrl}/CacheByFullQuery/Get?page=1&pageSize=3&t=1&t3=1&t2=1&t800".CreateHttpRequest().TryGetAsObjectAsync(), 32 | () => $"{BaseUrl}/CacheByFullQuery/Get?t=1&t3=1&t2=1&t800&page=1&pageSize=3".CreateHttpRequest().TryGetAsObjectAsync(), 33 | () => $"{BaseUrl}/CacheByFullQuery/Get?t=1&page=1&t3=1&t800&pageSize=3&t2=1".CreateHttpRequest().TryGetAsObjectAsync(), 34 | () => $"{BaseUrl}/CacheByFullQuery/Get?t3=1&page=1&t=1&t2=1&t800&pageSize=3".CreateHttpRequest().TryGetAsObjectAsync(), 35 | () => $"{BaseUrl}/CacheByFullQuery/Get?t2=1&page=1&pageSize=3&t=1&t3=1&t800".CreateHttpRequest().TryGetAsObjectAsync(), 36 | }; 37 | await ExecuteAsync(funcs, true, true, 3); 38 | } 39 | 40 | #endregion Public 方法 41 | } 42 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test/RequestTests/CacheByFullUrlTest.cs: -------------------------------------------------------------------------------- 1 | using Cuture.Http; 2 | 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | 5 | using ResponseCaching.Test.WebHost.Models; 6 | 7 | namespace ResponseCaching.Test.RequestTests; 8 | 9 | [TestClass] 10 | public class CacheByFullUrlTest : RequestTestBase 11 | { 12 | #region Private 字段 13 | 14 | private readonly long _t = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); 15 | 16 | #endregion Private 字段 17 | 18 | #region Public 方法 19 | 20 | [TestMethod] 21 | public async Task Should_Different_With_Different_Url() 22 | { 23 | var funcs = new Func>>[] { 24 | () => $"{BaseUrl}/CacheByFullUrl/Get?page=1&pageSize=5".CreateHttpRequest().TryGetAsObjectAsync(), 25 | () => $"{BaseUrl}/CacheByFullUrl/Get?page=2&pageSize=5".CreateHttpRequest().TryGetAsObjectAsync(), 26 | () => $"{BaseUrl}/CacheByFullUrl/Get?page=3&pageSize=5".CreateHttpRequest().TryGetAsObjectAsync(), 27 | () => $"{BaseUrl}/CacheByFullUrl/Get?page=1&pageSize=5&_t=1".CreateHttpRequest().TryGetAsObjectAsync(), 28 | () => $"{BaseUrl}/CacheByFullUrl/Get?page=1&pageSize=5&_t={_t}".CreateHttpRequest().TryGetAsObjectAsync(), 29 | () => $"{BaseUrl}/CacheByFullUrl/Get?page=1&pageSize=5&_t=1&_t1=0".CreateHttpRequest().TryGetAsObjectAsync(), 30 | }; 31 | 32 | await ExecuteAsync(funcs, true, false, 4); 33 | } 34 | 35 | #endregion Public 方法 36 | } 37 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test/RequestTests/CacheByHeaderTest.cs: -------------------------------------------------------------------------------- 1 | using Cuture.Http; 2 | 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | 5 | using ResponseCaching.Test.WebHost.Models; 6 | 7 | namespace ResponseCaching.Test.RequestTests; 8 | 9 | [TestClass] 10 | public class CacheByHeaderTest : RequestTestBase 11 | { 12 | #region Public 方法 13 | 14 | [TestMethod] 15 | public async Task Should_Different_With_Different_Headers() 16 | { 17 | var funcs = new Func>>[] { 18 | () => $"{BaseUrl}/CacheByHeader/Get".CreateHttpRequest().AddHeader("page","1").AddHeader("pageSize","5").TryGetAsObjectAsync(), 19 | () => $"{BaseUrl}/CacheByHeader/Get".CreateHttpRequest().AddHeader("page","1").AddHeader("pageSize","6").TryGetAsObjectAsync(), 20 | () => $"{BaseUrl}/CacheByHeader/Get".CreateHttpRequest().AddHeader("page","2").AddHeader("pageSize","4").TryGetAsObjectAsync(), 21 | () => $"{BaseUrl}/CacheByHeader/Get".CreateHttpRequest().AddHeader("page","2").AddHeader("pageSize","6").TryGetAsObjectAsync(), 22 | () => $"{BaseUrl}/CacheByHeader/Get".CreateHttpRequest().AddHeader("page","3").AddHeader("pageSize","3").TryGetAsObjectAsync(), 23 | }; 24 | 25 | await ExecuteAsync(funcs, true, false, 3); 26 | } 27 | 28 | #endregion Public 方法 29 | } 30 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test/RequestTests/CacheByModelTest.cs: -------------------------------------------------------------------------------- 1 | using Cuture.Http; 2 | 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | 5 | using ResponseCaching.Test.WebHost.Dtos; 6 | using ResponseCaching.Test.WebHost.Models; 7 | 8 | namespace ResponseCaching.Test.RequestTests; 9 | 10 | [TestClass] 11 | public class CacheByModelTest : RequestTestBase 12 | { 13 | #region Public 方法 14 | 15 | [TestMethod] 16 | public async Task Should_Different_With_Different_Models() 17 | { 18 | var funcs = new Func>>[] { 19 | () => $"{BaseUrl}/CacheByModel/Post".CreateHttpRequest().UsePost().WithJsonContent(new PageResultRequestDto(){ Page = 1,PageSize = 5 }).TryGetAsObjectAsync(), 20 | () => $"{BaseUrl}/CacheByModel/Post".CreateHttpRequest().UsePost().WithJsonContent(new PageResultRequestDto(){ Page = 1,PageSize = 6 }).TryGetAsObjectAsync(), 21 | () => $"{BaseUrl}/CacheByModel/Post".CreateHttpRequest().UsePost().WithJsonContent(new PageResultRequestDto(){ Page = 2,PageSize = 4 }).TryGetAsObjectAsync(), 22 | () => $"{BaseUrl}/CacheByModel/Post".CreateHttpRequest().UsePost().WithJsonContent(new PageResultRequestDto(){ Page = 2,PageSize = 6 }).TryGetAsObjectAsync(), 23 | () => $"{BaseUrl}/CacheByModel/Post".CreateHttpRequest().UsePost().WithJsonContent(new PageResultRequestDto(){ Page = 3,PageSize = 3 }).TryGetAsObjectAsync(), 24 | }; 25 | 26 | await ExecuteAsync(funcs, true, false, 3); 27 | } 28 | 29 | #endregion Public 方法 30 | } 31 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test/RequestTests/CacheByPathTest.cs: -------------------------------------------------------------------------------- 1 | using Cuture.Http; 2 | 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | 5 | using ResponseCaching.Test.WebHost.Models; 6 | 7 | namespace ResponseCaching.Test.RequestTests; 8 | 9 | [TestClass] 10 | public class CacheByPathTest : RequestTestBase 11 | { 12 | #region Public 方法 13 | 14 | [TestMethod] 15 | public async Task Should_Equals_With_Multi_Request() 16 | { 17 | var funcs = new Func>>[] { 18 | () => $"{BaseUrl}/CacheByPath/Get?page=1&pageSize=5".CreateHttpRequest().TryGetAsObjectAsync(), 19 | () => $"{BaseUrl}/CacheByPath/Get?page=1".CreateHttpRequest().TryGetAsObjectAsync(), 20 | () => $"{BaseUrl}/CacheByPath/Get".CreateHttpRequest().TryGetAsObjectAsync(), 21 | () => $"{BaseUrl}/CacheByPath/Post?page=1&pageSize=5".CreateHttpRequest().UsePost().TryGetAsObjectAsync(), 22 | () => $"{BaseUrl}/CacheByPath/Post?page=1".CreateHttpRequest().UsePost().TryGetAsObjectAsync(), 23 | () => $"{BaseUrl}/CacheByPath/Post".CreateHttpRequest().UsePost().TryGetAsObjectAsync(), 24 | }; 25 | 26 | await ExecuteAsync(funcs, false, false, 3); 27 | } 28 | 29 | [TestMethod] 30 | public async Task Should_Different_With_Different_Path() 31 | { 32 | var funcs = new Func>>[] { 33 | () => $"{BaseUrl}/CacheByPath/RelativeRoute1/R1/1".CreateHttpRequest().TryGetAsObjectAsync(), 34 | () => $"{BaseUrl}/CacheByPath/RelativeRoute1/R1/2".CreateHttpRequest().TryGetAsObjectAsync(), 35 | () => $"{BaseUrl}/CacheByPath/RelativeRoute1/R1/3".CreateHttpRequest().TryGetAsObjectAsync(), 36 | () => $"{BaseUrl}/R1/1".CreateHttpRequest().TryGetAsObjectAsync(), 37 | () => $"{BaseUrl}/R1/2".CreateHttpRequest().TryGetAsObjectAsync(), 38 | () => $"{BaseUrl}/R1/3".CreateHttpRequest().TryGetAsObjectAsync(), 39 | }; 40 | 41 | await ExecuteAsync(funcs, true, false, 3); 42 | } 43 | 44 | #endregion Public 方法 45 | } 46 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test/RequestTests/CacheByQueryTest.cs: -------------------------------------------------------------------------------- 1 | using Cuture.Http; 2 | 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | 5 | using ResponseCaching.Test.WebHost.Models; 6 | 7 | namespace ResponseCaching.Test.RequestTests; 8 | 9 | [TestClass] 10 | public class CacheByQueryTest : RequestTestBase 11 | { 12 | #region Public 方法 13 | 14 | [TestMethod] 15 | public async Task Should_Different_With_Different_Query() 16 | { 17 | var funcs = new Func>>[] { 18 | () => $"{BaseUrl}/CacheByQuery/Get?page=1&pageSize=5".CreateHttpRequest().TryGetAsObjectAsync(), 19 | () => $"{BaseUrl}/CacheByQuery/Get?page=1&pageSize=6".CreateHttpRequest().TryGetAsObjectAsync(), 20 | () => $"{BaseUrl}/CacheByQuery/Get?page=2&pageSize=4".CreateHttpRequest().TryGetAsObjectAsync(), 21 | () => $"{BaseUrl}/CacheByQuery/Get?page=2&pageSize=6".CreateHttpRequest().TryGetAsObjectAsync(), 22 | () => $"{BaseUrl}/CacheByQuery/Get?page=3&pageSize=3".CreateHttpRequest().TryGetAsObjectAsync(), 23 | }; 24 | await ExecuteAsync(funcs, true, false, 3); 25 | } 26 | 27 | #endregion Public 方法 28 | } 29 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test/RequestTests/CacheByQueryWithCookieAuthorizeTest.cs: -------------------------------------------------------------------------------- 1 | using Cuture.Http; 2 | using Cuture.Http.Util; 3 | 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | 6 | using ResponseCaching.Test.WebHost.Models; 7 | 8 | namespace ResponseCaching.Test.RequestTests; 9 | 10 | [TestClass] 11 | public class CacheByQueryWithCookieAuthorizeTest : RequestTestBase 12 | { 13 | #region Private 字段 14 | 15 | private string[] _cookies = null; 16 | 17 | #endregion Private 字段 18 | 19 | #region Public 方法 20 | 21 | [TestInitialize] 22 | public override async Task InitAsync() 23 | { 24 | await base.InitAsync(); 25 | 26 | var cookies = new List(); 27 | for (int i = 1; i < 6; i++) 28 | { 29 | var result = await $"{BaseUrl}/login/cookie?uid=testuser{i}".CreateHttpRequest().TryGetAsStringAsync(); 30 | var cookie = CookieUtility.Clean(result.ResponseMessage.GetCookie()); 31 | 32 | cookies.Add(cookie); 33 | } 34 | _cookies = cookies.ToArray(); 35 | } 36 | 37 | [TestMethod] 38 | public async Task Should_Different_With_Different_Cookie() 39 | { 40 | var funcs = new Func>>[] { 41 | () => $"{BaseUrl}/CacheByQueryWithAuthorize/Get?page=1&pageSize=5".CreateHttpRequest().UseCookie(_cookies[0]).TryGetAsObjectAsync(), 42 | () => $"{BaseUrl}/CacheByQueryWithAuthorize/Get?page=1&pageSize=6".CreateHttpRequest().UseCookie(_cookies[1]).TryGetAsObjectAsync(), 43 | () => $"{BaseUrl}/CacheByQueryWithAuthorize/Get?page=2&pageSize=4".CreateHttpRequest().UseCookie(_cookies[2]).TryGetAsObjectAsync(), 44 | () => $"{BaseUrl}/CacheByQueryWithAuthorize/Get?page=2&pageSize=6".CreateHttpRequest().UseCookie(_cookies[3]).TryGetAsObjectAsync(), 45 | () => $"{BaseUrl}/CacheByQueryWithAuthorize/Get?page=3&pageSize=3".CreateHttpRequest().UseCookie(_cookies[4]).TryGetAsObjectAsync(), 46 | }; 47 | await ExecuteAsync(funcs, true, false, 3); 48 | } 49 | 50 | #endregion Public 方法 51 | } 52 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test/RequestTests/CacheByQueryWithJWTAuthorizeTest.cs: -------------------------------------------------------------------------------- 1 | using Cuture.Http; 2 | 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | 5 | using ResponseCaching.Test.WebHost.Models; 6 | 7 | namespace ResponseCaching.Test.RequestTests; 8 | 9 | [TestClass] 10 | public class CacheByQueryWithJWTAuthorizeTest : RequestTestBase 11 | { 12 | #region Private 字段 13 | 14 | private string[] _jwts = null; 15 | 16 | #endregion Private 字段 17 | 18 | #region Public 方法 19 | 20 | [TestInitialize] 21 | public override async Task InitAsync() 22 | { 23 | await base.InitAsync(); 24 | 25 | var jwts = new List(); 26 | for (int i = 1; i < 6; i++) 27 | { 28 | var token = await $"{BaseUrl}/login/jwt?uid=testuser{i}".CreateHttpRequest().GetAsStringAsync(); 29 | 30 | jwts.Add(token); 31 | } 32 | _jwts = jwts.ToArray(); 33 | } 34 | 35 | [TestMethod] 36 | public async Task Should_Different_With_Different_Authorization() 37 | { 38 | var funcs = new Func>>[] { 39 | () => $"{BaseUrl}/CacheByQueryWithAuthorize/Get?page=1&pageSize=5".CreateHttpRequest().AddHeader("Authorization", $"Bearer {_jwts[0]}").TryGetAsObjectAsync(), 40 | () => $"{BaseUrl}/CacheByQueryWithAuthorize/Get?page=1&pageSize=6".CreateHttpRequest().AddHeader("Authorization", $"Bearer {_jwts[1]}").TryGetAsObjectAsync(), 41 | () => $"{BaseUrl}/CacheByQueryWithAuthorize/Get?page=2&pageSize=4".CreateHttpRequest().AddHeader("Authorization", $"Bearer {_jwts[2]}").TryGetAsObjectAsync(), 42 | () => $"{BaseUrl}/CacheByQueryWithAuthorize/Get?page=2&pageSize=6".CreateHttpRequest().AddHeader("Authorization", $"Bearer {_jwts[3]}").TryGetAsObjectAsync(), 43 | () => $"{BaseUrl}/CacheByQueryWithAuthorize/Get?page=3&pageSize=3".CreateHttpRequest().AddHeader("Authorization", $"Bearer {_jwts[4]}").TryGetAsObjectAsync(), 44 | }; 45 | await ExecuteAsync(funcs, true, false, 3); 46 | } 47 | 48 | #endregion Public 方法 49 | } 50 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test/RequestTests/CustomCachingProcessInterceptorTest.cs: -------------------------------------------------------------------------------- 1 | using Cuture.Http; 2 | 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using ResponseCaching.Test.WebHost.Models; 5 | 6 | namespace ResponseCaching.Test.RequestTests; 7 | 8 | [TestClass] 9 | public class CustomCachingProcessInterceptorTest : RequestTestBase 10 | { 11 | #region Public 方法 12 | 13 | [TestMethod] 14 | public async Task Should_Equals_With_Different_Request_Type() 15 | { 16 | var funcs = new Func>>[] { 17 | () => $"{BaseUrl}/CacheByPathWithCustomInterceptor/Get?page=1&pageSize=5".CreateHttpRequest().TryGetAsObjectAsync(), 18 | () => $"{BaseUrl}/CacheByPathWithCustomInterceptor/Get?page=1".CreateHttpRequest().TryGetAsObjectAsync(), 19 | () => $"{BaseUrl}/CacheByPathWithCustomInterceptor/Get".CreateHttpRequest().TryGetAsObjectAsync(), 20 | () => $"{BaseUrl}/CacheByPathWithCustomInterceptor/Get?page=1&pageSize=5".CreateHttpRequest().UsePost().TryGetAsObjectAsync(), 21 | () => $"{BaseUrl}/CacheByPathWithCustomInterceptor/Get?page=1".CreateHttpRequest().UsePost().TryGetAsObjectAsync(), 22 | () => $"{BaseUrl}/CacheByPathWithCustomInterceptor/Get".CreateHttpRequest().UsePost().TryGetAsObjectAsync(), 23 | }; 24 | await ExecuteAsync(funcs, true, false, 3, false); 25 | } 26 | 27 | #endregion Public 方法 28 | } 29 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test/RequestTests/ExecuteLockTimeoutTest.cs: -------------------------------------------------------------------------------- 1 | using Cuture.Http; 2 | 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | 5 | using ResponseCaching.Test.Base; 6 | using ResponseCaching.Test.WebHost.Models; 7 | 8 | namespace ResponseCaching.Test.RequestTests; 9 | 10 | [TestClass] 11 | public class ExecuteLockTimeoutTest : WebServerHostedTestBase 12 | { 13 | #region Public 方法 14 | 15 | [TestMethod] 16 | public async Task ShouldActionFilterReturn429Async() 17 | { 18 | await ExecuteAsync("ActionFilter"); 19 | } 20 | 21 | [TestMethod] 22 | public async Task ShouldResourceFilterReturn429Async() 23 | { 24 | await ExecuteAsync("ResourceFilter"); 25 | } 26 | 27 | #endregion Public 方法 28 | 29 | #region Private 方法 30 | 31 | private async Task ExecuteAsync(string actionName) 32 | { 33 | var waitUrl = $"{BaseUrl}/ExecuteLockTimeout/{actionName}?waitMilliseconds=2000"; 34 | var noWaitUrl = $"{BaseUrl}/ExecuteLockTimeout/{actionName}?waitMilliseconds=0"; 35 | 36 | var waitTask = GetRequestTasks(waitUrl); 37 | 38 | //延时 39 | await Task.Delay(200); 40 | 41 | var noWaitTasks = Enumerable.Range(0, 50).Select(_ => GetRequestTasks(noWaitUrl)).ToArray(); 42 | 43 | await Task.WhenAll(noWaitTasks); 44 | 45 | await waitTask; 46 | 47 | Assert.IsTrue(waitTask.IsCompletedSuccessfully); 48 | 49 | Assert.IsTrue(waitTask.Result?.Data?.Length > 0); 50 | 51 | for (int i = 0; i < noWaitTasks.Length; i++) 52 | { 53 | var item = noWaitTasks[i]; 54 | Assert.IsTrue(item.IsCompletedSuccessfully, $"fail at {i}"); 55 | Assert.IsNull(item.Result?.Data, $"fail at {i}"); 56 | Assert.IsNotNull(item.Result?.ResponseMessage, $"fail at {i}"); 57 | 58 | Assert.AreEqual(System.Net.HttpStatusCode.TooManyRequests, item.Result.ResponseMessage.StatusCode, $"fail at {i}"); 59 | } 60 | 61 | Task> GetRequestTasks(string url) 62 | { 63 | return url.CreateHttpRequest().TryGetAsObjectAsync(); 64 | } 65 | } 66 | 67 | #endregion Private 方法 68 | } 69 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test/RequestTests/MaxCacheableResponseLengthTest.cs: -------------------------------------------------------------------------------- 1 | using Cuture.Http; 2 | 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | 5 | using ResponseCaching.Test.Base; 6 | using ResponseCaching.Test.WebHost.Models; 7 | 8 | namespace ResponseCaching.Test.RequestTests; 9 | 10 | [TestClass] 11 | public class MaxCacheableResponseLengthTest : WebServerHostedTestBase 12 | { 13 | #region Public 方法 14 | 15 | [TestMethod] 16 | public async Task ShouldActionFilterNotCacheAsync() 17 | { 18 | await ExecuteAsync("by-action-filter"); 19 | } 20 | 21 | [TestMethod] 22 | public async Task ShouldResourceFilterNotCacheAsync() 23 | { 24 | await ExecuteAsync("by-resource-filter"); 25 | } 26 | 27 | #endregion Public 方法 28 | 29 | #region Private 方法 30 | 31 | private async Task ExecuteAsync(string actionName) 32 | { 33 | var cacheableUrl = $"{BaseUrl}/MaxCacheableResponseLength/{actionName}?count=1"; 34 | var notCacheableUrl = $"{BaseUrl}/MaxCacheableResponseLength/{actionName}?count=20"; 35 | 36 | var cachedData = await InternalRunAsync(Enumerable.Range(0, 100).Select(_ => GetRequestFuncs(cacheableUrl)).ToArray()); 37 | 38 | CheckForEachOther(cachedData, true); 39 | 40 | var notCachedData = await InternalRunAsync(Enumerable.Range(0, 100).Select(_ => GetRequestFuncs(notCacheableUrl)).ToArray()); 41 | 42 | CheckForEachOther(notCachedData, false); 43 | 44 | Func>> GetRequestFuncs(string url) 45 | { 46 | return () => url.CreateHttpRequest().TryGetAsObjectAsync(); 47 | } 48 | } 49 | 50 | #endregion Private 方法 51 | } 52 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test/RequestTests/NoneCachingTest.cs: -------------------------------------------------------------------------------- 1 | using Cuture.Http; 2 | 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using ResponseCaching.Test.WebHost.Models; 5 | 6 | namespace ResponseCaching.Test.RequestTests; 7 | 8 | /// 9 | /// 没有缓存的接口请求测试 10 | /// 11 | [TestClass] 12 | public class NoneCachingTest : RequestTestBase 13 | { 14 | #region Public 方法 15 | 16 | [TestMethod] 17 | public async Task Should_Different_With_ReRequest() 18 | { 19 | var funcs = new Func>>[] { 20 | () => $"{BaseUrl}/WeatherForecast/get?page=1&pageSize=5".CreateHttpRequest().TryGetAsObjectAsync(), 21 | () => $"{BaseUrl}/WeatherForecast/get?page=1&pageSize=6".CreateHttpRequest().TryGetAsObjectAsync(), 22 | () => $"{BaseUrl}/WeatherForecast/get?page=2&pageSize=4".CreateHttpRequest().TryGetAsObjectAsync(), 23 | () => $"{BaseUrl}/WeatherForecast/get?page=2&pageSize=6".CreateHttpRequest().TryGetAsObjectAsync(), 24 | () => $"{BaseUrl}/WeatherForecast/get?page=3&pageSize=3".CreateHttpRequest().TryGetAsObjectAsync(), 25 | }; 26 | await ExecuteAsync(funcs, true, false, 3, false); 27 | } 28 | 29 | #endregion Public 方法 30 | } 31 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test/ResponseCaches/MemoryResponseCacheTest.cs: -------------------------------------------------------------------------------- 1 | using Cuture.AspNetCore.ResponseCaching.ResponseCaches; 2 | 3 | using Microsoft.Extensions.Logging; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | 6 | namespace ResponseCaching.Test.ResponseCaches; 7 | 8 | [TestClass] 9 | public class MemoryResponseCacheTest : ResponseCacheTest 10 | { 11 | protected override Task GetResponseCache() 12 | { 13 | return Task.FromResult(new DefaultMemoryResponseCache(new LoggerFactory())); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test/ResponseCaches/RedisResponseCacheTest.cs: -------------------------------------------------------------------------------- 1 | using Cuture.AspNetCore.ResponseCaching; 2 | using Cuture.AspNetCore.ResponseCaching.ResponseCaches; 3 | 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.VisualStudio.TestTools.UnitTesting; 6 | 7 | using ResponseCaching.Test.WebHost; 8 | 9 | using StackExchange.Redis; 10 | 11 | namespace ResponseCaching.Test.ResponseCaches; 12 | 13 | [TestClass] 14 | public class RedisResponseCacheTest : ResponseCacheTest 15 | { 16 | private ConnectionMultiplexer _connectionMultiplexer; 17 | 18 | protected override async Task GetResponseCache() 19 | { 20 | var configuration = new ConfigurationBuilder().AddJsonFile("appsettings.json", true) 21 | .AddJsonFile("appsettings.Development.json", true) 22 | .AddEnvironmentVariables() 23 | .AddUserSecrets() 24 | .Build() 25 | .GetValue("ResponseCache_Test_Redis"); 26 | if (string.IsNullOrWhiteSpace(configuration)) 27 | { 28 | throw new ArgumentException("Must set ‘ResponseCache_Test_Redis’ first."); 29 | } 30 | _connectionMultiplexer = await ConnectionMultiplexer.ConnectAsync(configuration); 31 | return new RedisResponseCache(new RedisResponseCacheOptions() { ConnectionMultiplexer = _connectionMultiplexer }); 32 | } 33 | 34 | [TestCleanup] 35 | public override void Cleanup() 36 | { 37 | _connectionMultiplexer.Dispose(); 38 | _connectionMultiplexer = null; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test/ResponseCaching.Test.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | false 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | all 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /test/ResponseCaching.Test/TestUtil.cs: -------------------------------------------------------------------------------- 1 | using Cuture.AspNetCore.ResponseCaching.ResponseCaches; 2 | 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | 5 | namespace ResponseCaching.Test; 6 | 7 | public static class TestUtil 8 | { 9 | public static void EntryEquals(ResponseCacheEntry entry1, ResponseCacheEntry entry2) 10 | { 11 | Assert.AreEqual(entry1.ContentType, entry2.ContentType); 12 | Assert.AreEqual(entry1.Body.Length, entry2.Body.Length); 13 | Assert.IsTrue(Enumerable.SequenceEqual(entry1.Body.ToArray(), entry2.Body.ToArray())); 14 | } 15 | } 16 | --------------------------------------------------------------------------------