From c7422622f279b70834724bc9f46b22647f5f639a Mon Sep 17 00:00:00 2001 From: VahidN <vahid_nasiri@yahoo.com> Date: Mon, 6 Jan 2025 22:27:53 +0330 Subject: [PATCH] Add new caching providers --- .github/workflows/build.yml | 15 + EFCoreSecondLevelCacheInterceptor.sln | 90 +++ README.md | 115 +++- .../EFCacheManagerCoreProvider.cs | 4 +- .../EFCacheManagerCoreProviderOptions.cs | 52 ++ ...lCacheInterceptor.CacheManager.Core.csproj | 86 +++ .../_build.cmd | 2 + ...elCacheInterceptor.EasyCaching.Core.csproj | 86 +++ .../EFEasyCachingCoreProvider.cs | 33 +- .../EFEasyCachingCoreProviderOptions.cs | 129 ++++ .../_build.cmd | 2 + ...ndLevelCacheInterceptor.FusionCache.csproj | 80 +++ .../EFFusionCacheDependenciesStore.cs | 58 ++ .../EFFusionCacheProvider.cs | 88 +++ .../EFFusionCacheProviderOptions.cs | 48 ++ .../IEFFusionCacheDependenciesStore.cs | 26 + .../_build.cmd | 2 + ...ndLevelCacheInterceptor.HybridCache.csproj | 80 +++ .../EFHybridCacheDependenciesStore.cs | 72 +++ .../EFHybridCacheProvider.cs | 91 +++ .../EFHybridCacheProviderOptions.cs | 48 ++ .../IEFHybridCacheDependenciesStore.cs | 26 + .../_build.cmd | 2 + ...ndLevelCacheInterceptor.MemoryCache.csproj | 85 +++ .../EFMemoryCacheChangeTokenProvider.cs | 2 +- .../EFMemoryCacheProviderOptions.cs | 56 ++ .../EFMemoryCacheServiceProvider.cs | 33 +- .../IMemoryCacheChangeTokenProvider.cs | 2 +- .../_build.cmd | 2 + ...acheInterceptor.StackExchange.Redis.csproj | 83 +++ .../EFMessagePackDBNullFormatter.cs | 27 + .../EFMessagePackSerializer.cs | 39 ++ .../EFRedisCacheConfigurationOptions.cs | 19 + .../EFStackExchangeRedisCacheProvider.cs | 131 ++++ ...FStackExchangeRedisCacheProviderOptions.cs | 70 ++ .../IEFDataSerializer.cs | 17 + .../_build.cmd | 2 + .../EFCoreSecondLevelCacheInterceptor.csproj | 26 +- .../EFCoreSecondLevelCacheOptions.cs | 169 +---- .../EFCoreSecondLevelCacheSettings.cs | 15 +- .../EFServiceCollectionExtensions.cs | 28 +- .../IEFCacheServiceProvider.cs | 4 +- ...elCacheInterceptor.AspNetCoreSample.csproj | 35 +- ...terceptor.AspNetCoreSampleWithLamar.csproj | 43 +- ...LevelCacheInterceptor.ConsoleSample.csproj | 29 +- ...elCacheInterceptor.PerformanceTests.csproj | 37 +- ...velCacheInterceptor.Tests.DataLayer.csproj | 53 +- ...0250106171343_V2025_01_06_2043.Designer.cs | 547 ++++++++++++++++ .../20250106171343_V2025_01_06_2043.cs | 36 ++ .../ApplicationDbContextModelSnapshot.cs | 57 +- .../_01-add_migrations.cmd | 2 +- .../_02-update_db.cmd | 2 +- .../CacheAllQueriesTests.cs | 123 ++-- ...reSecondLevelCacheInterceptor.Tests.csproj | 6 +- .../Settings/DBNullFormatter.cs | 6 +- .../Settings/EFServiceProvider.cs | 600 +++++++++++------- .../EFCacheServiceProviderTests.cs | 6 +- ...condLevelCacheInterceptor.UnitTests.csproj | 7 +- .../EFServiceCollectionExtensionsTests.cs | 47 +- .../Issue123WithMessagePack.csproj | 53 +- .../Issues/Issue125EF5x/Issue125EF5x.csproj | 1 + .../Issues/Issue12MySQL/Issue12MySQL.csproj | 51 +- .../Issue12PostgreSql.csproj | 4 +- src/Tests/Issues/Issue154/Issue154.csproj | 57 +- src/Tests/Issues/Issue192/Issue192.csproj | 3 +- .../Issue4SpatialType.csproj | 1 + .../Issue9SQLiteInt32/App_Data/TestDb.sqlite | Bin 20480 -> 24576 bytes .../Issue9SQLiteInt32.csproj | 57 +- .../Issue9SQLiteInt32/_01-add_migrations.cmd | 2 +- 69 files changed, 3131 insertions(+), 779 deletions(-) rename src/{EFCoreSecondLevelCacheInterceptor => EFCoreSecondLevelCacheInterceptor.CacheManager.Core}/EFCacheManagerCoreProvider.cs (97%) create mode 100644 src/EFCoreSecondLevelCacheInterceptor.CacheManager.Core/EFCacheManagerCoreProviderOptions.cs create mode 100644 src/EFCoreSecondLevelCacheInterceptor.CacheManager.Core/EFCoreSecondLevelCacheInterceptor.CacheManager.Core.csproj create mode 100644 src/EFCoreSecondLevelCacheInterceptor.CacheManager.Core/_build.cmd create mode 100644 src/EFCoreSecondLevelCacheInterceptor.EasyCaching.Core/EFCoreSecondLevelCacheInterceptor.EasyCaching.Core.csproj rename src/{EFCoreSecondLevelCacheInterceptor => EFCoreSecondLevelCacheInterceptor.EasyCaching.Core}/EFEasyCachingCoreProvider.cs (85%) create mode 100644 src/EFCoreSecondLevelCacheInterceptor.EasyCaching.Core/EFEasyCachingCoreProviderOptions.cs create mode 100644 src/EFCoreSecondLevelCacheInterceptor.EasyCaching.Core/_build.cmd create mode 100644 src/EFCoreSecondLevelCacheInterceptor.FusionCache/EFCoreSecondLevelCacheInterceptor.FusionCache.csproj create mode 100644 src/EFCoreSecondLevelCacheInterceptor.FusionCache/EFFusionCacheDependenciesStore.cs create mode 100644 src/EFCoreSecondLevelCacheInterceptor.FusionCache/EFFusionCacheProvider.cs create mode 100644 src/EFCoreSecondLevelCacheInterceptor.FusionCache/EFFusionCacheProviderOptions.cs create mode 100644 src/EFCoreSecondLevelCacheInterceptor.FusionCache/IEFFusionCacheDependenciesStore.cs create mode 100644 src/EFCoreSecondLevelCacheInterceptor.FusionCache/_build.cmd create mode 100644 src/EFCoreSecondLevelCacheInterceptor.HybridCache/EFCoreSecondLevelCacheInterceptor.HybridCache.csproj create mode 100644 src/EFCoreSecondLevelCacheInterceptor.HybridCache/EFHybridCacheDependenciesStore.cs create mode 100644 src/EFCoreSecondLevelCacheInterceptor.HybridCache/EFHybridCacheProvider.cs create mode 100644 src/EFCoreSecondLevelCacheInterceptor.HybridCache/EFHybridCacheProviderOptions.cs create mode 100644 src/EFCoreSecondLevelCacheInterceptor.HybridCache/IEFHybridCacheDependenciesStore.cs create mode 100644 src/EFCoreSecondLevelCacheInterceptor.HybridCache/_build.cmd create mode 100644 src/EFCoreSecondLevelCacheInterceptor.MemoryCache/EFCoreSecondLevelCacheInterceptor.MemoryCache.csproj rename src/{EFCoreSecondLevelCacheInterceptor => EFCoreSecondLevelCacheInterceptor.MemoryCache}/EFMemoryCacheChangeTokenProvider.cs (97%) create mode 100644 src/EFCoreSecondLevelCacheInterceptor.MemoryCache/EFMemoryCacheProviderOptions.cs rename src/{EFCoreSecondLevelCacheInterceptor => EFCoreSecondLevelCacheInterceptor.MemoryCache}/EFMemoryCacheServiceProvider.cs (71%) rename src/{EFCoreSecondLevelCacheInterceptor => EFCoreSecondLevelCacheInterceptor.MemoryCache}/IMemoryCacheChangeTokenProvider.cs (90%) create mode 100644 src/EFCoreSecondLevelCacheInterceptor.MemoryCache/_build.cmd create mode 100644 src/EFCoreSecondLevelCacheInterceptor.StackExchange.Redis/EFCoreSecondLevelCacheInterceptor.StackExchange.Redis.csproj create mode 100644 src/EFCoreSecondLevelCacheInterceptor.StackExchange.Redis/EFMessagePackDBNullFormatter.cs create mode 100644 src/EFCoreSecondLevelCacheInterceptor.StackExchange.Redis/EFMessagePackSerializer.cs create mode 100644 src/EFCoreSecondLevelCacheInterceptor.StackExchange.Redis/EFRedisCacheConfigurationOptions.cs create mode 100644 src/EFCoreSecondLevelCacheInterceptor.StackExchange.Redis/EFStackExchangeRedisCacheProvider.cs create mode 100644 src/EFCoreSecondLevelCacheInterceptor.StackExchange.Redis/EFStackExchangeRedisCacheProviderOptions.cs create mode 100644 src/EFCoreSecondLevelCacheInterceptor.StackExchange.Redis/IEFDataSerializer.cs create mode 100644 src/EFCoreSecondLevelCacheInterceptor.StackExchange.Redis/_build.cmd create mode 100644 src/Tests/EFCoreSecondLevelCacheInterceptor.Tests.DataLayer/Migrations/20250106171343_V2025_01_06_2043.Designer.cs create mode 100644 src/Tests/EFCoreSecondLevelCacheInterceptor.Tests.DataLayer/Migrations/20250106171343_V2025_01_06_2043.cs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a0835a3..cde5e6a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -19,6 +19,21 @@ jobs: - name: Run EFCoreSecondLevelCacheInterceptor lib unit tests run: dotnet test ./src/Tests/EFCoreSecondLevelCacheInterceptor.UnitTests/EFCoreSecondLevelCacheInterceptor.UnitTests.csproj --logger "console;verbosity=detailed" + - name: Build EFCoreSecondLevelCacheInterceptor.CacheManager.Core lib + run: dotnet build ./src/EFCoreSecondLevelCacheInterceptor.CacheManager.Core/EFCoreSecondLevelCacheInterceptor.CacheManager.Core.csproj --configuration Release + + - name: Build EFCoreSecondLevelCacheInterceptor.EasyCaching.Core lib + run: dotnet build ./src/EFCoreSecondLevelCacheInterceptor.EasyCaching.Core/EFCoreSecondLevelCacheInterceptor.EasyCaching.Core.csproj --configuration Release + + - name: Build EFCoreSecondLevelCacheInterceptor.MemoryCache lib + run: dotnet build ./src/EFCoreSecondLevelCacheInterceptor.MemoryCache/EFCoreSecondLevelCacheInterceptor.MemoryCache.csproj --configuration Release + + - name: Build EFCoreSecondLevelCacheInterceptor.FusionCache lib + run: dotnet build ./src/EFCoreSecondLevelCacheInterceptor.FusionCache/EFCoreSecondLevelCacheInterceptor.FusionCache.csproj --configuration Release + + - name: Build EFCoreSecondLevelCacheInterceptor.StackExchange.Redis lib + run: dotnet build ./src/EFCoreSecondLevelCacheInterceptor.StackExchange.Redis/EFCoreSecondLevelCacheInterceptor.StackExchange.Redis.csproj --configuration Release + - name: Push Package to NuGet.org if: github.event_name == 'push' run: dotnet nuget push **\*.nupkg -k ${{ secrets.NUGET_API_KEY }} -s https://api.nuget.org/v3/index.json --skip-duplicate diff --git a/EFCoreSecondLevelCacheInterceptor.sln b/EFCoreSecondLevelCacheInterceptor.sln index de52c2f..a9d8a3f 100644 --- a/EFCoreSecondLevelCacheInterceptor.sln +++ b/EFCoreSecondLevelCacheInterceptor.sln @@ -43,6 +43,18 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Issue192", "src\Tests\Issue EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EFCoreSecondLevelCacheInterceptor.UnitTests", "src\Tests\EFCoreSecondLevelCacheInterceptor.UnitTests\EFCoreSecondLevelCacheInterceptor.UnitTests.csproj", "{F77DD140-4762-426E-8FE0-3AD34DE3867A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EFCoreSecondLevelCacheInterceptor.CacheManager.Core", "src\EFCoreSecondLevelCacheInterceptor.CacheManager.Core\EFCoreSecondLevelCacheInterceptor.CacheManager.Core.csproj", "{C6EB0761-6A95-4B19-9E6C-C3814D116937}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EFCoreSecondLevelCacheInterceptor.EasyCaching.Core", "src\EFCoreSecondLevelCacheInterceptor.EasyCaching.Core\EFCoreSecondLevelCacheInterceptor.EasyCaching.Core.csproj", "{0654DC71-AC65-4428-A4E6-A4CE469EA959}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EFCoreSecondLevelCacheInterceptor.MemoryCache", "src\EFCoreSecondLevelCacheInterceptor.MemoryCache\EFCoreSecondLevelCacheInterceptor.MemoryCache.csproj", "{404FFBBC-49A7-4AC3-8FFF-84B21AA70EAB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EFCoreSecondLevelCacheInterceptor.HybridCache", "src\EFCoreSecondLevelCacheInterceptor.HybridCache\EFCoreSecondLevelCacheInterceptor.HybridCache.csproj", "{863CE238-6FB4-4910-851D-6F115E39728A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EFCoreSecondLevelCacheInterceptor.FusionCache", "src\EFCoreSecondLevelCacheInterceptor.FusionCache\EFCoreSecondLevelCacheInterceptor.FusionCache.csproj", "{61463B61-8487-441D-AA68-1B738516C88C}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EFCoreSecondLevelCacheInterceptor.StackExchange.Redis", "src\EFCoreSecondLevelCacheInterceptor.StackExchange.Redis\EFCoreSecondLevelCacheInterceptor.StackExchange.Redis.csproj", "{70393506-60D8-412F-B053-B3CE8287F047}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -260,6 +272,78 @@ Global {F77DD140-4762-426E-8FE0-3AD34DE3867A}.Release|x64.Build.0 = Release|Any CPU {F77DD140-4762-426E-8FE0-3AD34DE3867A}.Release|x86.ActiveCfg = Release|Any CPU {F77DD140-4762-426E-8FE0-3AD34DE3867A}.Release|x86.Build.0 = Release|Any CPU + {C6EB0761-6A95-4B19-9E6C-C3814D116937}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C6EB0761-6A95-4B19-9E6C-C3814D116937}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C6EB0761-6A95-4B19-9E6C-C3814D116937}.Debug|x64.ActiveCfg = Debug|Any CPU + {C6EB0761-6A95-4B19-9E6C-C3814D116937}.Debug|x64.Build.0 = Debug|Any CPU + {C6EB0761-6A95-4B19-9E6C-C3814D116937}.Debug|x86.ActiveCfg = Debug|Any CPU + {C6EB0761-6A95-4B19-9E6C-C3814D116937}.Debug|x86.Build.0 = Debug|Any CPU + {C6EB0761-6A95-4B19-9E6C-C3814D116937}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C6EB0761-6A95-4B19-9E6C-C3814D116937}.Release|Any CPU.Build.0 = Release|Any CPU + {C6EB0761-6A95-4B19-9E6C-C3814D116937}.Release|x64.ActiveCfg = Release|Any CPU + {C6EB0761-6A95-4B19-9E6C-C3814D116937}.Release|x64.Build.0 = Release|Any CPU + {C6EB0761-6A95-4B19-9E6C-C3814D116937}.Release|x86.ActiveCfg = Release|Any CPU + {C6EB0761-6A95-4B19-9E6C-C3814D116937}.Release|x86.Build.0 = Release|Any CPU + {0654DC71-AC65-4428-A4E6-A4CE469EA959}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0654DC71-AC65-4428-A4E6-A4CE469EA959}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0654DC71-AC65-4428-A4E6-A4CE469EA959}.Debug|x64.ActiveCfg = Debug|Any CPU + {0654DC71-AC65-4428-A4E6-A4CE469EA959}.Debug|x64.Build.0 = Debug|Any CPU + {0654DC71-AC65-4428-A4E6-A4CE469EA959}.Debug|x86.ActiveCfg = Debug|Any CPU + {0654DC71-AC65-4428-A4E6-A4CE469EA959}.Debug|x86.Build.0 = Debug|Any CPU + {0654DC71-AC65-4428-A4E6-A4CE469EA959}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0654DC71-AC65-4428-A4E6-A4CE469EA959}.Release|Any CPU.Build.0 = Release|Any CPU + {0654DC71-AC65-4428-A4E6-A4CE469EA959}.Release|x64.ActiveCfg = Release|Any CPU + {0654DC71-AC65-4428-A4E6-A4CE469EA959}.Release|x64.Build.0 = Release|Any CPU + {0654DC71-AC65-4428-A4E6-A4CE469EA959}.Release|x86.ActiveCfg = Release|Any CPU + {0654DC71-AC65-4428-A4E6-A4CE469EA959}.Release|x86.Build.0 = Release|Any CPU + {404FFBBC-49A7-4AC3-8FFF-84B21AA70EAB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {404FFBBC-49A7-4AC3-8FFF-84B21AA70EAB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {404FFBBC-49A7-4AC3-8FFF-84B21AA70EAB}.Debug|x64.ActiveCfg = Debug|Any CPU + {404FFBBC-49A7-4AC3-8FFF-84B21AA70EAB}.Debug|x64.Build.0 = Debug|Any CPU + {404FFBBC-49A7-4AC3-8FFF-84B21AA70EAB}.Debug|x86.ActiveCfg = Debug|Any CPU + {404FFBBC-49A7-4AC3-8FFF-84B21AA70EAB}.Debug|x86.Build.0 = Debug|Any CPU + {404FFBBC-49A7-4AC3-8FFF-84B21AA70EAB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {404FFBBC-49A7-4AC3-8FFF-84B21AA70EAB}.Release|Any CPU.Build.0 = Release|Any CPU + {404FFBBC-49A7-4AC3-8FFF-84B21AA70EAB}.Release|x64.ActiveCfg = Release|Any CPU + {404FFBBC-49A7-4AC3-8FFF-84B21AA70EAB}.Release|x64.Build.0 = Release|Any CPU + {404FFBBC-49A7-4AC3-8FFF-84B21AA70EAB}.Release|x86.ActiveCfg = Release|Any CPU + {404FFBBC-49A7-4AC3-8FFF-84B21AA70EAB}.Release|x86.Build.0 = Release|Any CPU + {863CE238-6FB4-4910-851D-6F115E39728A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {863CE238-6FB4-4910-851D-6F115E39728A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {863CE238-6FB4-4910-851D-6F115E39728A}.Debug|x64.ActiveCfg = Debug|Any CPU + {863CE238-6FB4-4910-851D-6F115E39728A}.Debug|x64.Build.0 = Debug|Any CPU + {863CE238-6FB4-4910-851D-6F115E39728A}.Debug|x86.ActiveCfg = Debug|Any CPU + {863CE238-6FB4-4910-851D-6F115E39728A}.Debug|x86.Build.0 = Debug|Any CPU + {863CE238-6FB4-4910-851D-6F115E39728A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {863CE238-6FB4-4910-851D-6F115E39728A}.Release|Any CPU.Build.0 = Release|Any CPU + {863CE238-6FB4-4910-851D-6F115E39728A}.Release|x64.ActiveCfg = Release|Any CPU + {863CE238-6FB4-4910-851D-6F115E39728A}.Release|x64.Build.0 = Release|Any CPU + {863CE238-6FB4-4910-851D-6F115E39728A}.Release|x86.ActiveCfg = Release|Any CPU + {863CE238-6FB4-4910-851D-6F115E39728A}.Release|x86.Build.0 = Release|Any CPU + {61463B61-8487-441D-AA68-1B738516C88C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {61463B61-8487-441D-AA68-1B738516C88C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {61463B61-8487-441D-AA68-1B738516C88C}.Debug|x64.ActiveCfg = Debug|Any CPU + {61463B61-8487-441D-AA68-1B738516C88C}.Debug|x64.Build.0 = Debug|Any CPU + {61463B61-8487-441D-AA68-1B738516C88C}.Debug|x86.ActiveCfg = Debug|Any CPU + {61463B61-8487-441D-AA68-1B738516C88C}.Debug|x86.Build.0 = Debug|Any CPU + {61463B61-8487-441D-AA68-1B738516C88C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {61463B61-8487-441D-AA68-1B738516C88C}.Release|Any CPU.Build.0 = Release|Any CPU + {61463B61-8487-441D-AA68-1B738516C88C}.Release|x64.ActiveCfg = Release|Any CPU + {61463B61-8487-441D-AA68-1B738516C88C}.Release|x64.Build.0 = Release|Any CPU + {61463B61-8487-441D-AA68-1B738516C88C}.Release|x86.ActiveCfg = Release|Any CPU + {61463B61-8487-441D-AA68-1B738516C88C}.Release|x86.Build.0 = Release|Any CPU + {70393506-60D8-412F-B053-B3CE8287F047}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {70393506-60D8-412F-B053-B3CE8287F047}.Debug|Any CPU.Build.0 = Debug|Any CPU + {70393506-60D8-412F-B053-B3CE8287F047}.Debug|x64.ActiveCfg = Debug|Any CPU + {70393506-60D8-412F-B053-B3CE8287F047}.Debug|x64.Build.0 = Debug|Any CPU + {70393506-60D8-412F-B053-B3CE8287F047}.Debug|x86.ActiveCfg = Debug|Any CPU + {70393506-60D8-412F-B053-B3CE8287F047}.Debug|x86.Build.0 = Debug|Any CPU + {70393506-60D8-412F-B053-B3CE8287F047}.Release|Any CPU.ActiveCfg = Release|Any CPU + {70393506-60D8-412F-B053-B3CE8287F047}.Release|Any CPU.Build.0 = Release|Any CPU + {70393506-60D8-412F-B053-B3CE8287F047}.Release|x64.ActiveCfg = Release|Any CPU + {70393506-60D8-412F-B053-B3CE8287F047}.Release|x64.Build.0 = Release|Any CPU + {70393506-60D8-412F-B053-B3CE8287F047}.Release|x86.ActiveCfg = Release|Any CPU + {70393506-60D8-412F-B053-B3CE8287F047}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {EB8ADB13-3CE7-4A85-A70C-229972476023} = {CDC8E8F6-5261-4CF2-846E-F8C438438663} @@ -281,5 +365,11 @@ Global {93359BAE-B026-4680-9818-D06EE408B910} = {4E1F72D1-5A8F-4569-A57A-42E706B6A318} {72079056-6752-43FD-BA53-E0BF5EFFF2C2} = {4E1F72D1-5A8F-4569-A57A-42E706B6A318} {F77DD140-4762-426E-8FE0-3AD34DE3867A} = {C250B77E-090A-4FBE-BB73-2D91774A6929} + {C6EB0761-6A95-4B19-9E6C-C3814D116937} = {CDC8E8F6-5261-4CF2-846E-F8C438438663} + {0654DC71-AC65-4428-A4E6-A4CE469EA959} = {CDC8E8F6-5261-4CF2-846E-F8C438438663} + {404FFBBC-49A7-4AC3-8FFF-84B21AA70EAB} = {CDC8E8F6-5261-4CF2-846E-F8C438438663} + {863CE238-6FB4-4910-851D-6F115E39728A} = {CDC8E8F6-5261-4CF2-846E-F8C438438663} + {61463B61-8487-441D-AA68-1B738516C88C} = {CDC8E8F6-5261-4CF2-846E-F8C438438663} + {70393506-60D8-412F-B053-B3CE8287F047} = {CDC8E8F6-5261-4CF2-846E-F8C438438663} EndGlobalSection EndGlobal diff --git a/README.md b/README.md index 7769ff6..0633852 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,16 @@ -# EF Core 3.1.x, 5x, 6x, 7x & 8x Second Level Cache Interceptor +# EF Core Second Level Cache Interceptor [![EFCoreSecondLevelCacheInterceptor](https://github.com/VahidN/EFCoreSecondLevelCacheInterceptor/workflows/.NET%20Core%20Build/badge.svg)](https://github.com/VahidN/EFCoreSecondLevelCacheInterceptor) Second level caching is a query cache. The results of EF commands will be stored in the cache, so that the same EF commands will retrieve their data from the cache rather than executing them against the database again. -## Install via NuGet -To install EFCoreSecondLevelCacheInterceptor, run the following command in the Package Manager Console: +## How to upgrade to version 5 + +To support more advanced caching providers, this library uses different assemblies and NuGet packages now. +To upgrade to version 5, first remove the `EFCoreSecondLevelCacheInterceptor` dependency. It doesn't have any built-in caching provider anymore. +But you can still use it to introduce your own custom caching provider by calling its `options.UseCustomCacheProvider<T>()` method (and you won't need the new packages). +To install `EFCoreSecondLevelCacheInterceptor` as before, run the following command in the Package Manager Console: [![Nuget](https://img.shields.io/nuget/v/EFCoreSecondLevelCacheInterceptor)](http://www.nuget.org/packages/EFCoreSecondLevelCacheInterceptor/) @@ -14,7 +18,110 @@ To install EFCoreSecondLevelCacheInterceptor, run the following command in the P PM> Install-Package EFCoreSecondLevelCacheInterceptor ``` -You can also view the [package page](http://www.nuget.org/packages/EFCoreSecondLevelCacheInterceptor/) on NuGet. +But if you were using the built-in `In-Memory` cache provider, just install this new package: + +[![Nuget](https://img.shields.io/nuget/v/EFCoreSecondLevelCacheInterceptor.MemoryCache)](http://www.nuget.org/packages/EFCoreSecondLevelCacheInterceptor.MemoryCache/) +```powershell +PM> Install-Package EFCoreSecondLevelCacheInterceptor.MemoryCache +``` + +Or if you were using the `EasyCaching.Core provider`, install the new `EFCoreSecondLevelCacheInterceptor.EasyCaching.Core` package: + +[![Nuget](https://img.shields.io/nuget/v/EFCoreSecondLevelCacheInterceptor.EasyCaching.Core)](http://www.nuget.org/packages/EFCoreSecondLevelCacheInterceptor.EasyCaching.Core/) +```powershell +PM> Install-Package EFCoreSecondLevelCacheInterceptor.EasyCaching.Core +``` + +Or if you were using the `CacheManager.Core provider`, install the new `EFCoreSecondLevelCacheInterceptor.CacheManager.Core` package: + +[![Nuget](https://img.shields.io/nuget/v/EFCoreSecondLevelCacheInterceptor.CacheManager.Core)](http://www.nuget.org/packages/EFCoreSecondLevelCacheInterceptor.CacheManager.Core/) +```powershell +PM> Install-Package EFCoreSecondLevelCacheInterceptor.CacheManager.Core +``` + +Also there are two new caching providers available in V5: + +### 1- EFCoreSecondLevelCacheInterceptor.StackExchange.Redis + +This provider uses the StackExchange.Redis as a cache provider and it's preconfigured with a MessagePack serializer. To use it, first you should install its new NuGet package: + +[![Nuget](https://img.shields.io/nuget/v/EFCoreSecondLevelCacheInterceptor.StackExchange.Redis)](http://www.nuget.org/packages/EFCoreSecondLevelCacheInterceptor.StackExchange.Redis/) +```powershell +PM> Install-Package EFCoreSecondLevelCacheInterceptor.StackExchange.Redis +``` + +And then you need to register its required services: + +```csharp +var redisOptions = new ConfigurationOptions + { + EndPoints = new EndPointCollection + { + { + "127.0.0.1", 6379 + } + }, + AllowAdmin = true, + ConnectTimeout = 10000 + }; + +services.AddEFSecondLevelCache(options + => options.UseStackExchangeRedisCacheProvider(redisOptions, TimeSpan.FromMinutes(minutes: 5))); +``` + +### 2- EFCoreSecondLevelCacheInterceptor.FusionCache + +This provider uses the [FusionCache](https://github.com/ZiggyCreatures/FusionCache) as a cache provider. To use it, first you should install its new NuGet package: + +[![Nuget](https://img.shields.io/nuget/v/EFCoreSecondLevelCacheInterceptor.FusionCache)](http://www.nuget.org/packages/EFCoreSecondLevelCacheInterceptor.FusionCache/) +```powershell +PM> Install-Package EFCoreSecondLevelCacheInterceptor.FusionCache +``` + +And then this is how you can register its required services: + +```csharp +services.AddFusionCache() + .WithOptions(options => + { + options.DefaultEntryOptions = new FusionCacheEntryOptions + { + // CACHE DURATION + Duration = TimeSpan.FromMinutes(minutes: 1), + + // FAIL-SAFE OPTIONS + IsFailSafeEnabled = true, + FailSafeMaxDuration = TimeSpan.FromHours(hours: 2), + FailSafeThrottleDuration = TimeSpan.FromSeconds(seconds: 30), + + // FACTORY TIMEOUTS + FactorySoftTimeout = TimeSpan.FromMilliseconds(milliseconds: 500), + FactoryHardTimeout = TimeSpan.FromMilliseconds(milliseconds: 1500), + + // DISTRIBUTED CACHE + DistributedCacheSoftTimeout = TimeSpan.FromSeconds(seconds: 10), + DistributedCacheHardTimeout = TimeSpan.FromSeconds(seconds: 20), + AllowBackgroundDistributedCacheOperations = true, + + // JITTERING + JitterMaxDuration = TimeSpan.FromSeconds(seconds: 2) + }; + + // DISTIBUTED CACHE CIRCUIT-BREAKER + options.DistributedCacheCircuitBreakerDuration = TimeSpan.FromSeconds(seconds: 2); + + // CUSTOM LOG LEVELS + options.FailSafeActivationLogLevel = LogLevel.Debug; + options.SerializationErrorsLogLevel = LogLevel.Warning; + options.DistributedCacheSyntheticTimeoutsLogLevel = LogLevel.Debug; + options.DistributedCacheErrorsLogLevel = LogLevel.Error; + options.FactorySyntheticTimeoutsLogLevel = LogLevel.Debug; + options.FactoryErrorsLogLevel = LogLevel.Error; + }); + +services.AddEFSecondLevelCache(options => options.UseFusionCacheProvider()); +``` + ## Usage ([1](#1--register-a-preferred-cache-provider) & [2](#2--add-secondlevelcacheinterceptor-to-your-dbcontextoptionsbuilder-pipeline) are mandatory) diff --git a/src/EFCoreSecondLevelCacheInterceptor/EFCacheManagerCoreProvider.cs b/src/EFCoreSecondLevelCacheInterceptor.CacheManager.Core/EFCacheManagerCoreProvider.cs similarity index 97% rename from src/EFCoreSecondLevelCacheInterceptor/EFCacheManagerCoreProvider.cs rename to src/EFCoreSecondLevelCacheInterceptor.CacheManager.Core/EFCacheManagerCoreProvider.cs index 0c2b9a6..4507ce5 100644 --- a/src/EFCoreSecondLevelCacheInterceptor/EFCacheManagerCoreProvider.cs +++ b/src/EFCoreSecondLevelCacheInterceptor.CacheManager.Core/EFCacheManagerCoreProvider.cs @@ -17,7 +17,7 @@ public class EFCacheManagerCoreProvider : IEFCacheServiceProvider private readonly ICacheManager<EFCachedData> _valuesCacheManager; /// <summary> - /// Using IMemoryCache as a cache service. + /// Using CacheManagerCore as a cache service. /// </summary> public EFCacheManagerCoreProvider(ICacheManager<ISet<string>> dependenciesCacheManager, ICacheManager<EFCachedData> valuesCacheManager, @@ -46,7 +46,7 @@ public EFCacheManagerCoreProvider(ICacheManager<ISet<string>> dependenciesCacheM /// <param name="cacheKey">key</param> /// <param name="value">value</param> /// <param name="cachePolicy">Defines the expiration mode of the cache item.</param> - public void InsertValue(EFCacheKey cacheKey, EFCachedData value, EFCachePolicy cachePolicy) + public void InsertValue(EFCacheKey cacheKey, EFCachedData? value, EFCachePolicy cachePolicy) { if (cacheKey is null) { diff --git a/src/EFCoreSecondLevelCacheInterceptor.CacheManager.Core/EFCacheManagerCoreProviderOptions.cs b/src/EFCoreSecondLevelCacheInterceptor.CacheManager.Core/EFCacheManagerCoreProviderOptions.cs new file mode 100644 index 0000000..19cb429 --- /dev/null +++ b/src/EFCoreSecondLevelCacheInterceptor.CacheManager.Core/EFCacheManagerCoreProviderOptions.cs @@ -0,0 +1,52 @@ +using System; + +namespace EFCoreSecondLevelCacheInterceptor; + +/// <summary> +/// Defines EFCoreSecondLevel's Options +/// </summary> +public static class EFCacheManagerCoreProviderOptions +{ + /// <summary> + /// Introduces the built-in `CacheManagerCoreProvider` to be used as the CacheProvider. + /// </summary> + public static EFCoreSecondLevelCacheOptions UseCacheManagerCoreProvider(this EFCoreSecondLevelCacheOptions options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + options.Settings.CacheProvider = typeof(EFCacheManagerCoreProvider); + + return options; + } + + /// <summary> + /// Introduces the built-in `CacheManagerCoreProvider` to be used as the CacheProvider. + /// If you specify the `Cacheable()` method options, its setting will override this global setting. + /// </summary> + /// <param name="options"></param> + /// <param name="expirationMode">Defines the expiration mode of the cache items globally.</param> + /// <param name="timeout">The expiration timeout.</param> + public static EFCoreSecondLevelCacheOptions UseCacheManagerCoreProvider(this EFCoreSecondLevelCacheOptions options, + CacheExpirationMode expirationMode, + TimeSpan timeout) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + options.Settings.CacheProvider = typeof(EFCacheManagerCoreProvider); + + options.Settings.CachableQueriesOptions = new CachableQueriesOptions + { + ExpirationMode = expirationMode, + Timeout = timeout, + IsActive = true + }; + + return options; + } +} \ No newline at end of file diff --git a/src/EFCoreSecondLevelCacheInterceptor.CacheManager.Core/EFCoreSecondLevelCacheInterceptor.CacheManager.Core.csproj b/src/EFCoreSecondLevelCacheInterceptor.CacheManager.Core/EFCoreSecondLevelCacheInterceptor.CacheManager.Core.csproj new file mode 100644 index 0000000..9dcbb37 --- /dev/null +++ b/src/EFCoreSecondLevelCacheInterceptor.CacheManager.Core/EFCoreSecondLevelCacheInterceptor.CacheManager.Core.csproj @@ -0,0 +1,86 @@ +<Project Sdk="Microsoft.NET.Sdk"> + <PropertyGroup> + <Description>Entity Framework Core Second Level Caching Library.</Description> + <VersionPrefix>5.0.0</VersionPrefix> + <Authors>Vahid Nasiri</Authors> + <TargetFrameworks>net9.0;net8.0;net7.0;net6.0;net5.0;netstandard2.1;netstandard2.0;net462;netcoreapp3.1;</TargetFrameworks> + <GenerateDocumentationFile>true</GenerateDocumentationFile> + <AssemblyName>EFCoreSecondLevelCacheInterceptor.CacheManager.Core</AssemblyName> + <PackageId>EFCoreSecondLevelCacheInterceptor.CacheManager.Core</PackageId> + <PackageTags>EntityFramework;Cache;Caching;SecondLevelCache;EFCore;ORM;.NET Core;aspnetcore</PackageTags> + <PackageProjectUrl>https://github.com/VahidN/EFCoreSecondLevelCacheInterceptor</PackageProjectUrl> + <RepositoryUrl>https://github.com/VahidN/EFCoreSecondLevelCacheInterceptor</RepositoryUrl> + <RepositoryType>git</RepositoryType> + <PackageLicenseExpression>Apache-2.0</PackageLicenseExpression> + <GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute> + <GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute> + <GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute> + <AllowUnsafeBlocks>true</AllowUnsafeBlocks> + <PublishRepositoryUrl>true</PublishRepositoryUrl> + <DebugType>embedded</DebugType> + <EmbedUntrackedSources>true</EmbedUntrackedSources> + <AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder> + <GeneratePackageOnBuild>true</GeneratePackageOnBuild> + <NoWarn>NU5104</NoWarn> + <PackageReadmeFile>README.md</PackageReadmeFile> + </PropertyGroup> + <PropertyGroup> + <LangVersion>latest</LangVersion> + <AnalysisLevel>latest</AnalysisLevel> + <AnalysisMode>AllEnabledByDefault</AnalysisMode> + <CodeAnalysisTreatWarningsAsErrors>true</CodeAnalysisTreatWarningsAsErrors> + <EnableNETAnalyzers>true</EnableNETAnalyzers> + <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild> + <Nullable>enable</Nullable> + <TreatWarningsAsErrors>true</TreatWarningsAsErrors> + <RunAnalyzersDuringBuild>true</RunAnalyzersDuringBuild> + <RunAnalyzersDuringLiveAnalysis>true</RunAnalyzersDuringLiveAnalysis> + <Deterministic>true</Deterministic> + <Features>strict</Features> + <ReportAnalyzer>true</ReportAnalyzer> + </PropertyGroup> + <PropertyGroup> + <NuGetAudit>true</NuGetAudit> + <NuGetAuditMode>all</NuGetAuditMode> + <NuGetAuditLevel>low</NuGetAuditLevel> + <WarningsNotAsErrors Condition="'$(Configuration)' != 'Release'"> + $(WarningsNotAsErrors);NU1900;NU1901;NU1902;NU1903;NU1904 + </WarningsNotAsErrors> + </PropertyGroup> + <ItemGroup> + <PackageReference Include="Meziantou.Analyzer" Version="2.0.184"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + <PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.12.19"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + <PackageReference Include="AsyncFixer" Version="1.6.0"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + <PackageReference Include="Asyncify" Version="0.9.7"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + <PackageReference Include="SonarAnalyzer.CSharp" Version="10.4.0.108396"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + </ItemGroup> + <ItemGroup> + <ProjectReference Include="..\EFCoreSecondLevelCacheInterceptor\EFCoreSecondLevelCacheInterceptor.csproj" /> + </ItemGroup> + <ItemGroup> + <PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All"/> + <PackageReference Include="CacheManager.Core" Version="[1.2,)"/> + <None Include="../../README.md" Link="README.md" Pack="true" PackagePath="/" Visible="false"/> + </ItemGroup> + <ItemGroup Condition="('$(TargetFramework)' == 'net6.0')"> + <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="[6.0.2,)"/> + </ItemGroup> + <ItemGroup Condition="('$(TargetFramework)' == 'net8.0')"> + <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="[8.0.1,)"/> + </ItemGroup> +</Project> diff --git a/src/EFCoreSecondLevelCacheInterceptor.CacheManager.Core/_build.cmd b/src/EFCoreSecondLevelCacheInterceptor.CacheManager.Core/_build.cmd new file mode 100644 index 0000000..b4b0e75 --- /dev/null +++ b/src/EFCoreSecondLevelCacheInterceptor.CacheManager.Core/_build.cmd @@ -0,0 +1,2 @@ +dotnet build +pause \ No newline at end of file diff --git a/src/EFCoreSecondLevelCacheInterceptor.EasyCaching.Core/EFCoreSecondLevelCacheInterceptor.EasyCaching.Core.csproj b/src/EFCoreSecondLevelCacheInterceptor.EasyCaching.Core/EFCoreSecondLevelCacheInterceptor.EasyCaching.Core.csproj new file mode 100644 index 0000000..b48d7cf --- /dev/null +++ b/src/EFCoreSecondLevelCacheInterceptor.EasyCaching.Core/EFCoreSecondLevelCacheInterceptor.EasyCaching.Core.csproj @@ -0,0 +1,86 @@ +<Project Sdk="Microsoft.NET.Sdk"> + <PropertyGroup> + <Description>Entity Framework Core Second Level Caching Library.</Description> + <VersionPrefix>5.0.0</VersionPrefix> + <Authors>Vahid Nasiri</Authors> + <TargetFrameworks>net9.0;net8.0;net7.0;net6.0;net5.0;netstandard2.1;netstandard2.0;net462;netcoreapp3.1;</TargetFrameworks> + <GenerateDocumentationFile>true</GenerateDocumentationFile> + <AssemblyName>EFCoreSecondLevelCacheInterceptor.EasyCaching.Core</AssemblyName> + <PackageId>EFCoreSecondLevelCacheInterceptor.EasyCaching.Core</PackageId> + <PackageTags>EntityFramework;Cache;Caching;SecondLevelCache;EFCore;ORM;.NET Core;aspnetcore</PackageTags> + <PackageProjectUrl>https://github.com/VahidN/EFCoreSecondLevelCacheInterceptor</PackageProjectUrl> + <RepositoryUrl>https://github.com/VahidN/EFCoreSecondLevelCacheInterceptor</RepositoryUrl> + <RepositoryType>git</RepositoryType> + <PackageLicenseExpression>Apache-2.0</PackageLicenseExpression> + <GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute> + <GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute> + <GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute> + <AllowUnsafeBlocks>true</AllowUnsafeBlocks> + <PublishRepositoryUrl>true</PublishRepositoryUrl> + <DebugType>embedded</DebugType> + <EmbedUntrackedSources>true</EmbedUntrackedSources> + <AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder> + <GeneratePackageOnBuild>true</GeneratePackageOnBuild> + <NoWarn>NU5104</NoWarn> + <PackageReadmeFile>README.md</PackageReadmeFile> + </PropertyGroup> + <PropertyGroup> + <LangVersion>latest</LangVersion> + <AnalysisLevel>latest</AnalysisLevel> + <AnalysisMode>AllEnabledByDefault</AnalysisMode> + <CodeAnalysisTreatWarningsAsErrors>true</CodeAnalysisTreatWarningsAsErrors> + <EnableNETAnalyzers>true</EnableNETAnalyzers> + <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild> + <Nullable>enable</Nullable> + <TreatWarningsAsErrors>true</TreatWarningsAsErrors> + <RunAnalyzersDuringBuild>true</RunAnalyzersDuringBuild> + <RunAnalyzersDuringLiveAnalysis>true</RunAnalyzersDuringLiveAnalysis> + <Deterministic>true</Deterministic> + <Features>strict</Features> + <ReportAnalyzer>true</ReportAnalyzer> + </PropertyGroup> + <PropertyGroup> + <NuGetAudit>true</NuGetAudit> + <NuGetAuditMode>all</NuGetAuditMode> + <NuGetAuditLevel>low</NuGetAuditLevel> + <WarningsNotAsErrors Condition="'$(Configuration)' != 'Release'"> + $(WarningsNotAsErrors);NU1900;NU1901;NU1902;NU1903;NU1904 + </WarningsNotAsErrors> + </PropertyGroup> + <ItemGroup> + <PackageReference Include="Meziantou.Analyzer" Version="2.0.184"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + <PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.12.19"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + <PackageReference Include="AsyncFixer" Version="1.6.0"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + <PackageReference Include="Asyncify" Version="0.9.7"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + <PackageReference Include="SonarAnalyzer.CSharp" Version="10.4.0.108396"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + </ItemGroup> + <ItemGroup> + <ProjectReference Include="..\EFCoreSecondLevelCacheInterceptor\EFCoreSecondLevelCacheInterceptor.csproj" /> + </ItemGroup> + <ItemGroup> + <PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All"/> + <PackageReference Include="EasyCaching.Core" Version="[1.9.2,)"/> + <None Include="../../README.md" Link="README.md" Pack="true" PackagePath="/" Visible="false"/> + </ItemGroup> + <ItemGroup Condition="('$(TargetFramework)' == 'net6.0')"> + <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="[6.0.2,)"/> + </ItemGroup> + <ItemGroup Condition="('$(TargetFramework)' == 'net8.0')"> + <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="[8.0.1,)"/> + </ItemGroup> +</Project> diff --git a/src/EFCoreSecondLevelCacheInterceptor/EFEasyCachingCoreProvider.cs b/src/EFCoreSecondLevelCacheInterceptor.EasyCaching.Core/EFEasyCachingCoreProvider.cs similarity index 85% rename from src/EFCoreSecondLevelCacheInterceptor/EFEasyCachingCoreProvider.cs rename to src/EFCoreSecondLevelCacheInterceptor.EasyCaching.Core/EFEasyCachingCoreProvider.cs index 8569b04..35880cc 100644 --- a/src/EFCoreSecondLevelCacheInterceptor/EFEasyCachingCoreProvider.cs +++ b/src/EFCoreSecondLevelCacheInterceptor.EasyCaching.Core/EFEasyCachingCoreProvider.cs @@ -13,6 +13,7 @@ namespace EFCoreSecondLevelCacheInterceptor; /// </summary> public class EFEasyCachingCoreProvider : IEFCacheServiceProvider { + private readonly IEFCacheKeyPrefixProvider _cacheKeyPrefixProvider; private readonly EFCoreSecondLevelCacheSettings _cacheSettings; private readonly ILogger<EFEasyCachingCoreProvider> _easyCachingCoreProviderLogger; private readonly IEFDebugLogger _logger; @@ -22,11 +23,12 @@ public class EFEasyCachingCoreProvider : IEFCacheServiceProvider private readonly IServiceProvider _serviceProvider; /// <summary> - /// Using IMemoryCache as a cache service. + /// Using EasyCachingCore as a cache service. /// </summary> public EFEasyCachingCoreProvider(IOptions<EFCoreSecondLevelCacheSettings> cacheSettings, IServiceProvider serviceProvider, IEFDebugLogger logger, + IEFCacheKeyPrefixProvider cacheKeyPrefixProvider, ILogger<EFEasyCachingCoreProvider> easyCachingCoreProviderLogger) { if (cacheSettings == null) @@ -37,6 +39,7 @@ public EFEasyCachingCoreProvider(IOptions<EFCoreSecondLevelCacheSettings> cacheS _cacheSettings = cacheSettings.Value; _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); _logger = logger; + _cacheKeyPrefixProvider = cacheKeyPrefixProvider; _easyCachingCoreProviderLogger = easyCachingCoreProviderLogger; } @@ -46,7 +49,7 @@ public EFEasyCachingCoreProvider(IOptions<EFCoreSecondLevelCacheSettings> cacheS /// <param name="cacheKey">key</param> /// <param name="value">value</param> /// <param name="cachePolicy">Defines the expiration mode of the cache item.</param> - public void InsertValue(EFCacheKey cacheKey, EFCachedData value, EFCachePolicy cachePolicy) + public void InsertValue(EFCacheKey cacheKey, EFCachedData? value, EFCachePolicy cachePolicy) { if (cacheKey is null) { @@ -99,10 +102,28 @@ public void InsertValue(EFCacheKey cacheKey, EFCachedData value, EFCachePolicy c /// </summary> public void ClearAllCachedEntries() { - if (!_cacheSettings.IsHybridCache) + var easyCachingProvider = GetEasyCachingProvider(cacheKey: null); + + switch (easyCachingProvider) { - var easyCachingProvider = GetEasyCachingProvider(cacheKey: null); - ((IEasyCachingProvider)easyCachingProvider).Flush(); + case IHybridCachingProvider hcp: + // IHybridCachingProvider doesn't have a `Flush()` method: + // https://github.com/dotnetcore/EasyCaching/issues/351 + var cacheKeyPrefix = _cacheKeyPrefixProvider.GetCacheKeyPrefix(); + + if (string.IsNullOrWhiteSpace(cacheKeyPrefix)) + { + throw new InvalidOperationException( + message: "Please specify a CacheKeyPrefix by calling the `.UseCacheKeyPrefix(...)` method."); + } + + hcp.RemoveByPrefix(cacheKeyPrefix); + + break; + case IEasyCachingProvider ecp: + ecp.Flush(); + + break; } _logger.NotifyCacheInvalidation(clearAllCachedEntries: true, @@ -128,7 +149,7 @@ public void ClearAllCachedEntries() } /// <summary> - /// Invalidates all of the cache entries which are dependent on any of the specified root keys. + /// Invalidates all the cache entries which are dependent on any of the specified root keys. /// </summary> /// <param name="cacheKey">Stores information of the computed key of the input LINQ query.</param> public void InvalidateCacheDependencies(EFCacheKey cacheKey) diff --git a/src/EFCoreSecondLevelCacheInterceptor.EasyCaching.Core/EFEasyCachingCoreProviderOptions.cs b/src/EFCoreSecondLevelCacheInterceptor.EasyCaching.Core/EFEasyCachingCoreProviderOptions.cs new file mode 100644 index 0000000..0b6db56 --- /dev/null +++ b/src/EFCoreSecondLevelCacheInterceptor.EasyCaching.Core/EFEasyCachingCoreProviderOptions.cs @@ -0,0 +1,129 @@ +using System; + +namespace EFCoreSecondLevelCacheInterceptor; + +/// <summary> +/// Defines EFCoreSecondLevel's Options +/// </summary> +public static class EFEasyCachingCoreProviderOptions +{ + /// <summary> + /// Introduces the built-in `EasyCachingCoreProvider` to be used as the CacheProvider. + /// </summary> + /// <param name="options"></param> + /// <param name="providerName">Selected caching provider name.</param> + /// <param name="isHybridCache">Is an instance of EasyCaching.HybridCache</param> + public static EFCoreSecondLevelCacheOptions UseEasyCachingCoreProvider(this EFCoreSecondLevelCacheOptions options, + string providerName, + bool isHybridCache = false) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + options.Settings.CacheProvider = typeof(EFEasyCachingCoreProvider); + options.Settings.ProviderName = providerName; + options.Settings.IsHybridCache = isHybridCache; + + return options; + } + + /// <summary> + /// Introduces the built-in `EasyCachingCoreProvider` to be used as the CacheProvider. + /// </summary> + /// <param name="options"></param> + /// <param name="providerName"> + /// Selected caching provider name. + /// This option will let you choose a different redis database for your current tenant. + /// <![CDATA[ Such as: (serviceProvider, cacheKey) => "redis-db-" + serviceProvider.GetRequiredService<IHttpContextAccesor>().HttpContext.Request.Headers["tenant-id"]; ]]> + /// </param> + /// <param name="isHybridCache">Is an instance of EasyCaching.HybridCache</param> + public static EFCoreSecondLevelCacheOptions UseEasyCachingCoreProvider(this EFCoreSecondLevelCacheOptions options, + Func<IServiceProvider, EFCacheKey?, string> providerName, + bool isHybridCache = false) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + options.Settings.CacheProvider = typeof(EFEasyCachingCoreProvider); + options.Settings.CacheProviderName = providerName; + options.Settings.IsHybridCache = isHybridCache; + + return options; + } + + /// <summary> + /// Introduces the built-in `EasyCachingCoreProvider` to be used as the CacheProvider. + /// If you specify the `Cacheable()` method options, its setting will override this global setting. + /// </summary> + /// <param name="options"></param> + /// <param name="providerName">Selected caching provider name.</param> + /// <param name="expirationMode">Defines the expiration mode of the cache items globally.</param> + /// <param name="timeout">The expiration timeout.</param> + /// <param name="isHybridCache">Is an instance of EasyCaching.HybridCache</param> + public static EFCoreSecondLevelCacheOptions UseEasyCachingCoreProvider(this EFCoreSecondLevelCacheOptions options, + string providerName, + CacheExpirationMode expirationMode, + TimeSpan timeout, + bool isHybridCache = false) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + options.Settings.CacheProvider = typeof(EFEasyCachingCoreProvider); + options.Settings.ProviderName = providerName; + options.Settings.IsHybridCache = isHybridCache; + + options.Settings.CachableQueriesOptions = new CachableQueriesOptions + { + ExpirationMode = expirationMode, + Timeout = timeout, + IsActive = true + }; + + return options; + } + + /// <summary> + /// Introduces the built-in `EasyCachingCoreProvider` to be used as the CacheProvider. + /// If you specify the `Cacheable()` method options, its setting will override this global setting. + /// </summary> + /// <param name="options"></param> + /// <param name="providerName"> + /// Selected caching provider name. + /// This option will let you choose a different redis database for your current tenant. + /// <![CDATA[ Such as: (serviceProvider, cacheKey) => "redis-db-" + serviceProvider.GetRequiredService<IHttpContextAccesor>().HttpContext.Request.Headers["tenant-id"]; ]]> + /// </param> + /// <param name="expirationMode">Defines the expiration mode of the cache items globally.</param> + /// <param name="timeout">The expiration timeout.</param> + /// <param name="isHybridCache">Is an instance of EasyCaching.HybridCache</param> + public static EFCoreSecondLevelCacheOptions UseEasyCachingCoreProvider(this EFCoreSecondLevelCacheOptions options, + Func<IServiceProvider, EFCacheKey?, string> providerName, + CacheExpirationMode expirationMode, + TimeSpan timeout, + bool isHybridCache = false) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + options.Settings.CacheProvider = typeof(EFEasyCachingCoreProvider); + options.Settings.CacheProviderName = providerName; + options.Settings.IsHybridCache = isHybridCache; + + options.Settings.CachableQueriesOptions = new CachableQueriesOptions + { + ExpirationMode = expirationMode, + Timeout = timeout, + IsActive = true + }; + + return options; + } +} \ No newline at end of file diff --git a/src/EFCoreSecondLevelCacheInterceptor.EasyCaching.Core/_build.cmd b/src/EFCoreSecondLevelCacheInterceptor.EasyCaching.Core/_build.cmd new file mode 100644 index 0000000..b4b0e75 --- /dev/null +++ b/src/EFCoreSecondLevelCacheInterceptor.EasyCaching.Core/_build.cmd @@ -0,0 +1,2 @@ +dotnet build +pause \ No newline at end of file diff --git a/src/EFCoreSecondLevelCacheInterceptor.FusionCache/EFCoreSecondLevelCacheInterceptor.FusionCache.csproj b/src/EFCoreSecondLevelCacheInterceptor.FusionCache/EFCoreSecondLevelCacheInterceptor.FusionCache.csproj new file mode 100644 index 0000000..423f787 --- /dev/null +++ b/src/EFCoreSecondLevelCacheInterceptor.FusionCache/EFCoreSecondLevelCacheInterceptor.FusionCache.csproj @@ -0,0 +1,80 @@ +<Project Sdk="Microsoft.NET.Sdk"> + <PropertyGroup> + <Description>Entity Framework Core Second Level Caching Library.</Description> + <VersionPrefix>5.0.0</VersionPrefix> + <Authors>Vahid Nasiri</Authors> + <TargetFrameworks>net9.0;net8.0;</TargetFrameworks> + <GenerateDocumentationFile>true</GenerateDocumentationFile> + <AssemblyName>EFCoreSecondLevelCacheInterceptor.FusionCache</AssemblyName> + <PackageId>EFCoreSecondLevelCacheInterceptor.FusionCache</PackageId> + <PackageTags>EntityFramework;Cache;Caching;SecondLevelCache;EFCore;ORM;.NET Core;aspnetcore</PackageTags> + <PackageProjectUrl>https://github.com/VahidN/EFCoreSecondLevelCacheInterceptor</PackageProjectUrl> + <RepositoryUrl>https://github.com/VahidN/EFCoreSecondLevelCacheInterceptor</RepositoryUrl> + <RepositoryType>git</RepositoryType> + <PackageLicenseExpression>Apache-2.0</PackageLicenseExpression> + <GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute> + <GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute> + <GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute> + <AllowUnsafeBlocks>true</AllowUnsafeBlocks> + <PublishRepositoryUrl>true</PublishRepositoryUrl> + <DebugType>embedded</DebugType> + <EmbedUntrackedSources>true</EmbedUntrackedSources> + <AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder> + <GeneratePackageOnBuild>true</GeneratePackageOnBuild> + <NoWarn>NU5104</NoWarn> + <PackageReadmeFile>README.md</PackageReadmeFile> + </PropertyGroup> + <PropertyGroup> + <LangVersion>latest</LangVersion> + <AnalysisLevel>latest</AnalysisLevel> + <AnalysisMode>AllEnabledByDefault</AnalysisMode> + <CodeAnalysisTreatWarningsAsErrors>true</CodeAnalysisTreatWarningsAsErrors> + <EnableNETAnalyzers>true</EnableNETAnalyzers> + <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild> + <Nullable>enable</Nullable> + <TreatWarningsAsErrors>true</TreatWarningsAsErrors> + <RunAnalyzersDuringBuild>true</RunAnalyzersDuringBuild> + <RunAnalyzersDuringLiveAnalysis>true</RunAnalyzersDuringLiveAnalysis> + <Deterministic>true</Deterministic> + <Features>strict</Features> + <ReportAnalyzer>true</ReportAnalyzer> + </PropertyGroup> + <PropertyGroup> + <NuGetAudit>true</NuGetAudit> + <NuGetAuditMode>all</NuGetAuditMode> + <NuGetAuditLevel>low</NuGetAuditLevel> + <WarningsNotAsErrors Condition="'$(Configuration)' != 'Release'"> + $(WarningsNotAsErrors);NU1900;NU1901;NU1902;NU1903;NU1904 + </WarningsNotAsErrors> + </PropertyGroup> + <ItemGroup> + <PackageReference Include="Meziantou.Analyzer" Version="2.0.184"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + <PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.12.19"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + <PackageReference Include="AsyncFixer" Version="1.6.0"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + <PackageReference Include="Asyncify" Version="0.9.7"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + <PackageReference Include="SonarAnalyzer.CSharp" Version="10.4.0.108396"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + </ItemGroup> + <ItemGroup> + <ProjectReference Include="..\EFCoreSecondLevelCacheInterceptor\EFCoreSecondLevelCacheInterceptor.csproj" /> + </ItemGroup> + <ItemGroup> + <PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All"/> + <PackageReference Include="ZiggyCreatures.FusionCache" Version="[2.0.0-preview-4,)"/> + <None Include="../../README.md" Link="README.md" Pack="true" PackagePath="/" Visible="false"/> + </ItemGroup> +</Project> diff --git a/src/EFCoreSecondLevelCacheInterceptor.FusionCache/EFFusionCacheDependenciesStore.cs b/src/EFCoreSecondLevelCacheInterceptor.FusionCache/EFFusionCacheDependenciesStore.cs new file mode 100644 index 0000000..823089c --- /dev/null +++ b/src/EFCoreSecondLevelCacheInterceptor.FusionCache/EFFusionCacheDependenciesStore.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using ZiggyCreatures.Caching.Fusion; + +namespace EFCoreSecondLevelCacheInterceptor; + +/// <summary> +/// Provides information about the in-use cache-dependencies +/// </summary> +public class EFFusionCacheDependenciesStore(IFusionCache fusionCache) : IEFFusionCacheDependenciesStore +{ + private const string TagsCacheKey = "__EF__FusionCache__Tags__"; + + private static readonly TimeSpan TimeOut = TimeSpan.FromDays(value: 1); + + /// <summary> + /// Adds the given tags list to the list of current tags + /// </summary> + /// <param name="tags"></param> + public void AddCacheDependencies(IEnumerable<string>? tags) + { + tags ??= []; + var currentTags = fusionCache.GetOrDefault<List<string>>(TagsCacheKey) ?? []; + + foreach (var tag in tags) + { + if (!currentTags.Contains(tag)) + { + currentTags.Add(tag); + } + } + + fusionCache.Set(TagsCacheKey, currentTags, options => { options.SetDuration(TimeOut); }); + } + + /// <summary> + /// Removes the given tags list from the list of current tags + /// </summary> + /// <param name="tags"></param> + public void RemoveCacheDependencies(IEnumerable<string>? tags) + { + tags ??= []; + var currentTags = fusionCache.GetOrDefault<List<string>>(TagsCacheKey) ?? []; + + foreach (var tag in tags) + { + currentTags.Remove(tag); + } + + fusionCache.Set(TagsCacheKey, currentTags, options => { options.SetDuration(TimeOut); }); + } + + /// <summary> + /// Returns the cached entries added by this library. + /// </summary> + public ISet<string> GetAllCacheDependencies() + => new HashSet<string>(fusionCache.GetOrDefault<List<string>>(TagsCacheKey) ?? [], StringComparer.Ordinal); +} \ No newline at end of file diff --git a/src/EFCoreSecondLevelCacheInterceptor.FusionCache/EFFusionCacheProvider.cs b/src/EFCoreSecondLevelCacheInterceptor.FusionCache/EFFusionCacheProvider.cs new file mode 100644 index 0000000..717a6e8 --- /dev/null +++ b/src/EFCoreSecondLevelCacheInterceptor.FusionCache/EFFusionCacheProvider.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Generic; +using ZiggyCreatures.Caching.Fusion; + +namespace EFCoreSecondLevelCacheInterceptor; + +/// <remarks> +/// Using FusionCache as a cache service. +/// </remarks> +public class EFFusionCacheProvider( + IFusionCache fusionCache, + IEFFusionCacheDependenciesStore cacheDependenciesStore, + IEFDebugLogger logger) : IEFCacheServiceProvider +{ + /// <summary> + /// Adds a new item to the cache. + /// </summary> + /// <param name="cacheKey">key</param> + /// <param name="value">value</param> + /// <param name="cachePolicy">Defines the expiration mode of the cache item.</param> + public void InsertValue(EFCacheKey cacheKey, EFCachedData? value, EFCachePolicy cachePolicy) + { + ArgumentNullException.ThrowIfNull(cacheKey); + ArgumentNullException.ThrowIfNull(cachePolicy); + + value ??= new EFCachedData + { + IsNull = true + }; + + fusionCache.Set(cacheKey.KeyHash, value, entryOptions => + { + entryOptions.SetDuration(cachePolicy.CacheTimeout); + + if (cachePolicy.CacheExpirationMode == CacheExpirationMode.Sliding) + { + entryOptions.SetFailSafe(isEnabled: true, cachePolicy.CacheTimeout.Add(cachePolicy.CacheTimeout)); + } + }, cacheKey.CacheDependencies); + + cacheDependenciesStore.AddCacheDependencies(cacheKey.CacheDependencies); + } + + /// <summary> + /// Removes the cached entries added by this library. + /// </summary> + public void ClearAllCachedEntries() + { + InvalidateTaggedEntries(cacheDependenciesStore.GetAllCacheDependencies()); + + logger.NotifyCacheInvalidation(clearAllCachedEntries: true, + new HashSet<string>(StringComparer.OrdinalIgnoreCase)); + } + + /// <summary> + /// Gets a cached entry by key. + /// </summary> + /// <param name="cacheKey">key to find</param> + /// <returns>cached value</returns> + /// <param name="cachePolicy">Defines the expiration mode of the cache item.</param> + public EFCachedData? GetValue(EFCacheKey cacheKey, EFCachePolicy cachePolicy) + { + ArgumentNullException.ThrowIfNull(cacheKey); + + return fusionCache.GetOrDefault<EFCachedData>(cacheKey.KeyHash); + } + + /// <summary> + /// Invalidates all the cache entries which are dependent on any of the specified root keys. + /// </summary> + /// <param name="cacheKey">Stores information of the computed key of the input LINQ query.</param> + public void InvalidateCacheDependencies(EFCacheKey cacheKey) + { + ArgumentNullException.ThrowIfNull(cacheKey); + + InvalidateTaggedEntries(cacheKey.CacheDependencies); + } + + private void InvalidateTaggedEntries(ISet<string> cacheDependencies) + { + foreach (var rootCacheKey in cacheDependencies) + { + fusionCache.RemoveByTag(rootCacheKey); + } + + cacheDependenciesStore.RemoveCacheDependencies(cacheDependencies); + } +} \ No newline at end of file diff --git a/src/EFCoreSecondLevelCacheInterceptor.FusionCache/EFFusionCacheProviderOptions.cs b/src/EFCoreSecondLevelCacheInterceptor.FusionCache/EFFusionCacheProviderOptions.cs new file mode 100644 index 0000000..f009ae0 --- /dev/null +++ b/src/EFCoreSecondLevelCacheInterceptor.FusionCache/EFFusionCacheProviderOptions.cs @@ -0,0 +1,48 @@ +using System; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace EFCoreSecondLevelCacheInterceptor; + +/// <summary> +/// Defines EFCoreSecondLevel's Options +/// </summary> +public static class EFFusionCacheProviderOptions +{ + /// <summary> + /// Introduces the `EFFusionCacheProvider` to be used as the CacheProvider. + /// </summary> + public static EFCoreSecondLevelCacheOptions UseFusionCacheProvider(this EFCoreSecondLevelCacheOptions options) + { + ArgumentNullException.ThrowIfNull(options); + + options.Settings.Services?.TryAddSingleton<IEFFusionCacheDependenciesStore, EFFusionCacheDependenciesStore>(); + options.Settings.CacheProvider = typeof(EFFusionCacheProvider); + + return options; + } + + /// <summary> + /// Introduces the `EFFusionCacheProvider` to be used as the CacheProvider. + /// If you specify the `Cacheable()` method options, its setting will override this global setting. + /// </summary> + /// <param name="options"></param> + /// <param name="expirationMode">Defines the expiration mode of the cache items globally.</param> + /// <param name="timeout">The expiration timeout.</param> + public static EFCoreSecondLevelCacheOptions UseFusionCacheProvider(this EFCoreSecondLevelCacheOptions options, + CacheExpirationMode expirationMode, + TimeSpan timeout) + { + ArgumentNullException.ThrowIfNull(options); + + options.UseFusionCacheProvider(); + + options.Settings.CachableQueriesOptions = new CachableQueriesOptions + { + ExpirationMode = expirationMode, + Timeout = timeout, + IsActive = true + }; + + return options; + } +} \ No newline at end of file diff --git a/src/EFCoreSecondLevelCacheInterceptor.FusionCache/IEFFusionCacheDependenciesStore.cs b/src/EFCoreSecondLevelCacheInterceptor.FusionCache/IEFFusionCacheDependenciesStore.cs new file mode 100644 index 0000000..00aa9f1 --- /dev/null +++ b/src/EFCoreSecondLevelCacheInterceptor.FusionCache/IEFFusionCacheDependenciesStore.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace EFCoreSecondLevelCacheInterceptor; + +/// <summary> +/// Provides information about the in-use cache-dependencies +/// </summary> +public interface IEFFusionCacheDependenciesStore +{ + /// <summary> + /// Adds the given tags list to the list of current tags + /// </summary> + /// <param name="tags"></param> + void AddCacheDependencies(IEnumerable<string>? tags); + + /// <summary> + /// Removes the given tags list from the list of current tags + /// </summary> + /// <param name="tags"></param> + void RemoveCacheDependencies(IEnumerable<string>? tags); + + /// <summary> + /// Returns the cached entries added by this library. + /// </summary> + ISet<string> GetAllCacheDependencies(); +} \ No newline at end of file diff --git a/src/EFCoreSecondLevelCacheInterceptor.FusionCache/_build.cmd b/src/EFCoreSecondLevelCacheInterceptor.FusionCache/_build.cmd new file mode 100644 index 0000000..b4b0e75 --- /dev/null +++ b/src/EFCoreSecondLevelCacheInterceptor.FusionCache/_build.cmd @@ -0,0 +1,2 @@ +dotnet build +pause \ No newline at end of file diff --git a/src/EFCoreSecondLevelCacheInterceptor.HybridCache/EFCoreSecondLevelCacheInterceptor.HybridCache.csproj b/src/EFCoreSecondLevelCacheInterceptor.HybridCache/EFCoreSecondLevelCacheInterceptor.HybridCache.csproj new file mode 100644 index 0000000..c7ff3dc --- /dev/null +++ b/src/EFCoreSecondLevelCacheInterceptor.HybridCache/EFCoreSecondLevelCacheInterceptor.HybridCache.csproj @@ -0,0 +1,80 @@ +<Project Sdk="Microsoft.NET.Sdk"> + <PropertyGroup> + <Description>Entity Framework Core Second Level Caching Library.</Description> + <VersionPrefix>5.0.0</VersionPrefix> + <Authors>Vahid Nasiri</Authors> + <TargetFrameworks>net9.0;net8.0;</TargetFrameworks> + <GenerateDocumentationFile>true</GenerateDocumentationFile> + <AssemblyName>EFCoreSecondLevelCacheInterceptor.HybridCache</AssemblyName> + <PackageId>EFCoreSecondLevelCacheInterceptor.HybridCache</PackageId> + <PackageTags>EntityFramework;Cache;Caching;SecondLevelCache;EFCore;ORM;.NET Core;aspnetcore</PackageTags> + <PackageProjectUrl>https://github.com/VahidN/EFCoreSecondLevelCacheInterceptor</PackageProjectUrl> + <RepositoryUrl>https://github.com/VahidN/EFCoreSecondLevelCacheInterceptor</RepositoryUrl> + <RepositoryType>git</RepositoryType> + <PackageLicenseExpression>Apache-2.0</PackageLicenseExpression> + <GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute> + <GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute> + <GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute> + <AllowUnsafeBlocks>true</AllowUnsafeBlocks> + <PublishRepositoryUrl>true</PublishRepositoryUrl> + <DebugType>embedded</DebugType> + <EmbedUntrackedSources>true</EmbedUntrackedSources> + <AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder> + <GeneratePackageOnBuild>true</GeneratePackageOnBuild> + <NoWarn>NU5104</NoWarn> + <PackageReadmeFile>README.md</PackageReadmeFile> + </PropertyGroup> + <PropertyGroup> + <LangVersion>latest</LangVersion> + <AnalysisLevel>latest</AnalysisLevel> + <AnalysisMode>AllEnabledByDefault</AnalysisMode> + <CodeAnalysisTreatWarningsAsErrors>true</CodeAnalysisTreatWarningsAsErrors> + <EnableNETAnalyzers>true</EnableNETAnalyzers> + <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild> + <Nullable>enable</Nullable> + <TreatWarningsAsErrors>true</TreatWarningsAsErrors> + <RunAnalyzersDuringBuild>true</RunAnalyzersDuringBuild> + <RunAnalyzersDuringLiveAnalysis>true</RunAnalyzersDuringLiveAnalysis> + <Deterministic>true</Deterministic> + <Features>strict</Features> + <ReportAnalyzer>true</ReportAnalyzer> + </PropertyGroup> + <PropertyGroup> + <NuGetAudit>true</NuGetAudit> + <NuGetAuditMode>all</NuGetAuditMode> + <NuGetAuditLevel>low</NuGetAuditLevel> + <WarningsNotAsErrors Condition="'$(Configuration)' != 'Release'"> + $(WarningsNotAsErrors);NU1900;NU1901;NU1902;NU1903;NU1904 + </WarningsNotAsErrors> + </PropertyGroup> + <ItemGroup> + <PackageReference Include="Meziantou.Analyzer" Version="2.0.184"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + <PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.12.19"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + <PackageReference Include="AsyncFixer" Version="1.6.0"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + <PackageReference Include="Asyncify" Version="0.9.7"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + <PackageReference Include="SonarAnalyzer.CSharp" Version="10.4.0.108396"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + </ItemGroup> + <ItemGroup> + <ProjectReference Include="..\EFCoreSecondLevelCacheInterceptor\EFCoreSecondLevelCacheInterceptor.csproj" /> + </ItemGroup> + <ItemGroup> + <PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All"/> + <PackageReference Include="Microsoft.Extensions.Caching.Hybrid" Version="[9.0.0-preview.9.24556.5,)"/> + <None Include="../../README.md" Link="README.md" Pack="true" PackagePath="/" Visible="false"/> + </ItemGroup> +</Project> diff --git a/src/EFCoreSecondLevelCacheInterceptor.HybridCache/EFHybridCacheDependenciesStore.cs b/src/EFCoreSecondLevelCacheInterceptor.HybridCache/EFHybridCacheDependenciesStore.cs new file mode 100644 index 0000000..53b641d --- /dev/null +++ b/src/EFCoreSecondLevelCacheInterceptor.HybridCache/EFHybridCacheDependenciesStore.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Hybrid; + +namespace EFCoreSecondLevelCacheInterceptor; + +/// <summary> +/// Provides information about the in-use cache-dependencies +/// </summary> +public class EFHybridCacheDependenciesStore(HybridCache hybridCache) : IEFHybridCacheDependenciesStore +{ + private const string TagsCacheKey = "__EF__HybridCache__Tags__"; + + private static readonly HybridCacheEntryOptions Options = new() + { + Expiration = TimeSpan.FromDays(value: 1), + LocalCacheExpiration = TimeSpan.FromDays(value: 1) + }; + + /// <summary> + /// Adds the given tags list to the list of current tags + /// </summary> + /// <param name="tags"></param> + public void AddCacheDependencies(IEnumerable<string>? tags) + { + tags ??= []; + + // NOTE: hybridCache doesn't have a synchronous API!! So ... we will wait! + + var currentTags = await hybridCache.GetOrCreateAsync<List<string>>(TagsCacheKey, + factory => ValueTask.FromResult<List<string>>([])); + + foreach (var tag in tags) + { + if (!currentTags.Contains(tag, StringComparer.Ordinal)) + { + currentTags.Add(tag); + } + } + + hybridCache.SetAsync(TagsCacheKey, currentTags, Options); + } + + /// <summary> + /// Removes the given tags list from the list of current tags + /// </summary> + /// <param name="tags"></param> + public void RemoveCacheDependencies(IEnumerable<string>? tags) + { + tags ??= []; + + var currentTags = await hybridCache.GetOrCreateAsync<List<string>>(TagsCacheKey, + factory => ValueTask.FromResult<List<string>>([])); + + foreach (var tag in tags) + { + currentTags.Remove(tag); + } + + hybridCache.SetAsync(TagsCacheKey, currentTags, Options); + } + + /// <summary> + /// Returns the cached entries added by this library. + /// </summary> + public ISet<string> GetAllCacheDependencies() + => new HashSet<string>( + await hybridCache.GetOrCreateAsync<List<string>>(TagsCacheKey, + factory => ValueTask.FromResult<List<string>>([])), StringComparer.Ordinal); +} \ No newline at end of file diff --git a/src/EFCoreSecondLevelCacheInterceptor.HybridCache/EFHybridCacheProvider.cs b/src/EFCoreSecondLevelCacheInterceptor.HybridCache/EFHybridCacheProvider.cs new file mode 100644 index 0000000..d973486 --- /dev/null +++ b/src/EFCoreSecondLevelCacheInterceptor.HybridCache/EFHybridCacheProvider.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Extensions.Caching.Hybrid; + +namespace EFCoreSecondLevelCacheInterceptor; + +/// <remarks> +/// Using MS.HybridCache as a cache service. +/// </remarks> +public class EFHybridCacheProvider( + HybridCache hybridCache, + IEFHybridCacheDependenciesStore cacheDependenciesStore, + IEFDebugLogger logger) : IEFCacheServiceProvider +{ + /// <summary> + /// Adds a new item to the cache. + /// </summary> + /// <param name="cacheKey">key</param> + /// <param name="value">value</param> + /// <param name="cachePolicy">Defines the expiration mode of the cache item.</param> + public void InsertValue(EFCacheKey cacheKey, EFCachedData? value, EFCachePolicy cachePolicy) + { + ArgumentNullException.ThrowIfNull(cacheKey); + ArgumentNullException.ThrowIfNull(cachePolicy); + + // NOTE: hybridCache doesn't have a synchronous API!! So ... we will wait! + + value ??= new EFCachedData + { + IsNull = true + }; + + hybridCache.SetAsync(cacheKey.KeyHash, value, new HybridCacheEntryOptions + { + Expiration = cachePolicy.CacheTimeout, + LocalCacheExpiration = cachePolicy.CacheTimeout + }, cacheKey.CacheDependencies); + + cacheDependenciesStore.AddCacheDependencies(cacheKey.CacheDependencies); + } + + /// <summary> + /// Removes the cached entries added by this library. + /// </summary> + public void ClearAllCachedEntries() + { + InvalidateTaggedEntries(cacheDependenciesStore.GetAllCacheDependencies()); + + logger.NotifyCacheInvalidation(clearAllCachedEntries: true, + new HashSet<string>(StringComparer.OrdinalIgnoreCase)); + } + + /// <summary> + /// Gets a cached entry by key. + /// </summary> + /// <param name="cacheKey">key to find</param> + /// <returns>cached value</returns> + /// <param name="cachePolicy">Defines the expiration mode of the cache item.</param> + public EFCachedData? GetValue(EFCacheKey cacheKey, EFCachePolicy cachePolicy) + { + ArgumentNullException.ThrowIfNull(cacheKey); + + return hybridCache.GetOrCreateAsync<EFCachedData?>(cacheKey.KeyHash, factory + => ValueTask.FromResult<EFCachedData?>(new EFCachedData + { + IsNull = true + })); + } + + /// <summary> + /// Invalidates all the cache entries which are dependent on any of the specified root keys. + /// </summary> + /// <param name="cacheKey">Stores information of the computed key of the input LINQ query.</param> + public void InvalidateCacheDependencies(EFCacheKey cacheKey) + { + ArgumentNullException.ThrowIfNull(cacheKey); + + InvalidateTaggedEntries(cacheKey.CacheDependencies); + } + + private void InvalidateTaggedEntries(ISet<string> cacheDependencies) + { + foreach (var rootCacheKey in cacheDependencies) + { + hybridCache.RemoveByTagAsync(rootCacheKey); + } + + cacheDependenciesStore.RemoveCacheDependencies(cacheDependencies); + } +} \ No newline at end of file diff --git a/src/EFCoreSecondLevelCacheInterceptor.HybridCache/EFHybridCacheProviderOptions.cs b/src/EFCoreSecondLevelCacheInterceptor.HybridCache/EFHybridCacheProviderOptions.cs new file mode 100644 index 0000000..352d05f --- /dev/null +++ b/src/EFCoreSecondLevelCacheInterceptor.HybridCache/EFHybridCacheProviderOptions.cs @@ -0,0 +1,48 @@ +using System; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace EFCoreSecondLevelCacheInterceptor; + +/// <summary> +/// Defines EFCoreSecondLevel's Options +/// </summary> +public static class EFHybridCacheProviderOptions +{ + /// <summary> + /// Introduces the `EFHybridCacheProvider` to be used as the CacheProvider. + /// </summary> + public static EFCoreSecondLevelCacheOptions UseHybridCacheProvider(this EFCoreSecondLevelCacheOptions options) + { + ArgumentNullException.ThrowIfNull(options); + + options.Settings.Services?.TryAddSingleton<IEFHybridCacheDependenciesStore, EFHybridCacheDependenciesStore>(); + options.Settings.CacheProvider = typeof(EFHybridCacheProvider); + + return options; + } + + /// <summary> + /// Introduces the `EFHybridCacheProvider` to be used as the CacheProvider. + /// If you specify the `Cacheable()` method options, its setting will override this global setting. + /// </summary> + /// <param name="options"></param> + /// <param name="expirationMode">Defines the expiration mode of the cache items globally.</param> + /// <param name="timeout">The expiration timeout.</param> + public static EFCoreSecondLevelCacheOptions UseHybridCacheProvider(this EFCoreSecondLevelCacheOptions options, + CacheExpirationMode expirationMode, + TimeSpan timeout) + { + ArgumentNullException.ThrowIfNull(options); + + options.UseHybridCacheProvider(); + + options.Settings.CachableQueriesOptions = new CachableQueriesOptions + { + ExpirationMode = expirationMode, + Timeout = timeout, + IsActive = true + }; + + return options; + } +} \ No newline at end of file diff --git a/src/EFCoreSecondLevelCacheInterceptor.HybridCache/IEFHybridCacheDependenciesStore.cs b/src/EFCoreSecondLevelCacheInterceptor.HybridCache/IEFHybridCacheDependenciesStore.cs new file mode 100644 index 0000000..5c21e04 --- /dev/null +++ b/src/EFCoreSecondLevelCacheInterceptor.HybridCache/IEFHybridCacheDependenciesStore.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; + +namespace EFCoreSecondLevelCacheInterceptor; + +/// <summary> +/// Provides information about the in-use cache-dependencies +/// </summary> +public interface IEFHybridCacheDependenciesStore +{ + /// <summary> + /// Adds the given tags list to the list of current tags + /// </summary> + /// <param name="tags"></param> + void AddCacheDependencies(IEnumerable<string>? tags); + + /// <summary> + /// Removes the given tags list from the list of current tags + /// </summary> + /// <param name="tags"></param> + void RemoveCacheDependencies(IEnumerable<string>? tags); + + /// <summary> + /// Returns the cached entries added by this library. + /// </summary> + ISet<string> GetAllCacheDependencies(); +} \ No newline at end of file diff --git a/src/EFCoreSecondLevelCacheInterceptor.HybridCache/_build.cmd b/src/EFCoreSecondLevelCacheInterceptor.HybridCache/_build.cmd new file mode 100644 index 0000000..b4b0e75 --- /dev/null +++ b/src/EFCoreSecondLevelCacheInterceptor.HybridCache/_build.cmd @@ -0,0 +1,2 @@ +dotnet build +pause \ No newline at end of file diff --git a/src/EFCoreSecondLevelCacheInterceptor.MemoryCache/EFCoreSecondLevelCacheInterceptor.MemoryCache.csproj b/src/EFCoreSecondLevelCacheInterceptor.MemoryCache/EFCoreSecondLevelCacheInterceptor.MemoryCache.csproj new file mode 100644 index 0000000..117d5f0 --- /dev/null +++ b/src/EFCoreSecondLevelCacheInterceptor.MemoryCache/EFCoreSecondLevelCacheInterceptor.MemoryCache.csproj @@ -0,0 +1,85 @@ +<Project Sdk="Microsoft.NET.Sdk"> + <PropertyGroup> + <Description>Entity Framework Core Second Level Caching Library.</Description> + <VersionPrefix>5.0.0</VersionPrefix> + <Authors>Vahid Nasiri</Authors> + <TargetFrameworks>net9.0;net8.0;net7.0;net6.0;net5.0;netstandard2.1;netstandard2.0;net462;netcoreapp3.1;</TargetFrameworks> + <GenerateDocumentationFile>true</GenerateDocumentationFile> + <AssemblyName>EFCoreSecondLevelCacheInterceptor.MemoryCache</AssemblyName> + <PackageId>EFCoreSecondLevelCacheInterceptor.MemoryCache</PackageId> + <PackageTags>EntityFramework;Cache;Caching;SecondLevelCache;EFCore;ORM;.NET Core;aspnetcore</PackageTags> + <PackageProjectUrl>https://github.com/VahidN/EFCoreSecondLevelCacheInterceptor</PackageProjectUrl> + <RepositoryUrl>https://github.com/VahidN/EFCoreSecondLevelCacheInterceptor</RepositoryUrl> + <RepositoryType>git</RepositoryType> + <PackageLicenseExpression>Apache-2.0</PackageLicenseExpression> + <GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute> + <GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute> + <GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute> + <AllowUnsafeBlocks>true</AllowUnsafeBlocks> + <PublishRepositoryUrl>true</PublishRepositoryUrl> + <DebugType>embedded</DebugType> + <EmbedUntrackedSources>true</EmbedUntrackedSources> + <AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder> + <GeneratePackageOnBuild>true</GeneratePackageOnBuild> + <NoWarn>NU5104</NoWarn> + <PackageReadmeFile>README.md</PackageReadmeFile> + </PropertyGroup> + <PropertyGroup> + <LangVersion>latest</LangVersion> + <AnalysisLevel>latest</AnalysisLevel> + <AnalysisMode>AllEnabledByDefault</AnalysisMode> + <CodeAnalysisTreatWarningsAsErrors>true</CodeAnalysisTreatWarningsAsErrors> + <EnableNETAnalyzers>true</EnableNETAnalyzers> + <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild> + <Nullable>enable</Nullable> + <TreatWarningsAsErrors>true</TreatWarningsAsErrors> + <RunAnalyzersDuringBuild>true</RunAnalyzersDuringBuild> + <RunAnalyzersDuringLiveAnalysis>true</RunAnalyzersDuringLiveAnalysis> + <Deterministic>true</Deterministic> + <Features>strict</Features> + <ReportAnalyzer>true</ReportAnalyzer> + </PropertyGroup> + <PropertyGroup> + <NuGetAudit>true</NuGetAudit> + <NuGetAuditMode>all</NuGetAuditMode> + <NuGetAuditLevel>low</NuGetAuditLevel> + <WarningsNotAsErrors Condition="'$(Configuration)' != 'Release'"> + $(WarningsNotAsErrors);NU1900;NU1901;NU1902;NU1903;NU1904 + </WarningsNotAsErrors> + </PropertyGroup> + <ItemGroup> + <PackageReference Include="Meziantou.Analyzer" Version="2.0.184"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + <PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.12.19"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + <PackageReference Include="AsyncFixer" Version="1.6.0"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + <PackageReference Include="Asyncify" Version="0.9.7"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + <PackageReference Include="SonarAnalyzer.CSharp" Version="10.4.0.108396"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + </ItemGroup> + <ItemGroup> + <ProjectReference Include="..\EFCoreSecondLevelCacheInterceptor\EFCoreSecondLevelCacheInterceptor.csproj" /> + </ItemGroup> + <ItemGroup> + <PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All"/> + <None Include="../../README.md" Link="README.md" Pack="true" PackagePath="/" Visible="false"/> + </ItemGroup> + <ItemGroup Condition="('$(TargetFramework)' == 'net6.0')"> + <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="[6.0.2,)"/> + </ItemGroup> + <ItemGroup Condition="('$(TargetFramework)' == 'net8.0')"> + <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="[8.0.1,)"/> + </ItemGroup> +</Project> diff --git a/src/EFCoreSecondLevelCacheInterceptor/EFMemoryCacheChangeTokenProvider.cs b/src/EFCoreSecondLevelCacheInterceptor.MemoryCache/EFMemoryCacheChangeTokenProvider.cs similarity index 97% rename from src/EFCoreSecondLevelCacheInterceptor/EFMemoryCacheChangeTokenProvider.cs rename to src/EFCoreSecondLevelCacheInterceptor.MemoryCache/EFMemoryCacheChangeTokenProvider.cs index 8f058da..ac15fb6 100644 --- a/src/EFCoreSecondLevelCacheInterceptor/EFMemoryCacheChangeTokenProvider.cs +++ b/src/EFCoreSecondLevelCacheInterceptor.MemoryCache/EFMemoryCacheChangeTokenProvider.cs @@ -43,7 +43,7 @@ public void RemoveChangeToken(string key) } /// <summary> - /// Removes all of the change notification tokens. + /// Removes all the change notification tokens. /// </summary> public void RemoveAllChangeTokens() { diff --git a/src/EFCoreSecondLevelCacheInterceptor.MemoryCache/EFMemoryCacheProviderOptions.cs b/src/EFCoreSecondLevelCacheInterceptor.MemoryCache/EFMemoryCacheProviderOptions.cs new file mode 100644 index 0000000..e4fe7a7 --- /dev/null +++ b/src/EFCoreSecondLevelCacheInterceptor.MemoryCache/EFMemoryCacheProviderOptions.cs @@ -0,0 +1,56 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace EFCoreSecondLevelCacheInterceptor; + +/// <summary> +/// Defines EFCoreSecondLevel's Options +/// </summary> +public static class EFMemoryCacheProviderOptions +{ + /// <summary> + /// Introduces the built-in `EFMemoryCacheServiceProvider` to be used as the CacheProvider. + /// </summary> + public static EFCoreSecondLevelCacheOptions UseMemoryCacheProvider(this EFCoreSecondLevelCacheOptions options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + options.Settings.Services?.AddMemoryCache(); + options.Settings.Services?.TryAddSingleton<IMemoryCacheChangeTokenProvider, EFMemoryCacheChangeTokenProvider>(); + options.Settings.CacheProvider = typeof(EFMemoryCacheServiceProvider); + + return options; + } + + /// <summary> + /// Introduces the built-in `EFMemoryCacheServiceProvider` to be used as the CacheProvider. + /// If you specify the `Cacheable()` method options, its setting will override this global setting. + /// </summary> + /// <param name="options"></param> + /// <param name="expirationMode">Defines the expiration mode of the cache items globally.</param> + /// <param name="timeout">The expiration timeout.</param> + public static EFCoreSecondLevelCacheOptions UseMemoryCacheProvider(this EFCoreSecondLevelCacheOptions options, + CacheExpirationMode expirationMode, + TimeSpan timeout) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + options.UseMemoryCacheProvider(); + + options.Settings.CachableQueriesOptions = new CachableQueriesOptions + { + ExpirationMode = expirationMode, + Timeout = timeout, + IsActive = true + }; + + return options; + } +} \ No newline at end of file diff --git a/src/EFCoreSecondLevelCacheInterceptor/EFMemoryCacheServiceProvider.cs b/src/EFCoreSecondLevelCacheInterceptor.MemoryCache/EFMemoryCacheServiceProvider.cs similarity index 71% rename from src/EFCoreSecondLevelCacheInterceptor/EFMemoryCacheServiceProvider.cs rename to src/EFCoreSecondLevelCacheInterceptor.MemoryCache/EFMemoryCacheServiceProvider.cs index 37ba4d6..179c11a 100644 --- a/src/EFCoreSecondLevelCacheInterceptor/EFMemoryCacheServiceProvider.cs +++ b/src/EFCoreSecondLevelCacheInterceptor.MemoryCache/EFMemoryCacheServiceProvider.cs @@ -1,27 +1,24 @@ using System; +using System.Collections.Generic; using Microsoft.Extensions.Caching.Memory; namespace EFCoreSecondLevelCacheInterceptor; -/// <summary> -/// Using IMemoryCache as a cache service. -/// </summary> /// <remarks> /// Using IMemoryCache as a cache service. /// </remarks> -public class EFMemoryCacheServiceProvider(IMemoryCache memoryCache, IMemoryCacheChangeTokenProvider signal) - : IEFCacheServiceProvider +public class EFMemoryCacheServiceProvider( + IMemoryCache memoryCache, + IMemoryCacheChangeTokenProvider signal, + IEFDebugLogger logger) : IEFCacheServiceProvider { - private readonly IMemoryCache _memoryCache = memoryCache; - private readonly IMemoryCacheChangeTokenProvider _signal = signal; - /// <summary> /// Adds a new item to the cache. /// </summary> /// <param name="cacheKey">key</param> /// <param name="value">value</param> /// <param name="cachePolicy">Defines the expiration mode of the cache item.</param> - public void InsertValue(EFCacheKey cacheKey, EFCachedData value, EFCachePolicy cachePolicy) + public void InsertValue(EFCacheKey cacheKey, EFCachedData? value, EFCachePolicy cachePolicy) { if (cacheKey is null) { @@ -54,16 +51,22 @@ public void InsertValue(EFCacheKey cacheKey, EFCachedData value, EFCachePolicy c foreach (var rootCacheKey in cacheKey.CacheDependencies) { - options.ExpirationTokens.Add(_signal.GetChangeToken(rootCacheKey)); + options.ExpirationTokens.Add(signal.GetChangeToken(rootCacheKey)); } - _memoryCache.Set(cacheKey.KeyHash, value, options); + memoryCache.Set(cacheKey.KeyHash, value, options); } /// <summary> /// Removes the cached entries added by this library. /// </summary> - public void ClearAllCachedEntries() => _signal.RemoveAllChangeTokens(); + public void ClearAllCachedEntries() + { + signal.RemoveAllChangeTokens(); + + logger.NotifyCacheInvalidation(clearAllCachedEntries: true, + new HashSet<string>(StringComparer.OrdinalIgnoreCase)); + } /// <summary> /// Gets a cached entry by key. @@ -78,11 +81,11 @@ public void InsertValue(EFCacheKey cacheKey, EFCachedData value, EFCachePolicy c throw new ArgumentNullException(nameof(cacheKey)); } - return _memoryCache.Get<EFCachedData>(cacheKey.KeyHash); + return memoryCache.Get<EFCachedData>(cacheKey.KeyHash); } /// <summary> - /// Invalidates all of the cache entries which are dependent on any of the specified root keys. + /// Invalidates all the cache entries which are dependent on any of the specified root keys. /// </summary> /// <param name="cacheKey">Stores information of the computed key of the input LINQ query.</param> public void InvalidateCacheDependencies(EFCacheKey cacheKey) @@ -94,7 +97,7 @@ public void InvalidateCacheDependencies(EFCacheKey cacheKey) foreach (var rootCacheKey in cacheKey.CacheDependencies) { - _signal.RemoveChangeToken(rootCacheKey); + signal.RemoveChangeToken(rootCacheKey); } } } \ No newline at end of file diff --git a/src/EFCoreSecondLevelCacheInterceptor/IMemoryCacheChangeTokenProvider.cs b/src/EFCoreSecondLevelCacheInterceptor.MemoryCache/IMemoryCacheChangeTokenProvider.cs similarity index 90% rename from src/EFCoreSecondLevelCacheInterceptor/IMemoryCacheChangeTokenProvider.cs rename to src/EFCoreSecondLevelCacheInterceptor.MemoryCache/IMemoryCacheChangeTokenProvider.cs index 143deb2..afa0d38 100644 --- a/src/EFCoreSecondLevelCacheInterceptor/IMemoryCacheChangeTokenProvider.cs +++ b/src/EFCoreSecondLevelCacheInterceptor.MemoryCache/IMemoryCacheChangeTokenProvider.cs @@ -18,7 +18,7 @@ public interface IMemoryCacheChangeTokenProvider void RemoveChangeToken(string key); /// <summary> - /// Removes all of the change notification tokens. + /// Removes all the change notification tokens. /// </summary> void RemoveAllChangeTokens(); } \ No newline at end of file diff --git a/src/EFCoreSecondLevelCacheInterceptor.MemoryCache/_build.cmd b/src/EFCoreSecondLevelCacheInterceptor.MemoryCache/_build.cmd new file mode 100644 index 0000000..b4b0e75 --- /dev/null +++ b/src/EFCoreSecondLevelCacheInterceptor.MemoryCache/_build.cmd @@ -0,0 +1,2 @@ +dotnet build +pause \ No newline at end of file diff --git a/src/EFCoreSecondLevelCacheInterceptor.StackExchange.Redis/EFCoreSecondLevelCacheInterceptor.StackExchange.Redis.csproj b/src/EFCoreSecondLevelCacheInterceptor.StackExchange.Redis/EFCoreSecondLevelCacheInterceptor.StackExchange.Redis.csproj new file mode 100644 index 0000000..d50b373 --- /dev/null +++ b/src/EFCoreSecondLevelCacheInterceptor.StackExchange.Redis/EFCoreSecondLevelCacheInterceptor.StackExchange.Redis.csproj @@ -0,0 +1,83 @@ +<Project Sdk="Microsoft.NET.Sdk"> + <PropertyGroup> + <Description>Entity Framework Core Second Level Caching Library.</Description> + <VersionPrefix>5.0.0</VersionPrefix> + <Authors>Vahid Nasiri</Authors> + <TargetFrameworks>net9.0;net8.0;net7.0;net6.0;net5.0;netstandard2.1;netstandard2.0;net462;netcoreapp3.1;</TargetFrameworks> + <GenerateDocumentationFile>true</GenerateDocumentationFile> + <AssemblyName>EFCoreSecondLevelCacheInterceptor.StackExchange.Redis</AssemblyName> + <PackageId>EFCoreSecondLevelCacheInterceptor.StackExchange.Redis</PackageId> + <PackageTags>EntityFramework;Cache;Caching;SecondLevelCache;EFCore;ORM;.NET Core;aspnetcore</PackageTags> + <PackageProjectUrl>https://github.com/VahidN/EFCoreSecondLevelCacheInterceptor</PackageProjectUrl> + <RepositoryUrl>https://github.com/VahidN/EFCoreSecondLevelCacheInterceptor</RepositoryUrl> + <RepositoryType>git</RepositoryType> + <PackageLicenseExpression>Apache-2.0</PackageLicenseExpression> + <GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute> + <GenerateAssemblyCompanyAttribute>false</GenerateAssemblyCompanyAttribute> + <GenerateAssemblyProductAttribute>false</GenerateAssemblyProductAttribute> + <AllowUnsafeBlocks>true</AllowUnsafeBlocks> + <PublishRepositoryUrl>true</PublishRepositoryUrl> + <DebugType>embedded</DebugType> + <EmbedUntrackedSources>true</EmbedUntrackedSources> + <AllowedOutputExtensionsInPackageBuildOutputFolder>$(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb</AllowedOutputExtensionsInPackageBuildOutputFolder> + <GeneratePackageOnBuild>true</GeneratePackageOnBuild> + <NoWarn>NU5104</NoWarn> + <PackageReadmeFile>README.md</PackageReadmeFile> + </PropertyGroup> + <PropertyGroup> + <LangVersion>latest</LangVersion> + <AnalysisLevel>latest</AnalysisLevel> + <AnalysisMode>AllEnabledByDefault</AnalysisMode> + <CodeAnalysisTreatWarningsAsErrors>true</CodeAnalysisTreatWarningsAsErrors> + <EnableNETAnalyzers>true</EnableNETAnalyzers> + <EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild> + <Nullable>enable</Nullable> + <TreatWarningsAsErrors>true</TreatWarningsAsErrors> + <RunAnalyzersDuringBuild>true</RunAnalyzersDuringBuild> + <RunAnalyzersDuringLiveAnalysis>true</RunAnalyzersDuringLiveAnalysis> + <Deterministic>true</Deterministic> + <Features>strict</Features> + <ReportAnalyzer>true</ReportAnalyzer> + </PropertyGroup> + <PropertyGroup> + <NuGetAudit>true</NuGetAudit> + <NuGetAuditMode>all</NuGetAuditMode> + <NuGetAuditLevel>low</NuGetAuditLevel> + <WarningsNotAsErrors Condition="'$(Configuration)' != 'Release'"> + $(WarningsNotAsErrors);NU1900;NU1901;NU1902;NU1903;NU1904 + </WarningsNotAsErrors> + </PropertyGroup> + <ItemGroup> + <PackageReference Include="Meziantou.Analyzer" Version="2.0.184"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + <PackageReference Include="Microsoft.VisualStudio.Threading.Analyzers" Version="17.12.19"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + <PackageReference Include="AsyncFixer" Version="1.6.0"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + <PackageReference Include="Asyncify" Version="0.9.7"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + <PackageReference Include="SonarAnalyzer.CSharp" Version="10.4.0.108396"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + </ItemGroup> + <ItemGroup> + <ProjectReference Include="..\EFCoreSecondLevelCacheInterceptor\EFCoreSecondLevelCacheInterceptor.csproj" /> + </ItemGroup> + <ItemGroup> + <PackageReference Include="Microsoft.SourceLink.GitHub" Version="8.0.0" PrivateAssets="All"/> + <None Include="../../README.md" Link="README.md" Pack="true" PackagePath="/" Visible="false"/> + </ItemGroup> + <ItemGroup> + <PackageReference Include="MessagePack" Version="3.1.1"/> + <PackageReference Include="StackExchange.Redis" Version="2.8.24"/> + </ItemGroup> +</Project> diff --git a/src/EFCoreSecondLevelCacheInterceptor.StackExchange.Redis/EFMessagePackDBNullFormatter.cs b/src/EFCoreSecondLevelCacheInterceptor.StackExchange.Redis/EFMessagePackDBNullFormatter.cs new file mode 100644 index 0000000..76cd5c3 --- /dev/null +++ b/src/EFCoreSecondLevelCacheInterceptor.StackExchange.Redis/EFMessagePackDBNullFormatter.cs @@ -0,0 +1,27 @@ +using System; +using MessagePack; +using MessagePack.Formatters; + +namespace EFCoreSecondLevelCacheInterceptor; + +/// <summary> +/// The contract for serialization of some specific type. +/// </summary> +public class EFMessagePackDBNullFormatter : IMessagePackFormatter<DBNull?> +{ + /// <summary> + /// DBNullFormatter instance + /// </summary> + public static readonly EFMessagePackDBNullFormatter Instance = new(); + + private EFMessagePackDBNullFormatter() + { + } + + /// <inheritdoc /> + public void Serialize(ref MessagePackWriter writer, DBNull? value, MessagePackSerializerOptions options) + => writer.WriteNil(); + + /// <inheritdoc /> + public DBNull Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) => DBNull.Value; +} \ No newline at end of file diff --git a/src/EFCoreSecondLevelCacheInterceptor.StackExchange.Redis/EFMessagePackSerializer.cs b/src/EFCoreSecondLevelCacheInterceptor.StackExchange.Redis/EFMessagePackSerializer.cs new file mode 100644 index 0000000..6cc40fb --- /dev/null +++ b/src/EFCoreSecondLevelCacheInterceptor.StackExchange.Redis/EFMessagePackSerializer.cs @@ -0,0 +1,39 @@ +using MessagePack; +using MessagePack.Resolvers; + +namespace EFCoreSecondLevelCacheInterceptor; + +/// <summary> +/// High-Level API of MessagePack for C#. +/// </summary> +public class EFMessagePackSerializer : IEFDataSerializer +{ + private static readonly IFormatterResolver CustomResolvers = CompositeResolver.Create( + [EFMessagePackDBNullFormatter.Instance], + [ + NativeDateTimeResolver.Instance, ContractlessStandardResolver.Instance, + StandardResolverAllowPrivate.Instance, TypelessContractlessStandardResolver.Instance, + DynamicGenericResolver.Instance + ]); + + /// <summary> + /// Serializes a given value with the specified buffer writer. + /// </summary> + /// <param name="obj"></param> + /// <typeparam name="T"></typeparam> + /// <returns></returns> + public byte[] Serialize<T>(T? obj) + => MessagePackSerializer.Serialize(obj, MessagePackSerializerOptions.Standard.WithResolver(CustomResolvers)); + + /// <summary> + /// Deserializes a value of a given type from a sequence of bytes. + /// </summary> + /// <param name="data"></param> + /// <typeparam name="T"></typeparam> + /// <returns></returns> + public T? Deserialize<T>(byte[]? data) + => data is null + ? default + : MessagePackSerializer.Deserialize<T>(data, + MessagePackSerializerOptions.Standard.WithResolver(CustomResolvers)); +} \ No newline at end of file diff --git a/src/EFCoreSecondLevelCacheInterceptor.StackExchange.Redis/EFRedisCacheConfigurationOptions.cs b/src/EFCoreSecondLevelCacheInterceptor.StackExchange.Redis/EFRedisCacheConfigurationOptions.cs new file mode 100644 index 0000000..bdc94c5 --- /dev/null +++ b/src/EFCoreSecondLevelCacheInterceptor.StackExchange.Redis/EFRedisCacheConfigurationOptions.cs @@ -0,0 +1,19 @@ +using StackExchange.Redis; + +namespace EFCoreSecondLevelCacheInterceptor; + +/// <summary> +/// The options relevant to a set of redis connections. +/// </summary> +public class EFRedisCacheConfigurationOptions +{ + /// <summary> + /// The options relevant to a set of redis connections. + /// </summary> + public ConfigurationOptions? ConfigurationOptions { set; get; } + + /// <summary> + /// Redis ConnectionString + /// </summary> + public string? RedisConnectionString { set; get; } +} \ No newline at end of file diff --git a/src/EFCoreSecondLevelCacheInterceptor.StackExchange.Redis/EFStackExchangeRedisCacheProvider.cs b/src/EFCoreSecondLevelCacheInterceptor.StackExchange.Redis/EFStackExchangeRedisCacheProvider.cs new file mode 100644 index 0000000..08c4f5e --- /dev/null +++ b/src/EFCoreSecondLevelCacheInterceptor.StackExchange.Redis/EFStackExchangeRedisCacheProvider.cs @@ -0,0 +1,131 @@ +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Options; +using StackExchange.Redis; + +namespace EFCoreSecondLevelCacheInterceptor; + +/// <summary> +/// Stack Exchange Redis Cache Provider +/// </summary> +public class EFStackExchangeRedisCacheProvider( + IOptions<EFCoreSecondLevelCacheSettings> cacheSettings, + IEFDebugLogger logger, + IEFDataSerializer dataSerializer) : IEFCacheServiceProvider +{ + private ConnectionMultiplexer? _redisConnection; + + private ConnectionMultiplexer RedisConnection => _redisConnection ??= GetRedisConnection(); + + /// <inheritdoc /> + public void InsertValue(EFCacheKey cacheKey, EFCachedData? value, EFCachePolicy cachePolicy) + { + if (cacheKey is null) + { + throw new ArgumentNullException(nameof(cacheKey)); + } + + if (cachePolicy is null) + { + throw new ArgumentNullException(nameof(cachePolicy)); + } + + value ??= new EFCachedData + { + IsNull = true + }; + + var redisDb = RedisConnection.GetDatabase(); + var keyHash = cacheKey.KeyHash; + + foreach (var rootCacheKey in cacheKey.CacheDependencies) + { + if (string.IsNullOrWhiteSpace(rootCacheKey)) + { + continue; + } + + var expiryTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() + + cachePolicy.CacheTimeout.TotalMilliseconds; + + redisDb.SortedSetAdd(rootCacheKey, keyHash, expiryTime); + } + + var data = dataSerializer.Serialize(value); + + redisDb.StringSet(keyHash, data, cachePolicy.CacheTimeout); + } + + /// <inheritdoc /> + public void ClearAllCachedEntries() + => logger.NotifyCacheInvalidation(clearAllCachedEntries: true, + new HashSet<string>(StringComparer.OrdinalIgnoreCase)); + + /// <inheritdoc /> + public EFCachedData? GetValue(EFCacheKey cacheKey, EFCachePolicy cachePolicy) + { + if (cacheKey is null) + { + throw new ArgumentNullException(nameof(cacheKey)); + } + + var redisDb = RedisConnection.GetDatabase(); + var maybeValue = redisDb.StringGet(cacheKey.KeyHash); + + return maybeValue.HasValue ? dataSerializer.Deserialize<EFCachedData>(maybeValue) : null; + } + + /// <inheritdoc /> + public void InvalidateCacheDependencies(EFCacheKey cacheKey) + { + if (cacheKey is null) + { + throw new ArgumentNullException(nameof(cacheKey)); + } + + var redisDb = RedisConnection.GetDatabase(); + + foreach (var rootCacheKey in cacheKey.CacheDependencies) + { + if (string.IsNullOrWhiteSpace(rootCacheKey)) + { + continue; + } + + var dependencyKeys = new HashSet<string>(StringComparer.Ordinal); + + foreach (var item in redisDb.SortedSetScan(rootCacheKey)) + { + _ = dependencyKeys.Add(item.Element.ToString()); + } + + if (dependencyKeys.Count > 0) + { + redisDb.KeyDelete([.. dependencyKeys]); + } + + redisDb.KeyDelete(rootCacheKey); + } + } + + private ConnectionMultiplexer GetRedisConnection() + { + var options = cacheSettings.Value.AdditionalData as EFRedisCacheConfigurationOptions ?? + throw new InvalidOperationException( + message: "Please call the UseStackExchangeRedisCacheProvider() method."); + + if (options.RedisConnectionString is not null) + { + return ConnectionMultiplexer.Connect(options.RedisConnectionString); + } + + if (options.ConfigurationOptions is not null) + { + return ConnectionMultiplexer.Connect(options.ConfigurationOptions); + } + + throw new InvalidOperationException( + message: + "Please specify the `RedisConnectionString` or `ConfigurationOptions` by calling the UseStackExchangeRedisCacheProvider() method."); + } +} \ No newline at end of file diff --git a/src/EFCoreSecondLevelCacheInterceptor.StackExchange.Redis/EFStackExchangeRedisCacheProviderOptions.cs b/src/EFCoreSecondLevelCacheInterceptor.StackExchange.Redis/EFStackExchangeRedisCacheProviderOptions.cs new file mode 100644 index 0000000..05183bd --- /dev/null +++ b/src/EFCoreSecondLevelCacheInterceptor.StackExchange.Redis/EFStackExchangeRedisCacheProviderOptions.cs @@ -0,0 +1,70 @@ +using System; +using Microsoft.Extensions.DependencyInjection.Extensions; +using StackExchange.Redis; + +namespace EFCoreSecondLevelCacheInterceptor; + +/// <summary> +/// Defines EFCoreSecondLevel's Options +/// </summary> +public static class EFStackExchangeRedisCacheProviderOptions +{ + /// <summary> + /// Introduces the `EFStackExchangeRedisCacheProvider` to be used as the CacheProvider. + /// If you specify the `Cacheable()` method options, its setting will override this global setting. + /// </summary> + public static EFCoreSecondLevelCacheOptions UseStackExchangeRedisCacheProvider( + this EFCoreSecondLevelCacheOptions options, + ConfigurationOptions configurationOptions, + TimeSpan timeout) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + SetOptions(options, timeout, new EFRedisCacheConfigurationOptions + { + ConfigurationOptions = configurationOptions + }); + + return options; + } + + /// <summary> + /// Introduces the `EFStackExchangeRedisCacheProvider` to be used as the CacheProvider. + /// If you specify the `Cacheable()` method options, its setting will override this global setting. + /// </summary> + public static EFCoreSecondLevelCacheOptions UseStackExchangeRedisCacheProvider( + this EFCoreSecondLevelCacheOptions options, + string redisConnectionString, + TimeSpan timeout) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + SetOptions(options, timeout, new EFRedisCacheConfigurationOptions + { + RedisConnectionString = redisConnectionString + }); + + return options; + } + + private static void SetOptions(EFCoreSecondLevelCacheOptions options, + TimeSpan timeout, + EFRedisCacheConfigurationOptions configurationOptions) + { + options.Settings.Services?.TryAddSingleton<IEFDataSerializer, EFMessagePackSerializer>(); + options.Settings.CacheProvider = typeof(EFStackExchangeRedisCacheProvider); + options.Settings.AdditionalData = configurationOptions; + + options.Settings.CachableQueriesOptions = new CachableQueriesOptions + { + Timeout = timeout, + IsActive = true + }; + } +} \ No newline at end of file diff --git a/src/EFCoreSecondLevelCacheInterceptor.StackExchange.Redis/IEFDataSerializer.cs b/src/EFCoreSecondLevelCacheInterceptor.StackExchange.Redis/IEFDataSerializer.cs new file mode 100644 index 0000000..a0b2181 --- /dev/null +++ b/src/EFCoreSecondLevelCacheInterceptor.StackExchange.Redis/IEFDataSerializer.cs @@ -0,0 +1,17 @@ +namespace EFCoreSecondLevelCacheInterceptor; + +/// <summary> +/// High-Level API of a Serializer. +/// </summary> +public interface IEFDataSerializer +{ + /// <summary> + /// Serializes a given value with the specified buffer writer. + /// </summary> + byte[] Serialize<T>(T? obj); + + /// <summary> + /// Deserializes a value of a given type from a sequence of bytes. + /// </summary> + T? Deserialize<T>(byte[]? data); +} \ No newline at end of file diff --git a/src/EFCoreSecondLevelCacheInterceptor.StackExchange.Redis/_build.cmd b/src/EFCoreSecondLevelCacheInterceptor.StackExchange.Redis/_build.cmd new file mode 100644 index 0000000..b4b0e75 --- /dev/null +++ b/src/EFCoreSecondLevelCacheInterceptor.StackExchange.Redis/_build.cmd @@ -0,0 +1,2 @@ +dotnet build +pause \ No newline at end of file diff --git a/src/EFCoreSecondLevelCacheInterceptor/EFCoreSecondLevelCacheInterceptor.csproj b/src/EFCoreSecondLevelCacheInterceptor/EFCoreSecondLevelCacheInterceptor.csproj index 513c71e..ec22eff 100644 --- a/src/EFCoreSecondLevelCacheInterceptor/EFCoreSecondLevelCacheInterceptor.csproj +++ b/src/EFCoreSecondLevelCacheInterceptor/EFCoreSecondLevelCacheInterceptor.csproj @@ -1,7 +1,7 @@ <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <Description>Entity Framework Core Second Level Caching Library.</Description> - <VersionPrefix>4.9.0</VersionPrefix> + <VersionPrefix>5.0.0</VersionPrefix> <Authors>Vahid Nasiri</Authors> <TargetFrameworks>net9.0;net8.0;net7.0;net6.0;net5.0;netstandard2.1;netstandard2.0;net462;netcoreapp3.1;</TargetFrameworks> <GenerateDocumentationFile>true</GenerateDocumentationFile> @@ -48,7 +48,7 @@ </WarningsNotAsErrors> </PropertyGroup> <ItemGroup> - <PackageReference Include="Meziantou.Analyzer" Version="2.0.182"> + <PackageReference Include="Meziantou.Analyzer" Version="2.0.184"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> @@ -64,7 +64,7 @@ <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> - <PackageReference Include="SonarAnalyzer.CSharp" Version="10.3.0.106239"> + <PackageReference Include="SonarAnalyzer.CSharp" Version="10.4.0.108396"> <PrivateAssets>all</PrivateAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> </PackageReference> @@ -79,8 +79,6 @@ <Reference Include="Microsoft.CSharp"/> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="[3.1,4)"/> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="[3.1,4)"/> - <PackageReference Include="CacheManager.Core" Version="[1.2,)"/> - <PackageReference Include="EasyCaching.Core" Version="[1.9.2,)"/> </ItemGroup> <PropertyGroup Condition="'$(TargetFramework)' == 'net462'"> <DefineConstants>NET4_6_2</DefineConstants> @@ -89,8 +87,6 @@ <ItemGroup Condition=" ('$(TargetFramework)' == 'netstandard2.0')"> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="[3.1,4)"/> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="[3.1,4)"/> - <PackageReference Include="CacheManager.Core" Version="[1.2,)"/> - <PackageReference Include="EasyCaching.Core" Version="[1.9.2,)"/> </ItemGroup> <PropertyGroup Condition="('$(TargetFramework)' == 'netstandard2.0')"> <DefineConstants>NETSTANDARD2_0</DefineConstants> @@ -99,8 +95,6 @@ <ItemGroup Condition="('$(TargetFramework)' == 'netcoreapp3.1')"> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="[3.1,6)"/> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="[3.1,6)"/> - <PackageReference Include="CacheManager.Core" Version="[1.2,)"/> - <PackageReference Include="EasyCaching.Core" Version="[1.9.2,)"/> </ItemGroup> <PropertyGroup Condition="('$(TargetFramework)' == 'netcoreapp3.1')"> <DefineConstants>NETCORE3_1</DefineConstants> @@ -109,8 +103,6 @@ <ItemGroup Condition="('$(TargetFramework)' == 'netstandard2.1')"> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="[5,)"/> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="[5,)"/> - <PackageReference Include="CacheManager.Core" Version="[1.2,)"/> - <PackageReference Include="EasyCaching.Core" Version="[1.9.2,)"/> </ItemGroup> <PropertyGroup Condition="('$(TargetFramework)' == 'netstandard2.1')"> <DefineConstants>NETSTANDARD2_1</DefineConstants> @@ -119,8 +111,6 @@ <ItemGroup Condition="('$(TargetFramework)' == 'net5.0')"> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="[5,)"/> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="[5,)"/> - <PackageReference Include="CacheManager.Core" Version="[1.2,)"/> - <PackageReference Include="EasyCaching.Core" Version="[1.9.2,)"/> </ItemGroup> <PropertyGroup Condition="('$(TargetFramework)' == 'net5.0')"> <DefineConstants>NET5_0</DefineConstants> @@ -129,9 +119,6 @@ <ItemGroup Condition="('$(TargetFramework)' == 'net6.0')"> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="[6,)"/> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="[6,)"/> - <PackageReference Include="CacheManager.Core" Version="[1.2,)"/> - <PackageReference Include="EasyCaching.Core" Version="[1.9.2,)"/> - <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="[6.0.2,)"/> </ItemGroup> <PropertyGroup Condition="('$(TargetFramework)' == 'net6.0')"> <DefineConstants>NET6_0</DefineConstants> @@ -140,8 +127,6 @@ <ItemGroup Condition="('$(TargetFramework)' == 'net7.0')"> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="[7,)"/> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="[7,)"/> - <PackageReference Include="CacheManager.Core" Version="[1.2,)"/> - <PackageReference Include="EasyCaching.Core" Version="[1.9.2,)"/> </ItemGroup> <PropertyGroup Condition="('$(TargetFramework)' == 'net7.0')"> <DefineConstants>NET7_0</DefineConstants> @@ -151,9 +136,6 @@ <PackageReference Include="System.IO.Hashing" Version="[8.0.0,)"/> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="[8.0.0,)"/> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="[8.0.0,)"/> - <PackageReference Include="CacheManager.Core" Version="[1.2,)"/> - <PackageReference Include="EasyCaching.Core" Version="[1.9.2,)"/> - <PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="[8.0.1,)"/> </ItemGroup> <PropertyGroup Condition="('$(TargetFramework)' == 'net8.0')"> <DefineConstants>NET8_0</DefineConstants> @@ -163,8 +145,6 @@ <PackageReference Include="System.IO.Hashing" Version="[9.0.0,)"/> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="[9.0.0,)"/> <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="[9.0.0,)"/> - <PackageReference Include="CacheManager.Core" Version="[1.2,)"/> - <PackageReference Include="EasyCaching.Core" Version="[1.9.2,)"/> </ItemGroup> <PropertyGroup Condition="('$(TargetFramework)' == 'net9.0')"> <DefineConstants>NET9_0</DefineConstants> diff --git a/src/EFCoreSecondLevelCacheInterceptor/EFCoreSecondLevelCacheOptions.cs b/src/EFCoreSecondLevelCacheInterceptor/EFCoreSecondLevelCacheOptions.cs index 8ae4391..13378df 100644 --- a/src/EFCoreSecondLevelCacheInterceptor/EFCoreSecondLevelCacheOptions.cs +++ b/src/EFCoreSecondLevelCacheInterceptor/EFCoreSecondLevelCacheOptions.cs @@ -10,7 +10,10 @@ namespace EFCoreSecondLevelCacheInterceptor; /// </summary> public class EFCoreSecondLevelCacheOptions { - internal EFCoreSecondLevelCacheSettings Settings { get; } = new(); + /// <summary> + /// Global Cache Settings + /// </summary> + public EFCoreSecondLevelCacheSettings Settings { get; } = new(); #if NET9_0 || NET5_0 || NET6_0 || NET7_0 || NET8_0 /// <summary> @@ -57,7 +60,7 @@ public EFCoreSecondLevelCacheOptions CacheAllQueries(CacheExpirationMode expirat /// <param name="realTableNames"> /// The real table names. /// Queries containing these names will be cached. - /// Table names are not case sensitive. + /// Table names are not case-sensitive. /// </param> public EFCoreSecondLevelCacheOptions CacheQueriesContainingTableNames(CacheExpirationMode expirationMode, TimeSpan timeout, @@ -150,166 +153,12 @@ public EFCoreSecondLevelCacheOptions UseCustomCacheProvider<T>(CacheExpirationMo return this; } - /// <summary> - /// Introduces the built-in `EFMemoryCacheServiceProvider` to be used as the CacheProvider. - /// </summary> - public EFCoreSecondLevelCacheOptions UseMemoryCacheProvider() - { - Settings.CacheProvider = typeof(EFMemoryCacheServiceProvider); - - return this; - } - - /// <summary> - /// Introduces the built-in `EFMemoryCacheServiceProvider` to be used as the CacheProvider. - /// If you specify the `Cacheable()` method options, its setting will override this global setting. - /// </summary> - /// <param name="expirationMode">Defines the expiration mode of the cache items globally.</param> - /// <param name="timeout">The expiration timeout.</param> - public EFCoreSecondLevelCacheOptions UseMemoryCacheProvider(CacheExpirationMode expirationMode, TimeSpan timeout) - { - Settings.CacheProvider = typeof(EFMemoryCacheServiceProvider); - - Settings.CachableQueriesOptions = new CachableQueriesOptions - { - ExpirationMode = expirationMode, - Timeout = timeout, - IsActive = true - }; - - return this; - } - - /// <summary> - /// Introduces the built-in `CacheManagerCoreProvider` to be used as the CacheProvider. - /// </summary> - public EFCoreSecondLevelCacheOptions UseCacheManagerCoreProvider() - { - Settings.CacheProvider = typeof(EFCacheManagerCoreProvider); - - return this; - } - - /// <summary> - /// Introduces the built-in `CacheManagerCoreProvider` to be used as the CacheProvider. - /// If you specify the `Cacheable()` method options, its setting will override this global setting. - /// </summary> - /// <param name="expirationMode">Defines the expiration mode of the cache items globally.</param> - /// <param name="timeout">The expiration timeout.</param> - public EFCoreSecondLevelCacheOptions UseCacheManagerCoreProvider(CacheExpirationMode expirationMode, - TimeSpan timeout) - { - Settings.CacheProvider = typeof(EFCacheManagerCoreProvider); - - Settings.CachableQueriesOptions = new CachableQueriesOptions - { - ExpirationMode = expirationMode, - Timeout = timeout, - IsActive = true - }; - - return this; - } - - /// <summary> - /// Introduces the built-in `EasyCachingCoreProvider` to be used as the CacheProvider. - /// </summary> - /// <param name="providerName">Selected caching provider name.</param> - /// <param name="isHybridCache">Is an instance of EasyCaching.HybridCache</param> - public EFCoreSecondLevelCacheOptions UseEasyCachingCoreProvider(string providerName, bool isHybridCache = false) - { - Settings.CacheProvider = typeof(EFEasyCachingCoreProvider); - Settings.ProviderName = providerName; - Settings.IsHybridCache = isHybridCache; - - return this; - } - - /// <summary> - /// Introduces the built-in `EasyCachingCoreProvider` to be used as the CacheProvider. - /// </summary> - /// <param name="providerName"> - /// Selected caching provider name. - /// This option will let you to choose a different redis database for your current tenant. - /// <![CDATA[ Such as: (serviceProvider, cacheKey) => "redis-db-" + serviceProvider.GetRequiredService<IHttpContextAccesor>().HttpContext.Request.Headers["tenant-id"]; ]]> - /// </param> - /// <param name="isHybridCache">Is an instance of EasyCaching.HybridCache</param> - public EFCoreSecondLevelCacheOptions UseEasyCachingCoreProvider( - Func<IServiceProvider, EFCacheKey?, string> providerName, - bool isHybridCache = false) - { - Settings.CacheProvider = typeof(EFEasyCachingCoreProvider); - Settings.CacheProviderName = providerName; - Settings.IsHybridCache = isHybridCache; - - return this; - } - - /// <summary> - /// Introduces the built-in `EasyCachingCoreProvider` to be used as the CacheProvider. - /// If you specify the `Cacheable()` method options, its setting will override this global setting. - /// </summary> - /// <param name="providerName">Selected caching provider name.</param> - /// <param name="expirationMode">Defines the expiration mode of the cache items globally.</param> - /// <param name="timeout">The expiration timeout.</param> - /// <param name="isHybridCache">Is an instance of EasyCaching.HybridCache</param> - public EFCoreSecondLevelCacheOptions UseEasyCachingCoreProvider(string providerName, - CacheExpirationMode expirationMode, - TimeSpan timeout, - bool isHybridCache = false) - { - Settings.CacheProvider = typeof(EFEasyCachingCoreProvider); - Settings.ProviderName = providerName; - Settings.IsHybridCache = isHybridCache; - - Settings.CachableQueriesOptions = new CachableQueriesOptions - { - ExpirationMode = expirationMode, - Timeout = timeout, - IsActive = true - }; - - return this; - } - - /// <summary> - /// Introduces the built-in `EasyCachingCoreProvider` to be used as the CacheProvider. - /// If you specify the `Cacheable()` method options, its setting will override this global setting. - /// </summary> - /// <param name="providerName"> - /// Selected caching provider name. - /// This option will let you to choose a different redis database for your current tenant. - /// <![CDATA[ Such as: (serviceProvider, cacheKey) => "redis-db-" + serviceProvider.GetRequiredService<IHttpContextAccesor>().HttpContext.Request.Headers["tenant-id"]; ]]> - /// </param> - /// <param name="expirationMode">Defines the expiration mode of the cache items globally.</param> - /// <param name="timeout">The expiration timeout.</param> - /// <param name="isHybridCache">Is an instance of EasyCaching.HybridCache</param> - public EFCoreSecondLevelCacheOptions UseEasyCachingCoreProvider( - Func<IServiceProvider, EFCacheKey?, string> providerName, - CacheExpirationMode expirationMode, - TimeSpan timeout, - bool isHybridCache = false) - { - Settings.CacheProvider = typeof(EFEasyCachingCoreProvider); - Settings.CacheProviderName = providerName; - Settings.IsHybridCache = isHybridCache; - - Settings.CachableQueriesOptions = new CachableQueriesOptions - { - ExpirationMode = expirationMode, - Timeout = timeout, - IsActive = true - }; - - return this; - } - /// <summary> /// Sets a dynamic prefix for the current cachedKey. /// </summary> /// <param name="prefix"> /// Selected cache key prefix. - /// This option will let you to choose a different cache key prefix for your current tenant. + /// This option will let you choose a different cache key prefix for your current tenant. /// <![CDATA[ Such as: serviceProvider => "EF_" + serviceProvider.GetRequiredService<IHttpContextAccesor>().HttpContext.Request.Headers["tenant-id"] ]]> /// </param> /// <returns>EFCoreSecondLevelCacheOptions.</returns> @@ -322,7 +171,7 @@ public EFCoreSecondLevelCacheOptions UseCacheKeyPrefix(Func<IServiceProvider, st /// <summary> /// Uses the cache key prefix. - /// Sets the prefix to all of the cachedKey's. + /// Sets the prefix to all the cachedKey's. /// Its default value is `EF_`. /// </summary> /// <param name="prefix">The prefix.</param> @@ -375,7 +224,7 @@ public EFCoreSecondLevelCacheOptions UseDbCallsIfCachingProviderIsDown(TimeSpan } /// <summary> - /// Set it to false to disable this caching interceptor entirely. + /// Set it to `false` to disable this caching interceptor entirely. /// Its default value is `true`. /// </summary> public EFCoreSecondLevelCacheOptions EnableCachingInterceptor(bool enable = true) @@ -450,7 +299,7 @@ public EFCoreSecondLevelCacheOptions SkipCachingDbContexts(params Type[]? dbCont /// <param name="realTableNames"> /// The real table names. /// Queries containing these names will not be cached. - /// Table names are not case sensitive. + /// Table names are not case-sensitive. /// </param> public EFCoreSecondLevelCacheOptions CacheAllQueriesExceptContainingTableNames(CacheExpirationMode expirationMode, TimeSpan timeout, diff --git a/src/EFCoreSecondLevelCacheInterceptor/EFCoreSecondLevelCacheSettings.cs b/src/EFCoreSecondLevelCacheInterceptor/EFCoreSecondLevelCacheSettings.cs index 5149d2d..e736635 100644 --- a/src/EFCoreSecondLevelCacheInterceptor/EFCoreSecondLevelCacheSettings.cs +++ b/src/EFCoreSecondLevelCacheInterceptor/EFCoreSecondLevelCacheSettings.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Microsoft.Extensions.DependencyInjection; #if NET9_0 || NET5_0 || NET6_0 || NET7_0 || NET8_0 using System.Text.Json; #endif @@ -11,6 +12,11 @@ namespace EFCoreSecondLevelCacheInterceptor; /// </summary> public class EFCoreSecondLevelCacheSettings { + /// <summary> + /// Returns a collection of required services + /// </summary> + public IServiceCollection? Services { get; set; } + #if NET9_0 || NET5_0 || NET6_0 || NET7_0 || NET8_0 /// <summary> /// Options to control the serialization behavior @@ -34,7 +40,7 @@ public class EFCoreSecondLevelCacheSettings public string? ProviderName { get; set; } /// <summary> - /// This option will let you to choose a different redis database for your current tenant. + /// This option will let you choose a different redis database for your current tenant. /// <![CDATA[ Such as: (serviceProvider, cacheKey) => "redis-db-" + serviceProvider.GetRequiredService<IHttpContextAccesor>().HttpContext.Request.Headers["tenant-id"]; ]]> /// </summary> public Func<IServiceProvider, EFCacheKey?, string>? CacheProviderName { set; get; } @@ -88,7 +94,7 @@ public class EFCoreSecondLevelCacheSettings public bool UseDbCallsIfCachingProviderIsDown { set; get; } /// <summary> - /// Set it to false to disable this caching interceptor. + /// Set it to `false` to disable this caching interceptor. /// Its default value is `true`. /// </summary> public bool IsCachingInterceptorEnabled { set; get; } = true; @@ -133,4 +139,9 @@ public class EFCoreSecondLevelCacheSettings /// Determines which entities are involved in the current cache-invalidation event. /// </summary> public Action<EFCacheInvalidationInfo>? CacheInvalidationEvent { set; get; } + + /// <summary> + /// Provides some optional data + /// </summary> + public object? AdditionalData { set; get; } } \ No newline at end of file diff --git a/src/EFCoreSecondLevelCacheInterceptor/EFServiceCollectionExtensions.cs b/src/EFCoreSecondLevelCacheInterceptor/EFServiceCollectionExtensions.cs index cc7e140..35481da 100644 --- a/src/EFCoreSecondLevelCacheInterceptor/EFServiceCollectionExtensions.cs +++ b/src/EFCoreSecondLevelCacheInterceptor/EFServiceCollectionExtensions.cs @@ -13,8 +13,7 @@ public static class EFServiceCollectionExtensions /// <summary> /// Registers the required services of the EFCoreSecondLevelCacheInterceptor. /// </summary> - public static IServiceCollection AddEFSecondLevelCache( - this IServiceCollection services, + public static IServiceCollection AddEFSecondLevelCache(this IServiceCollection services, Action<EFCoreSecondLevelCacheOptions> options) { if (options == null) @@ -22,7 +21,6 @@ public static IServiceCollection AddEFSecondLevelCache( throw new ArgumentNullException(nameof(options)); } - services.AddMemoryCache(); services.TryAddSingleton<IEFDebugLogger, EFDebugLogger>(); services.TryAddSingleton<IEFCacheServiceCheck, EFCacheServiceCheck>(); services.TryAddSingleton<IEFCacheKeyPrefixProvider, EFCacheKeyPrefixProvider>(); @@ -31,7 +29,6 @@ public static IServiceCollection AddEFSecondLevelCache( services.TryAddSingleton<IEFSqlCommandsProcessor, EFSqlCommandsProcessor>(); services.TryAddSingleton<IEFCacheDependenciesProcessor, EFCacheDependenciesProcessor>(); services.TryAddSingleton<ILockProvider, LockProvider>(); - services.TryAddSingleton<IMemoryCacheChangeTokenProvider, EFMemoryCacheChangeTokenProvider>(); services.TryAddSingleton<IDbCommandInterceptorProcessor, DbCommandInterceptorProcessor>(); services.TryAddSingleton<SecondLevelCacheInterceptor>(); @@ -42,7 +39,14 @@ public static IServiceCollection AddEFSecondLevelCache( private static void ConfigOptions(IServiceCollection services, Action<EFCoreSecondLevelCacheOptions> options) { - var cacheOptions = new EFCoreSecondLevelCacheOptions(); + var cacheOptions = new EFCoreSecondLevelCacheOptions + { + Settings = + { + Services = services + } + }; + options.Invoke(cacheOptions); AddHashProvider(services, cacheOptions); @@ -63,19 +67,17 @@ private static void AddHashProvider(IServiceCollection services, EFCoreSecondLev } private static void AddOptions(IServiceCollection services, EFCoreSecondLevelCacheOptions cacheOptions) - { - services.TryAddSingleton(Options.Create(cacheOptions.Settings)); - } + => services.TryAddSingleton(Options.Create(cacheOptions.Settings)); private static void AddCacheServiceProvider(IServiceCollection services, EFCoreSecondLevelCacheOptions cacheOptions) { if (cacheOptions.Settings.CacheProvider == null) { - services.TryAddSingleton<IEFCacheServiceProvider, EFMemoryCacheServiceProvider>(); - } - else - { - services.TryAddSingleton(typeof(IEFCacheServiceProvider), cacheOptions.Settings.CacheProvider); + throw new InvalidOperationException( + message: + "Please select an appropriate IEFCacheServiceProvider, such as `UseMemoryCacheProvider()`, `UseEasyCachingCoreProvider()`, etc. Each of these providers requires a separate nuget package. More info: https://github.com/VahidN/EFCoreSecondLevelCacheInterceptor/blob/master/README.md"); } + + services.TryAddSingleton(typeof(IEFCacheServiceProvider), cacheOptions.Settings.CacheProvider); } } \ No newline at end of file diff --git a/src/EFCoreSecondLevelCacheInterceptor/IEFCacheServiceProvider.cs b/src/EFCoreSecondLevelCacheInterceptor/IEFCacheServiceProvider.cs index 865b0bb..0b03f37 100644 --- a/src/EFCoreSecondLevelCacheInterceptor/IEFCacheServiceProvider.cs +++ b/src/EFCoreSecondLevelCacheInterceptor/IEFCacheServiceProvider.cs @@ -24,10 +24,10 @@ public interface IEFCacheServiceProvider /// <param name="cacheKey">key</param> /// <param name="value">value</param> /// <param name="cachePolicy">Defines the expiration mode of the cache item.</param> - void InsertValue(EFCacheKey cacheKey, EFCachedData value, EFCachePolicy cachePolicy); + void InsertValue(EFCacheKey cacheKey, EFCachedData? value, EFCachePolicy cachePolicy); /// <summary> - /// Invalidates all of the cache entries which are dependent on any of the specified root keys. + /// Invalidates all the cache entries which are dependent on any of the specified root keys. /// </summary> /// <param name="cacheKey">Stores information of the computed key of the input LINQ query.</param> void InvalidateCacheDependencies(EFCacheKey cacheKey); diff --git a/src/Tests/EFCoreSecondLevelCacheInterceptor.AspNetCoreSample/EFCoreSecondLevelCacheInterceptor.AspNetCoreSample.csproj b/src/Tests/EFCoreSecondLevelCacheInterceptor.AspNetCoreSample/EFCoreSecondLevelCacheInterceptor.AspNetCoreSample.csproj index aa830d7..ecbd5f1 100644 --- a/src/Tests/EFCoreSecondLevelCacheInterceptor.AspNetCoreSample/EFCoreSecondLevelCacheInterceptor.AspNetCoreSample.csproj +++ b/src/Tests/EFCoreSecondLevelCacheInterceptor.AspNetCoreSample/EFCoreSecondLevelCacheInterceptor.AspNetCoreSample.csproj @@ -1,19 +1,20 @@ <Project Sdk="Microsoft.NET.Sdk.Web"> - <PropertyGroup> - <TargetFramework>net9.0</TargetFramework> - <NoWarn>RCS1090</NoWarn> - </PropertyGroup> - <ItemGroup> - <ProjectReference Include="..\..\EFCoreSecondLevelCacheInterceptor\EFCoreSecondLevelCacheInterceptor.csproj" /> - <ProjectReference Include="..\EFCoreSecondLevelCacheInterceptor.Tests.DataLayer\EFCoreSecondLevelCacheInterceptor.Tests.DataLayer.csproj" /> - </ItemGroup> - <ItemGroup> - <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0"> - <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> - <PrivateAssets>all</PrivateAssets> - </PackageReference> - <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.0" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0" /> - </ItemGroup> + <PropertyGroup> + <TargetFramework>net9.0</TargetFramework> + <NoWarn>RCS1090</NoWarn> + </PropertyGroup> + <ItemGroup> + <ProjectReference Include="..\..\EFCoreSecondLevelCacheInterceptor.MemoryCache\EFCoreSecondLevelCacheInterceptor.MemoryCache.csproj"/> + <ProjectReference Include="..\..\EFCoreSecondLevelCacheInterceptor\EFCoreSecondLevelCacheInterceptor.csproj"/> + <ProjectReference Include="..\EFCoreSecondLevelCacheInterceptor.Tests.DataLayer\EFCoreSecondLevelCacheInterceptor.Tests.DataLayer.csproj"/> + </ItemGroup> + <ItemGroup> + <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0"/> + <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0"> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + <PrivateAssets>all</PrivateAssets> + </PackageReference> + <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.0"/> + <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0"/> + </ItemGroup> </Project> \ No newline at end of file diff --git a/src/Tests/EFCoreSecondLevelCacheInterceptor.AspNetCoreSampleWithLamar/EFCoreSecondLevelCacheInterceptor.AspNetCoreSampleWithLamar.csproj b/src/Tests/EFCoreSecondLevelCacheInterceptor.AspNetCoreSampleWithLamar/EFCoreSecondLevelCacheInterceptor.AspNetCoreSampleWithLamar.csproj index c10cd0b..c92e61c 100644 --- a/src/Tests/EFCoreSecondLevelCacheInterceptor.AspNetCoreSampleWithLamar/EFCoreSecondLevelCacheInterceptor.AspNetCoreSampleWithLamar.csproj +++ b/src/Tests/EFCoreSecondLevelCacheInterceptor.AspNetCoreSampleWithLamar/EFCoreSecondLevelCacheInterceptor.AspNetCoreSampleWithLamar.csproj @@ -1,23 +1,24 @@ <Project Sdk="Microsoft.NET.Sdk.Web"> - <PropertyGroup> - <TargetFramework>net9.0</TargetFramework> - <NoWarn>RCS1090</NoWarn> - </PropertyGroup> - <ItemGroup> - <ProjectReference Include="..\..\EFCoreSecondLevelCacheInterceptor\EFCoreSecondLevelCacheInterceptor.csproj" /> - <ProjectReference Include="..\EFCoreSecondLevelCacheInterceptor.Tests.DataLayer\EFCoreSecondLevelCacheInterceptor.Tests.DataLayer.csproj" /> - </ItemGroup> - <ItemGroup> - <PackageReference Include="Lamar.Microsoft.DependencyInjection" Version="14.0.1" /> - <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0"> - <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> - <PrivateAssets>all</PrivateAssets> - </PackageReference> - <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.0" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0" /> - <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" /> - <PackageReference Include="Microsoft.Extensions.Options" Version="9.0.0" /> - <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" /> - </ItemGroup> + <PropertyGroup> + <TargetFramework>net9.0</TargetFramework> + <NoWarn>RCS1090</NoWarn> + </PropertyGroup> + <ItemGroup> + <ProjectReference Include="..\..\EFCoreSecondLevelCacheInterceptor.MemoryCache\EFCoreSecondLevelCacheInterceptor.MemoryCache.csproj"/> + <ProjectReference Include="..\..\EFCoreSecondLevelCacheInterceptor\EFCoreSecondLevelCacheInterceptor.csproj"/> + <ProjectReference Include="..\EFCoreSecondLevelCacheInterceptor.Tests.DataLayer\EFCoreSecondLevelCacheInterceptor.Tests.DataLayer.csproj"/> + </ItemGroup> + <ItemGroup> + <PackageReference Include="Lamar.Microsoft.DependencyInjection" Version="14.0.1"/> + <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0"/> + <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0"> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + <PrivateAssets>all</PrivateAssets> + </PackageReference> + <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.0"/> + <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0"/> + <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0"/> + <PackageReference Include="Microsoft.Extensions.Options" Version="9.0.0"/> + <PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0"/> + </ItemGroup> </Project> \ No newline at end of file diff --git a/src/Tests/EFCoreSecondLevelCacheInterceptor.ConsoleSample/EFCoreSecondLevelCacheInterceptor.ConsoleSample.csproj b/src/Tests/EFCoreSecondLevelCacheInterceptor.ConsoleSample/EFCoreSecondLevelCacheInterceptor.ConsoleSample.csproj index 887ac12..23f5176 100644 --- a/src/Tests/EFCoreSecondLevelCacheInterceptor.ConsoleSample/EFCoreSecondLevelCacheInterceptor.ConsoleSample.csproj +++ b/src/Tests/EFCoreSecondLevelCacheInterceptor.ConsoleSample/EFCoreSecondLevelCacheInterceptor.ConsoleSample.csproj @@ -1,16 +1,17 @@ <Project Sdk="Microsoft.NET.Sdk"> - <PropertyGroup> - <OutputType>Exe</OutputType> - <TargetFramework>net9.0</TargetFramework> - </PropertyGroup> - <ItemGroup> - <ProjectReference Include="..\..\EFCoreSecondLevelCacheInterceptor\EFCoreSecondLevelCacheInterceptor.csproj" /> - <ProjectReference Include="..\EFCoreSecondLevelCacheInterceptor.Tests.DataLayer\EFCoreSecondLevelCacheInterceptor.Tests.DataLayer.csproj" /> - </ItemGroup> - <ItemGroup> - <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" /> - <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" /> - <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.0" /> - <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.0" /> - </ItemGroup> + <PropertyGroup> + <OutputType>Exe</OutputType> + <TargetFramework>net9.0</TargetFramework> + </PropertyGroup> + <ItemGroup> + <ProjectReference Include="..\..\EFCoreSecondLevelCacheInterceptor.MemoryCache\EFCoreSecondLevelCacheInterceptor.MemoryCache.csproj"/> + <ProjectReference Include="..\..\EFCoreSecondLevelCacheInterceptor\EFCoreSecondLevelCacheInterceptor.csproj"/> + <ProjectReference Include="..\EFCoreSecondLevelCacheInterceptor.Tests.DataLayer\EFCoreSecondLevelCacheInterceptor.Tests.DataLayer.csproj"/> + </ItemGroup> + <ItemGroup> + <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0"/> + <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0"/> + <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.0"/> + <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.0"/> + </ItemGroup> </Project> diff --git a/src/Tests/EFCoreSecondLevelCacheInterceptor.PerformanceTests/EFCoreSecondLevelCacheInterceptor.PerformanceTests.csproj b/src/Tests/EFCoreSecondLevelCacheInterceptor.PerformanceTests/EFCoreSecondLevelCacheInterceptor.PerformanceTests.csproj index f305a23..6506f39 100644 --- a/src/Tests/EFCoreSecondLevelCacheInterceptor.PerformanceTests/EFCoreSecondLevelCacheInterceptor.PerformanceTests.csproj +++ b/src/Tests/EFCoreSecondLevelCacheInterceptor.PerformanceTests/EFCoreSecondLevelCacheInterceptor.PerformanceTests.csproj @@ -1,20 +1,21 @@ <Project Sdk="Microsoft.NET.Sdk"> - <PropertyGroup> - <OutputType>Exe</OutputType> - <TargetFramework>net9.0</TargetFramework> - </PropertyGroup> - <ItemGroup> - <ProjectReference Include="..\..\EFCoreSecondLevelCacheInterceptor\EFCoreSecondLevelCacheInterceptor.csproj" /> - <ProjectReference Include="..\EFCoreSecondLevelCacheInterceptor.Tests.DataLayer\EFCoreSecondLevelCacheInterceptor.Tests.DataLayer.csproj" /> - </ItemGroup> - <ItemGroup> - <None Include="appsettings.json" CopyToOutputDirectory="PreserveNewest" /> - </ItemGroup> - <ItemGroup> - <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" /> - <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.0" /> - <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.0" /> - <PackageReference Include="BenchmarkDotNet" Version="0.14.0" /> - <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" /> - </ItemGroup> + <PropertyGroup> + <OutputType>Exe</OutputType> + <TargetFramework>net9.0</TargetFramework> + </PropertyGroup> + <ItemGroup> + <ProjectReference Include="..\..\EFCoreSecondLevelCacheInterceptor.MemoryCache\EFCoreSecondLevelCacheInterceptor.MemoryCache.csproj"/> + <ProjectReference Include="..\..\EFCoreSecondLevelCacheInterceptor\EFCoreSecondLevelCacheInterceptor.csproj"/> + <ProjectReference Include="..\EFCoreSecondLevelCacheInterceptor.Tests.DataLayer\EFCoreSecondLevelCacheInterceptor.Tests.DataLayer.csproj"/> + </ItemGroup> + <ItemGroup> + <None Include="appsettings.json" CopyToOutputDirectory="PreserveNewest"/> + </ItemGroup> + <ItemGroup> + <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0"/> + <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.0"/> + <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.0"/> + <PackageReference Include="BenchmarkDotNet" Version="0.14.0"/> + <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0"/> + </ItemGroup> </Project> \ No newline at end of file diff --git a/src/Tests/EFCoreSecondLevelCacheInterceptor.Tests.DataLayer/EFCoreSecondLevelCacheInterceptor.Tests.DataLayer.csproj b/src/Tests/EFCoreSecondLevelCacheInterceptor.Tests.DataLayer/EFCoreSecondLevelCacheInterceptor.Tests.DataLayer.csproj index 93f05ed..b6fc4b8 100644 --- a/src/Tests/EFCoreSecondLevelCacheInterceptor.Tests.DataLayer/EFCoreSecondLevelCacheInterceptor.Tests.DataLayer.csproj +++ b/src/Tests/EFCoreSecondLevelCacheInterceptor.Tests.DataLayer/EFCoreSecondLevelCacheInterceptor.Tests.DataLayer.csproj @@ -1,28 +1,29 @@ <Project Sdk="Microsoft.NET.Sdk"> - <PropertyGroup> - <TargetFramework>net9.0</TargetFramework> - </PropertyGroup> - <ItemGroup> - <ProjectReference Include="..\..\EFCoreSecondLevelCacheInterceptor\EFCoreSecondLevelCacheInterceptor.csproj" /> - </ItemGroup> - <ItemGroup> - <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0"> - <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> - <PrivateAssets>all</PrivateAssets> - </PackageReference> - <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0"> - <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> - <PrivateAssets>all</PrivateAssets> - </PackageReference> - <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.0" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0" /> - <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0" /> - <PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.0" /> - <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.0" /> - <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0" /> - <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.0" /> - <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" /> - <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.0" /> - </ItemGroup> + <PropertyGroup> + <TargetFramework>net9.0</TargetFramework> + </PropertyGroup> + <ItemGroup> + <ProjectReference Include="..\..\EFCoreSecondLevelCacheInterceptor.MemoryCache\EFCoreSecondLevelCacheInterceptor.MemoryCache.csproj"/> + <ProjectReference Include="..\..\EFCoreSecondLevelCacheInterceptor\EFCoreSecondLevelCacheInterceptor.csproj"/> + </ItemGroup> + <ItemGroup> + <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0"/> + <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0"> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + <PrivateAssets>all</PrivateAssets> + </PackageReference> + <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0"> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + <PrivateAssets>all</PrivateAssets> + </PackageReference> + <PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.0"/> + <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0"/> + <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0"/> + <PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.0"/> + <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.0"/> + <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0"/> + <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.0"/> + <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0"/> + <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.0"/> + </ItemGroup> </Project> \ No newline at end of file diff --git a/src/Tests/EFCoreSecondLevelCacheInterceptor.Tests.DataLayer/Migrations/20250106171343_V2025_01_06_2043.Designer.cs b/src/Tests/EFCoreSecondLevelCacheInterceptor.Tests.DataLayer/Migrations/20250106171343_V2025_01_06_2043.Designer.cs new file mode 100644 index 0000000..8c759d4 --- /dev/null +++ b/src/Tests/EFCoreSecondLevelCacheInterceptor.Tests.DataLayer/Migrations/20250106171343_V2025_01_06_2043.Designer.cs @@ -0,0 +1,547 @@ +// <auto-generated /> +using System; +using EFCoreSecondLevelCacheInterceptor.Tests.DataLayer; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace EFCoreSecondLevelCacheInterceptor.Tests.DataLayer.Migrations +{ + [DbContext(typeof(ApplicationDbContext))] + [Migration("20250106171343_V2025_01_06_2043")] + partial class V2025_01_06_2043 + { + /// <inheritdoc /> + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("EFCoreSecondLevelCacheInterceptor.Tests.DataLayer.Entities.Blog", b => + { + b.Property<int>("BlogId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("BlogId")); + + b.Property<string>("Url") + .HasColumnType("nvarchar(max)"); + + b.HasKey("BlogId"); + + b.ToTable("Blogs"); + + b.HasData( + new + { + BlogId = 1, + Url = "https://site1.com" + }, + new + { + BlogId = 2, + Url = "https://site2.com" + }); + }); + + modelBuilder.Entity("EFCoreSecondLevelCacheInterceptor.Tests.DataLayer.Entities.BlogData", b => + { + b.Property<int>("Id") + .HasColumnType("int"); + + b.Property<string>("SiteUrl") + .HasColumnType("nvarchar(max)"); + + b.ToTable("BlogData"); + }); + + modelBuilder.Entity("EFCoreSecondLevelCacheInterceptor.Tests.DataLayer.Entities.DateType", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id")); + + b.Property<DateTime?>("AddDate") + .HasColumnType("datetime2"); + + b.Property<DateTimeOffset?>("AddDateValue") + .HasColumnType("datetimeoffset"); + + b.Property<TimeSpan?>("RelativeAddTimeValue") + .HasColumnType("time"); + + b.Property<TimeSpan>("RelativeUpdateTimeValue") + .HasColumnType("time"); + + b.Property<DateTime>("UpdateDate") + .HasColumnType("datetime2"); + + b.Property<DateTimeOffset>("UpdateDateValue") + .HasColumnType("datetimeoffset"); + + b.HasKey("Id"); + + b.ToTable("DateTypes"); + }); + + modelBuilder.Entity("EFCoreSecondLevelCacheInterceptor.Tests.DataLayer.Entities.EngineVersion", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id")); + + b.HasKey("Id"); + + b.ToTable("EngineVersions"); + }); + + modelBuilder.Entity("EFCoreSecondLevelCacheInterceptor.Tests.DataLayer.Entities.Post", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id")); + + b.Property<int>("BlogId") + .HasColumnType("int"); + + b.Property<string>("Title") + .HasColumnType("nvarchar(max)"); + + b.Property<int>("UserId") + .HasColumnType("int"); + + b.Property<string>("post_type") + .IsRequired() + .HasMaxLength(13) + .HasColumnType("nvarchar(13)"); + + b.HasKey("Id"); + + b.HasIndex("BlogId"); + + b.HasIndex("UserId"); + + b.ToTable("Posts"); + + b.HasDiscriminator<string>("post_type").HasValue("post_base"); + + b.UseTphMappingStrategy(); + + b.HasData( + new + { + Id = 1, + BlogId = 1, + Title = "Post1", + UserId = 1 + }, + new + { + Id = 2, + BlogId = 1, + Title = "Post2", + UserId = 1 + }); + }); + + modelBuilder.Entity("EFCoreSecondLevelCacheInterceptor.Tests.DataLayer.Entities.Product", b => + { + b.Property<int>("ProductId") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ProductId")); + + b.Property<bool>("IsActive") + .HasColumnType("bit"); + + b.Property<string>("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property<string>("ProductName") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property<string>("ProductNumber") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.Property<int>("UserId") + .HasColumnType("int"); + + b.HasKey("ProductId"); + + b.HasIndex("ProductName") + .IsUnique(); + + b.HasIndex("UserId"); + + b.ToTable("Products"); + + b.HasData( + new + { + ProductId = 1, + IsActive = false, + Notes = "Notes ...", + ProductName = "Product4", + ProductNumber = "004", + UserId = 1 + }, + new + { + ProductId = 2, + IsActive = true, + Notes = "Notes ...", + ProductName = "Product1", + ProductNumber = "001", + UserId = 1 + }, + new + { + ProductId = 3, + IsActive = true, + Notes = "Notes ...", + ProductName = "Product2", + ProductNumber = "002", + UserId = 1 + }, + new + { + ProductId = 4, + IsActive = true, + Notes = "Notes ...", + ProductName = "Product3", + ProductNumber = "003", + UserId = 1 + }); + }); + + modelBuilder.Entity("EFCoreSecondLevelCacheInterceptor.Tests.DataLayer.Entities.Tag", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id")); + + b.Property<string>("Name") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Tags"); + + b.HasData( + new + { + Id = 1, + Name = "Tag4" + }, + new + { + Id = 2, + Name = "Tag1" + }, + new + { + Id = 3, + Name = "Tag2" + }, + new + { + Id = 4, + Name = "Tag3" + }); + }); + + modelBuilder.Entity("EFCoreSecondLevelCacheInterceptor.Tests.DataLayer.Entities.TagProduct", b => + { + b.Property<int>("TagId") + .HasColumnType("int"); + + b.Property<int>("ProductProductId") + .HasColumnType("int"); + + b.HasKey("TagId", "ProductProductId"); + + b.HasIndex("ProductProductId"); + + b.HasIndex("TagId"); + + b.ToTable("TagProducts"); + + b.HasData( + new + { + TagId = 1, + ProductProductId = 1 + }, + new + { + TagId = 2, + ProductProductId = 2 + }, + new + { + TagId = 3, + ProductProductId = 3 + }, + new + { + TagId = 4, + ProductProductId = 4 + }); + }); + + modelBuilder.Entity("EFCoreSecondLevelCacheInterceptor.Tests.DataLayer.Entities.User", b => + { + b.Property<int>("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id")); + + b.Property<DateTime?>("AddDate") + .HasColumnType("datetime2"); + + b.Property<byte[]>("ByteArrayValue") + .HasColumnType("varbinary(max)"); + + b.Property<byte>("ByteValue") + .HasColumnType("tinyint"); + + b.Property<string>("CharValue") + .IsRequired() + .HasColumnType("nvarchar(1)"); + + b.Property<DateTimeOffset?>("DateTimeOffsetValue") + .HasColumnType("datetimeoffset"); + + b.Property<decimal>("DecimalValue") + .HasColumnType("decimal(18,2)"); + + b.Property<double>("DoubleValue") + .HasColumnType("float"); + + b.Property<float>("FloatValue") + .HasColumnType("real"); + + b.Property<Guid>("GuidValue") + .HasColumnType("uniqueidentifier"); + + b.Property<byte[]>("ImageData") + .HasColumnType("varbinary(max)"); + + b.Property<bool>("IsActive") + .HasColumnType("bit"); + + b.Property<string>("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property<long>("Points") + .HasColumnType("bigint"); + + b.Property<short>("ShortValue") + .HasColumnType("smallint"); + + b.Property<TimeSpan?>("TimeSpanValue") + .HasColumnType("time"); + + b.Property<long>("UintValue") + .HasColumnType("bigint"); + + b.Property<decimal>("UlongValue") + .HasColumnType("decimal(20,0)"); + + b.Property<DateTime?>("UpdateDate") + .HasColumnType("datetime2"); + + b.Property<int>("UserStatus") + .HasColumnType("int"); + + b.Property<decimal>("UshortValue") + .HasColumnType("decimal(20,0)"); + + b.HasKey("Id"); + + b.ToTable("Users"); + + b.HasData( + new + { + Id = 1, + ByteArrayValue = new byte[] { 1, 2 }, + ByteValue = (byte)1, + CharValue = "C", + DecimalValue = 1.1m, + DoubleValue = 1.3, + FloatValue = 1.2f, + GuidValue = new Guid("236bbe40-b861-433c-8789-b152a99cfe3e"), + IsActive = true, + Name = "User1", + Points = 1000L, + ShortValue = (short)2, + UintValue = 1L, + UlongValue = 1m, + UserStatus = 0, + UshortValue = 1m + }); + }); + + modelBuilder.Entity("EFCoreSecondLevelCacheInterceptor.Tests.DataLayer.Entities.Page", b => + { + b.HasBaseType("EFCoreSecondLevelCacheInterceptor.Tests.DataLayer.Entities.Post"); + + b.HasDiscriminator().HasValue("post_page"); + }); + + modelBuilder.Entity("EFCoreSecondLevelCacheInterceptor.Tests.DataLayer.Entities.EngineVersion", b => + { + b.OwnsOne("EFCoreSecondLevelCacheInterceptor.Tests.DataLayer.Entities.EngineProductVersion", "Commercial", b1 => + { + b1.Property<int>("EngineVersionId") + .HasColumnType("int"); + + b1.Property<int>("Major") + .HasColumnType("int"); + + b1.Property<int>("Minor") + .HasColumnType("int"); + + b1.Property<int>("Patch") + .HasColumnType("int"); + + b1.Property<int>("Revision") + .HasColumnType("int"); + + b1.HasKey("EngineVersionId"); + + b1.ToTable("EngineVersions"); + + b1.WithOwner() + .HasForeignKey("EngineVersionId"); + }); + + b.OwnsOne("EFCoreSecondLevelCacheInterceptor.Tests.DataLayer.Entities.EngineProductVersion", "Retail", b1 => + { + b1.Property<int>("EngineVersionId") + .HasColumnType("int"); + + b1.Property<int>("Major") + .HasColumnType("int"); + + b1.Property<int>("Minor") + .HasColumnType("int"); + + b1.Property<int>("Patch") + .HasColumnType("int"); + + b1.Property<int>("Revision") + .HasColumnType("int"); + + b1.HasKey("EngineVersionId"); + + b1.ToTable("EngineVersions"); + + b1.WithOwner() + .HasForeignKey("EngineVersionId"); + }); + + b.Navigation("Commercial"); + + b.Navigation("Retail"); + }); + + modelBuilder.Entity("EFCoreSecondLevelCacheInterceptor.Tests.DataLayer.Entities.Post", b => + { + b.HasOne("EFCoreSecondLevelCacheInterceptor.Tests.DataLayer.Entities.Blog", "Blog") + .WithMany("Posts") + .HasForeignKey("BlogId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("EFCoreSecondLevelCacheInterceptor.Tests.DataLayer.Entities.User", "User") + .WithMany("Posts") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Blog"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("EFCoreSecondLevelCacheInterceptor.Tests.DataLayer.Entities.Product", b => + { + b.HasOne("EFCoreSecondLevelCacheInterceptor.Tests.DataLayer.Entities.User", "User") + .WithMany("Products") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("User"); + }); + + modelBuilder.Entity("EFCoreSecondLevelCacheInterceptor.Tests.DataLayer.Entities.TagProduct", b => + { + b.HasOne("EFCoreSecondLevelCacheInterceptor.Tests.DataLayer.Entities.Product", "Product") + .WithMany("TagProducts") + .HasForeignKey("ProductProductId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("EFCoreSecondLevelCacheInterceptor.Tests.DataLayer.Entities.Tag", "Tag") + .WithMany("TagProducts") + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Product"); + + b.Navigation("Tag"); + }); + + modelBuilder.Entity("EFCoreSecondLevelCacheInterceptor.Tests.DataLayer.Entities.Blog", b => + { + b.Navigation("Posts"); + }); + + modelBuilder.Entity("EFCoreSecondLevelCacheInterceptor.Tests.DataLayer.Entities.Product", b => + { + b.Navigation("TagProducts"); + }); + + modelBuilder.Entity("EFCoreSecondLevelCacheInterceptor.Tests.DataLayer.Entities.Tag", b => + { + b.Navigation("TagProducts"); + }); + + modelBuilder.Entity("EFCoreSecondLevelCacheInterceptor.Tests.DataLayer.Entities.User", b => + { + b.Navigation("Posts"); + + b.Navigation("Products"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Tests/EFCoreSecondLevelCacheInterceptor.Tests.DataLayer/Migrations/20250106171343_V2025_01_06_2043.cs b/src/Tests/EFCoreSecondLevelCacheInterceptor.Tests.DataLayer/Migrations/20250106171343_V2025_01_06_2043.cs new file mode 100644 index 0000000..b15e692 --- /dev/null +++ b/src/Tests/EFCoreSecondLevelCacheInterceptor.Tests.DataLayer/Migrations/20250106171343_V2025_01_06_2043.cs @@ -0,0 +1,36 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace EFCoreSecondLevelCacheInterceptor.Tests.DataLayer.Migrations +{ + /// <inheritdoc /> + public partial class V2025_01_06_2043 : Migration + { + /// <inheritdoc /> + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn<string>( + name: "post_type", + table: "Posts", + type: "nvarchar(13)", + maxLength: 13, + nullable: false, + oldClrType: typeof(string), + oldType: "nvarchar(max)"); + } + + /// <inheritdoc /> + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.AlterColumn<string>( + name: "post_type", + table: "Posts", + type: "nvarchar(max)", + nullable: false, + oldClrType: typeof(string), + oldType: "nvarchar(13)", + oldMaxLength: 13); + } + } +} diff --git a/src/Tests/EFCoreSecondLevelCacheInterceptor.Tests.DataLayer/Migrations/ApplicationDbContextModelSnapshot.cs b/src/Tests/EFCoreSecondLevelCacheInterceptor.Tests.DataLayer/Migrations/ApplicationDbContextModelSnapshot.cs index c45274d..fcd76c0 100644 --- a/src/Tests/EFCoreSecondLevelCacheInterceptor.Tests.DataLayer/Migrations/ApplicationDbContextModelSnapshot.cs +++ b/src/Tests/EFCoreSecondLevelCacheInterceptor.Tests.DataLayer/Migrations/ApplicationDbContextModelSnapshot.cs @@ -6,6 +6,8 @@ using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +#nullable disable + namespace EFCoreSecondLevelCacheInterceptor.Tests.DataLayer.Migrations { [DbContext(typeof(ApplicationDbContext))] @@ -15,16 +17,18 @@ protected override void BuildModel(ModelBuilder modelBuilder) { #pragma warning disable 612, 618 modelBuilder - .UseIdentityColumns() - .HasAnnotation("Relational:MaxIdentifierLength", 128) - .HasAnnotation("ProductVersion", "5.0.0-rc.2.20475.6"); + .HasAnnotation("ProductVersion", "9.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); modelBuilder.Entity("EFCoreSecondLevelCacheInterceptor.Tests.DataLayer.Entities.Blog", b => { b.Property<int>("BlogId") .ValueGeneratedOnAdd() - .HasColumnType("int") - .UseIdentityColumn(); + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("BlogId")); b.Property<string>("Url") .HasColumnType("nvarchar(max)"); @@ -61,8 +65,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Property<int>("Id") .ValueGeneratedOnAdd() - .HasColumnType("int") - .UseIdentityColumn(); + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id")); b.Property<DateTime?>("AddDate") .HasColumnType("datetime2"); @@ -91,8 +96,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Property<int>("Id") .ValueGeneratedOnAdd() - .HasColumnType("int") - .UseIdentityColumn(); + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id")); b.HasKey("Id"); @@ -103,8 +109,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Property<int>("Id") .ValueGeneratedOnAdd() - .HasColumnType("int") - .UseIdentityColumn(); + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id")); b.Property<int>("BlogId") .HasColumnType("int"); @@ -117,7 +124,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property<string>("post_type") .IsRequired() - .HasColumnType("nvarchar(max)"); + .HasMaxLength(13) + .HasColumnType("nvarchar(13)"); b.HasKey("Id"); @@ -129,6 +137,8 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.HasDiscriminator<string>("post_type").HasValue("post_base"); + b.UseTphMappingStrategy(); + b.HasData( new { @@ -150,8 +160,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Property<int>("ProductId") .ValueGeneratedOnAdd() - .HasColumnType("int") - .UseIdentityColumn(); + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("ProductId")); b.Property<bool>("IsActive") .HasColumnType("bit"); @@ -224,8 +235,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Property<int>("Id") .ValueGeneratedOnAdd() - .HasColumnType("int") - .UseIdentityColumn(); + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id")); b.Property<string>("Name") .HasColumnType("nvarchar(max)"); @@ -300,8 +312,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) { b.Property<int>("Id") .ValueGeneratedOnAdd() - .HasColumnType("int") - .UseIdentityColumn(); + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id")); b.Property<DateTime?>("AddDate") .HasColumnType("datetime2"); @@ -403,9 +416,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.OwnsOne("EFCoreSecondLevelCacheInterceptor.Tests.DataLayer.Entities.EngineProductVersion", "Commercial", b1 => { b1.Property<int>("EngineVersionId") - .ValueGeneratedOnAdd() - .HasColumnType("int") - .UseIdentityColumn(); + .HasColumnType("int"); b1.Property<int>("Major") .HasColumnType("int"); @@ -430,9 +441,7 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.OwnsOne("EFCoreSecondLevelCacheInterceptor.Tests.DataLayer.Entities.EngineProductVersion", "Retail", b1 => { b1.Property<int>("EngineVersionId") - .ValueGeneratedOnAdd() - .HasColumnType("int") - .UseIdentityColumn(); + .HasColumnType("int"); b1.Property<int>("Major") .HasColumnType("int"); diff --git a/src/Tests/EFCoreSecondLevelCacheInterceptor.Tests.DataLayer/_01-add_migrations.cmd b/src/Tests/EFCoreSecondLevelCacheInterceptor.Tests.DataLayer/_01-add_migrations.cmd index 219e652..6be8366 100644 --- a/src/Tests/EFCoreSecondLevelCacheInterceptor.Tests.DataLayer/_01-add_migrations.cmd +++ b/src/Tests/EFCoreSecondLevelCacheInterceptor.Tests.DataLayer/_01-add_migrations.cmd @@ -1,6 +1,6 @@ For /f "tokens=2-4 delims=/ " %%a in ('date /t') do (set mydate=%%c_%%a_%%b) For /f "tokens=1-2 delims=/:" %%a in ("%TIME: =0%") do (set mytime=%%a%%b) -dotnet tool update --global dotnet-ef --version 6.0.7 +dotnet tool update --global dotnet-ef --version 9.0.0 dotnet build dotnet ef migrations --startup-project ../EFCoreSecondLevelCacheInterceptor.AspNetCoreSample/ add V%mydate%_%mytime% --context ApplicationDbContext pause \ No newline at end of file diff --git a/src/Tests/EFCoreSecondLevelCacheInterceptor.Tests.DataLayer/_02-update_db.cmd b/src/Tests/EFCoreSecondLevelCacheInterceptor.Tests.DataLayer/_02-update_db.cmd index bc157ee..400dd8a 100644 --- a/src/Tests/EFCoreSecondLevelCacheInterceptor.Tests.DataLayer/_02-update_db.cmd +++ b/src/Tests/EFCoreSecondLevelCacheInterceptor.Tests.DataLayer/_02-update_db.cmd @@ -1,4 +1,4 @@ -dotnet tool update --global dotnet-ef --version 6.0.7 +dotnet tool update --global dotnet-ef --version 9.0.0 dotnet build dotnet ef --startup-project ../EFCoreSecondLevelCacheInterceptor.AspNetCoreSample/ database update --context ApplicationDbContext pause \ No newline at end of file diff --git a/src/Tests/EFCoreSecondLevelCacheInterceptor.Tests/CacheAllQueriesTests.cs b/src/Tests/EFCoreSecondLevelCacheInterceptor.Tests/CacheAllQueriesTests.cs index c03af86..47535d0 100644 --- a/src/Tests/EFCoreSecondLevelCacheInterceptor.Tests/CacheAllQueriesTests.cs +++ b/src/Tests/EFCoreSecondLevelCacheInterceptor.Tests/CacheAllQueriesTests.cs @@ -4,71 +4,72 @@ using Microsoft.Extensions.Logging; using Microsoft.VisualStudio.TestTools.UnitTesting; -namespace EFCoreSecondLevelCacheInterceptor.Tests +namespace EFCoreSecondLevelCacheInterceptor.Tests; + +[TestClass] +public class CacheAllQueriesTests { - [TestClass] - public class CacheAllQueriesTests - { - [DataTestMethod] - [DataRow(TestCacheProvider.BuiltInInMemory)] - [DataRow(TestCacheProvider.CacheManagerCoreInMemory)] - [DataRow(TestCacheProvider.CacheManagerCoreRedis)] - [DataRow(TestCacheProvider.EasyCachingCoreInMemory)] - [DataRow(TestCacheProvider.EasyCachingCoreRedis)] - public async Task TestCacheAllQueriesWorks(TestCacheProvider cacheProvider) - { - await EFServiceProvider.RunInContextAsync(cacheProvider, LogLevel.Debug, true, - async (context, loggerProvider) => - { - var isActive = true; - var name = "Product1"; + [DataTestMethod] + [DataRow(TestCacheProvider.BuiltInInMemory)] + [DataRow(TestCacheProvider.CacheManagerCoreInMemory)] + [DataRow(TestCacheProvider.CacheManagerCoreRedis)] + [DataRow(TestCacheProvider.EasyCachingCoreInMemory)] + [DataRow(TestCacheProvider.EasyCachingCoreRedis)] + [DataRow(TestCacheProvider.FusionCache)] + [DataRow(TestCacheProvider.StackExchangeRedis)] + public async Task TestCacheAllQueriesWorks(TestCacheProvider cacheProvider) + => await EFServiceProvider.RunInContextAsync(cacheProvider, LogLevel.Debug, cacheAllQueries: true, + async (context, loggerProvider) => + { + loggerProvider.ClearItems(); + + var isActive = true; + var name = "Product1"; + + var list1 = await Queryable.OrderBy(context.Products, product => product.ProductNumber) + .Where(product => product.IsActive == isActive && product.ProductName == name) + .ToListAsync(); + + Assert.IsTrue(list1.Any()); + Assert.AreEqual(expected: 0, loggerProvider.GetCacheHitCount()); + + var list2 = await Queryable.OrderBy(context.Products, product => product.ProductNumber) + .Where(product => product.IsActive == isActive && product.ProductName == name) + .ToListAsync(); + + Assert.IsTrue(list2.Any()); + Assert.AreEqual(expected: 1, loggerProvider.GetCacheHitCount()); + }); - var list1 = await context.Products - .OrderBy(product => product.ProductNumber) - .Where(product => product.IsActive == isActive && product.ProductName == name) - .ToListAsync(); - Assert.AreEqual(0, loggerProvider.GetCacheHitCount()); - Assert.IsTrue(list1.Any()); + [DataTestMethod] + [DataRow(TestCacheProvider.BuiltInInMemory)] + [DataRow(TestCacheProvider.CacheManagerCoreInMemory)] + [DataRow(TestCacheProvider.CacheManagerCoreRedis)] + [DataRow(TestCacheProvider.EasyCachingCoreInMemory)] + [DataRow(TestCacheProvider.EasyCachingCoreRedis)] + [DataRow(TestCacheProvider.FusionCache)] + [DataRow(TestCacheProvider.StackExchangeRedis)] + public async Task TestCacheAllQueriesWithNotCacheableWorks(TestCacheProvider cacheProvider) + => await EFServiceProvider.RunInContextAsync(cacheProvider, LogLevel.Debug, cacheAllQueries: true, + async (context, loggerProvider) => + { + var isActive = true; + var name = "Product1"; - var list2 = await context.Products - .OrderBy(product => product.ProductNumber) - .Where(product => product.IsActive == isActive && product.ProductName == name) - .ToListAsync(); - Assert.AreEqual(1, loggerProvider.GetCacheHitCount()); - Assert.IsTrue(list2.Any()); - }); - } + var list1 = await Queryable.OrderBy(context.Products, product => product.ProductNumber) + .Where(product => product.IsActive == isActive && product.ProductName == name) + .NotCacheable() + .ToListAsync(); - [DataTestMethod] - [DataRow(TestCacheProvider.BuiltInInMemory)] - [DataRow(TestCacheProvider.CacheManagerCoreInMemory)] - [DataRow(TestCacheProvider.CacheManagerCoreRedis)] - [DataRow(TestCacheProvider.EasyCachingCoreInMemory)] - [DataRow(TestCacheProvider.EasyCachingCoreRedis)] - public async Task TestCacheAllQueriesWithNotCacheableWorks(TestCacheProvider cacheProvider) - { - await EFServiceProvider.RunInContextAsync(cacheProvider, LogLevel.Debug, true, - async (context, loggerProvider) => - { - var isActive = true; - var name = "Product1"; + Assert.AreEqual(expected: 0, loggerProvider.GetCacheHitCount()); + Assert.IsTrue(list1.Any()); - var list1 = await context.Products - .OrderBy(product => product.ProductNumber) - .Where(product => product.IsActive == isActive && product.ProductName == name) - .NotCacheable() - .ToListAsync(); - Assert.AreEqual(0, loggerProvider.GetCacheHitCount()); - Assert.IsTrue(list1.Any()); + var list2 = await Queryable.OrderBy(context.Products, product => product.ProductNumber) + .Where(product => product.IsActive == isActive && product.ProductName == name) + .NotCacheable() + .ToListAsync(); - var list2 = await context.Products - .OrderBy(product => product.ProductNumber) - .Where(product => product.IsActive == isActive && product.ProductName == name) - .NotCacheable() - .ToListAsync(); - Assert.AreEqual(0, loggerProvider.GetCacheHitCount()); - Assert.IsTrue(list2.Any()); - }); - } - } + Assert.AreEqual(expected: 0, loggerProvider.GetCacheHitCount()); + Assert.IsTrue(list2.Any()); + }); } \ No newline at end of file diff --git a/src/Tests/EFCoreSecondLevelCacheInterceptor.Tests/EFCoreSecondLevelCacheInterceptor.Tests.csproj b/src/Tests/EFCoreSecondLevelCacheInterceptor.Tests/EFCoreSecondLevelCacheInterceptor.Tests.csproj index b48ca3e..f5cbbdc 100644 --- a/src/Tests/EFCoreSecondLevelCacheInterceptor.Tests/EFCoreSecondLevelCacheInterceptor.Tests.csproj +++ b/src/Tests/EFCoreSecondLevelCacheInterceptor.Tests/EFCoreSecondLevelCacheInterceptor.Tests.csproj @@ -11,6 +11,10 @@ <NoWarn>RCS1090</NoWarn> </PropertyGroup> <ItemGroup> + <ProjectReference Include="..\..\EFCoreSecondLevelCacheInterceptor.CacheManager.Core\EFCoreSecondLevelCacheInterceptor.CacheManager.Core.csproj"/> + <ProjectReference Include="..\..\EFCoreSecondLevelCacheInterceptor.EasyCaching.Core\EFCoreSecondLevelCacheInterceptor.EasyCaching.Core.csproj"/> + <ProjectReference Include="..\..\EFCoreSecondLevelCacheInterceptor.FusionCache\EFCoreSecondLevelCacheInterceptor.FusionCache.csproj"/> + <ProjectReference Include="..\..\EFCoreSecondLevelCacheInterceptor.StackExchange.Redis\EFCoreSecondLevelCacheInterceptor.StackExchange.Redis.csproj"/> <ProjectReference Include="..\..\EFCoreSecondLevelCacheInterceptor\EFCoreSecondLevelCacheInterceptor.csproj"/> <ProjectReference Include="..\EFCoreSecondLevelCacheInterceptor.Tests.DataLayer\EFCoreSecondLevelCacheInterceptor.Tests.DataLayer.csproj"/> </ItemGroup> @@ -24,7 +28,7 @@ <PackageReference Include="EasyCaching.Bus.Redis" Version="1.9.2"/> <PackageReference Include="EasyCaching.Serialization.MessagePack" Version="1.9.2"/> <PackageReference Include="EasyCaching.Serialization.SystemTextJson" Version="1.9.2"/> - <PackageReference Include="MessagePack" Version="3.1.0"/> + <PackageReference Include="MessagePack" Version="3.1.1"/> <PackageReference Include="StackExchange.Redis" Version="2.8.24"/> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0"/> diff --git a/src/Tests/EFCoreSecondLevelCacheInterceptor.Tests/Settings/DBNullFormatter.cs b/src/Tests/EFCoreSecondLevelCacheInterceptor.Tests/Settings/DBNullFormatter.cs index acb648a..74c9a76 100644 --- a/src/Tests/EFCoreSecondLevelCacheInterceptor.Tests/Settings/DBNullFormatter.cs +++ b/src/Tests/EFCoreSecondLevelCacheInterceptor.Tests/Settings/DBNullFormatter.cs @@ -6,16 +6,14 @@ namespace EFCoreSecondLevelCacheInterceptor.Tests; public class DBNullFormatter : IMessagePackFormatter<DBNull> { - public static DBNullFormatter Instance = new(); + public static readonly DBNullFormatter Instance = new(); private DBNullFormatter() { } public void Serialize(ref MessagePackWriter writer, DBNull value, MessagePackSerializerOptions options) - { - writer.WriteNil(); - } + => writer.WriteNil(); public DBNull Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) => DBNull.Value; } \ No newline at end of file diff --git a/src/Tests/EFCoreSecondLevelCacheInterceptor.Tests/Settings/EFServiceProvider.cs b/src/Tests/EFCoreSecondLevelCacheInterceptor.Tests/Settings/EFServiceProvider.cs index d3600d2..46a51ca 100644 --- a/src/Tests/EFCoreSecondLevelCacheInterceptor.Tests/Settings/EFServiceProvider.cs +++ b/src/Tests/EFCoreSecondLevelCacheInterceptor.Tests/Settings/EFServiceProvider.cs @@ -13,6 +13,8 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Newtonsoft.Json; +using StackExchange.Redis; +using ZiggyCreatures.Caching.Fusion; using ConfigurationBuilder = Microsoft.Extensions.Configuration.ConfigurationBuilder; namespace EFCoreSecondLevelCacheInterceptor.Tests; @@ -25,6 +27,8 @@ public enum TestCacheProvider EasyCachingCoreInMemory, EasyCachingCoreRedis, EasyCachingCoreHybrid, + FusionCache, + StackExchangeRedis } public class SpecialTypesConverter : JsonConverter @@ -33,20 +37,20 @@ public class SpecialTypesConverter : JsonConverter public override bool CanWrite => true; - public override bool CanConvert(Type objectType) => - objectType == typeof(TimeSpan) || objectType == typeof(TimeSpan?) - || objectType == typeof(DateTime) || objectType == typeof(DateTime?) - || objectType == typeof(DateTimeOffset) || objectType == typeof(DateTimeOffset?); + public override bool CanConvert(Type objectType) + => objectType == typeof(TimeSpan) || objectType == typeof(TimeSpan?) || objectType == typeof(DateTime) || + objectType == typeof(DateTime?) || objectType == typeof(DateTimeOffset) || + objectType == typeof(DateTimeOffset?); - public override object - ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) => reader.Value; + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + => reader.Value; public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { writer.WriteStartObject(); - writer.WritePropertyName("$type"); // Deserializer helper + writer.WritePropertyName(name: "$type"); // Deserializer helper writer.WriteValue(value.GetType().FullName); - writer.WritePropertyName("$value"); + writer.WritePropertyName(name: "$value"); writer.WriteValue(value); writer.WriteEndObject(); } @@ -65,51 +69,140 @@ public static IEFCacheServiceProvider GetCacheServiceProvider(TestCacheProvider switch (provider) { case TestCacheProvider.BuiltInInMemory: - services.AddEFSecondLevelCache(options => options.UseMemoryCacheProvider()); + services.AddEFSecondLevelCache(options + => options.UseMemoryCacheProvider().ConfigureLogging(enable: true)); + break; case TestCacheProvider.CacheManagerCoreInMemory: - services.AddEFSecondLevelCache(options => options.UseCacheManagerCoreProvider()); + services.AddEFSecondLevelCache(options + => options.UseCacheManagerCoreProvider().ConfigureLogging(enable: true)); + addCacheManagerCoreInMemory(services); + break; case TestCacheProvider.CacheManagerCoreRedis: - services.AddEFSecondLevelCache(options => options.UseCacheManagerCoreProvider()); + services.AddEFSecondLevelCache(options + => options.UseCacheManagerCoreProvider().ConfigureLogging(enable: true)); + addCacheManagerCoreRedis(services); + break; case TestCacheProvider.EasyCachingCoreInMemory: const string providerName1 = "InMemory1"; - services.AddEFSecondLevelCache(options => options.UseEasyCachingCoreProvider(providerName1)); + + services.AddEFSecondLevelCache(options + => options.UseEasyCachingCoreProvider(providerName1).ConfigureLogging(enable: true)); + addEasyCachingCoreInMemory(services, providerName1); + break; case TestCacheProvider.EasyCachingCoreRedis: const string providerName2 = "Redis1"; - services.AddEFSecondLevelCache(options => options.UseEasyCachingCoreProvider(providerName2)); + + services.AddEFSecondLevelCache(options + => options.UseEasyCachingCoreProvider(providerName2).ConfigureLogging(enable: true)); + addEasyCachingCoreRedis(services, providerName2); + break; case TestCacheProvider.EasyCachingCoreHybrid: const string providerName3 = "Hybrid1"; - services.AddEFSecondLevelCache(options => options.UseEasyCachingCoreProvider(providerName3, true)); + + services.AddEFSecondLevelCache(options + => options.UseEasyCachingCoreProvider(providerName3, isHybridCache: true) + .ConfigureLogging(enable: true)); + addEasyCachingCoreHybrid(services, providerName3); + + break; + case TestCacheProvider.FusionCache: + AddFusionCache(services); + + services.AddEFSecondLevelCache(options + => options.UseFusionCacheProvider().ConfigureLogging(enable: true)); + + break; + case TestCacheProvider.StackExchangeRedis: + + var redisOptions = new ConfigurationOptions + { + EndPoints = new EndPointCollection + { + { + "127.0.0.1", 6379 + } + }, + AllowAdmin = true, + ConnectTimeout = 10000 + }; + + services.AddEFSecondLevelCache(options + => options.UseStackExchangeRedisCacheProvider(redisOptions, TimeSpan.FromMinutes(minutes: 5)) + .ConfigureLogging(enable: true)); + break; + default: + throw new ArgumentOutOfRangeException(nameof(provider), provider, message: null); } var serviceProvider = services.BuildServiceProvider(); var cacheProvider = serviceProvider.GetRequiredService<IEFCacheServiceProvider>(); cacheProvider.ClearAllCachedEntries(); + return cacheProvider; } + private static void AddFusionCache(ServiceCollection services) + => services.AddFusionCache() + .WithOptions(options => + { + options.DefaultEntryOptions = new FusionCacheEntryOptions + { + // CACHE DURATION + Duration = TimeSpan.FromMinutes(minutes: 1), + + // FAIL-SAFE OPTIONS + IsFailSafeEnabled = true, + FailSafeMaxDuration = TimeSpan.FromHours(hours: 2), + FailSafeThrottleDuration = TimeSpan.FromSeconds(seconds: 30), + + // FACTORY TIMEOUTS + FactorySoftTimeout = TimeSpan.FromMilliseconds(milliseconds: 500), + FactoryHardTimeout = TimeSpan.FromMilliseconds(milliseconds: 1500), + + // DISTRIBUTED CACHE + DistributedCacheSoftTimeout = TimeSpan.FromSeconds(seconds: 10), + DistributedCacheHardTimeout = TimeSpan.FromSeconds(seconds: 20), + AllowBackgroundDistributedCacheOperations = true, + + // JITTERING + JitterMaxDuration = TimeSpan.FromSeconds(seconds: 2) + }; + + // DISTIBUTED CACHE CIRCUIT-BREAKER + options.DistributedCacheCircuitBreakerDuration = TimeSpan.FromSeconds(seconds: 2); + + // CUSTOM LOG LEVELS + options.FailSafeActivationLogLevel = LogLevel.Debug; + options.SerializationErrorsLogLevel = LogLevel.Warning; + options.DistributedCacheSyntheticTimeoutsLogLevel = LogLevel.Debug; + options.DistributedCacheErrorsLogLevel = LogLevel.Error; + options.FactorySyntheticTimeoutsLogLevel = LogLevel.Debug; + options.FactoryErrorsLogLevel = LogLevel.Error; + }); + public static T GetRequiredService<T>() { var services = new ServiceCollection(); services.AddOptions(); services.AddLogging(cfg => cfg.AddConsole().AddDebug()); - services.AddEFSecondLevelCache(options => options.UseMemoryCacheProvider()); + services.AddEFSecondLevelCache(options => options.UseMemoryCacheProvider().ConfigureLogging(enable: true)); var serviceProvider = services.BuildServiceProvider(); + return serviceProvider.GetRequiredService<T>(); } - public static IServiceProvider GetConfiguredContextServiceProvider( - TestCacheProvider cacheProvider, + public static IServiceProvider GetConfiguredContextServiceProvider(TestCacheProvider cacheProvider, LogLevel logLevel, bool cacheAllQueries) { @@ -117,53 +210,84 @@ public static IServiceProvider GetConfiguredContextServiceProvider( services.AddOptions(); var basePath = Directory.GetCurrentDirectory(); Console.WriteLine($"Using `{basePath}` as the ContentRootPath"); - var configuration = new ConfigurationBuilder() - .SetBasePath(basePath) - .AddJsonFile("appsettings.json", false, true) - .Build(); + + var configuration = new ConfigurationBuilder().SetBasePath(basePath) + .AddJsonFile(path: "appsettings.json", optional: false, reloadOnChange: true) + .Build(); + services.AddSingleton(_ => configuration); var loggerProvider = new DebugLoggerProvider(); services.AddLogging(cfg => cfg.AddConsole().AddDebug().AddProvider(loggerProvider).SetMinimumLevel(logLevel)); services.AddEFSecondLevelCache(options => - { - switch (cacheProvider) - { - case TestCacheProvider.BuiltInInMemory: - options.UseMemoryCacheProvider(); - break; - case TestCacheProvider.CacheManagerCoreInMemory: - options.UseCacheManagerCoreProvider(); - addCacheManagerCoreInMemory(services); - break; - case TestCacheProvider.CacheManagerCoreRedis: - options.UseCacheManagerCoreProvider(); - addCacheManagerCoreRedis(services); - break; - case TestCacheProvider.EasyCachingCoreInMemory: - const string providerName1 = "InMemory-1"; - options.UseEasyCachingCoreProvider(providerName1); - addEasyCachingCoreInMemory(services, providerName1); - break; - case TestCacheProvider.EasyCachingCoreRedis: - const string providerName2 = "Redis-1"; - options.UseEasyCachingCoreProvider(providerName2); - addEasyCachingCoreRedis(services, providerName2); - break; - case TestCacheProvider.EasyCachingCoreHybrid: - const string providerName3 = "Hybrid1"; - options.UseEasyCachingCoreProvider(providerName3, true); - addEasyCachingCoreHybrid(services, providerName3); - break; - } - - if (cacheAllQueries) - { - options.CacheAllQueries(CacheExpirationMode.Absolute, - TimeSpan.FromMinutes(30)); - } - }); + { + options.ConfigureLogging(enable: true); + + switch (cacheProvider) + { + case TestCacheProvider.BuiltInInMemory: + options.UseMemoryCacheProvider(); + + break; + case TestCacheProvider.CacheManagerCoreInMemory: + options.UseCacheManagerCoreProvider(); + addCacheManagerCoreInMemory(services); + + break; + case TestCacheProvider.CacheManagerCoreRedis: + options.UseCacheManagerCoreProvider(); + addCacheManagerCoreRedis(services); + + break; + case TestCacheProvider.EasyCachingCoreInMemory: + const string providerName1 = "InMemory-1"; + options.UseEasyCachingCoreProvider(providerName1); + addEasyCachingCoreInMemory(services, providerName1); + + break; + case TestCacheProvider.EasyCachingCoreRedis: + const string providerName2 = "Redis-1"; + options.UseEasyCachingCoreProvider(providerName2); + addEasyCachingCoreRedis(services, providerName2); + + break; + case TestCacheProvider.EasyCachingCoreHybrid: + const string providerName3 = "Hybrid1"; + options.UseEasyCachingCoreProvider(providerName3, isHybridCache: true); + addEasyCachingCoreHybrid(services, providerName3); + + break; + case TestCacheProvider.FusionCache: + AddFusionCache(services); + options.UseFusionCacheProvider(); + + break; + case TestCacheProvider.StackExchangeRedis: + var redisOptions = new ConfigurationOptions + { + EndPoints = new EndPointCollection + { + { + "127.0.0.1", 6379 + } + }, + AllowAdmin = true, + ConnectTimeout = 10000 + }; + + options.UseStackExchangeRedisCacheProvider(redisOptions, TimeSpan.FromMinutes(minutes: 5)); + + break; + default: + throw new ArgumentOutOfRangeException(nameof(cacheProvider), cacheProvider, message: null); + } + + if (cacheAllQueries) + { + options.CacheAllQueries(CacheExpirationMode.Absolute, TimeSpan.FromMinutes(minutes: 30)); + } + }); services.AddConfiguredMsSqlDbContext(GetConnectionString(basePath, configuration)); @@ -174,205 +298,221 @@ private static void addEasyCachingCoreHybrid(ServiceCollection services, string { const string redisProvider = "redis"; const string memoryProvider = "memory"; + // More info: https://easycaching.readthedocs.io/en/latest/HybridCachingProvider/#how-to-use services.AddEasyCaching(option => - { - option.UseRedis(config => - { - config.DBConfig.AllowAdmin = true; - config.DBConfig.Endpoints.Add(new ServerEndPoint("127.0.0.1", - 6379)); - config.SerializerName = "MySerializer"; - config.DBConfig.ConnectionTimeout = 10000; - }, redisProvider) - .WithMessagePack(so => - { - so.EnableCustomResolver = true; - so.CustomResolvers = CompositeResolver.Create( - new IMessagePackFormatter[] - { - DBNullFormatter - .Instance, // This is necessary for the null values - }, - new IFormatterResolver[] - { - NativeDateTimeResolver.Instance, - ContractlessStandardResolver.Instance, - StandardResolverAllowPrivate.Instance, - }); - }, - "MySerializer") - //.WithSystemTextJson("MySerializer") - .UseInMemory(config => - { - config.DBConfig = new InMemoryCachingOptions - { - // scan time, default value is 60s - ExpirationScanFrequency = 60, - // total count of cache items, default value is 10000 - SizeLimit = 100, - - // enable deep clone when reading object from cache or not, default value is true. - EnableReadDeepClone = false, - // enable deep clone when writing object to cache or not, default value is false. - EnableWriteDeepClone = false, - }; - // the max random second will be added to cache's expiration, default value is 120 - config.MaxRdSecond = 120; - // whether enable logging, default is false - config.EnableLogging = false; - // mutex key's alive time(ms), default is 5000 - config.LockMs = 5000; - // when mutex key alive, it will sleep some time, default is 300 - config.SleepMs = 300; - }, memoryProvider) - .UseHybrid(config => - { - config.TopicName = "topic"; - config.LocalCacheProviderName = memoryProvider; - config.DistributedCacheProviderName = redisProvider; - }, - hybridProviderName) - .WithRedisBus(busConf => - { - busConf.Endpoints - .Add(new ServerEndPoint("127.0.0.1", 6379)); - // busConf.Database = 2; - busConf.AllowAdmin = true; - }); - }); + { + option.UseRedis(config => + { + config.DBConfig.AllowAdmin = true; + config.DBConfig.Endpoints.Add(new ServerEndPoint(host: "127.0.0.1", port: 6379)); + config.SerializerName = "MySerializer"; + config.DBConfig.ConnectionTimeout = 10000; + }, redisProvider) + .WithMessagePack(so => + { + so.EnableCustomResolver = true; + + so.CustomResolvers = CompositeResolver.Create(new IMessagePackFormatter[] + { + DBNullFormatter.Instance // This is necessary for the null values + }, new IFormatterResolver[] + { + NativeDateTimeResolver.Instance, ContractlessStandardResolver.Instance, + StandardResolverAllowPrivate.Instance + }); + }, name: "MySerializer") + + //.WithSystemTextJson("MySerializer") + .UseInMemory(config => + { + config.DBConfig = new InMemoryCachingOptions + { + // scan time, default value is 60s + ExpirationScanFrequency = 60, + + // total count of cache items, default value is 10000 + SizeLimit = 100, + + // enable deep clone when reading object from cache or not, default value is true. + EnableReadDeepClone = false, + + // enable deep clone when writing object to cache or not, default value is false. + EnableWriteDeepClone = false + }; + + // the max random second will be added to cache's expiration, default value is 120 + config.MaxRdSecond = 120; + + // whether enable logging, default is false + config.EnableLogging = false; + + // mutex key's alive time(ms), default is 5000 + config.LockMs = 5000; + + // when mutex key alive, it will sleep some time, default is 300 + config.SleepMs = 300; + }, memoryProvider) + .UseHybrid(config => + { + config.TopicName = "topic"; + config.LocalCacheProviderName = memoryProvider; + config.DistributedCacheProviderName = redisProvider; + }, hybridProviderName) + .WithRedisBus(busConf => + { + busConf.Endpoints.Add(new ServerEndPoint(host: "127.0.0.1", port: 6379)); + + // busConf.Database = 2; + busConf.AllowAdmin = true; + }); + }); } private static void addEasyCachingCoreRedis(ServiceCollection services, string providerName) - { - // It needs <PackageReference Include="EasyCaching.Redis" Version="0.8.8" /> - // More info: https://easycaching.readthedocs.io/en/latest/Redis/ - services.AddEasyCaching(option => - { - option.UseRedis(config => - { - config.DBConfig.AllowAdmin = true; - config.DBConfig.Endpoints.Add(new ServerEndPoint("127.0.0.1", - 6379)); - config.SerializerName = "MySerializer"; - config.DBConfig.ConnectionTimeout = 10000; - }, providerName) - .WithMessagePack(so => - { - so.EnableCustomResolver = true; - so.CustomResolvers = CompositeResolver.Create( - new IMessagePackFormatter[] - { - DBNullFormatter - .Instance, // This is necessary for the null values - }, - new IFormatterResolver[] - { - NativeDateTimeResolver.Instance, - ContractlessStandardResolver.Instance, - StandardResolverAllowPrivate.Instance, - }); - }, - "MySerializer"); - //.WithSystemTextJson("MySerializer"); - }); - } + => + + // It needs <PackageReference Include="EasyCaching.Redis" Version="0.8.8" /> + // More info: https://easycaching.readthedocs.io/en/latest/Redis/ + services.AddEasyCaching(option => + { + option.UseRedis(config => + { + config.DBConfig.AllowAdmin = true; + config.DBConfig.Endpoints.Add(new ServerEndPoint(host: "127.0.0.1", port: 6379)); + config.SerializerName = "MySerializer"; + config.DBConfig.ConnectionTimeout = 10000; + }, providerName) + .WithMessagePack(so => + { + so.EnableCustomResolver = true; + + so.CustomResolvers = CompositeResolver.Create(new IMessagePackFormatter[] + { + DBNullFormatter.Instance // This is necessary for the null values + }, new IFormatterResolver[] + { + NativeDateTimeResolver.Instance, ContractlessStandardResolver.Instance, + StandardResolverAllowPrivate.Instance + }); + }, name: "MySerializer"); + + //.WithSystemTextJson("MySerializer"); + }); private static void addEasyCachingCoreInMemory(ServiceCollection services, string providerName) - { - // It needs <PackageReference Include="EasyCaching.InMemory" Version="0.8.8" /> - // More info: https://easycaching.readthedocs.io/en/latest/In-Memory/ - services.AddEasyCaching(options => - { - // use memory cache with your own configuration - options.UseInMemory(config => - { - config.DBConfig = new InMemoryCachingOptions - { - // scan time, default value is 60s - ExpirationScanFrequency = 60, - // total count of cache items, default value is 10000 - SizeLimit = 100, - - // enable deep clone when reading object from cache or not, default value is true. - EnableReadDeepClone = false, - // enable deep clone when writing object to cache or not, default value is false. - EnableWriteDeepClone = false, - }; - // the max random second will be added to cache's expiration, default value is 120 - config.MaxRdSecond = 120; - // whether enable logging, default is false - config.EnableLogging = false; - // mutex key's alive time(ms), default is 5000 - config.LockMs = 5000; - // when mutex key alive, it will sleep some time, default is 300 - config.SleepMs = 300; - }, providerName); - }); - } + => + + // It needs <PackageReference Include="EasyCaching.InMemory" Version="0.8.8" /> + // More info: https://easycaching.readthedocs.io/en/latest/In-Memory/ + services.AddEasyCaching(options => + { + // use memory cache with your own configuration + options.UseInMemory(config => + { + config.DBConfig = new InMemoryCachingOptions + { + // scan time, default value is 60s + ExpirationScanFrequency = 60, + + // total count of cache items, default value is 10000 + SizeLimit = 100, + + // enable deep clone when reading object from cache or not, default value is true. + EnableReadDeepClone = false, + + // enable deep clone when writing object to cache or not, default value is false. + EnableWriteDeepClone = false + }; + + // the max random second will be added to cache's expiration, default value is 120 + config.MaxRdSecond = 120; + + // whether enable logging, default is false + config.EnableLogging = false; + + // mutex key's alive time(ms), default is 5000 + config.LockMs = 5000; + + // when mutex key alive, it will sleep some time, default is 300 + config.SleepMs = 300; + }, providerName); + }); private static void addCacheManagerCoreRedis(ServiceCollection services) { var jss = new JsonSerializerSettings - { - NullValueHandling = NullValueHandling.Ignore, - ReferenceLoopHandling = ReferenceLoopHandling.Ignore, - TypeNameHandling = TypeNameHandling.Auto, - Converters = { new SpecialTypesConverter() }, - }; + { + NullValueHandling = NullValueHandling.Ignore, + ReferenceLoopHandling = ReferenceLoopHandling.Ignore, + TypeNameHandling = TypeNameHandling.Auto, + Converters = + { + new SpecialTypesConverter() + } + }; const string redisConfigurationKey = "redis"; - services.AddSingleton(typeof(ICacheManagerConfiguration), - new CacheManager.Core.ConfigurationBuilder() - .WithJsonSerializer(jss, jss) - .WithUpdateMode(CacheUpdateMode.Up) - .WithRedisConfiguration(redisConfigurationKey, config => - { - config.WithAllowAdmin() - .WithDatabase(0) - .WithEndpoint("localhost", 6379) - // Enables keyspace notifications to react on eviction/expiration of items. - // Make sure that all servers are configured correctly and 'notify-keyspace-events' is at least set to 'Exe', otherwise CacheManager will not retrieve any events. - // See https://redis.io/topics/notifications#configuration for configuration details. - .EnableKeyspaceEvents(); - }) - .WithMaxRetries(100) - .WithRetryTimeout(50) - .WithRedisCacheHandle(redisConfigurationKey) - .DisablePerformanceCounters() - .DisableStatistics() - .Build()); + + services.AddSingleton(typeof(ICacheManagerConfiguration), new CacheManager.Core.ConfigurationBuilder() + .WithJsonSerializer(jss, jss) + .WithUpdateMode(CacheUpdateMode.Up) + .WithRedisConfiguration(redisConfigurationKey, config => + { + config.WithAllowAdmin() + .WithDatabase(databaseIndex: 0) + .WithEndpoint(host: "localhost", port: 6379) + + // Enables keyspace notifications to react on eviction/expiration of items. + // Make sure that all servers are configured correctly and 'notify-keyspace-events' is at least set to 'Exe', otherwise CacheManager will not retrieve any events. + // See https://redis.io/topics/notifications#configuration for configuration details. + .EnableKeyspaceEvents(); + }) + .WithMaxRetries(retries: 100) + .WithRetryTimeout(timeoutMillis: 50) + .WithRedisCacheHandle(redisConfigurationKey) + .DisablePerformanceCounters() + .DisableStatistics() + .Build()); + services.AddSingleton(typeof(ICacheManager<>), typeof(BaseCacheManager<>)); } private static void addCacheManagerCoreInMemory(ServiceCollection services) { services.AddSingleton(typeof(ICacheManagerConfiguration), - new CacheManager.Core.ConfigurationBuilder() - .WithJsonSerializer() - .WithMicrosoftMemoryCacheHandle("MemoryCache1") - .DisablePerformanceCounters() - .DisableStatistics() - .Build()); + new CacheManager.Core.ConfigurationBuilder().WithJsonSerializer() + .WithMicrosoftMemoryCacheHandle(instanceName: "MemoryCache1") + .DisablePerformanceCounters() + .DisableStatistics() + .Build()); + services.AddSingleton(typeof(ICacheManager<>), typeof(BaseCacheManager<>)); } public static string GetConnectionString(string basePath, IConfigurationRoot configuration) { - var testsFolder = basePath.Split(new[] { "\\Tests\\" }, StringSplitOptions.RemoveEmptyEntries)[0]; - var contentRootPath = Path.Combine(testsFolder, "Tests", "EFCoreSecondLevelCacheInterceptor.AspNetCoreSample"); - var connectionString = configuration["ConnectionStrings:ApplicationDbContextConnection"]; - if (connectionString.Contains("%CONTENTROOTPATH%")) + var testsFolder = basePath.Split(new[] { - connectionString = connectionString.Replace("%CONTENTROOTPATH%", contentRootPath); + "\\Tests\\" + }, StringSplitOptions.RemoveEmptyEntries)[0]; + + var contentRootPath = Path.Combine(testsFolder, path2: "Tests", + path3: "EFCoreSecondLevelCacheInterceptor.AspNetCoreSample"); + + var connectionString = configuration[key: "ConnectionStrings:ApplicationDbContextConnection"]; + + if (connectionString.Contains(value: "%CONTENTROOTPATH%")) + { + connectionString = connectionString.Replace(oldValue: "%CONTENTROOTPATH%", contentRootPath); } Console.WriteLine($"Using {connectionString}"); + return connectionString; } - public static void RunInContext( - TestCacheProvider cacheProvider, + public static void RunInContext(TestCacheProvider cacheProvider, LogLevel logLevel, bool cacheAllQueries, params Action<ApplicationDbContext, DebugLoggerProvider>[] actions) @@ -382,6 +522,7 @@ public static void RunInContext( var serviceProvider = GetConfiguredContextServiceProvider(cacheProvider, logLevel, cacheAllQueries); var cacheServiceProvider = serviceProvider.GetRequiredService<IEFCacheServiceProvider>(); cacheServiceProvider.ClearAllCachedEntries(); + using (var serviceScope = serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope()) { foreach (var action in actions) @@ -395,8 +536,7 @@ public static void RunInContext( } } - public static async Task RunInContextAsync( - TestCacheProvider cacheProvider, + public static async Task RunInContextAsync(TestCacheProvider cacheProvider, LogLevel logLevel, bool cacheAllQueries, params Func<ApplicationDbContext, DebugLoggerProvider, Task>[] actions) @@ -406,6 +546,7 @@ public static async Task RunInContextAsync( var serviceProvider = GetConfiguredContextServiceProvider(cacheProvider, logLevel, cacheAllQueries); var cacheServiceProvider = serviceProvider.GetRequiredService<IEFCacheServiceProvider>(); cacheServiceProvider.ClearAllCachedEntries(); + using (var serviceScope = serviceProvider.GetRequiredService<IServiceScopeFactory>().CreateScope()) { foreach (var action in actions) @@ -413,7 +554,7 @@ public static async Task RunInContextAsync( using (var context = serviceScope.ServiceProvider.GetRequiredService<ApplicationDbContext>()) { await action(context, - (DebugLoggerProvider)serviceProvider.GetRequiredService<ILoggerProvider>()); + (DebugLoggerProvider)serviceProvider.GetRequiredService<ILoggerProvider>()); } } } @@ -423,6 +564,7 @@ await action(context, public static void ExecuteInParallel(Action test, int count = 40) { var tests = new Action[count]; + for (var i = 0; i < count; i++) { tests[i] = test; diff --git a/src/Tests/EFCoreSecondLevelCacheInterceptor.UnitTests/EFCacheServiceProviderTests.cs b/src/Tests/EFCoreSecondLevelCacheInterceptor.UnitTests/EFCacheServiceProviderTests.cs index c5d264e..ff2536b 100644 --- a/src/Tests/EFCoreSecondLevelCacheInterceptor.UnitTests/EFCacheServiceProviderTests.cs +++ b/src/Tests/EFCoreSecondLevelCacheInterceptor.UnitTests/EFCacheServiceProviderTests.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.DependencyInjection; +using Moq; namespace EFCoreSecondLevelCacheInterceptor.UnitTests; @@ -435,9 +436,12 @@ private static IEFCacheServiceProvider CreateMemoryCacheServiceProvider() var services = new ServiceCollection(); services.AddMemoryCache(); services.AddSingleton<IMemoryCacheChangeTokenProvider, EFMemoryCacheChangeTokenProvider>(); + services.AddSingleton<IEFDebugLogger, EFDebugLogger>(); var serviceProvider = services.BuildServiceProvider(); + var loggerMock = new Mock<IEFDebugLogger>(); + return new EFMemoryCacheServiceProvider(serviceProvider.GetRequiredService<IMemoryCache>(), - serviceProvider.GetRequiredService<IMemoryCacheChangeTokenProvider>()); + serviceProvider.GetRequiredService<IMemoryCacheChangeTokenProvider>(), loggerMock.Object); } } \ No newline at end of file diff --git a/src/Tests/EFCoreSecondLevelCacheInterceptor.UnitTests/EFCoreSecondLevelCacheInterceptor.UnitTests.csproj b/src/Tests/EFCoreSecondLevelCacheInterceptor.UnitTests/EFCoreSecondLevelCacheInterceptor.UnitTests.csproj index 227661d..d4fe936 100644 --- a/src/Tests/EFCoreSecondLevelCacheInterceptor.UnitTests/EFCoreSecondLevelCacheInterceptor.UnitTests.csproj +++ b/src/Tests/EFCoreSecondLevelCacheInterceptor.UnitTests/EFCoreSecondLevelCacheInterceptor.UnitTests.csproj @@ -13,9 +13,9 @@ </PropertyGroup> <ItemGroup> - <PackageReference Include="coverlet.collector" Version="6.0.2"/> + <PackageReference Include="coverlet.collector" Version="6.0.3"/> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0"/> - <PackageReference Include="Moq" Version="4.20.72" /> + <PackageReference Include="Moq" Version="4.20.72"/> <PackageReference Include="xunit" Version="2.9.2"/> <PackageReference Include="xunit.runner.visualstudio" Version="3.0.0"/> </ItemGroup> @@ -25,6 +25,7 @@ </ItemGroup> <ItemGroup> - <ProjectReference Include="..\..\EFCoreSecondLevelCacheInterceptor\EFCoreSecondLevelCacheInterceptor.csproj" /> + <ProjectReference Include="..\..\EFCoreSecondLevelCacheInterceptor.MemoryCache\EFCoreSecondLevelCacheInterceptor.MemoryCache.csproj"/> + <ProjectReference Include="..\..\EFCoreSecondLevelCacheInterceptor\EFCoreSecondLevelCacheInterceptor.csproj"/> </ItemGroup> </Project> diff --git a/src/Tests/EFCoreSecondLevelCacheInterceptor.UnitTests/EFServiceCollectionExtensionsTests.cs b/src/Tests/EFCoreSecondLevelCacheInterceptor.UnitTests/EFServiceCollectionExtensionsTests.cs index ed74857..b93560b 100644 --- a/src/Tests/EFCoreSecondLevelCacheInterceptor.UnitTests/EFServiceCollectionExtensionsTests.cs +++ b/src/Tests/EFCoreSecondLevelCacheInterceptor.UnitTests/EFServiceCollectionExtensionsTests.cs @@ -25,7 +25,7 @@ public void AddEFSecondLevelCache_RegistersRequiredServices() // Act services.TryAddSingleton(typeof(ILogger<>), typeof(Logger<>)); - services.AddEFSecondLevelCache(_ => { }); + services.AddEFSecondLevelCache(options => options.UseMemoryCacheProvider()); // Assert var serviceProvider = services.BuildServiceProvider(); @@ -50,7 +50,11 @@ public void AddEFSecondLevelCache_RegistersDefaultHashProvider_WhenHashProviderI var services = new ServiceCollection(); // Act - services.AddEFSecondLevelCache(options => { options.Settings.HashProvider = null; }); + services.AddEFSecondLevelCache(options => + { + options.UseMemoryCacheProvider(); + options.Settings.HashProvider = null; + }); // Assert var serviceProvider = services.BuildServiceProvider(); @@ -65,7 +69,11 @@ public void AddEFSecondLevelCache_RegistersCustomHashProvider_WhenHashProviderIs var services = new ServiceCollection(); // Act - services.AddEFSecondLevelCache(options => { options.Settings.HashProvider = typeof(CustomHashProvider); }); + services.AddEFSecondLevelCache(options => + { + options.UseMemoryCacheProvider(); + options.Settings.HashProvider = typeof(CustomHashProvider); + }); // Assert var serviceProvider = services.BuildServiceProvider(); @@ -76,16 +84,12 @@ public void AddEFSecondLevelCache_RegistersCustomHashProvider_WhenHashProviderIs [Fact] public void AddEFSecondLevelCache_RegistersDefaultCacheProvider_WhenCacheProviderIsNull() { - // Arrange var services = new ServiceCollection(); - // Act - services.AddEFSecondLevelCache(options => { options.Settings.CacheProvider = null; }); - - // Assert - var serviceProvider = services.BuildServiceProvider(); - - Assert.IsType<EFMemoryCacheServiceProvider>(serviceProvider.GetService<IEFCacheServiceProvider>()); + Assert.Throws<InvalidOperationException>(() => services.AddEFSecondLevelCache(options => + { + options.Settings.CacheProvider = null; + })); } [Fact] @@ -105,13 +109,18 @@ public void AddEFSecondLevelCache_RegistersCustomCacheProvider_WhenCacheProvider private class Logger<TCategoryName> : ILogger<TCategoryName> { - public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, - Func<TState, Exception, string> formatter) => throw new NotImplementedException(); + public void Log<TState>(LogLevel logLevel, + EventId eventId, + TState state, + Exception exception, + Func<TState, Exception, string> formatter) + => throw new NotImplementedException(); public bool IsEnabled(LogLevel logLevel) => throw new NotImplementedException(); - public IDisposable BeginScope<TState>(TState state) where TState : notnull => - throw new NotImplementedException(); + public IDisposable BeginScope<TState>(TState state) + where TState : notnull + => throw new NotImplementedException(); } private class CustomHashProvider : IEFHashProvider @@ -127,11 +136,11 @@ private class CustomCacheProvider : IEFCacheServiceProvider { public void ClearAllCachedEntries() => throw new NotImplementedException(); - public EFCachedData GetValue(EFCacheKey cacheKey, EFCachePolicy cachePolicy) => - throw new NotImplementedException(); + public EFCachedData GetValue(EFCacheKey cacheKey, EFCachePolicy cachePolicy) + => throw new NotImplementedException(); - public void InsertValue(EFCacheKey cacheKey, EFCachedData value, EFCachePolicy cachePolicy) => - throw new NotImplementedException(); + public void InsertValue(EFCacheKey cacheKey, EFCachedData? value, EFCachePolicy cachePolicy) + => throw new NotImplementedException(); public void InvalidateCacheDependencies(EFCacheKey cacheKey) => throw new NotImplementedException(); } diff --git a/src/Tests/Issues/Issue123WithMessagePack/Issue123WithMessagePack.csproj b/src/Tests/Issues/Issue123WithMessagePack/Issue123WithMessagePack.csproj index 17e109c..ac79653 100644 --- a/src/Tests/Issues/Issue123WithMessagePack/Issue123WithMessagePack.csproj +++ b/src/Tests/Issues/Issue123WithMessagePack/Issue123WithMessagePack.csproj @@ -1,28 +1,29 @@ <Project Sdk="Microsoft.NET.Sdk"> - <PropertyGroup> - <OutputType>Exe</OutputType> - <TargetFramework>net9.0</TargetFramework> - </PropertyGroup> - <ItemGroup> - <ProjectReference Include="..\..\..\EFCoreSecondLevelCacheInterceptor\EFCoreSecondLevelCacheInterceptor.csproj" /> - </ItemGroup> - <ItemGroup> - <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0"> - <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> - <PrivateAssets>all</PrivateAssets> - </PackageReference> - <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0" /> - <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" /> - <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.0" /> - <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0" /> - <PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="9.0.0" /> - <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.0" /> - <PackageReference Include="EasyCaching.Core" Version="1.9.2" /> - <PackageReference Include="EasyCaching.Redis" Version="1.9.2" /> - <PackageReference Include="EasyCaching.Serialization.MessagePack" Version="1.9.2" /> - </ItemGroup> - <ItemGroup> - <None Include="appsettings.json" CopyToOutputDirectory="PreserveNewest" /> - </ItemGroup> + <PropertyGroup> + <OutputType>Exe</OutputType> + <TargetFramework>net9.0</TargetFramework> + </PropertyGroup> + <ItemGroup> + <ProjectReference Include="..\..\..\EFCoreSecondLevelCacheInterceptor.EasyCaching.Core\EFCoreSecondLevelCacheInterceptor.EasyCaching.Core.csproj"/> + <ProjectReference Include="..\..\..\EFCoreSecondLevelCacheInterceptor\EFCoreSecondLevelCacheInterceptor.csproj"/> + </ItemGroup> + <ItemGroup> + <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0"/> + <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0"> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + <PrivateAssets>all</PrivateAssets> + </PackageReference> + <PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.0"/> + <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0"/> + <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.0"/> + <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0"/> + <PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="9.0.0"/> + <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.0"/> + <PackageReference Include="EasyCaching.Core" Version="1.9.2"/> + <PackageReference Include="EasyCaching.Redis" Version="1.9.2"/> + <PackageReference Include="EasyCaching.Serialization.MessagePack" Version="1.9.2"/> + </ItemGroup> + <ItemGroup> + <None Include="appsettings.json" CopyToOutputDirectory="PreserveNewest"/> + </ItemGroup> </Project> \ No newline at end of file diff --git a/src/Tests/Issues/Issue125EF5x/Issue125EF5x.csproj b/src/Tests/Issues/Issue125EF5x/Issue125EF5x.csproj index fd617bc..57aac3c 100644 --- a/src/Tests/Issues/Issue125EF5x/Issue125EF5x.csproj +++ b/src/Tests/Issues/Issue125EF5x/Issue125EF5x.csproj @@ -4,6 +4,7 @@ <TargetFramework>net9.0</TargetFramework> </PropertyGroup> <ItemGroup> + <ProjectReference Include="..\..\..\EFCoreSecondLevelCacheInterceptor.EasyCaching.Core\EFCoreSecondLevelCacheInterceptor.EasyCaching.Core.csproj"/> <ProjectReference Include="..\..\..\EFCoreSecondLevelCacheInterceptor\EFCoreSecondLevelCacheInterceptor.csproj"/> </ItemGroup> <ItemGroup> diff --git a/src/Tests/Issues/Issue12MySQL/Issue12MySQL.csproj b/src/Tests/Issues/Issue12MySQL/Issue12MySQL.csproj index e2786d9..4f8a141 100644 --- a/src/Tests/Issues/Issue12MySQL/Issue12MySQL.csproj +++ b/src/Tests/Issues/Issue12MySQL/Issue12MySQL.csproj @@ -1,27 +1,28 @@ <Project Sdk="Microsoft.NET.Sdk"> - <PropertyGroup> - <OutputType>Exe</OutputType> - <TargetFramework>net9.0</TargetFramework> - </PropertyGroup> - <ItemGroup> - <ProjectReference Include="..\..\..\EFCoreSecondLevelCacheInterceptor\EFCoreSecondLevelCacheInterceptor.csproj" /> - </ItemGroup> - <ItemGroup> - <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.0" /> - <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" /> - <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.0" /> - <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0"> - <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> - <PrivateAssets>all</PrivateAssets> - </PackageReference> - <PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="9.0.0" /> - <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.0" /> - <PackageReference Include="MySql.EntityFrameworkCore" Version="9.0.0" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0"> - <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> - <PrivateAssets>all</PrivateAssets> - </PackageReference> - </ItemGroup> + <PropertyGroup> + <OutputType>Exe</OutputType> + <TargetFramework>net9.0</TargetFramework> + </PropertyGroup> + <ItemGroup> + <ProjectReference Include="..\..\..\EFCoreSecondLevelCacheInterceptor.MemoryCache\EFCoreSecondLevelCacheInterceptor.MemoryCache.csproj"/> + <ProjectReference Include="..\..\..\EFCoreSecondLevelCacheInterceptor\EFCoreSecondLevelCacheInterceptor.csproj"/> + </ItemGroup> + <ItemGroup> + <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0"/> + <PackageReference Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.0"/> + <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0"/> + <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.0"/> + <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0"/> + <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0"> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + <PrivateAssets>all</PrivateAssets> + </PackageReference> + <PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="9.0.0"/> + <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.0"/> + <PackageReference Include="MySql.EntityFrameworkCore" Version="9.0.0-preview"/> + <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.0"> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + <PrivateAssets>all</PrivateAssets> + </PackageReference> + </ItemGroup> </Project> diff --git a/src/Tests/Issues/Issue12PostgreSql/Issue12PostgreSql.csproj b/src/Tests/Issues/Issue12PostgreSql/Issue12PostgreSql.csproj index ee08fa4..90853e0 100644 --- a/src/Tests/Issues/Issue12PostgreSql/Issue12PostgreSql.csproj +++ b/src/Tests/Issues/Issue12PostgreSql/Issue12PostgreSql.csproj @@ -4,6 +4,8 @@ <TargetFramework>net9.0</TargetFramework> </PropertyGroup> <ItemGroup> + <ProjectReference Include="..\..\..\EFCoreSecondLevelCacheInterceptor.EasyCaching.Core\EFCoreSecondLevelCacheInterceptor.EasyCaching.Core.csproj"/> + <ProjectReference Include="..\..\..\EFCoreSecondLevelCacheInterceptor.MemoryCache\EFCoreSecondLevelCacheInterceptor.MemoryCache.csproj"/> <ProjectReference Include="..\..\..\EFCoreSecondLevelCacheInterceptor\EFCoreSecondLevelCacheInterceptor.csproj"/> </ItemGroup> <ItemGroup> @@ -26,7 +28,7 @@ <PackageReference Include="EasyCaching.Redis" Version="1.9.2"/> <PackageReference Include="EasyCaching.HybridCache" Version="1.9.2"/> <PackageReference Include="EasyCaching.Bus.Redis" Version="1.9.2"/> - <PackageReference Include="EasyCaching.Serialization.MessagePack" Version="1.9.2" /> + <PackageReference Include="EasyCaching.Serialization.MessagePack" Version="1.9.2"/> </ItemGroup> <ItemGroup> diff --git a/src/Tests/Issues/Issue154/Issue154.csproj b/src/Tests/Issues/Issue154/Issue154.csproj index 5b25e61..9794ace 100644 --- a/src/Tests/Issues/Issue154/Issue154.csproj +++ b/src/Tests/Issues/Issue154/Issue154.csproj @@ -1,31 +1,32 @@ <Project Sdk="Microsoft.NET.Sdk"> - <PropertyGroup> - <OutputType>Exe</OutputType> - <TargetFramework>net9.0</TargetFramework> - </PropertyGroup> - <ItemGroup> - <ProjectReference Include="..\..\..\EFCoreSecondLevelCacheInterceptor\EFCoreSecondLevelCacheInterceptor.csproj" /> - </ItemGroup> - <ItemGroup> - <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" /> - <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" /> - <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.0" /> - <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0"> - <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> - <PrivateAssets>all</PrivateAssets> - </PackageReference> - <PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="9.0.0" /> - <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.0" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="9.0.0" /> - <PackageReference Include="CacheManager.StackExchange.Redis" Version="1.2.0" /> - <PackageReference Include="CacheManager.Core" Version="1.2.0" /> - <PackageReference Include="CacheManager.Microsoft.Extensions.Caching.Memory" Version="1.2.0" /> - <PackageReference Include="CacheManager.Serialization.Json" Version="1.2.0" /> - </ItemGroup> + <PropertyGroup> + <OutputType>Exe</OutputType> + <TargetFramework>net9.0</TargetFramework> + </PropertyGroup> + <ItemGroup> + <ProjectReference Include="..\..\..\EFCoreSecondLevelCacheInterceptor.MemoryCache\EFCoreSecondLevelCacheInterceptor.MemoryCache.csproj"/> + <ProjectReference Include="..\..\..\EFCoreSecondLevelCacheInterceptor\EFCoreSecondLevelCacheInterceptor.csproj"/> + </ItemGroup> + <ItemGroup> + <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0"/> + <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0"/> + <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.0"/> + <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0"/> + <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0"> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + <PrivateAssets>all</PrivateAssets> + </PackageReference> + <PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="9.0.0"/> + <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.0"/> + <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0"/> + <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="9.0.0"/> + <PackageReference Include="CacheManager.StackExchange.Redis" Version="1.2.0"/> + <PackageReference Include="CacheManager.Core" Version="1.2.0"/> + <PackageReference Include="CacheManager.Microsoft.Extensions.Caching.Memory" Version="1.2.0"/> + <PackageReference Include="CacheManager.Serialization.Json" Version="1.2.0"/> + </ItemGroup> - <ItemGroup> - <None Include="appsettings.json" CopyToOutputDirectory="PreserveNewest" /> - </ItemGroup> + <ItemGroup> + <None Include="appsettings.json" CopyToOutputDirectory="PreserveNewest"/> + </ItemGroup> </Project> \ No newline at end of file diff --git a/src/Tests/Issues/Issue192/Issue192.csproj b/src/Tests/Issues/Issue192/Issue192.csproj index af4af8c..6fd3bd5 100644 --- a/src/Tests/Issues/Issue192/Issue192.csproj +++ b/src/Tests/Issues/Issue192/Issue192.csproj @@ -4,6 +4,7 @@ <TargetFramework>net9.0</TargetFramework> </PropertyGroup> <ItemGroup> + <ProjectReference Include="..\..\..\EFCoreSecondLevelCacheInterceptor.MemoryCache\EFCoreSecondLevelCacheInterceptor.MemoryCache.csproj"/> <ProjectReference Include="..\..\..\EFCoreSecondLevelCacheInterceptor\EFCoreSecondLevelCacheInterceptor.csproj"/> </ItemGroup> <ItemGroup> @@ -21,7 +22,7 @@ <PackageReference Include="EasyCaching.Core" Version="1.9.2"/> <PackageReference Include="EasyCaching.Redis" Version="1.9.2"/> <PackageReference Include="EasyCaching.Serialization.MessagePack" Version="1.9.2"/> - <PackageReference Include="EFCore.BulkExtensions" Version="9.0.0"/> + <PackageReference Include="EFCore.BulkExtensions" Version="9.0.0-rc.1"/> </ItemGroup> <ItemGroup> <None Include="appsettings.json" CopyToOutputDirectory="PreserveNewest"/> diff --git a/src/Tests/Issues/Issue4SpatialType/Issue4SpatialType.csproj b/src/Tests/Issues/Issue4SpatialType/Issue4SpatialType.csproj index 11e62ec..6124b9f 100644 --- a/src/Tests/Issues/Issue4SpatialType/Issue4SpatialType.csproj +++ b/src/Tests/Issues/Issue4SpatialType/Issue4SpatialType.csproj @@ -4,6 +4,7 @@ <TargetFramework>net9.0</TargetFramework> </PropertyGroup> <ItemGroup> + <ProjectReference Include="..\..\..\EFCoreSecondLevelCacheInterceptor.MemoryCache\EFCoreSecondLevelCacheInterceptor.MemoryCache.csproj"/> <ProjectReference Include="..\..\..\EFCoreSecondLevelCacheInterceptor\EFCoreSecondLevelCacheInterceptor.csproj"/> </ItemGroup> <ItemGroup> diff --git a/src/Tests/Issues/Issue9SQLiteInt32/App_Data/TestDb.sqlite b/src/Tests/Issues/Issue9SQLiteInt32/App_Data/TestDb.sqlite index f921e167cda0364661f9023e963b8131950e28a0..c0c46a9f73b19c0d622d839dfde015e9f8feb03e 100644 GIT binary patch delta 675 zcmZ{hPiqrF7{+IIlWhJpyBkV~rHE@NEyd0{v%9mC359fb-J&#YN=e0>O4OjCsi`(6 zFKs+{@nYF8AmUwYj)Eu%g`y&Y7Y_w*ibwqb?zZL>`!MedGtc|-{9yK;!M(Te>10j- z03^a1Dz-mo76hO_yr0cr5SNaCGB3;WPw6Oq{HkA<#K*Z<u%A2!;{$c<oe*qhN-!S0 z$jrh-@F_DhX$i-Li3#{5*K6MFv{qZ)?O~4=-V0C&zRETfgE8fK*k3AF=VM`ZGRp5e zP%01tAEA@wjSYWsxqatBv)At4+o^VMZH~SK8RdK|vVu2By*la_wbiw2%g$OuTk;!o z*|Zj_SZ{B&c6!aN?V?uqZ`8GwT3GAVYBoKajW|>Qly8IF!e?cA3W}g+V1pt|5VkeK z%9xZbs&fN#N{mYwmoYw2zbI+0V;#>xaj6h|zkDHnay`%zYT^(m4f$}8Q(w!weMQKF zf(L|DN?9I<aQz}wQ>hfZ-n`qsJ(7c&&P`%6YKT9)GhKEHL3AvLlsmpp9EO-ft{}^I z33B;j32~d44!3uS?Ubx=GNKMo_#Hxj3g$un6sa~YN&k=}x83RdFVm(LrJSEZ6uOLY yK13pwKOZ7}to%031uzYO;dtC-21h<;F0wq+LXK(qh%#z<wu=dMyuq(ZUi}Lc@t=nP delta 483 zcmZ{gF-yZh6vyu_scjle8YI*zYMn&z!d>o?G+k<vCI||;=qic@3za&!igs}kigI5- zT%26&7f=vf?4rBqCOGQgXezqtpZ6ZT_vi6`yu)jFcm_|#4H*ECN%L3I=^Qg<VDa*> z`T^uz7ibUqWv;89_l&|=PC7Cq(a(-TxwoX=AaRkeKv_KJXJJO<)tT{6^i(XB;IXl{ zvDs>@G}^nrb+B-YAQXn?incx>E;U8$X!{U}H*G<YQh?k5;;Q_h=b1$<dj?92Me$Ob z%6zw1WT-Io2=sOBF-5u44mt`lK`{VGQS@j8!qsV5P!t8OZEQEUY+@5k7`E*)^{b9u z<;?Oblrbv3M*o`3qZUC@QV(5+`Z10ZPBG^+!J*GQ?APNc2xC_F()oc!NcB4qM5r?f zO<+bpFxl)6Og3z`T7R8m9^uS!|A9$e-{Xv0wDxg7P`~!h*nIjP-=S_y0~d#5oVb`r Xl@R-3#8Wnx)RQ3a63^|uEt`ce*;j1Y diff --git a/src/Tests/Issues/Issue9SQLiteInt32/Issue9SQLiteInt32.csproj b/src/Tests/Issues/Issue9SQLiteInt32/Issue9SQLiteInt32.csproj index 5b25e61..9794ace 100644 --- a/src/Tests/Issues/Issue9SQLiteInt32/Issue9SQLiteInt32.csproj +++ b/src/Tests/Issues/Issue9SQLiteInt32/Issue9SQLiteInt32.csproj @@ -1,31 +1,32 @@ <Project Sdk="Microsoft.NET.Sdk"> - <PropertyGroup> - <OutputType>Exe</OutputType> - <TargetFramework>net9.0</TargetFramework> - </PropertyGroup> - <ItemGroup> - <ProjectReference Include="..\..\..\EFCoreSecondLevelCacheInterceptor\EFCoreSecondLevelCacheInterceptor.csproj" /> - </ItemGroup> - <ItemGroup> - <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0" /> - <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0" /> - <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.0" /> - <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0"> - <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> - <PrivateAssets>all</PrivateAssets> - </PackageReference> - <PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="9.0.0" /> - <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.0" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0" /> - <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="9.0.0" /> - <PackageReference Include="CacheManager.StackExchange.Redis" Version="1.2.0" /> - <PackageReference Include="CacheManager.Core" Version="1.2.0" /> - <PackageReference Include="CacheManager.Microsoft.Extensions.Caching.Memory" Version="1.2.0" /> - <PackageReference Include="CacheManager.Serialization.Json" Version="1.2.0" /> - </ItemGroup> + <PropertyGroup> + <OutputType>Exe</OutputType> + <TargetFramework>net9.0</TargetFramework> + </PropertyGroup> + <ItemGroup> + <ProjectReference Include="..\..\..\EFCoreSecondLevelCacheInterceptor.MemoryCache\EFCoreSecondLevelCacheInterceptor.MemoryCache.csproj"/> + <ProjectReference Include="..\..\..\EFCoreSecondLevelCacheInterceptor\EFCoreSecondLevelCacheInterceptor.csproj"/> + </ItemGroup> + <ItemGroup> + <PackageReference Include="Microsoft.EntityFrameworkCore" Version="9.0.0"/> + <PackageReference Include="Microsoft.Extensions.Logging" Version="9.0.0"/> + <PackageReference Include="Microsoft.Extensions.Logging.Console" Version="9.0.0"/> + <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0"/> + <PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.0"> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + <PrivateAssets>all</PrivateAssets> + </PackageReference> + <PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="9.0.0"/> + <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="9.0.0"/> + <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.0"/> + <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite.Core" Version="9.0.0"/> + <PackageReference Include="CacheManager.StackExchange.Redis" Version="1.2.0"/> + <PackageReference Include="CacheManager.Core" Version="1.2.0"/> + <PackageReference Include="CacheManager.Microsoft.Extensions.Caching.Memory" Version="1.2.0"/> + <PackageReference Include="CacheManager.Serialization.Json" Version="1.2.0"/> + </ItemGroup> - <ItemGroup> - <None Include="appsettings.json" CopyToOutputDirectory="PreserveNewest" /> - </ItemGroup> + <ItemGroup> + <None Include="appsettings.json" CopyToOutputDirectory="PreserveNewest"/> + </ItemGroup> </Project> \ No newline at end of file diff --git a/src/Tests/Issues/Issue9SQLiteInt32/_01-add_migrations.cmd b/src/Tests/Issues/Issue9SQLiteInt32/_01-add_migrations.cmd index 8707538..89f7a00 100644 --- a/src/Tests/Issues/Issue9SQLiteInt32/_01-add_migrations.cmd +++ b/src/Tests/Issues/Issue9SQLiteInt32/_01-add_migrations.cmd @@ -1,6 +1,6 @@ For /f "tokens=2-4 delims=/ " %%a in ('date /t') do (set mydate=%%c_%%a_%%b) For /f "tokens=1-2 delims=/:" %%a in ("%TIME: =0%") do (set mytime=%%a%%b) -dotnet tool update --global dotnet-ef --version 6.0.7 +dotnet tool update --global dotnet-ef --version 9.0.0 dotnet build dotnet ef migrations add V%mydate%_%mytime% --context ApplicationDbContext pause \ No newline at end of file