├── .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