From e020e2f5aa3a79f2c1001e436eb4a5e5ffd64780 Mon Sep 17 00:00:00 2001 From: Thomas Galliker Date: Mon, 6 Apr 2026 15:00:36 +0200 Subject: [PATCH 01/19] Add global json + release notes txt --- HttpClient.Caching.sln | 2 ++ ReleaseNotes.txt | 8 ++++++++ global.json | 7 +++++++ 3 files changed, 17 insertions(+) create mode 100644 ReleaseNotes.txt create mode 100644 global.json diff --git a/HttpClient.Caching.sln b/HttpClient.Caching.sln index f0c0010..a6b4236 100644 --- a/HttpClient.Caching.sln +++ b/HttpClient.Caching.sln @@ -15,6 +15,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution .gitignore = .gitignore azure-pipelines.yml = azure-pipelines.yml README.md = README.md + global.json = global.json + ReleaseNotes.txt = ReleaseNotes.txt EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleAppSample", "Samples\ConsoleAppSample\ConsoleAppSample.csproj", "{592B2324-79AA-4973-8CE5-F65BD503641F}" diff --git a/ReleaseNotes.txt b/ReleaseNotes.txt new file mode 100644 index 0000000..93392c2 --- /dev/null +++ b/ReleaseNotes.txt @@ -0,0 +1,8 @@ +2.0 +- Replace own implementation of MemoryCache with Microsoft.Extensions.Caching.Memory. + +1.3 +- Add ICacheKeysProvider which allows to specify custom cache keys. + +1.0 +- Initial release. \ No newline at end of file diff --git a/global.json b/global.json new file mode 100644 index 0000000..31d78e1 --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "10.0.201", + "rollForward": "latestFeature", + "allowPrerelease": false + } +} From e7593c55590e60622eb163122e9b570760e17d6e Mon Sep 17 00:00:00 2001 From: Thomas Galliker Date: Mon, 6 Apr 2026 15:02:16 +0200 Subject: [PATCH 02/19] Update azure build config --- azure-pipelines.yml | 210 ++++++++++++++++++++++---------------------- 1 file changed, 107 insertions(+), 103 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 7318766..1450094 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -1,32 +1,31 @@ #################################################################### -# VSTS Build Configuration, Version 1.4 +# Azure DevOps Build Configuration # -# (c)2022 superdev GmbH +# (c)2026 superdev GmbH #################################################################### name: $[format('{0}', variables['buildName'])] pool: - vmImage: 'windows-2022' + vmImage: 'windows-latest' trigger: branches: include: - - main - - develop - - feature/* - - bugfix/* + - main + - develop + - feature/* + - bugfix/* paths: exclude: - - Docs/* + - Docs/* variables: - solution: 'HttpClient.Caching.sln' buildPlatform: 'Any CPU' buildConfiguration: 'Release' majorVersion: 1 - minorVersion: 6 + minorVersion: 7 patchVersion: $[counter(format('{0}.{1}', variables.majorVersion, variables.minorVersion), 0)] ${{ if eq(variables['Build.SourceBranch'], 'refs/heads/main') }}: # Versioning: 1.0.0 @@ -40,96 +39,101 @@ variables: buildName: $[format('{0}', variables.semVersion)] steps: -- task: Bash@3 - displayName: 'Print all variables' - inputs: - targetType: 'inline' - script: 'env | sort' - -- task: Assembly-Info-NetCore@2 - displayName: 'Update Assembly Info' - inputs: - Path: '$(Build.SourcesDirectory)' - FileNames: | - **/*.csproj - InsertAttributes: true - FileEncoding: 'auto' - WriteBOM: false - Product: 'HttpClient.Caching' - Description: '' - Company: 'superdev GmbH' - Copyright: '(c) $(date:YYYY) superdev GmbH' - VersionNumber: '$(Build.BuildNumber)' - FileVersionNumber: '$(Build.BuildNumber)' - InformationalVersion: '$(Build.BuildNumber)' - PackageVersion: '$(Build.BuildNumber)' - LogLevel: 'verbose' - FailOnWarning: false - DisableTelemetry: true - -- task: UseDotNet@2 - displayName: 'Use .NET 8.x' - inputs: - version: 8.x - -- task: NuGetToolInstaller@0 - displayName: 'Use NuGet 6.x' - inputs: - versionSpec: 6.x - -- task: DotNetCoreCLI@2 - displayName: 'NuGet restore' - inputs: - command: restore - projects: '$(solution)' - -- task: DotNetCoreCLI@2 - displayName: 'Build solution' - inputs: - projects: '$(solution)' - arguments: '--no-restore --configuration $(buildConfiguration)' - -- task: DotNetCoreCLI@2 - displayName: 'Run UnitTests' - inputs: - command: test - projects: '**/*.Tests.csproj' - arguments: '--no-restore --no-build --configuration $(buildConfiguration) --framework net8.0 /p:CollectCoverage=true /p:Exclude="[Microsoft*]*%2C[Mono*]*%2C[xunit*]*%2C[*.Testdata]*" /p:CoverletOutput=UnitTests.coverage.cobertura.xml /p:MergeWith=$(Build.SourcesDirectory)/Tests/CoverletOutput/coverage.json /p:CoverletOutputFormat=cobertura' - -- task: reportgenerator@5 - displayName: 'Create Code Coverage Report' - inputs: - reports: '$(Build.SourcesDirectory)/Tests/**/*.coverage.cobertura*.xml' - targetdir: '$(Build.SourcesDirectory)/CodeCoverage' - reporttypes: 'Cobertura' - assemblyfilters: '-xunit*' - -- task: DotNetCoreCLI@2 - displayName: 'Pack HttpClient.Caching' - inputs: - command: pack - packagesToPack: HttpClient.Caching/HttpClient.Caching.csproj - versioningScheme: byEnvVar - versionEnvVar: semVersion - nobuild: true - -- task: CopyFiles@2 - displayName: 'Copy Files to: $(Build.ArtifactStagingDirectory)' - inputs: - SourceFolder: '$(system.defaultworkingdirectory)' - - Contents: | - **\bin\$(BuildConfiguration)\** - **\bin\*.nupkg - - TargetFolder: '$(Build.ArtifactStagingDirectory)' - -- task: PublishCodeCoverageResults@2 - displayName: 'Publish code coverage' - inputs: - codeCoverageTool: Cobertura - summaryFileLocation: '$(Build.SourcesDirectory)/CodeCoverage/Cobertura.xml' - reportDirectory: '$(Build.SourcesDirectory)/CodeCoverage' - -- task: PublishBuildArtifacts@1 - displayName: 'Publish Artifact: drop' + - task: Bash@3 + displayName: 'Print all variables' + inputs: + targetType: 'inline' + script: 'env | sort' + + - task: Assembly-Info-NetCore@3 + displayName: 'Update Assembly Info' + inputs: + Path: '$(Build.SourcesDirectory)' + FileNames: | + **/*.csproj + InsertAttributes: true + FileEncoding: 'auto' + WriteBOM: false + Product: 'HttpClient.Caching' + Description: '' + Company: 'superdev GmbH' + Copyright: '(c) $(date:YYYY) superdev GmbH' + VersionNumber: '$(Build.BuildNumber)' + FileVersionNumber: '$(Build.BuildNumber)' + InformationalVersion: '$(Build.BuildNumber)' + PackageVersion: '$(Build.BuildNumber)' + LogLevel: 'verbose' + FailOnWarning: false + DisableTelemetry: true + + - task: UseDotNet@2 + displayName: 'Use .NET SDK from global.json' + inputs: + packageType: 'sdk' + useGlobalJson: true + workingDirectory: '$(Build.SourcesDirectory)' + + - task: NuGetToolInstaller@0 + displayName: 'Use NuGet 6.x' + inputs: + versionSpec: 6.x + + - task: DotNetCoreCLI@2 + displayName: 'NuGet restore' + inputs: + command: restore + projects: | + HttpClient.Caching/**/*.csproj + **/*.Tests.csproj + + - task: DotNetCoreCLI@2 + displayName: 'Build solution' + inputs: + projects: | + HttpClient.Caching/**/*.csproj + **/*.Tests.csproj + arguments: '--no-restore --configuration $(buildConfiguration)' + + - task: DotNetCoreCLI@2 + displayName: 'Run UnitTests' + inputs: + command: test + projects: '**/*.Tests.csproj' + arguments: '--no-restore --no-build --configuration $(buildConfiguration) /p:CollectCoverage=true /p:Exclude="[Microsoft*]*%2C[Mono*]*%2C[xunit*]*%2C[*.Testdata]*" /p:CoverletOutput=UnitTests.coverage.cobertura.xml /p:MergeWith=$(Build.SourcesDirectory)/Tests/CoverletOutput/coverage.json /p:CoverletOutputFormat=cobertura' + + - task: reportgenerator@5 + displayName: 'Create Code Coverage Report' + inputs: + reports: '$(Build.SourcesDirectory)/Tests/**/*.coverage.cobertura*.xml' + targetdir: '$(Build.SourcesDirectory)/CodeCoverage' + reporttypes: 'Cobertura' + assemblyfilters: '-xunit*' + + - task: DotNetCoreCLI@2 + displayName: 'Pack HttpClient.Caching' + inputs: + command: pack + packagesToPack: HttpClient.Caching/HttpClient.Caching.csproj + versioningScheme: byEnvVar + versionEnvVar: semVersion + nobuild: true + + - task: CopyFiles@2 + displayName: 'Copy Files to: $(Build.ArtifactStagingDirectory)' + inputs: + SourceFolder: '$(system.defaultworkingdirectory)' + Contents: | + **\bin\*.nupkg + **\bin\*.snupkg + **\ReleaseNotes.txt + TargetFolder: '$(Build.ArtifactStagingDirectory)' + + - task: PublishCodeCoverageResults@2 + displayName: 'Publish code coverage' + inputs: + codeCoverageTool: Cobertura + summaryFileLocation: '$(Build.SourcesDirectory)/CodeCoverage/Cobertura.xml' + reportDirectory: '$(Build.SourcesDirectory)/CodeCoverage' + + - task: PublishBuildArtifacts@1 + displayName: 'Publish Artifact: drop' From dd7fae6718a74e0714931622ed5f9ca06a582e1c Mon Sep 17 00:00:00 2001 From: Thomas Galliker Date: Mon, 6 Apr 2026 15:15:55 +0200 Subject: [PATCH 03/19] Bump version to 2.0 --- azure-pipelines.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 1450094..f068a3c 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -24,8 +24,8 @@ trigger: variables: buildPlatform: 'Any CPU' buildConfiguration: 'Release' - majorVersion: 1 - minorVersion: 7 + majorVersion: 2 + minorVersion: 0 patchVersion: $[counter(format('{0}.{1}', variables.majorVersion, variables.minorVersion), 0)] ${{ if eq(variables['Build.SourceBranch'], 'refs/heads/main') }}: # Versioning: 1.0.0 From 74d9f2d699826a41ae0e0356f894498ac057e8f1 Mon Sep 17 00:00:00 2001 From: Thomas Galliker Date: Mon, 6 Apr 2026 15:16:05 +0200 Subject: [PATCH 04/19] Update release notes --- ReleaseNotes.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ReleaseNotes.txt b/ReleaseNotes.txt index 93392c2..2cf4915 100644 --- a/ReleaseNotes.txt +++ b/ReleaseNotes.txt @@ -1,5 +1,7 @@ 2.0 -- Replace own implementation of MemoryCache with Microsoft.Extensions.Caching.Memory. +- Replace Newtonsoft.Json with System.Text.Json. +- Drop support for netstandard 1.2. +- Maintenance updates. 1.3 - Add ICacheKeysProvider which allows to specify custom cache keys. From 2906f18e50f7dedae5dd44449fb155678955b3e6 Mon Sep 17 00:00:00 2001 From: Thomas Galliker Date: Mon, 6 Apr 2026 15:17:01 +0200 Subject: [PATCH 05/19] Update namespace --- .../InMemory/InMemoryCacheFallbackHandlerTests.cs | 2 +- .../InMemory/InMemoryCacheHandlerTests.cs | 2 +- Tests/HttpClient.Caching.Tests/MemoryCacheTests.cs | 2 +- Tests/HttpClient.Caching.Tests/Testdata/TestMessageHandler.cs | 2 +- Tests/HttpClient.Caching.Tests/Testdata/TestPayload.cs | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Tests/HttpClient.Caching.Tests/InMemory/InMemoryCacheFallbackHandlerTests.cs b/Tests/HttpClient.Caching.Tests/InMemory/InMemoryCacheFallbackHandlerTests.cs index 67b1492..9483887 100644 --- a/Tests/HttpClient.Caching.Tests/InMemory/InMemoryCacheFallbackHandlerTests.cs +++ b/Tests/HttpClient.Caching.Tests/InMemory/InMemoryCacheFallbackHandlerTests.cs @@ -3,7 +3,7 @@ using System.Net.Http; using System.Threading.Tasks; using FluentAssertions; -using HttpClient.Caching.Tests.Testdata; +using HttpClient.Caching.Tests.TestData; using Microsoft.Extensions.Caching.Abstractions; using Microsoft.Extensions.Caching.InMemory; using Moq; diff --git a/Tests/HttpClient.Caching.Tests/InMemory/InMemoryCacheHandlerTests.cs b/Tests/HttpClient.Caching.Tests/InMemory/InMemoryCacheHandlerTests.cs index 2c82da3..39b6dfe 100644 --- a/Tests/HttpClient.Caching.Tests/InMemory/InMemoryCacheHandlerTests.cs +++ b/Tests/HttpClient.Caching.Tests/InMemory/InMemoryCacheHandlerTests.cs @@ -6,7 +6,7 @@ using System.Text; using System.Threading.Tasks; using FluentAssertions; -using HttpClient.Caching.Tests.Testdata; +using HttpClient.Caching.Tests.TestData; using Microsoft.Extensions.Caching.InMemory; using Xunit; diff --git a/Tests/HttpClient.Caching.Tests/MemoryCacheTests.cs b/Tests/HttpClient.Caching.Tests/MemoryCacheTests.cs index 4d89f7f..0b04984 100644 --- a/Tests/HttpClient.Caching.Tests/MemoryCacheTests.cs +++ b/Tests/HttpClient.Caching.Tests/MemoryCacheTests.cs @@ -1,6 +1,6 @@ using System; using FluentAssertions; -using HttpClient.Caching.Tests.Testdata; +using HttpClient.Caching.Tests.TestData; using Microsoft.Extensions.Caching.Abstractions; using Microsoft.Extensions.Caching.InMemory; using Xunit; diff --git a/Tests/HttpClient.Caching.Tests/Testdata/TestMessageHandler.cs b/Tests/HttpClient.Caching.Tests/Testdata/TestMessageHandler.cs index 6e45993..75a0923 100644 --- a/Tests/HttpClient.Caching.Tests/Testdata/TestMessageHandler.cs +++ b/Tests/HttpClient.Caching.Tests/Testdata/TestMessageHandler.cs @@ -6,7 +6,7 @@ using System.Threading; using System.Threading.Tasks; -namespace HttpClient.Caching.Tests.Testdata +namespace HttpClient.Caching.Tests.TestData { internal class TestMessageHandler : HttpMessageHandler { diff --git a/Tests/HttpClient.Caching.Tests/Testdata/TestPayload.cs b/Tests/HttpClient.Caching.Tests/Testdata/TestPayload.cs index d9534ce..d9a6bc9 100644 --- a/Tests/HttpClient.Caching.Tests/Testdata/TestPayload.cs +++ b/Tests/HttpClient.Caching.Tests/Testdata/TestPayload.cs @@ -1,7 +1,7 @@ using System; using System.Diagnostics; -namespace HttpClient.Caching.Tests.Testdata +namespace HttpClient.Caching.Tests.TestData { [DebuggerDisplay("{this.Id}")] public class TestPayload From 28138980c2e3d2833dff667e250f204cb1f8ac34 Mon Sep 17 00:00:00 2001 From: Thomas Galliker Date: Mon, 6 Apr 2026 15:25:09 +0200 Subject: [PATCH 06/19] Update sample project --- Samples/ConsoleAppSample/ConsoleAppSample.csproj | 2 +- Samples/ConsoleAppSample/Program.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Samples/ConsoleAppSample/ConsoleAppSample.csproj b/Samples/ConsoleAppSample/ConsoleAppSample.csproj index 908a3d3..c1f0fb2 100644 --- a/Samples/ConsoleAppSample/ConsoleAppSample.csproj +++ b/Samples/ConsoleAppSample/ConsoleAppSample.csproj @@ -2,7 +2,7 @@ Exe - net8.0 + net10.0 diff --git a/Samples/ConsoleAppSample/Program.cs b/Samples/ConsoleAppSample/Program.cs index 47a274e..4b25a2b 100644 --- a/Samples/ConsoleAppSample/Program.cs +++ b/Samples/ConsoleAppSample/Program.cs @@ -12,7 +12,7 @@ internal class Program { private static async Task Main(string[] args) { - const string url = "http://worldtimeapi.org/api/timezone/Europe/Zurich"; + const string url = "https://www.timeapi.io/api/v1/time/current/utc"; // HttpClient uses an HttpClientHandler nested into InMemoryCacheHandler in order to handle http get response caching var httpClientHandler = new HttpClientHandler(); From 6d242dbe45e6dc3ad3c3a53c056bb82664058376 Mon Sep 17 00:00:00 2001 From: Thomas Galliker Date: Mon, 6 Apr 2026 15:46:29 +0200 Subject: [PATCH 07/19] Update license --- LICENSE | 2 +- logo.png | Bin 0 -> 11744 bytes 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 logo.png diff --git a/LICENSE b/LICENSE index ae6ec5c..cddc001 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 Thomas Galliker +Copyright (c) 2026 Thomas Galliker Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/logo.png b/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..db190bbdca4f2105c7b37113aad08c1ce12079bf GIT binary patch literal 11744 zcmdsdby!qy6y}{7Lb@ACX(XgUx)e!8N*ZaTOLAzWrMm?LlmSVpk?!v9M!IVUf4lqN zKKt)JJI|T9bI*6)bKdjC{bJ^esQP#%qOYlI8{5I|K?UDkrt(~`~0lHJSdxwjREw>77a4MMnlKfQq3@%q{G`P=jR zI|u|g3I;d|1v&`_Ig13jybN{~3vm+5F!9m`+N#ZfXQV z^wJ{r(;^MhqYTrdjWS}4GrxY!iZ#oQGtY^)$VsrwO|Z&Mw9ZSi$xpWZmSSI!>QI>G zSd{KmlQt|wnu4>=Z{>kpLsrI`MzcOP*{Oqd4Yd1M{|BByrAn(QFqJtp4Q@?){@?~(%!Zo zeH}mhJ7EJ|6@%TshI%T8da8zdtB3n)M*3<;`|HLAevb{-j}JDC4>e8a0oG&is`Ke#+Uw6Zw7x;V1BG`hAlwzfRJ zzC5wMGO@8bwYfIExi+)4F}uAnx4k*Pv$?RdwYa;zgpj?RmHoZ7zx(S4`x^&;HxCcC zjt+N@k9JRv_D+uXPfz}yo*bN=BINM={P^PH^z!oT>gxRJ>f-wP^7{Jf=Jxvb?)L8f z{{G?N0nwxWKX^^x27N=MfoA_o+X(=$|2)1ywO_6G5lT{LSuN*xc4p3QMvkU{y^*D@ zGl#9IGYu~XHwQ0)ghd+wu)cjQBdPALzdwHwFeXERdN_nbNGQ|hKP4%f~TXXSEL{#FPA1{N#zhe`fAmLe=~yy4?@we?K;*DZUuYH@9;;xVp_w`y6X;)3o;~ zwmYJo)Fgtt4(JjOki?MUQW`N|Y8{{(hxJe}N^(P!H0HkF_1$;w)l zn15KJLTF|bv5gpdb;1&vF<@dPlz4zABe9eNuoFuFjWW_gZv_>-Q3m!7V1yl{&`m}P zLx$ep6j+&M3bXs2)`P00gRJr>aQ!dsxE%oIA1Qn~;KO063nLOd)8CW_TSAW&b?KOf zm-Nx!6S$89=|%RRP>5=h4B$FI)OmzenQZ?U39zmaj3F5Q2uB2?1L{lb+x%WSJ)NBfSdRR|_iq$fQ?aKQ&%>NL%9L~T%BrT!Obu`4NHAguLsmhR9a8XvHZ6-*a5lzdE@ z4)8ph(P50*v|>kkEaU_x1XiE}a47&ao8t@M6+W5h(&_uHUE_BEDME7vF;pUvccv!3 zMZ{&n1*{VU^A8OFg#1g^>tO}wi^;ntI04uE*O6xlz1TPi5Fc=GmC#L#~Xuve-!uSZvKuSG)1AaHX~RRlJaPmOY# z*Z`ILU)3B^tz9&*V()iq**|h*0Vb3kY{s#TADaMtD5`&h*~E%Hb*8h#B* z8*GV2JP_A8QWkBLU0v@MX*r@X1n2?fJh4q@Tu=3eHDHVa4{GAB^jV+MxG-RfeuW4%dqD;nuKmZS>eL^Ra@)}zTG{Ky z^c-o%>@3{JLKKPnzv7=W_@r*LAgXpUfKP{YK>G@_FI`CN>OXhH4g6PcDK|($!X6xf z9i?;t^Rm8jq^iFC&jv7-CJ2I&w>vB_-Xl7PKsk7+|EdL4^eF&>%4G|bHmprTzkkJI`ka(qp&VlPl$hcjh-(#4Pnb*(82Kw6d<5arwdqH|2R>!UYC$hS zFta3$bzrPQpUu@aJ#g~>`~Vt{Ulvx^N?O8n-O3wCWSwb5 zpWL^mCCHnY!6^%{_<8UX1nZVu@VOaN;{%0DoL$VAzb*q#f54m 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 literal 0 HcmV?d00001 From 25c245479a89eca256fe0198069df331db1ba9a5 Mon Sep 17 00:00:00 2001 From: Thomas Galliker Date: Mon, 6 Apr 2026 15:46:36 +0200 Subject: [PATCH 08/19] Move logo --- Images/logo.png | Bin 11744 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 Images/logo.png diff --git a/Images/logo.png b/Images/logo.png deleted file mode 100644 index db190bbdca4f2105c7b37113aad08c1ce12079bf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11744 zcmdsdby!qy6y}{7Lb@ACX(XgUx)e!8N*ZaTOLAzWrMm?LlmSVpk?!v9M!IVUf4lqN zKKt)JJI|T9bI*6)bKdjC{bJ^esQP#%qOYlI8{5I|K?UDkrt(~`~0lHJSdxwjREw>77a4MMnlKfQq3@%q{G`P=jR zI|u|g3I;d|1v&`_Ig13jybN{~3vm+5F!9m`+N#ZfXQV z^wJ{r(;^MhqYTrdjWS}4GrxY!iZ#oQGtY^)$VsrwO|Z&Mw9ZSi$xpWZmSSI!>QI>G zSd{KmlQt|wnu4>=Z{>kpLsrI`MzcOP*{Oqd4Yd1M{|BByrAn(QFqJtp4Q@?){@?~(%!Zo zeH}mhJ7EJ|6@%TshI%T8da8zdtB3n)M*3<;`|HLAevb{-j}JDC4>e8a0oG&is`Ke#+Uw6Zw7x;V1BG`hAlwzfRJ zzC5wMGO@8bwYfIExi+)4F}uAnx4k*Pv$?RdwYa;zgpj?RmHoZ7zx(S4`x^&;HxCcC zjt+N@k9JRv_D+uXPfz}yo*bN=BINM={P^PH^z!oT>gxRJ>f-wP^7{Jf=Jxvb?)L8f z{{G?N0nwxWKX^^x27N=MfoA_o+X(=$|2)1ywO_6G5lT{LSuN*xc4p3QMvkU{y^*D@ zGl#9IGYu~XHwQ0)ghd+wu)cjQBdPALzdwHwFeXERdN_nbNGQ|hKP4%f~TXXSEL{#FPA1{N#zhe`fAmLe=~yy4?@we?K;*DZUuYH@9;;xVp_w`y6X;)3o;~ zwmYJo)Fgtt4(JjOki?MUQW`N|Y8{{(hxJe}N^(P!H0HkF_1$;w)l zn15KJLTF|bv5gpdb;1&vF<@dPlz4zABe9eNuoFuFjWW_gZv_>-Q3m!7V1yl{&`m}P zLx$ep6j+&M3bXs2)`P00gRJr>aQ!dsxE%oIA1Qn~;KO063nLOd)8CW_TSAW&b?KOf zm-Nx!6S$89=|%RRP>5=h4B$FI)OmzenQZ?U39zmaj3F5Q2uB2?1L{lb+x%WSJ)NBfSdRR|_iq$fQ?aKQ&%>NL%9L~T%BrT!Obu`4NHAguLsmhR9a8XvHZ6-*a5lzdE@ z4)8ph(P50*v|>kkEaU_x1XiE}a47&ao8t@M6+W5h(&_uHUE_BEDME7vF;pUvccv!3 zMZ{&n1*{VU^A8OFg#1g^>tO}wi^;ntI04uE*O6xlz1TPi5Fc=GmC#L#~Xuve-!uSZvKuSG)1AaHX~RRlJaPmOY# z*Z`ILU)3B^tz9&*V()iq**|h*0Vb3kY{s#TADaMtD5`&h*~E%Hb*8h#B* z8*GV2JP_A8QWkBLU0v@MX*r@X1n2?fJh4q@Tu=3eHDHVa4{GAB^jV+MxG-RfeuW4%dqD;nuKmZS>eL^Ra@)}zTG{Ky z^c-o%>@3{JLKKPnzv7=W_@r*LAgXpUfKP{YK>G@_FI`CN>OXhH4g6PcDK|($!X6xf z9i?;t^Rm8jq^iFC&jv7-CJ2I&w>vB_-Xl7PKsk7+|EdL4^eF&>%4G|bHmprTzkkJI`ka(qp&VlPl$hcjh-(#4Pnb*(82Kw6d<5arwdqH|2R>!UYC$hS zFta3$bzrPQpUu@aJ#g~>`~Vt{Ulvx^N?O8n-O3wCWSwb5 zpWL^mCCHnY!6^%{_<8UX1nZVu@VOaN;{%0DoL$VAzb*q#f54m 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 From 5400d6b88f817bd14a0e21cf668099015ca0e52b Mon Sep 17 00:00:00 2001 From: Thomas Galliker Date: Mon, 6 Apr 2026 16:06:44 +0200 Subject: [PATCH 09/19] Update csproj infos --- HttpClient.Caching/HttpClient.Caching.csproj | 82 ++++++++++--------- README.md | 2 +- .../HttpClient.Caching.Tests.csproj | 16 ++-- 3 files changed, 51 insertions(+), 49 deletions(-) diff --git a/HttpClient.Caching/HttpClient.Caching.csproj b/HttpClient.Caching/HttpClient.Caching.csproj index 07a42b5..5fd69ae 100644 --- a/HttpClient.Caching/HttpClient.Caching.csproj +++ b/HttpClient.Caching/HttpClient.Caching.csproj @@ -1,53 +1,55 @@  - - HttpClient.Caching adds http response caching to HttpClient. - HttpClient.Caching - 1.0.0 - 1.0.0 - Thomas Galliker - net48;netstandard1.2;netstandard2.0;netstandard2.1;net6.0;net7.0;net8.0 - HttpClient.Caching + + net48;netstandard2.0;netstandard2.1;net8.0;net9.0;net10.0 + Library Microsoft.Extensions.Caching - HttpClient.Caching - httpclient.caching;httpclient;caching;cache;inmemory + enable + latest + enable + true + True + + + + + HttpClient.Caching + HTTP response caching for HttpClient. + 1.0.0 + Thomas Galliker + HttpClient.Caching + retry;resilience;resilient;fault;failure;http;handler;HttpRetryHelper;ApiClient;ApiService logo.png - README.md LICENSE - https://github.com/thomasgalliker/HttpClient.Caching - git - https://github.com/thomasgalliker/HttpClient.Caching - $(PackageTargetFallback);netcoreapp1.0 - 1.6.1 - True - latest - - - - superdev GmbH - HttpClient.Caching - false + README.md + https://github.com/thomasgalliker/HttpClient.Caching + git + https://github.com/thomasgalliker/HttpClient.Caching + superdev GmbH + false Copyright $([System.DateTime]::Now.ToString(`yyyy`)) © Thomas Galliker - -1.3 -- Add ICacheKeysProvider which allows to specify custom cache keys + $([System.IO.File]::ReadAllText("$(MSBuildProjectDirectory)/../ReleaseNotes.txt")) + true + snupkg + true + true + -1.0 -- Initial release - - - - + - - - - - - - + + + + + + true + + + + + diff --git a/README.md b/README.md index 2aab6cd..07bcb27 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ public async Task GetAsync(string uri, TimeSpan? cacheExpirati var httpResponseMessage = await this.HandleRequest(() => this.httpClient.GetAsync(uri)); var jsonResponse = await this.HandleResponse(httpResponseMessage); - result = await Task.Run(() => JsonConvert.DeserializeObject(jsonResponse, this.serializerSettings)); + result = await Task.Run(() => JsonSerializer.Deserialize(jsonResponse, this.serializerSettings)); if (caching) { diff --git a/Tests/HttpClient.Caching.Tests/HttpClient.Caching.Tests.csproj b/Tests/HttpClient.Caching.Tests/HttpClient.Caching.Tests.csproj index b6a2143..853ec1f 100644 --- a/Tests/HttpClient.Caching.Tests/HttpClient.Caching.Tests.csproj +++ b/Tests/HttpClient.Caching.Tests/HttpClient.Caching.Tests.csproj @@ -1,7 +1,7 @@  - net48;net8.0 + net48;net10.0 HttpClient.Caching.Tests HttpClient.Caching.Tests false @@ -10,19 +10,19 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - - + + From 06f2f57fc93ed46f44e16e0ac6bc63c91bf17fe8 Mon Sep 17 00:00:00 2001 From: Thomas Galliker Date: Mon, 6 Apr 2026 16:07:22 +0200 Subject: [PATCH 10/19] Replace Newtonsoft.Json with System.Text.Json --- .../Abstractions/CacheDataExtensions.cs | 28 +++-- .../Internals/CacheDataJsonConverter.cs | 89 ++++++++++++++ .../Abstractions/CacheDataExtensionsTests.cs | 114 ++++++++++++++++++ 3 files changed, 218 insertions(+), 13 deletions(-) create mode 100644 HttpClient.Caching/Internals/CacheDataJsonConverter.cs create mode 100644 Tests/HttpClient.Caching.Tests/Abstractions/CacheDataExtensionsTests.cs diff --git a/HttpClient.Caching/Abstractions/CacheDataExtensions.cs b/HttpClient.Caching/Abstractions/CacheDataExtensions.cs index 5e1e016..479a626 100644 --- a/HttpClient.Caching/Abstractions/CacheDataExtensions.cs +++ b/HttpClient.Caching/Abstractions/CacheDataExtensions.cs @@ -1,32 +1,34 @@ -using System; -using Newtonsoft.Json; +using System.Net; +using System.Net.Http; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Extensions.Caching.Internals; namespace Microsoft.Extensions.Caching.Abstractions { public static class CacheDataExtensions { + private static readonly JsonSerializerOptions SerializerOptions = new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + Converters = { new CacheDataJsonConverter() } + }; + public static byte[] Serialize(this CacheData cacheData) { - var json = JsonConvert.SerializeObject(cacheData); - var bytes = new byte[json.Length * sizeof(char)]; - Buffer.BlockCopy(json.ToCharArray(), 0, bytes, 0, bytes.Length); - return bytes; + return JsonSerializer.SerializeToUtf8Bytes(cacheData, SerializerOptions); } public static CacheData Deserialize(this byte[] cacheData) { try { - var chars = new char[cacheData.Length / sizeof(char)]; - Buffer.BlockCopy(cacheData, 0, chars, 0, cacheData.Length); - var json = new string(chars); - var data = JsonConvert.DeserializeObject(json); - return data; + return JsonSerializer.Deserialize(cacheData, SerializerOptions)!; } catch { - return null; + return null!; } } } -} \ No newline at end of file +} diff --git a/HttpClient.Caching/Internals/CacheDataJsonConverter.cs b/HttpClient.Caching/Internals/CacheDataJsonConverter.cs new file mode 100644 index 0000000..721edf8 --- /dev/null +++ b/HttpClient.Caching/Internals/CacheDataJsonConverter.cs @@ -0,0 +1,89 @@ +using System.Net; +using System.Net.Http; +using System.Text.Json; +using System.Text.Json.Serialization; +using Microsoft.Extensions.Caching.Abstractions; + +namespace Microsoft.Extensions.Caching.Internals +{ + internal sealed class CacheDataJsonConverter : JsonConverter + { + public override CacheData Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + using var document = JsonDocument.ParseValue(ref reader); + var root = document.RootElement; + + var data = root.TryGetProperty("data", out var dataElement) + ? dataElement.Deserialize(options) ?? Array.Empty() + : Array.Empty(); + + var reasonPhrase = root.TryGetProperty("reasonPhrase", out var reasonPhraseElement) + ? reasonPhraseElement.GetString() + : null; + + var statusCode = root.TryGetProperty("statusCode", out var statusCodeElement) + ? statusCodeElement.GetInt32() + : (int)HttpStatusCode.OK; + + var version = root.TryGetProperty("version", out var versionElement) + ? versionElement.GetString() + : null; + + var headers = root.TryGetProperty("headers", out var headersElement) + ? headersElement.Deserialize>(options) + : null; + + var contentHeaders = root.TryGetProperty("contentHeaders", out var contentHeadersElement) + ? contentHeadersElement.Deserialize>(options) + : null; + + var httpResponseMessage = new HttpResponseMessage { ReasonPhrase = reasonPhrase, StatusCode = (HttpStatusCode)statusCode, }; + + if (ParseVersion(version) is Version v) + { + httpResponseMessage.Version = v; + } + + return new CacheData( + data, + httpResponseMessage, + ConvertHeaders(headers), + ConvertHeaders(contentHeaders)); + } + + public override void Write(Utf8JsonWriter writer, CacheData value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + writer.WriteBase64String("data", value.Data); + writer.WriteString("reasonPhrase", value.CachableResponse.ReasonPhrase); + writer.WriteNumber("statusCode", (int)value.CachableResponse.StatusCode); + writer.WriteString("version", value.CachableResponse.Version?.ToString()); + + writer.WritePropertyName("headers"); + JsonSerializer.Serialize(writer, ConvertHeaders(value.Headers), options); + + writer.WritePropertyName("contentHeaders"); + JsonSerializer.Serialize(writer, ConvertHeaders(value.ContentHeaders), options); + + writer.WriteEndObject(); + } + + + private static Dictionary ConvertHeaders(Dictionary>? headers) + { + return headers?.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.ToArray()) ?? new Dictionary(); + } + + private static Dictionary> ConvertHeaders(Dictionary? headers) + { + return headers?.ToDictionary(kvp => kvp.Key, kvp => (IEnumerable)kvp.Value) ?? new Dictionary>(); + } + + private static Version? ParseVersion(string? version) + { + return string.IsNullOrWhiteSpace(version) + ? null + : Version.Parse(version); + } + } +} \ No newline at end of file diff --git a/Tests/HttpClient.Caching.Tests/Abstractions/CacheDataExtensionsTests.cs b/Tests/HttpClient.Caching.Tests/Abstractions/CacheDataExtensionsTests.cs new file mode 100644 index 0000000..8907820 --- /dev/null +++ b/Tests/HttpClient.Caching.Tests/Abstractions/CacheDataExtensionsTests.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using FluentAssertions; +using Microsoft.Extensions.Caching.Abstractions; +using Xunit; + +namespace HttpClient.Caching.Tests.Abstractions +{ + public class CacheDataExtensionsTests + { + [Fact] + public void SerializeAndDeserialize_RoundTripsCacheData() + { + // Arrange + var httpResponseMessage = new HttpResponseMessage + { + ReasonPhrase = "Accepted", + StatusCode = HttpStatusCode.Accepted, + Version = new Version(2, 0) + }; + + var cacheData = new CacheData( + new byte[] { 1, 2, 3 }, + httpResponseMessage, + new Dictionary> + { + ["X-Test"] = new[] { "one", "two" } + }, + new Dictionary> + { + ["Content-Type"] = new[] { "application/json" } + }); + + // Act + var serialized = cacheData.Serialize(); + var deserialized = serialized.Deserialize(); + + // Assert + deserialized.Should().NotBeNull(); + deserialized.Data.Should().Equal(1, 2, 3); + deserialized.CachableResponse.StatusCode.Should().Be(HttpStatusCode.Accepted); + deserialized.CachableResponse.ReasonPhrase.Should().Be("Accepted"); + deserialized.CachableResponse.Version.Should().Be(new Version(2, 0)); + deserialized.Headers.Should().ContainKey("X-Test"); + deserialized.Headers["X-Test"].Should().Equal("one", "two"); + deserialized.ContentHeaders.Should().ContainKey("Content-Type"); + deserialized.ContentHeaders["Content-Type"].Should().Equal("application/json"); + } + + [Fact] + public void Deserialize_InvalidPayload_ReturnsNull() + { + // Arrange + var bytes = new byte[] { 1, 2, 3 }; + + // Act + var deserialized = bytes.Deserialize(); + + // Assert + deserialized.Should().BeNull(); + } + + [Fact] + public void Deserialize_LegacyNewtonsoftPayload_ReturnsNull() + { + // Arrange + const string legacyJson = @"{ + ""CachableResponse"": { + ""Version"": ""2.0"", + ""Content"": { + ""Headers"": [] + }, + ""StatusCode"": 202, + ""ReasonPhrase"": ""Accepted"", + ""Headers"": [ + { + ""Key"": ""X-From-Response"", + ""Value"": [ + ""header-value"" + ] + } + ], + ""TrailingHeaders"": [], + ""RequestMessage"": null, + ""IsSuccessStatusCode"": true + }, + ""Data"": ""AQID"", + ""Headers"": { + ""X-Test"": [ + ""one"", + ""two"" + ] + }, + ""ContentHeaders"": { + ""Content-Type"": [ + ""application/json"" + ] + } +}"; + + var chars = legacyJson.ToCharArray(); + var legacyBytes = new byte[chars.Length * sizeof(char)]; + Buffer.BlockCopy(chars, 0, legacyBytes, 0, legacyBytes.Length); + + // Act + var deserialized = legacyBytes.Deserialize(); + + // Assert + deserialized.Should().BeNull(); + } + } +} From 926fe379c2e4e6af801d1e891072e013183c3de3 Mon Sep 17 00:00:00 2001 From: Thomas Galliker Date: Mon, 6 Apr 2026 16:18:14 +0200 Subject: [PATCH 11/19] Cleanup nullable reference warnings --- HttpClient.Caching/Abstractions/CacheData.cs | 1 - .../Abstractions/CacheExtensions.cs | 34 ++-- .../HttpResponseMessageExtensions.cs | 2 +- .../PostEvictionCallbackRegistration.cs | 4 +- .../Abstractions/PostEvictionDelegate.cs | 2 +- HttpClient.Caching/InMemory/CacheEntry.cs | 35 ++--- .../InMemory/CacheEntryExtensions.cs | 8 +- HttpClient.Caching/InMemory/IMemoryCache.cs | 3 +- .../InMemory/IMemoryCacheExtensions.cs | 40 +---- .../InMemory/InMemoryCacheFallbackHandler.cs | 12 +- .../InMemory/InMemoryCacheHandler.cs | 43 +++-- HttpClient.Caching/InMemory/MemoryCache.cs | 12 +- .../InMemory/MemoryCacheEntryOptions.cs | 2 +- .../InMemory/MemoryCacheOptions.cs | 5 +- .../MethodUriHeadersCacheKeysProvider.cs | 9 +- HttpClient.Caching/Internals/Nullable.cs | 148 ++++++++++++++++++ .../InMemory/InMemoryCacheHandlerTests.cs | 70 ++++----- .../Testdata/TestMessageHandler.cs | 2 +- 18 files changed, 265 insertions(+), 167 deletions(-) create mode 100644 HttpClient.Caching/Internals/Nullable.cs diff --git a/HttpClient.Caching/Abstractions/CacheData.cs b/HttpClient.Caching/Abstractions/CacheData.cs index 06d59a9..40f346b 100644 --- a/HttpClient.Caching/Abstractions/CacheData.cs +++ b/HttpClient.Caching/Abstractions/CacheData.cs @@ -1,4 +1,3 @@ -using System.Collections.Generic; using System.Net.Http; namespace Microsoft.Extensions.Caching.Abstractions diff --git a/HttpClient.Caching/Abstractions/CacheExtensions.cs b/HttpClient.Caching/Abstractions/CacheExtensions.cs index 2df33f4..4e1c51e 100644 --- a/HttpClient.Caching/Abstractions/CacheExtensions.cs +++ b/HttpClient.Caching/Abstractions/CacheExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using Microsoft.Extensions.Caching.InMemory; @@ -6,19 +7,19 @@ namespace Microsoft.Extensions.Caching.Abstractions { public static class CacheExtensions { - public static object Get(this IMemoryCache cache, object key) + public static object? Get(this IMemoryCache cache, object key) { cache.TryGetValue(key, out var obj); return obj; } - public static TItem Get(this IMemoryCache cache, object key) + public static TItem? Get(this IMemoryCache cache, object key) { - cache.TryGetValue(key, out TItem obj); + cache.TryGetValue(key, out TItem? obj); return obj; } - public static bool TryGetValue(this IMemoryCache cache, object key, out TItem value) + public static bool TryGetValue(this IMemoryCache cache, object key, [NotNullWhen(true)] out TItem? value) { if (cache.TryGetValue(key, out var obj)) { @@ -30,7 +31,7 @@ public static bool TryGetValue(this IMemoryCache cache, object key, out T return false; } - public static TItem Set(this IMemoryCache cache, object key, TItem value) + public static TItem Set(this IMemoryCache cache, object key, TItem value) where TItem : notnull { var entry = cache.CreateEntry(key); entry.Value = value; @@ -38,7 +39,7 @@ public static TItem Set(this IMemoryCache cache, object key, TItem value) return value; } - public static TItem Set(this IMemoryCache cache, object key, TItem value, DateTimeOffset absoluteExpiration) + public static TItem Set(this IMemoryCache cache, object key, TItem value, DateTimeOffset absoluteExpiration) where TItem : notnull { var entry = cache.CreateEntry(key); DateTimeOffset? nullable = absoluteExpiration; @@ -48,7 +49,7 @@ public static TItem Set(this IMemoryCache cache, object key, TItem value, return value; } - public static TItem Set(this IMemoryCache cache, object key, TItem value, TimeSpan absoluteExpirationRelativeToNow) + public static TItem Set(this IMemoryCache cache, object key, TItem value, TimeSpan absoluteExpirationRelativeToNow) where TItem : notnull { var entry = cache.CreateEntry(key); TimeSpan? nullable = absoluteExpirationRelativeToNow; @@ -58,7 +59,7 @@ public static TItem Set(this IMemoryCache cache, object key, TItem value, return value; } - public static TItem Set(this IMemoryCache cache, object key, TItem value, IChangeToken expirationToken) + public static TItem Set(this IMemoryCache cache, object key, TItem value, IChangeToken expirationToken) where TItem : notnull { var entry = cache.CreateEntry(key); var expirationToken1 = expirationToken; @@ -68,22 +69,23 @@ public static TItem Set(this IMemoryCache cache, object key, TItem value, return value; } - public static TItem Set(this IMemoryCache cache, object key, TItem value, MemoryCacheEntryOptions options) + public static TItem Set(this IMemoryCache cache, object key, TItem value, MemoryCacheEntryOptions options) where TItem : notnull { - using (var entry = cache.CreateEntry(key)) + if (options == null) { - if (options != null) - { - entry.SetOptions(options); - } + throw new ArgumentNullException(nameof(options)); + } + using (var entry = cache.CreateEntry(key)) + { + entry.SetOptions(options); entry.Value = value; } return value; } - public static TItem GetOrCreate(this IMemoryCache cache, object key, Func factory) + public static TItem GetOrCreate(this IMemoryCache cache, object key, Func factory) where TItem : notnull { if (!cache.TryGetValue(key, out var obj)) { @@ -96,7 +98,7 @@ public static TItem GetOrCreate(this IMemoryCache cache, object key, Func return (TItem)obj; } - public static async Task GetOrCreateAsync(this IMemoryCache cache, object key, Func> factory) + public static async Task GetOrCreateAsync(this IMemoryCache cache, object key, Func> factory) where TItem : notnull { if (!cache.TryGetValue(key, out var obj)) { diff --git a/HttpClient.Caching/Abstractions/HttpResponseMessageExtensions.cs b/HttpClient.Caching/Abstractions/HttpResponseMessageExtensions.cs index c50c0c3..a064ab1 100644 --- a/HttpClient.Caching/Abstractions/HttpResponseMessageExtensions.cs +++ b/HttpClient.Caching/Abstractions/HttpResponseMessageExtensions.cs @@ -16,7 +16,7 @@ public static async Task ToCacheEntryAsync(this HttpResponseMessage h return httpResponseMessage.ToCacheEntry(contentBytes); } -#if NET5_0_OR_GREATER +#if NET8_0_OR_GREATER public static CacheData ToCacheEntry(this HttpResponseMessage httpResponseMessage) { using var contentStream = httpResponseMessage.Content.ReadAsStream(); diff --git a/HttpClient.Caching/Abstractions/PostEvictionCallbackRegistration.cs b/HttpClient.Caching/Abstractions/PostEvictionCallbackRegistration.cs index e1c438b..29985ee 100644 --- a/HttpClient.Caching/Abstractions/PostEvictionCallbackRegistration.cs +++ b/HttpClient.Caching/Abstractions/PostEvictionCallbackRegistration.cs @@ -2,8 +2,8 @@ { public class PostEvictionCallbackRegistration { - public PostEvictionDelegate EvictionCallback { get; set; } + public PostEvictionDelegate EvictionCallback { get; set; } = null!; - public object State { get; set; } + public object? State { get; set; } } } \ No newline at end of file diff --git a/HttpClient.Caching/Abstractions/PostEvictionDelegate.cs b/HttpClient.Caching/Abstractions/PostEvictionDelegate.cs index 44905b2..c6718fb 100644 --- a/HttpClient.Caching/Abstractions/PostEvictionDelegate.cs +++ b/HttpClient.Caching/Abstractions/PostEvictionDelegate.cs @@ -7,5 +7,5 @@ /// /// The . /// The information that was passed when registering the callback. - public delegate void PostEvictionDelegate(object key, object value, EvictionReason reason, object state); + public delegate void PostEvictionDelegate(object key, object value, EvictionReason evictionReason, object? state); } \ No newline at end of file diff --git a/HttpClient.Caching/InMemory/CacheEntry.cs b/HttpClient.Caching/InMemory/CacheEntry.cs index 804153c..08ce5b9 100644 --- a/HttpClient.Caching/InMemory/CacheEntry.cs +++ b/HttpClient.Caching/InMemory/CacheEntry.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Threading; -using System.Threading.Tasks; +using System.Diagnostics; using Microsoft.Extensions.Caching.Abstractions; namespace Microsoft.Extensions.Caching.InMemory @@ -14,10 +10,10 @@ internal class CacheEntry : ICacheEntry private bool added; private readonly Action notifyCacheOfExpiration; private readonly Action notifyCacheEntryDisposed; - private IList expirationTokenRegistrations; - private IList postEvictionCallbacks; + private IList? expirationTokenRegistrations; + private IList? postEvictionCallbacks; private bool isExpired; - internal IList expirationTokens; + internal IList? expirationTokens; internal DateTimeOffset? absoluteExpiration; internal TimeSpan? absoluteExpirationRelativeToNow; private TimeSpan? slidingExpiration; @@ -98,7 +94,7 @@ internal CacheEntry(object key, Action notifyCacheEntryDisposed, Act { if (key == null) { - throw new ArgumentNullException("key"); + throw new ArgumentNullException(nameof(key)); } if (notifyCacheEntryDisposed == null) @@ -140,11 +136,7 @@ internal bool CheckExpired(DateTimeOffset now) internal void SetExpired(EvictionReason reason) { - if (this.EvictionReason == null) - { - this.EvictionReason = reason; - } - + this.EvictionReason = reason; this.isExpired = true; this.DetachTokens(); } @@ -267,14 +259,11 @@ private static void InvokeCallbacks(CacheEntry entry) try { var evictionCallback = callbackRegistration.EvictionCallback; - if (evictionCallback != null) - { - var key = entry.Key; - var obj = entry.Value; - var evictionReason = entry.EvictionReason; - var state = callbackRegistration.State; - evictionCallback.Invoke(key, obj, evictionReason, state); - } + var key = entry.Key; + var obj = entry.Value; + var evictionReason = entry.EvictionReason; + var state = callbackRegistration.State; + evictionCallback.Invoke(key, obj, evictionReason, state); } catch (Exception ex) { @@ -283,7 +272,7 @@ private static void InvokeCallbacks(CacheEntry entry) } } - internal void PropagateOptions(CacheEntry parent) + internal void PropagateOptions(CacheEntry? parent) { if (parent == null) { diff --git a/HttpClient.Caching/InMemory/CacheEntryExtensions.cs b/HttpClient.Caching/InMemory/CacheEntryExtensions.cs index 3046896..b56fdcd 100644 --- a/HttpClient.Caching/InMemory/CacheEntryExtensions.cs +++ b/HttpClient.Caching/InMemory/CacheEntryExtensions.cs @@ -75,7 +75,7 @@ public static ICacheEntry RegisterPostEvictionCallback(this ICacheEntry entry, P { if (callback == null) { - throw new ArgumentNullException("callback"); + throw new ArgumentNullException(nameof(callback)); } return entry.RegisterPostEvictionCallback(callback, null); @@ -87,14 +87,14 @@ public static ICacheEntry RegisterPostEvictionCallback(this ICacheEntry entry, P /// /// /// - public static ICacheEntry RegisterPostEvictionCallback(this ICacheEntry entry, PostEvictionDelegate callback, object state) + public static ICacheEntry RegisterPostEvictionCallback(this ICacheEntry entry, PostEvictionDelegate callback, object? state) { if (callback == null) { - throw new ArgumentNullException("callback"); + throw new ArgumentNullException(nameof(callback)); } - entry.PostEvictionCallbacks.Add(new PostEvictionCallbackRegistration() { EvictionCallback = callback, State = state }); + entry.PostEvictionCallbacks.Add(new PostEvictionCallbackRegistration { EvictionCallback = callback, State = state }); return entry; } diff --git a/HttpClient.Caching/InMemory/IMemoryCache.cs b/HttpClient.Caching/InMemory/IMemoryCache.cs index 9799319..6793350 100644 --- a/HttpClient.Caching/InMemory/IMemoryCache.cs +++ b/HttpClient.Caching/InMemory/IMemoryCache.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Caching.Abstractions; namespace Microsoft.Extensions.Caching.InMemory @@ -14,7 +15,7 @@ public interface IMemoryCache : IDisposable /// An object identifying the requested entry. /// The located value or null. /// True if the key was found. - bool TryGetValue(object key, out object value); + bool TryGetValue(object key, [NotNullWhen(true)] out object? value); /// Create or overwrite an entry in the cache. /// An object identifying the entry. diff --git a/HttpClient.Caching/InMemory/IMemoryCacheExtensions.cs b/HttpClient.Caching/InMemory/IMemoryCacheExtensions.cs index b38c4e0..cc2b328 100644 --- a/HttpClient.Caching/InMemory/IMemoryCacheExtensions.cs +++ b/HttpClient.Caching/InMemory/IMemoryCacheExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; using Microsoft.Extensions.Caching.Abstractions; +using System.Diagnostics.CodeAnalysis; namespace Microsoft.Extensions.Caching.InMemory { @@ -9,39 +10,14 @@ namespace Microsoft.Extensions.Caching.InMemory /// internal static class IMemoryCacheExtensions { - /// - /// Tries to get the data from cache, that is, ignoring all exceptions. - /// - /// The in memory cache. - /// The key to retrieve from the cache. - /// The data of the cache entry, or null if not found or on any error. - [Obsolete("Use TryGetCacheData")] - public static Task TryGetAsync(this IMemoryCache cache, string key) - { - try - { - if (cache.TryGetValue(key, out byte[] binaryData)) - { - return Task.FromResult(binaryData.Deserialize()); - } - - return Task.FromResult(default(CacheData)); - } - catch (Exception) - { - // ignore all exceptions; return null - return Task.FromResult(default(CacheData)); - } - } - - public static bool TryGetCacheData(this IMemoryCache cache, string key, out CacheData cacheData) + public static bool TryGetCacheData(this IMemoryCache cache, string key, [NotNullWhen(true)] out CacheData? cacheData) { var result = false; - cacheData = default; + cacheData = null; try { - if (cache.TryGetValue(key, out byte[] binaryData)) + if (cache.TryGetValue(key, out byte[]? binaryData)) { cacheData = binaryData.Deserialize(); result = true; @@ -60,14 +36,14 @@ public static bool TryGetCacheData(this IMemoryCache cache, string key, out Cach /// /// The in memory cache. /// The key for this cache entry. - /// The value of this cache entry. + /// The value of this cache entry. /// Expiration relative to now. /// A task, when completed, has tried to put the entry into the cache. - public static Task TrySetAsync(this IMemoryCache cache, string key, CacheData value, TimeSpan absoluteExpirationRelativeToNow) + public static Task TrySetAsync(this IMemoryCache cache, string key, CacheData cacheData, TimeSpan absoluteExpirationRelativeToNow) { try { - cache.Set(key, value.Serialize(), absoluteExpirationRelativeToNow); + cache.Set(key, cacheData.Serialize(), absoluteExpirationRelativeToNow); return Task.FromResult(true); } catch (Exception) @@ -79,7 +55,7 @@ public static Task TrySetAsync(this IMemoryCache cache, string key, CacheData va public static bool TrySetCacheData(this IMemoryCache cache, string key, CacheData value, TimeSpan absoluteExpirationRelativeToNow) { - var result = false; + bool result; try { diff --git a/HttpClient.Caching/InMemory/InMemoryCacheFallbackHandler.cs b/HttpClient.Caching/InMemory/InMemoryCacheFallbackHandler.cs index 320e95e..b291a75 100644 --- a/HttpClient.Caching/InMemory/InMemoryCacheFallbackHandler.cs +++ b/HttpClient.Caching/InMemory/InMemoryCacheFallbackHandler.cs @@ -28,7 +28,7 @@ public class InMemoryCacheFallbackHandler : DelegatingHandler /// An that records statistic information about the caching /// behavior. /// - public InMemoryCacheFallbackHandler(HttpMessageHandler innerHandler, TimeSpan maxTimeout, TimeSpan cacheDuration, IStatsProvider statsProvider = null) + public InMemoryCacheFallbackHandler(HttpMessageHandler innerHandler, TimeSpan maxTimeout, TimeSpan cacheDuration, IStatsProvider? statsProvider = null) : this(innerHandler, maxTimeout, cacheDuration, statsProvider, new MemoryCache(new MemoryCacheOptions())) { } @@ -44,7 +44,7 @@ public InMemoryCacheFallbackHandler(HttpMessageHandler innerHandler, TimeSpan ma /// behavior. /// /// The cache to be used. - internal InMemoryCacheFallbackHandler(HttpMessageHandler innerHandler, TimeSpan maxTimeout, TimeSpan cacheDuration, IStatsProvider statsProvider, IMemoryCache cache) : base(innerHandler ?? new HttpClientHandler()) + internal InMemoryCacheFallbackHandler(HttpMessageHandler innerHandler, TimeSpan maxTimeout, TimeSpan cacheDuration, IStatsProvider? statsProvider, IMemoryCache? cache) : base(innerHandler ?? new HttpClientHandler()) { this.StatsProvider = statsProvider ?? new StatsProvider(nameof(InMemoryCacheHandler)); this.maxTimeout = maxTimeout; @@ -64,7 +64,7 @@ protected override async Task SendAsync(HttpRequestMessage return await base.SendAsync(request, cancellationToken); } - var key = CacheFallbackKeyPrefix + request.Method + request.RequestUri.ToString(); + var key = $"{CacheFallbackKeyPrefix}{request.Method}{request.RequestUri}"; // start 3 tasks var httpSendTask = base.SendAsync(request, cancellationToken); @@ -111,9 +111,9 @@ protected override async Task SendAsync(HttpRequestMessage return response; } - private HttpResponseMessage ExtractCachedResponse(HttpRequestMessage request, string key) + private HttpResponseMessage? ExtractCachedResponse(HttpRequestMessage request, string key) { - // it's in the cache, return that result + // It's in the cache, return that result if (this.responseCache.TryGetCacheData(key, out var data)) { // get the data from the cache @@ -125,7 +125,7 @@ private HttpResponseMessage ExtractCachedResponse(HttpRequestMessage request, st return null; } - private async Task SaveToCache(HttpResponseMessage response, string key) + private async Task SaveToCache(HttpResponseMessage response, string key) { if ((int)response.StatusCode < 500 && TimeSpan.Zero != this.cacheDuration) { diff --git a/HttpClient.Caching/InMemory/InMemoryCacheHandler.cs b/HttpClient.Caching/InMemory/InMemoryCacheHandler.cs index 6ebf556..fc34694 100644 --- a/HttpClient.Caching/InMemory/InMemoryCacheHandler.cs +++ b/HttpClient.Caching/InMemory/InMemoryCacheHandler.cs @@ -1,10 +1,7 @@ -using System; -using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Net; using System.Net.Http; using System.Net.Http.Headers; -using System.Threading; -using System.Threading.Tasks; using Microsoft.Extensions.Caching.Abstractions; namespace Microsoft.Extensions.Caching.InMemory @@ -15,7 +12,7 @@ namespace Microsoft.Extensions.Caching.InMemory /// public class InMemoryCacheHandler : DelegatingHandler { -#if NET5_0_OR_GREATER +#if NET8_0_OR_GREATER /// /// The key to use to store the UseCache value in the HttpRequestMessage.Options dictionary. /// This key is used to determine if the cache should be checked for the request. @@ -66,10 +63,10 @@ public class InMemoryCacheHandler : DelegatingHandler /// /// An that provides keys to retrieve and store items in the cache /// - public InMemoryCacheHandler(HttpMessageHandler innerHandler = null, - IDictionary cacheExpirationPerHttpResponseCode = null, - IStatsProvider statsProvider = null, - ICacheKeysProvider cacheKeysProvider = null) + public InMemoryCacheHandler(HttpMessageHandler? innerHandler = null, + IDictionary? cacheExpirationPerHttpResponseCode = null, + IStatsProvider? statsProvider = null, + ICacheKeysProvider? cacheKeysProvider = null) : this(innerHandler, cacheExpirationPerHttpResponseCode, statsProvider, @@ -93,11 +90,11 @@ public InMemoryCacheHandler(HttpMessageHandler innerHandler = null, /// The cache to be used. /// The cache keys provider to use internal InMemoryCacheHandler( - HttpMessageHandler innerHandler, - IDictionary cacheExpirationPerHttpResponseCode, - IStatsProvider statsProvider, - IMemoryCache cache, - ICacheKeysProvider cacheKeysProvider) + HttpMessageHandler? innerHandler, + IDictionary? cacheExpirationPerHttpResponseCode, + IStatsProvider? statsProvider, + IMemoryCache? cache, + ICacheKeysProvider? cacheKeysProvider) : base(innerHandler ?? new HttpClientHandler()) { this.StatsProvider = statsProvider ?? new StatsProvider(nameof(InMemoryCacheHandler)); @@ -111,7 +108,7 @@ internal InMemoryCacheHandler( /// /// The URI to invalidate. /// An optional to invalidate. If none is provided, the cache is cleaned for all methods. - public void InvalidateCache(Uri uri, HttpMethod httpMethod = null) + public void InvalidateCache(Uri uri, HttpMethod? httpMethod = null) { var httpMethods = httpMethod != null ? new HashSet { httpMethod } @@ -132,7 +129,7 @@ public void InvalidateCache(Uri uri, HttpMethod httpMethod = null) /// A bool representing if the cache should be cached or not private static bool ShouldTheCacheBeChecked(HttpRequestMessage request) { -#if NET5_0_OR_GREATER +#if NET8_0_OR_GREATER var useCacheOption = request.Options.TryGetValue(UseCache, out var useCache) == false || useCache == true; #else var useCacheOption = request.Properties.TryGetValue(UseCache, out var useCache) == false || (bool)useCache == true; @@ -165,7 +162,7 @@ private static bool ShouldCacheResponse(HttpResponseMessage response) /// The HttpResponseMessage from cache, or a newly invoked one. protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) { - string key = null; + string? key = null; // Gets the data from cache, and returns the data if it's a cache hit var isCachedHttpMethod = CachedHttpMethods.Contains(request.Method); @@ -199,7 +196,7 @@ protected override async Task SendAsync(HttpRequestMessage if (ShouldCacheResponse(response) && TimeSpan.Zero != maxCacheTime) { var entry = await response.ToCacheEntryAsync(); - await this.responseCache.TrySetAsync(key, entry, maxCacheTime); + await this.responseCache.TrySetAsync(key!, entry, maxCacheTime); return request.PrepareCachedEntry(entry); } } @@ -208,10 +205,10 @@ protected override async Task SendAsync(HttpRequestMessage return response; } -#if NET5_0_OR_GREATER +#if NET8_0_OR_GREATER protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) { - string key = null; + string? key = null; // Gets the data from cache, and returns the data if it's a cache hit var isCachedHttpMethod = CachedHttpMethods.Contains(request.Method); @@ -244,7 +241,7 @@ protected override HttpResponseMessage Send(HttpRequestMessage request, Cancella if (ShouldCacheResponse(response) && TimeSpan.Zero != maxCacheTime) { var cacheData = response.ToCacheEntry(); - this.responseCache.TrySetCacheData(key, cacheData, maxCacheTime); + this.responseCache.TrySetCacheData(key!, cacheData, maxCacheTime); return request.PrepareCachedEntry(cacheData); } } @@ -254,7 +251,7 @@ protected override HttpResponseMessage Send(HttpRequestMessage request, Cancella } #endif - private bool TryGetCachedHttpResponseMessage(HttpRequestMessage request, string key, out HttpResponseMessage cachedResponse) + private bool TryGetCachedHttpResponseMessage(HttpRequestMessage request, string key, [NotNullWhen(true)] out HttpResponseMessage? cachedResponse) { if (this.responseCache.TryGetCacheData(key, out var cacheData)) { @@ -263,7 +260,7 @@ private bool TryGetCachedHttpResponseMessage(HttpRequestMessage request, string return true; } - cachedResponse = default; + cachedResponse = null; return false; } } diff --git a/HttpClient.Caching/InMemory/MemoryCache.cs b/HttpClient.Caching/InMemory/MemoryCache.cs index 92517a9..37c277f 100644 --- a/HttpClient.Caching/InMemory/MemoryCache.cs +++ b/HttpClient.Caching/InMemory/MemoryCache.cs @@ -1,9 +1,5 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Threading; -using System.Threading.Tasks; +using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Caching.Abstractions; using Microsoft.Extensions.Caching.InMemory.Internal; @@ -125,7 +121,7 @@ private void SetEntry(CacheEntry entry) this.StartScanForExpiredItems(); } - public bool TryGetValue(object key, out object result) + public bool TryGetValue(object key, [NotNullWhen(true)] out object? result) { if (key == null) { @@ -216,7 +212,7 @@ private void StartScanForExpiredItems() var factory = Task.Factory; var none = CancellationToken.None; var scheduler = TaskScheduler.Default; - factory.StartNew(state => ScanForExpiredItems((MemoryCache)state), this, none, TaskCreationOptions.DenyChildAttach, scheduler); + factory.StartNew(state => ScanForExpiredItems((MemoryCache)state!), this, none, TaskCreationOptions.DenyChildAttach, scheduler); } private static void ScanForExpiredItems(MemoryCache cache) diff --git a/HttpClient.Caching/InMemory/MemoryCacheEntryOptions.cs b/HttpClient.Caching/InMemory/MemoryCacheEntryOptions.cs index 4b8f55a..6f05f24 100644 --- a/HttpClient.Caching/InMemory/MemoryCacheEntryOptions.cs +++ b/HttpClient.Caching/InMemory/MemoryCacheEntryOptions.cs @@ -58,7 +58,7 @@ public TimeSpan? AbsoluteExpirationRelativeToNow /// public TimeSpan? SlidingExpiration { - get { return this.slidingExpiration; } + get => this.slidingExpiration; set { var nullable = value; diff --git a/HttpClient.Caching/InMemory/MemoryCacheOptions.cs b/HttpClient.Caching/InMemory/MemoryCacheOptions.cs index 5c2316f..94c646b 100644 --- a/HttpClient.Caching/InMemory/MemoryCacheOptions.cs +++ b/HttpClient.Caching/InMemory/MemoryCacheOptions.cs @@ -1,5 +1,4 @@ -using System; -using Microsoft.Extensions.Caching.InMemory.Internal; +using Microsoft.Extensions.Caching.InMemory.Internal; namespace Microsoft.Extensions.Caching.InMemory { @@ -7,6 +6,6 @@ public class MemoryCacheOptions { public TimeSpan ExpirationScanFrequency { get; set; } = TimeSpan.FromMinutes(1.0); - public ISystemClock Clock { get; set; } + public ISystemClock? Clock { get; set; } } } \ No newline at end of file diff --git a/HttpClient.Caching/InMemory/MethodUriHeadersCacheKeysProvider.cs b/HttpClient.Caching/InMemory/MethodUriHeadersCacheKeysProvider.cs index b172460..9efce4d 100644 --- a/HttpClient.Caching/InMemory/MethodUriHeadersCacheKeysProvider.cs +++ b/HttpClient.Caching/InMemory/MethodUriHeadersCacheKeysProvider.cs @@ -16,13 +16,10 @@ public class MethodUriHeadersCacheKeysProvider : ICacheKeysProvider /// /// Initialize the cache key provider passing the headers name that will be used to compose /// - /// - public MethodUriHeadersCacheKeysProvider(string[] headersName) + /// The header names to be included in the cache key. + public MethodUriHeadersCacheKeysProvider(string[]? headersName) { - if (headersName != null) - { - this.headersName = headersName.OrderBy(i => i).ToArray(); - } + this.headersName = headersName?.OrderBy(i => i).ToArray() ?? Array.Empty(); } /// diff --git a/HttpClient.Caching/Internals/Nullable.cs b/HttpClient.Caching/Internals/Nullable.cs new file mode 100644 index 0000000..8954a29 --- /dev/null +++ b/HttpClient.Caching/Internals/Nullable.cs @@ -0,0 +1,148 @@ +#if NET48_OR_GREATER || NETSTANDARD1_2 || NETSTANDARD2_0 +// ReSharper disable once CheckNamespace +namespace System.Diagnostics.CodeAnalysis +{ + /* + * Nullable Reference Types (NRT) are a compiler feature introduced with C# 8. + * Older target frameworks (.NET Framework 4.8, .NET Standard 2.0 and earlier) + * do not provide the nullable-analysis attributes that newer frameworks include. + * + * These attribute stubs are included only for those older TFMs so the compiler + * can emit correct nullability metadata and consumers can benefit from NRT + * annotations. They have no runtime behavior and should NOT be included when the + * framework already provides them (.NET Core 3.0+, .NET Standard 2.1+, .NET 5+). + */ + + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] + [ExcludeFromCodeCoverage, DebuggerNonUserCode] + internal sealed class AllowNullAttribute : Attribute + { + public AllowNullAttribute() { } + } + + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property, Inherited = false)] + [ExcludeFromCodeCoverage, DebuggerNonUserCode] + internal sealed class DisallowNullAttribute : Attribute + { + public DisallowNullAttribute() { } + } + + [AttributeUsage(AttributeTargets.Method, Inherited = false)] + [ExcludeFromCodeCoverage, DebuggerNonUserCode] + internal sealed class DoesNotReturnAttribute : Attribute + { + public DoesNotReturnAttribute() { } + } + + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + [ExcludeFromCodeCoverage, DebuggerNonUserCode] + internal sealed class DoesNotReturnIfAttribute : Attribute + { + public bool ParameterValue { get; } + + public DoesNotReturnIfAttribute(bool parameterValue) + { + this.ParameterValue = parameterValue; + } + } + + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, + Inherited = false)] + [ExcludeFromCodeCoverage, DebuggerNonUserCode] + internal sealed class MaybeNullAttribute : Attribute + { + public MaybeNullAttribute() { } + } + + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + [ExcludeFromCodeCoverage, DebuggerNonUserCode] + internal sealed class MaybeNullWhenAttribute : Attribute + { + public bool ReturnValue { get; } + + public MaybeNullWhenAttribute(bool returnValue) + { + this.ReturnValue = returnValue; + } + } + + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = true, Inherited = false)] + [ExcludeFromCodeCoverage, DebuggerNonUserCode] + internal sealed class MemberNotNullAttribute : Attribute + { + public string[] Members { get; } + + public MemberNotNullAttribute(string member) + { + this.Members = new[] { member }; + } + + public MemberNotNullAttribute(params string[] members) + { + this.Members = members; + } + } + + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, AllowMultiple = true, Inherited = false)] + [ExcludeFromCodeCoverage, DebuggerNonUserCode] + internal sealed class MemberNotNullWhenAttribute : Attribute + { + public bool ReturnValue { get; } + + public string[] Members { get; } + + public MemberNotNullWhenAttribute(bool returnValue, string member) + { + this.ReturnValue = returnValue; + this.Members = new[] { member }; + } + + public MemberNotNullWhenAttribute(bool returnValue, params string[] members) + { + this.ReturnValue = returnValue; + this.Members = members; + } + } + + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, + Inherited = false)] + [ExcludeFromCodeCoverage, DebuggerNonUserCode] + internal sealed class NotNullAttribute : Attribute + { + public NotNullAttribute() { } + } + + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Property | AttributeTargets.ReturnValue, + AllowMultiple = true, + Inherited = false)] + [ExcludeFromCodeCoverage, DebuggerNonUserCode] + internal sealed class NotNullIfNotNullAttribute : Attribute + { + public string ParameterName { get; } + + public NotNullIfNotNullAttribute(string parameterName) + { + this.ParameterName = parameterName; + } + } + + [AttributeUsage(AttributeTargets.Parameter, Inherited = false)] + [ExcludeFromCodeCoverage, DebuggerNonUserCode] + internal sealed class NotNullWhenAttribute : Attribute + { + public bool ReturnValue { get; } + + public NotNullWhenAttribute(bool returnValue) + { + this.ReturnValue = returnValue; + } + } + +#if NETSTANDARD1_2 + [AttributeUsage(AttributeTargets.Class, Inherited = false)] + internal sealed class ExcludeFromCodeCoverage : Attribute + { + } +#endif +} +#endif diff --git a/Tests/HttpClient.Caching.Tests/InMemory/InMemoryCacheHandlerTests.cs b/Tests/HttpClient.Caching.Tests/InMemory/InMemoryCacheHandlerTests.cs index 39b6dfe..1e61731 100644 --- a/Tests/HttpClient.Caching.Tests/InMemory/InMemoryCacheHandlerTests.cs +++ b/Tests/HttpClient.Caching.Tests/InMemory/InMemoryCacheHandlerTests.cs @@ -12,6 +12,8 @@ namespace HttpClient.Caching.Tests.InMemory { + using HttpClient = System.Net.Http.HttpClient; + public class InMemoryCacheHandlerTests { [Fact] @@ -19,7 +21,7 @@ public async Task CachesTheResult() { // Arrange var testMessageHandler = new TestMessageHandler(); - var client = new System.Net.Http.HttpClient(new InMemoryCacheHandler(testMessageHandler)); + var client = new HttpClient(new InMemoryCacheHandler(testMessageHandler)); // Act await client.GetAsync("http://unittest"); @@ -29,13 +31,13 @@ public async Task CachesTheResult() testMessageHandler.NumberOfCalls.Should().Be(1); } -#if NET5_0_OR_GREATER +#if NET8_0_OR_GREATER [Fact] public void CachesTheResult_Send() { // Arrange var testMessageHandler = new TestMessageHandler(); - var client = new System.Net.Http.HttpClient(new InMemoryCacheHandler(testMessageHandler)); + var client = new HttpClient(new InMemoryCacheHandler(testMessageHandler)); // Act var response1 = client.Send(new HttpRequestMessage(HttpMethod.Get, "http://unittest")); @@ -53,7 +55,6 @@ public void CachesTheResult_Send() /// By using without any header then should /// behave like using /// - /// [Fact] public async Task CachesTheResult_MethodUriHeadersCacheKeysProvider() { @@ -61,7 +62,7 @@ public async Task CachesTheResult_MethodUriHeadersCacheKeysProvider() var testMessageHandler = new TestMessageHandler(); // no headers are provided so ICacheKeyProvider MethodUriHeadersCacheKeysProvider should behave like DefaultCacheKeysProvider var cacheKeyProvider = new MethodUriHeadersCacheKeysProvider(null); - var client = new System.Net.Http.HttpClient(new InMemoryCacheHandler(testMessageHandler, cacheKeysProvider: cacheKeyProvider)); + var client = new HttpClient(new InMemoryCacheHandler(testMessageHandler, cacheKeysProvider: cacheKeyProvider)); // Act await client.GetAsync("http://unittest"); @@ -80,8 +81,8 @@ public async Task CachesTheResult_MethodUriHeadersCacheKeysProviderWithCustomHea { // Arrange var testMessageHandler = new TestMessageHandler(); - var cacheKeyProvider = new MethodUriHeadersCacheKeysProvider(new string[] { "CUSTOM-HEADER" }); //this is the header that will be included in cache key generator - var client = new System.Net.Http.HttpClient(new InMemoryCacheHandler(testMessageHandler, cacheKeysProvider: cacheKeyProvider)); + var cacheKeyProvider = new MethodUriHeadersCacheKeysProvider(new [] { "CUSTOM-HEADER" }); //this is the header that will be included in cache key generator + var client = new HttpClient(new InMemoryCacheHandler(testMessageHandler, cacheKeysProvider: cacheKeyProvider)); var request1 = new HttpRequestMessage(HttpMethod.Get, "http://unittest"); var request2 = new HttpRequestMessage(HttpMethod.Get, "http://unittest"); @@ -97,14 +98,13 @@ public async Task CachesTheResult_MethodUriHeadersCacheKeysProviderWithCustomHea /// use with specific headers, /// both requests specify different value for the header /// - /// [Fact] public async Task CachesTheResult_MethodUriHeadersCacheKeysProviderWithCustomHeaders_DifferentValues() { // Arrange var testMessageHandler = new TestMessageHandler(); - var cacheKeyProvider = new MethodUriHeadersCacheKeysProvider(new string[] { "CUSTOM-HEADER" }); //this is the header that will be included in cache key generator - var client = new System.Net.Http.HttpClient(new InMemoryCacheHandler(testMessageHandler, cacheKeysProvider: cacheKeyProvider)); + var cacheKeyProvider = new MethodUriHeadersCacheKeysProvider(new [] { "CUSTOM-HEADER" }); //this is the header that will be included in cache key generator + var client = new HttpClient(new InMemoryCacheHandler(testMessageHandler, cacheKeysProvider: cacheKeyProvider)); var request1 = new HttpRequestMessage(HttpMethod.Get, "http://unittest"); request1.Headers.Add("CUSTOM-HEADER", "Value1"); var request2 = new HttpRequestMessage(HttpMethod.Get, "http://unittest"); @@ -122,14 +122,13 @@ public async Task CachesTheResult_MethodUriHeadersCacheKeysProviderWithCustomHea /// use with specific headers, /// one request specify a value for the header, the other none doesn't specify any hader value /// - /// [Fact] public async Task CachesTheResult_MethodUriHeadersCacheKeysProviderWithCustomHeaders_HeaderValueInOneRequest() { // Arrange var testMessageHandler = new TestMessageHandler(); - var cacheKeyProvider = new MethodUriHeadersCacheKeysProvider(new string[] { "CUSTOM-HEADER" }); //this is the header that will be included in cache key generator - var client = new System.Net.Http.HttpClient(new InMemoryCacheHandler(testMessageHandler, cacheKeysProvider: cacheKeyProvider)); + var cacheKeyProvider = new MethodUriHeadersCacheKeysProvider(new [] { "CUSTOM-HEADER" }); //this is the header that will be included in cache key generator + var client = new HttpClient(new InMemoryCacheHandler(testMessageHandler, cacheKeysProvider: cacheKeyProvider)); var request1 = new HttpRequestMessage(HttpMethod.Get, "http://unittest"); request1.Headers.Add("CUSTOM-HEADER", "Value1"); var request2 = new HttpRequestMessage(HttpMethod.Get, "http://unittest"); @@ -146,14 +145,13 @@ public async Task CachesTheResult_MethodUriHeadersCacheKeysProviderWithCustomHea /// use with specific headers, /// both requests specify same value for the header /// - /// [Fact] public async Task CachesTheResult_MethodUriHeadersCacheKeysProviderWithCustomHeaders_SameValues() { // Arrange var testMessageHandler = new TestMessageHandler(); - var cacheKeyProvider = new MethodUriHeadersCacheKeysProvider(new string[] { "CUSTOM-HEADER" }); //this is the header that will be included in cache key generator - var client = new System.Net.Http.HttpClient(new InMemoryCacheHandler(testMessageHandler, cacheKeysProvider: cacheKeyProvider)); + var cacheKeyProvider = new MethodUriHeadersCacheKeysProvider(new [] { "CUSTOM-HEADER" }); //this is the header that will be included in cache key generator + var client = new HttpClient(new InMemoryCacheHandler(testMessageHandler, cacheKeysProvider: cacheKeyProvider)); var request1 = new HttpRequestMessage(HttpMethod.Get, "http://unittest"); request1.Headers.Add("CUSTOM-HEADER", "Value1"); var request2 = new HttpRequestMessage(HttpMethod.Get, "http://unittest"); @@ -171,14 +169,13 @@ public async Task CachesTheResult_MethodUriHeadersCacheKeysProviderWithCustomHea /// use with specific headers, /// both requests specify same value for the headers /// - /// [Fact] public async Task CachesTheResult_MethodUriHeadersCacheKeysProviderWithCustomHeaders_SameValues_MultipleHeaders() { // Arrange var testMessageHandler = new TestMessageHandler(); - var cacheKeyProvider = new MethodUriHeadersCacheKeysProvider(new string[] { "CUSTOM-HEADER", "ANOTHER-HEADER", "HEADER3" }); //this is the header that will be included in cache key generator - var client = new System.Net.Http.HttpClient(new InMemoryCacheHandler(testMessageHandler, cacheKeysProvider: cacheKeyProvider)); + var cacheKeyProvider = new MethodUriHeadersCacheKeysProvider(new [] { "CUSTOM-HEADER", "ANOTHER-HEADER", "HEADER3" }); //this is the header that will be included in cache key generator + var client = new HttpClient(new InMemoryCacheHandler(testMessageHandler, cacheKeysProvider: cacheKeyProvider)); var request1 = new HttpRequestMessage(HttpMethod.Get, "http://unittest"); request1.Headers.Add("CUSTOM-HEADER", "Value1"); request1.Headers.Add("ANOTHER-HEADER", "Value2"); @@ -201,14 +198,13 @@ public async Task CachesTheResult_MethodUriHeadersCacheKeysProviderWithCustomHea /// both requests specify same value for the headers that are common but not all specific request contains the same /// headers /// - /// [Fact] public async Task CachesTheResult_MethodUriHeadersCacheKeysProviderWithCustomHeaders_SameValues_MultipleHeaders2() { // Arrange var testMessageHandler = new TestMessageHandler(); - var cacheKeyProvider = new MethodUriHeadersCacheKeysProvider(new string[] { "CUSTOM-HEADER", "ANOTHER-HEADER", "HEADER3" }); //this is the header that will be included in cache key generator - var client = new System.Net.Http.HttpClient(new InMemoryCacheHandler(testMessageHandler, cacheKeysProvider: cacheKeyProvider)); + var cacheKeyProvider = new MethodUriHeadersCacheKeysProvider(new [] { "CUSTOM-HEADER", "ANOTHER-HEADER", "HEADER3" }); //this is the header that will be included in cache key generator + var client = new HttpClient(new InMemoryCacheHandler(testMessageHandler, cacheKeysProvider: cacheKeyProvider)); var request1 = new HttpRequestMessage(HttpMethod.Get, "http://unittest"); request1.Headers.Add("CUSTOM-HEADER", "Value1"); request1.Headers.Add("ANOTHER-HEADER", "Value2"); @@ -229,14 +225,13 @@ public async Task CachesTheResult_MethodUriHeadersCacheKeysProviderWithCustomHea /// use with specific headers, /// both requests specify same value for the headers but in different order /// - /// [Fact] public async Task CachesTheResult_MethodUriHeadersCacheKeysProviderWithCustomHeaders_SameValues_MultipleHeaders_DifferentOrder() { // Arrange var testMessageHandler = new TestMessageHandler(); - var cacheKeyProvider = new MethodUriHeadersCacheKeysProvider(new string[] { "CUSTOM-HEADER", "ANOTHER-HEADER", "HEADER3" }); //this is the header that will be included in cache key generator - var client = new System.Net.Http.HttpClient(new InMemoryCacheHandler(testMessageHandler, cacheKeysProvider: cacheKeyProvider)); + var cacheKeyProvider = new MethodUriHeadersCacheKeysProvider(new [] { "CUSTOM-HEADER", "ANOTHER-HEADER", "HEADER3" }); //this is the header that will be included in cache key generator + var client = new HttpClient(new InMemoryCacheHandler(testMessageHandler, cacheKeysProvider: cacheKeyProvider)); var request1 = new HttpRequestMessage(HttpMethod.Get, "http://unittest"); request1.Headers.Add("CUSTOM-HEADER", "Value1"); request1.Headers.Add("HEADER3", "Value3"); @@ -259,14 +254,13 @@ public async Task CachesTheResult_MethodUriHeadersCacheKeysProviderWithCustomHea /// both requests specify same value for specific header. /// A request include some headers which aren't considered for cache key composition /// - /// [Fact] public async Task CachesTheResult_MethodUriHeadersCacheKeysProviderWithCustomHeaders_SameValues2() { // Arrange var testMessageHandler = new TestMessageHandler(); var cacheKeyProvider = new MethodUriHeadersCacheKeysProvider(new [] { "CUSTOM-HEADER" }); //this is the header that will be included in cache key generator - var client = new System.Net.Http.HttpClient(new InMemoryCacheHandler(testMessageHandler, cacheKeysProvider: cacheKeyProvider)); + var client = new HttpClient(new InMemoryCacheHandler(testMessageHandler, cacheKeysProvider: cacheKeyProvider)); var request1 = new HttpRequestMessage(HttpMethod.Get, "http://unittest"); request1.Headers.Add("CUSTOM-HEADER", "Value1"); request1.Headers.Add("ANOTHER-CUSTOM-HEADER", "ValueX"); // this header isn't considered for cache key composition @@ -290,7 +284,7 @@ public async Task GetsTheDataAgainAfterEntryIsGoneFromCache() var testMessageHandler = new TestMessageHandler(); var cache = new MemoryCache(new MemoryCacheOptions()); var inMemoryCacheHandler = new InMemoryCacheHandler(testMessageHandler, null, null, cache, null); - var client = new System.Net.Http.HttpClient(inMemoryCacheHandler); + var client = new HttpClient(inMemoryCacheHandler); // Act await client.GetAsync("http://unittest"); @@ -308,7 +302,7 @@ public async Task IsCaseSensitive() // Arrange var testMessageHandler = new TestMessageHandler(); var cache = new MemoryCache(new MemoryCacheOptions()); - var client = new System.Net.Http.HttpClient(new InMemoryCacheHandler(testMessageHandler, null, null, cache, null)); + var client = new HttpClient(new InMemoryCacheHandler(testMessageHandler, null, null, cache, null)); // Act for different URLs, only different by casing await client.GetAsync("http://unittest/foo.html"); @@ -324,7 +318,7 @@ public async Task CachesPerUrl() // Arrange var testMessageHandler = new TestMessageHandler(); var cache = new MemoryCache(new MemoryCacheOptions()); - var client = new System.Net.Http.HttpClient(new InMemoryCacheHandler(testMessageHandler, null, null, cache, null)); + var client = new HttpClient(new InMemoryCacheHandler(testMessageHandler, null, null, cache, null)); // Act for different URLs await client.GetAsync("http://unittest1"); @@ -341,7 +335,7 @@ public async Task CachesWithCacheControlHeaders_NoCacheTrue() var cacheControl = new CacheControlHeaderValue{ NoCache = true, NoStore = true }; var testMessageHandler = new TestMessageHandler(cacheControl: cacheControl); var cache = new MemoryCache(new MemoryCacheOptions()); - var client = new System.Net.Http.HttpClient(new InMemoryCacheHandler(testMessageHandler, null, null, cache, null)); + var client = new HttpClient(new InMemoryCacheHandler(testMessageHandler, null, null, cache, null)); var request1 = new HttpRequestMessage(HttpMethod.Get, "http://unittest"); var request2 = new HttpRequestMessage(HttpMethod.Get, "http://unittest"); @@ -360,7 +354,7 @@ public async Task OnlyCachesGetAndHeadResults() // Arrange var testMessageHandler = new TestMessageHandler(); var cache = new MemoryCache(new MemoryCacheOptions()); - var client = new System.Net.Http.HttpClient(new InMemoryCacheHandler(testMessageHandler, null, null, cache, null)); + var client = new HttpClient(new InMemoryCacheHandler(testMessageHandler, null, null, cache, null)); // Act for different methods await client.PostAsync("http://unittest", new StringContent(string.Empty)); @@ -380,7 +374,7 @@ public async Task CachesHeadAndGetRequestWithoutConflict() // Arrange var testMessageHandler = new TestMessageHandler(); var cache = new MemoryCache(new MemoryCacheOptions()); - var client = new System.Net.Http.HttpClient(new InMemoryCacheHandler(testMessageHandler, null, null, cache, null)); + var client = new HttpClient(new InMemoryCacheHandler(testMessageHandler, null, null, cache, null)); // Act for different methods await client.SendAsync(new HttpRequestMessage(HttpMethod.Head, "http://unittest")); @@ -396,7 +390,7 @@ public async Task DataFromCallMatchesDataFromCache() // Arrange var testMessageHandler = new TestMessageHandler(); var cache = new MemoryCache(new MemoryCacheOptions()); - var client = new System.Net.Http.HttpClient(new InMemoryCacheHandler(testMessageHandler, null, null, cache, null)); + var client = new HttpClient(new InMemoryCacheHandler(testMessageHandler, null, null, cache, null)); // Act for different methods var originalResult = await client.GetAsync("http://unittest"); @@ -414,7 +408,7 @@ public async Task ReturnsResponseHeader() { // Arrange var testMessageHandler = new TestMessageHandler(HttpStatusCode.OK, "test content", "text/plain", Encoding.UTF8); - var client = new System.Net.Http.HttpClient(new InMemoryCacheHandler(testMessageHandler)); + var client = new HttpClient(new InMemoryCacheHandler(testMessageHandler)); // Act var response = await client.GetAsync("http://unittest"); @@ -434,7 +428,7 @@ public async Task DisableCachePerStatusCode() }; var testMessageHandler = new TestMessageHandler(); - var client = new System.Net.Http.HttpClient(new InMemoryCacheHandler(testMessageHandler, cacheExpirationPerStatusCode)); + var client = new HttpClient(new InMemoryCacheHandler(testMessageHandler, cacheExpirationPerStatusCode)); // Act await client.GetAsync("http://unittest"); @@ -450,7 +444,7 @@ public async Task InvalidatesCacheCorrectly() // Arrange var testMessageHandler = new TestMessageHandler(); var handler = new InMemoryCacheHandler(testMessageHandler); - var client = new System.Net.Http.HttpClient(handler); + var client = new HttpClient(handler); // Act, with cache invalidation in between var uri = new Uri("http://unittest"); @@ -468,7 +462,7 @@ public async Task InvalidatesCachePerMethod() // Arrange var testMessageHandler = new TestMessageHandler(); var handler = new InMemoryCacheHandler(testMessageHandler); - var client = new System.Net.Http.HttpClient(handler); + var client = new HttpClient(handler); // Act with two methods, and clean up one cache var uri = new Uri("http://unittest"); diff --git a/Tests/HttpClient.Caching.Tests/Testdata/TestMessageHandler.cs b/Tests/HttpClient.Caching.Tests/Testdata/TestMessageHandler.cs index 75a0923..74797e6 100644 --- a/Tests/HttpClient.Caching.Tests/Testdata/TestMessageHandler.cs +++ b/Tests/HttpClient.Caching.Tests/Testdata/TestMessageHandler.cs @@ -67,7 +67,7 @@ protected override async Task SendAsync(HttpRequestMessage return this.CreateHttpResponseMessage(); } -#if NET5_0_OR_GREATER +#if NET8_0_OR_GREATER protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken) { this.NumberOfCalls++; From 51a68559d2ac0220ea05b5c93d85f6f309cb44c3 Mon Sep 17 00:00:00 2001 From: Thomas Galliker Date: Mon, 6 Apr 2026 16:26:55 +0200 Subject: [PATCH 12/19] Update package tags --- HttpClient.Caching/HttpClient.Caching.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/HttpClient.Caching/HttpClient.Caching.csproj b/HttpClient.Caching/HttpClient.Caching.csproj index 5fd69ae..80df6e8 100644 --- a/HttpClient.Caching/HttpClient.Caching.csproj +++ b/HttpClient.Caching/HttpClient.Caching.csproj @@ -18,7 +18,7 @@ 1.0.0 Thomas Galliker HttpClient.Caching - retry;resilience;resilient;fault;failure;http;handler;HttpRetryHelper;ApiClient;ApiService + httpclient.caching;httpclient;caching;cache;inmemory logo.png LICENSE README.md From 7252de847f23f4c2322f5ac08d53c6c2d6b74c6e Mon Sep 17 00:00:00 2001 From: Thomas Galliker Date: Mon, 6 Apr 2026 16:27:08 +0200 Subject: [PATCH 13/19] Update csproj of test project --- .../HttpClient.Caching.Tests.csproj | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/Tests/HttpClient.Caching.Tests/HttpClient.Caching.Tests.csproj b/Tests/HttpClient.Caching.Tests/HttpClient.Caching.Tests.csproj index 853ec1f..42e8e0f 100644 --- a/Tests/HttpClient.Caching.Tests/HttpClient.Caching.Tests.csproj +++ b/Tests/HttpClient.Caching.Tests/HttpClient.Caching.Tests.csproj @@ -2,11 +2,9 @@ net48;net10.0 - HttpClient.Caching.Tests - HttpClient.Caching.Tests - false - HttpClient.Caching.Tests - en + enable + latest + enable From 638f1a57bff464506da61451daa589ad2f1943b9 Mon Sep 17 00:00:00 2001 From: Thomas Galliker Date: Mon, 6 Apr 2026 17:09:57 +0200 Subject: [PATCH 14/19] Replace MemoryCache implementation with Microsoft.Extensions.Caching.Memory --- .../Abstractions/CacheExtensions.cs | 115 ------ .../Abstractions/CacheItemPriority.cs | 13 - .../Abstractions/EvictionReason.cs | 12 - .../Abstractions/ICacheEntry.cs | 51 --- .../Abstractions/ICacheKeysProvider.cs | 4 +- .../Abstractions/IChangeToken.cs | 26 -- .../PostEvictionCallbackRegistration.cs | 9 - .../Abstractions/PostEvictionDelegate.cs | 11 - .../Abstractions/StatusCodeExtensions.cs | 2 - HttpClient.Caching/HttpClient.Caching.csproj | 5 +- HttpClient.Caching/InMemory/CacheEntry.cs | 318 ----------------- .../InMemory/CacheEntryExtensions.cs | 140 -------- HttpClient.Caching/InMemory/IMemoryCache.cs | 33 -- .../InMemory/IMemoryCacheExtensions.cs | 7 +- .../InMemory/InMemoryCacheFallbackHandler.cs | 3 +- .../InMemory/InMemoryCacheHandler.cs | 3 +- .../InMemory/Internal/ISystemClock.cs | 11 - .../InMemory/Internal/SystemClock.cs | 13 - HttpClient.Caching/InMemory/MemoryCache.cs | 333 ------------------ .../InMemory/MemoryCacheEntryOptions.cs | 75 ---- .../InMemory/MemoryCacheOptions.cs | 11 - HttpClient.Caching/Internals/Nullable.cs | 2 +- README.md | 16 +- ReleaseNotes.txt | 1 + Samples/ConsoleAppSample/Program.cs | 3 +- .../HttpClient.Caching.Tests.csproj | 6 +- .../InMemoryCacheFallbackHandlerTests.cs | 70 ++-- .../InMemory/InMemoryCacheHandlerTests.cs | 5 +- .../MemoryCacheTests.cs | 4 +- .../Testdata/TestMessageHandler.cs | 23 +- 30 files changed, 79 insertions(+), 1246 deletions(-) delete mode 100644 HttpClient.Caching/Abstractions/CacheExtensions.cs delete mode 100644 HttpClient.Caching/Abstractions/CacheItemPriority.cs delete mode 100644 HttpClient.Caching/Abstractions/EvictionReason.cs delete mode 100644 HttpClient.Caching/Abstractions/ICacheEntry.cs delete mode 100644 HttpClient.Caching/Abstractions/IChangeToken.cs delete mode 100644 HttpClient.Caching/Abstractions/PostEvictionCallbackRegistration.cs delete mode 100644 HttpClient.Caching/Abstractions/PostEvictionDelegate.cs delete mode 100644 HttpClient.Caching/InMemory/CacheEntry.cs delete mode 100644 HttpClient.Caching/InMemory/CacheEntryExtensions.cs delete mode 100644 HttpClient.Caching/InMemory/IMemoryCache.cs delete mode 100644 HttpClient.Caching/InMemory/Internal/ISystemClock.cs delete mode 100644 HttpClient.Caching/InMemory/Internal/SystemClock.cs delete mode 100644 HttpClient.Caching/InMemory/MemoryCache.cs delete mode 100644 HttpClient.Caching/InMemory/MemoryCacheEntryOptions.cs delete mode 100644 HttpClient.Caching/InMemory/MemoryCacheOptions.cs diff --git a/HttpClient.Caching/Abstractions/CacheExtensions.cs b/HttpClient.Caching/Abstractions/CacheExtensions.cs deleted file mode 100644 index 4e1c51e..0000000 --- a/HttpClient.Caching/Abstractions/CacheExtensions.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.Threading.Tasks; -using Microsoft.Extensions.Caching.InMemory; - -namespace Microsoft.Extensions.Caching.Abstractions -{ - public static class CacheExtensions - { - public static object? Get(this IMemoryCache cache, object key) - { - cache.TryGetValue(key, out var obj); - return obj; - } - - public static TItem? Get(this IMemoryCache cache, object key) - { - cache.TryGetValue(key, out TItem? obj); - return obj; - } - - public static bool TryGetValue(this IMemoryCache cache, object key, [NotNullWhen(true)] out TItem? value) - { - if (cache.TryGetValue(key, out var obj)) - { - value = (TItem)obj; - return true; - } - - value = default; - return false; - } - - public static TItem Set(this IMemoryCache cache, object key, TItem value) where TItem : notnull - { - var entry = cache.CreateEntry(key); - entry.Value = value; - entry.Dispose(); - return value; - } - - public static TItem Set(this IMemoryCache cache, object key, TItem value, DateTimeOffset absoluteExpiration) where TItem : notnull - { - var entry = cache.CreateEntry(key); - DateTimeOffset? nullable = absoluteExpiration; - entry.AbsoluteExpiration = nullable; - entry.Value = value; - entry.Dispose(); - return value; - } - - public static TItem Set(this IMemoryCache cache, object key, TItem value, TimeSpan absoluteExpirationRelativeToNow) where TItem : notnull - { - var entry = cache.CreateEntry(key); - TimeSpan? nullable = absoluteExpirationRelativeToNow; - entry.AbsoluteExpirationRelativeToNow = nullable; - entry.Value = value; - entry.Dispose(); - return value; - } - - public static TItem Set(this IMemoryCache cache, object key, TItem value, IChangeToken expirationToken) where TItem : notnull - { - var entry = cache.CreateEntry(key); - var expirationToken1 = expirationToken; - entry.AddExpirationToken(expirationToken1); - entry.Value = value; - entry.Dispose(); - return value; - } - - public static TItem Set(this IMemoryCache cache, object key, TItem value, MemoryCacheEntryOptions options) where TItem : notnull - { - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - - using (var entry = cache.CreateEntry(key)) - { - entry.SetOptions(options); - entry.Value = value; - } - - return value; - } - - public static TItem GetOrCreate(this IMemoryCache cache, object key, Func factory) where TItem : notnull - { - if (!cache.TryGetValue(key, out var obj)) - { - var entry = cache.CreateEntry(key); - obj = factory(entry); - entry.SetValue(obj); - entry.Dispose(); - } - - return (TItem)obj; - } - - public static async Task GetOrCreateAsync(this IMemoryCache cache, object key, Func> factory) where TItem : notnull - { - if (!cache.TryGetValue(key, out var obj)) - { - var entry = cache.CreateEntry(key); - obj = await factory(entry); - entry.SetValue(obj); - entry.Dispose(); - entry = null; - } - - return (TItem)obj; - } - } -} \ No newline at end of file diff --git a/HttpClient.Caching/Abstractions/CacheItemPriority.cs b/HttpClient.Caching/Abstractions/CacheItemPriority.cs deleted file mode 100644 index d4a06bf..0000000 --- a/HttpClient.Caching/Abstractions/CacheItemPriority.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace Microsoft.Extensions.Caching.Abstractions -{ - /// - /// Specifies how items are prioritized for preservation during a memory pressure triggered cleanup. - /// - public enum CacheItemPriority - { - Low, - Normal, - High, - NeverRemove, - } -} \ No newline at end of file diff --git a/HttpClient.Caching/Abstractions/EvictionReason.cs b/HttpClient.Caching/Abstractions/EvictionReason.cs deleted file mode 100644 index 1283c21..0000000 --- a/HttpClient.Caching/Abstractions/EvictionReason.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace Microsoft.Extensions.Caching.Abstractions -{ - public enum EvictionReason - { - None = 0, - Removed = 1, - Replaced = 2, - Expired = 3, - TokenExpired = 4, - Capacity = 5, - } -} \ No newline at end of file diff --git a/HttpClient.Caching/Abstractions/ICacheEntry.cs b/HttpClient.Caching/Abstractions/ICacheEntry.cs deleted file mode 100644 index 87ea776..0000000 --- a/HttpClient.Caching/Abstractions/ICacheEntry.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Microsoft.Extensions.Caching.Abstractions -{ - /// - /// Represents an entry in the implementation. - /// - public interface ICacheEntry : IDisposable - { - /// Gets the key of the cache entry. - object Key { get; } - - /// Gets or set the value of the cache entry. - object Value { get; set; } - - /// - /// Gets or sets an absolute expiration date for the cache entry. - /// - DateTimeOffset? AbsoluteExpiration { get; set; } - - /// - /// Gets or sets an absolute expiration time, relative to now. - /// - TimeSpan? AbsoluteExpirationRelativeToNow { get; set; } - - /// - /// Gets or sets how long a cache entry can be inactive (e.g. not accessed) before it will be removed. - /// This will not extend the entry lifetime beyond the absolute expiration (if set). - /// - TimeSpan? SlidingExpiration { get; set; } - - /// - /// Gets the instances which cause the cache - /// entry to expire. - /// - IList ExpirationTokens { get; } - - /// - /// Gets or sets the callbacks will be fired after the cache entry is evicted from the cache. - /// - IList PostEvictionCallbacks { get; } - - /// - /// Gets or sets the priority for keeping the cache entry in the cache during a - /// memory pressure triggered cleanup. The default is - /// . - /// - CacheItemPriority Priority { get; set; } - } -} \ No newline at end of file diff --git a/HttpClient.Caching/Abstractions/ICacheKeysProvider.cs b/HttpClient.Caching/Abstractions/ICacheKeysProvider.cs index 34c4e2a..372df7d 100644 --- a/HttpClient.Caching/Abstractions/ICacheKeysProvider.cs +++ b/HttpClient.Caching/Abstractions/ICacheKeysProvider.cs @@ -10,8 +10,8 @@ public interface ICacheKeysProvider /// /// Return the key for the request message /// - /// - /// + /// The http request message. + /// The cache key. string GetKey(HttpRequestMessage request); } } diff --git a/HttpClient.Caching/Abstractions/IChangeToken.cs b/HttpClient.Caching/Abstractions/IChangeToken.cs deleted file mode 100644 index 39b4bc8..0000000 --- a/HttpClient.Caching/Abstractions/IChangeToken.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; - -namespace Microsoft.Extensions.Caching.Abstractions -{ - /// Propagates notifications that a change has occured. - public interface IChangeToken - { - /// Gets a value that indicates if a change has occured. - bool HasChanged { get; } - - /// - /// Indicates if this token will pro-actively raise callbacks. Callbacks are still guaranteed to fire, eventually. - /// - bool ActiveChangeCallbacks { get; } - - /// - /// Registers for a callback that will be invoked when the entry has changed. - /// MUST be set before the callback - /// is invoked. - /// - /// The to invoke. - /// State to be passed into the callback. - /// An that is used to unregister the callback. - IDisposable RegisterChangeCallback(Action callback, object state); - } -} \ No newline at end of file diff --git a/HttpClient.Caching/Abstractions/PostEvictionCallbackRegistration.cs b/HttpClient.Caching/Abstractions/PostEvictionCallbackRegistration.cs deleted file mode 100644 index 29985ee..0000000 --- a/HttpClient.Caching/Abstractions/PostEvictionCallbackRegistration.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace Microsoft.Extensions.Caching.Abstractions -{ - public class PostEvictionCallbackRegistration - { - public PostEvictionDelegate EvictionCallback { get; set; } = null!; - - public object? State { get; set; } - } -} \ No newline at end of file diff --git a/HttpClient.Caching/Abstractions/PostEvictionDelegate.cs b/HttpClient.Caching/Abstractions/PostEvictionDelegate.cs deleted file mode 100644 index c6718fb..0000000 --- a/HttpClient.Caching/Abstractions/PostEvictionDelegate.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace Microsoft.Extensions.Caching.Abstractions -{ - /// - /// Signature of the callback which gets called when a cache entry expires. - /// - /// - /// - /// The . - /// The information that was passed when registering the callback. - public delegate void PostEvictionDelegate(object key, object value, EvictionReason evictionReason, object? state); -} \ No newline at end of file diff --git a/HttpClient.Caching/Abstractions/StatusCodeExtensions.cs b/HttpClient.Caching/Abstractions/StatusCodeExtensions.cs index ce1e1bb..2eae74d 100644 --- a/HttpClient.Caching/Abstractions/StatusCodeExtensions.cs +++ b/HttpClient.Caching/Abstractions/StatusCodeExtensions.cs @@ -1,5 +1,3 @@ -using System; -using System.Collections.Generic; using System.Net; namespace Microsoft.Extensions.Caching.Abstractions diff --git a/HttpClient.Caching/HttpClient.Caching.csproj b/HttpClient.Caching/HttpClient.Caching.csproj index 80df6e8..fb98019 100644 --- a/HttpClient.Caching/HttpClient.Caching.csproj +++ b/HttpClient.Caching/HttpClient.Caching.csproj @@ -1,7 +1,7 @@  - net48;netstandard2.0;netstandard2.1;net8.0;net9.0;net10.0 + net462;netstandard2.0;netstandard2.1;net8.0;net9.0;net10.0 Library Microsoft.Extensions.Caching enable @@ -43,13 +43,14 @@ + true - + diff --git a/HttpClient.Caching/InMemory/CacheEntry.cs b/HttpClient.Caching/InMemory/CacheEntry.cs deleted file mode 100644 index 08ce5b9..0000000 --- a/HttpClient.Caching/InMemory/CacheEntry.cs +++ /dev/null @@ -1,318 +0,0 @@ -using System.Diagnostics; -using Microsoft.Extensions.Caching.Abstractions; - -namespace Microsoft.Extensions.Caching.InMemory -{ - internal class CacheEntry : ICacheEntry - { - private static readonly Action ExpirationCallback = ExpirationTokensExpired; - internal readonly object Lock = new object(); - private bool added; - private readonly Action notifyCacheOfExpiration; - private readonly Action notifyCacheEntryDisposed; - private IList? expirationTokenRegistrations; - private IList? postEvictionCallbacks; - private bool isExpired; - internal IList? expirationTokens; - internal DateTimeOffset? absoluteExpiration; - internal TimeSpan? absoluteExpirationRelativeToNow; - private TimeSpan? slidingExpiration; - - public CacheItemPriority Priority { get; set; } = CacheItemPriority.Normal; - - public DateTimeOffset? AbsoluteExpiration - { - get { return this.absoluteExpiration; } - set { this.absoluteExpiration = value; } - } - - public TimeSpan? AbsoluteExpirationRelativeToNow - { - get { return this.absoluteExpirationRelativeToNow; } - set - { - var nullable = value; - if ((nullable.HasValue ? (nullable.GetValueOrDefault() <= TimeSpan.Zero ? 1 : 0) : 0) != 0) - { - throw new ArgumentOutOfRangeException(nameof(this.AbsoluteExpirationRelativeToNow), value, "The relative expiration value must be positive."); - } - - this.absoluteExpirationRelativeToNow = value; - } - } - - public TimeSpan? SlidingExpiration - { - get { return this.slidingExpiration; } - set - { - var nullable = value; - if ((nullable.HasValue ? (nullable.GetValueOrDefault() <= TimeSpan.Zero ? 1 : 0) : 0) != 0) - { - throw new ArgumentOutOfRangeException(nameof(this.SlidingExpiration), value, "The sliding expiration value must be positive."); - } - - this.slidingExpiration = value; - } - } - - public IList ExpirationTokens - { - get - { - if (this.expirationTokens == null) - { - this.expirationTokens = new List(); - } - - return this.expirationTokens; - } - } - - public IList PostEvictionCallbacks - { - get - { - if (this.postEvictionCallbacks == null) - { - this.postEvictionCallbacks = new List(); - } - - return this.postEvictionCallbacks; - } - } - - public object Key { get; private set; } - - public object Value { get; set; } - - internal DateTimeOffset LastAccessed { get; set; } - - internal EvictionReason EvictionReason { get; private set; } - - internal CacheEntry(object key, Action notifyCacheEntryDisposed, Action notifyCacheOfExpiration) - { - if (key == null) - { - throw new ArgumentNullException(nameof(key)); - } - - if (notifyCacheEntryDisposed == null) - { - throw new ArgumentNullException(nameof(notifyCacheEntryDisposed)); - } - - if (notifyCacheOfExpiration == null) - { - throw new ArgumentNullException(nameof(notifyCacheOfExpiration)); - } - - this.Key = key; - this.notifyCacheEntryDisposed = notifyCacheEntryDisposed; - this.notifyCacheOfExpiration = notifyCacheOfExpiration; - } - - public void Dispose() - { - if (this.added) - { - return; - } - - this.added = true; - this.notifyCacheEntryDisposed(this); - //this.PropagateOptions(_added); - } - - internal bool CheckExpired(DateTimeOffset now) - { - if (!this.isExpired && !this.CheckForExpiredTime(now)) - { - return this.CheckForExpiredTokens(); - } - - return true; - } - - internal void SetExpired(EvictionReason reason) - { - this.EvictionReason = reason; - this.isExpired = true; - this.DetachTokens(); - } - - private bool CheckForExpiredTime(DateTimeOffset now) - { - if (this.absoluteExpiration.HasValue && this.absoluteExpiration.Value <= now) - { - this.SetExpired(EvictionReason.Expired); - return true; - } - - if (this.slidingExpiration.HasValue) - { - var timeSpan = now - this.LastAccessed; - var slidingExpiration = this.slidingExpiration; - if ((slidingExpiration.HasValue ? (timeSpan >= slidingExpiration.GetValueOrDefault() ? 1 : 0) : 0) != 0) - { - this.SetExpired(EvictionReason.Expired); - return true; - } - } - - return false; - } - - internal bool CheckForExpiredTokens() - { - if (this.expirationTokens != null) - { - for (var index = 0; index < this.expirationTokens.Count; ++index) - { - if (this.expirationTokens[index].HasChanged) - { - this.SetExpired(EvictionReason.TokenExpired); - return true; - } - } - } - - return false; - } - - internal void AttachTokens() - { - if (this.expirationTokens == null) - { - return; - } - - lock (this.Lock) - { - for (var i = 0; i < this.expirationTokens.Count; ++i) - { - var expirationToken = this.expirationTokens[i]; - if (expirationToken.ActiveChangeCallbacks) - { - if (this.expirationTokenRegistrations == null) - { - this.expirationTokenRegistrations = new List(1); - } - - this.expirationTokenRegistrations.Add(expirationToken.RegisterChangeCallback(ExpirationCallback, this)); - } - } - } - } - - private static void ExpirationTokensExpired(object obj) - { - Task.Factory.StartNew(state => - { - var cacheEntry = (CacheEntry)state; - cacheEntry.SetExpired(EvictionReason.TokenExpired); - cacheEntry.notifyCacheOfExpiration(cacheEntry); - }, obj, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default); - } - - private void DetachTokens() - { - lock (this.Lock) - { - var tokenRegistrations = this.expirationTokenRegistrations; - if (tokenRegistrations == null) - { - return; - } - - this.expirationTokenRegistrations = null; - foreach (var disposable in tokenRegistrations) - { - disposable.Dispose(); - } - } - } - - internal void InvokeEvictionCallbacks() - { - if (this.postEvictionCallbacks == null) - { - return; - } - - var factory = Task.Factory; - var none = CancellationToken.None; - var scheduler = TaskScheduler.Default; - factory.StartNew(state => InvokeCallbacks((CacheEntry)state), this, none, TaskCreationOptions.DenyChildAttach, scheduler); - } - - private static void InvokeCallbacks(CacheEntry entry) - { - var callbackRegistrationList = Interlocked.Exchange(ref entry.postEvictionCallbacks, null); - if (callbackRegistrationList == null) - { - return; - } - - foreach (var callbackRegistration in callbackRegistrationList) - { - try - { - var evictionCallback = callbackRegistration.EvictionCallback; - var key = entry.Key; - var obj = entry.Value; - var evictionReason = entry.EvictionReason; - var state = callbackRegistration.State; - evictionCallback.Invoke(key, obj, evictionReason, state); - } - catch (Exception ex) - { - Debug.WriteLine($"{ex}"); - } - } - } - - internal void PropagateOptions(CacheEntry? parent) - { - if (parent == null) - { - return; - } - - if (this.expirationTokens != null) - { - lock (this.Lock) - { - lock (parent.Lock) - { - using (var changeTokenEnumerator = this.expirationTokens.GetEnumerator()) - { - while (changeTokenEnumerator.MoveNext()) - { - var changeToken = changeTokenEnumerator.Current; - parent.AddExpirationToken(changeToken); - } - } - } - } - } - - if (!this.absoluteExpiration.HasValue) - { - return; - } - - if (parent.absoluteExpiration.HasValue) - { - var absoluteExpiration = this.absoluteExpiration; - var parentAbsoluteExpiration = parent.absoluteExpiration; - if ((absoluteExpiration.HasValue & parentAbsoluteExpiration.HasValue ? (absoluteExpiration.GetValueOrDefault() < parentAbsoluteExpiration.GetValueOrDefault() ? 1 : 0) : 0) == 0) - { - return; - } - } - - parent.absoluteExpiration = this.absoluteExpiration; - } - } -} \ No newline at end of file diff --git a/HttpClient.Caching/InMemory/CacheEntryExtensions.cs b/HttpClient.Caching/InMemory/CacheEntryExtensions.cs deleted file mode 100644 index b56fdcd..0000000 --- a/HttpClient.Caching/InMemory/CacheEntryExtensions.cs +++ /dev/null @@ -1,140 +0,0 @@ -using System; -using Microsoft.Extensions.Caching.Abstractions; - -namespace Microsoft.Extensions.Caching.InMemory -{ - public static class CacheEntryExtensions - { - /// - /// Sets the priority for keeping the cache entry in the cache during a memory pressure tokened cleanup. - /// - /// - /// - public static ICacheEntry SetPriority(this ICacheEntry entry, CacheItemPriority priority) - { - entry.Priority = priority; - return entry; - } - - /// - /// Expire the cache entry if the given - /// expires. - /// - /// The . - /// - /// The that causes the cache - /// entry to expire. - /// - public static ICacheEntry AddExpirationToken(this ICacheEntry entry, IChangeToken expirationToken) - { - if (expirationToken == null) - { - throw new ArgumentNullException(nameof(expirationToken)); - } - - entry.ExpirationTokens.Add(expirationToken); - return entry; - } - - /// Sets an absolute expiration time, relative to now. - /// - /// - public static ICacheEntry SetAbsoluteExpiration(this ICacheEntry entry, TimeSpan relative) - { - entry.AbsoluteExpirationRelativeToNow = relative; - return entry; - } - - /// Sets an absolute expiration date for the cache entry. - /// - /// - public static ICacheEntry SetAbsoluteExpiration(this ICacheEntry entry, DateTimeOffset absolute) - { - entry.AbsoluteExpiration = absolute; - return entry; - } - - /// - /// Sets how long the cache entry can be inactive (e.g. not accessed) before it will be removed. - /// This will not extend the entry lifetime beyond the absolute expiration (if set). - /// - /// - /// - public static ICacheEntry SetSlidingExpiration(this ICacheEntry entry, TimeSpan offset) - { - entry.SlidingExpiration = offset; - return entry; - } - - /// - /// The given callback will be fired after the cache entry is evicted from the cache. - /// - /// - /// - public static ICacheEntry RegisterPostEvictionCallback(this ICacheEntry entry, PostEvictionDelegate callback) - { - if (callback == null) - { - throw new ArgumentNullException(nameof(callback)); - } - - return entry.RegisterPostEvictionCallback(callback, null); - } - - /// - /// The given callback will be fired after the cache entry is evicted from the cache. - /// - /// - /// - /// - public static ICacheEntry RegisterPostEvictionCallback(this ICacheEntry entry, PostEvictionDelegate callback, object? state) - { - if (callback == null) - { - throw new ArgumentNullException(nameof(callback)); - } - - entry.PostEvictionCallbacks.Add(new PostEvictionCallbackRegistration { EvictionCallback = callback, State = state }); - return entry; - } - - /// Sets the value of the cache entry. - /// - /// - public static ICacheEntry SetValue(this ICacheEntry entry, object value) - { - entry.Value = value; - return entry; - } - - /// - /// Applies the values of an existing to - /// the entry. - /// - /// - /// - public static ICacheEntry SetOptions(this ICacheEntry entry, MemoryCacheEntryOptions options) - { - if (options == null) - { - throw new ArgumentNullException(nameof(options)); - } - - entry.AbsoluteExpiration = options.AbsoluteExpiration; - entry.AbsoluteExpirationRelativeToNow = options.AbsoluteExpirationRelativeToNow; - entry.SlidingExpiration = options.SlidingExpiration; - entry.Priority = options.Priority; - foreach (var expirationToken in options.ExpirationTokens) - { - entry.AddExpirationToken(expirationToken); - } - - foreach (var evictionCallback in options.PostEvictionCallbacks) - { - entry.RegisterPostEvictionCallback(evictionCallback.EvictionCallback, evictionCallback.State); - } - - return entry; - } - } -} \ No newline at end of file diff --git a/HttpClient.Caching/InMemory/IMemoryCache.cs b/HttpClient.Caching/InMemory/IMemoryCache.cs deleted file mode 100644 index 6793350..0000000 --- a/HttpClient.Caching/InMemory/IMemoryCache.cs +++ /dev/null @@ -1,33 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using Microsoft.Extensions.Caching.Abstractions; - -namespace Microsoft.Extensions.Caching.InMemory -{ - /// - /// Represents a local in-memory cache whose values are not serialized. - /// - public interface IMemoryCache : IDisposable - { - int Count { get; } - - /// Gets the item associated with this key if present. - /// An object identifying the requested entry. - /// The located value or null. - /// True if the key was found. - bool TryGetValue(object key, [NotNullWhen(true)] out object? value); - - /// Create or overwrite an entry in the cache. - /// An object identifying the entry. - /// The newly created instance. - ICacheEntry CreateEntry(object key); - - /// Removes the object associated with the given key. - /// An object identifying the entry. - void Remove(object key); - - void Compact(double percentage); - - void Clear(); - } -} \ No newline at end of file diff --git a/HttpClient.Caching/InMemory/IMemoryCacheExtensions.cs b/HttpClient.Caching/InMemory/IMemoryCacheExtensions.cs index cc2b328..fce2eb0 100644 --- a/HttpClient.Caching/InMemory/IMemoryCacheExtensions.cs +++ b/HttpClient.Caching/InMemory/IMemoryCacheExtensions.cs @@ -1,7 +1,8 @@ using System; using System.Threading.Tasks; -using Microsoft.Extensions.Caching.Abstractions; using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Caching.Abstractions; +using Microsoft.Extensions.Caching.Memory; namespace Microsoft.Extensions.Caching.InMemory { @@ -17,7 +18,7 @@ public static bool TryGetCacheData(this IMemoryCache cache, string key, [NotNull try { - if (cache.TryGetValue(key, out byte[]? binaryData)) + if (cache.TryGetValue(key, out var binaryData)) { cacheData = binaryData.Deserialize(); result = true; @@ -71,4 +72,4 @@ public static bool TrySetCacheData(this IMemoryCache cache, string key, CacheDat return result; } } -} \ No newline at end of file +} diff --git a/HttpClient.Caching/InMemory/InMemoryCacheFallbackHandler.cs b/HttpClient.Caching/InMemory/InMemoryCacheFallbackHandler.cs index b291a75..552b900 100644 --- a/HttpClient.Caching/InMemory/InMemoryCacheFallbackHandler.cs +++ b/HttpClient.Caching/InMemory/InMemoryCacheFallbackHandler.cs @@ -3,6 +3,7 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Caching.Abstractions; +using Microsoft.Extensions.Caching.Memory; namespace Microsoft.Extensions.Caching.InMemory { @@ -137,4 +138,4 @@ protected override async Task SendAsync(HttpRequestMessage return null; } } -} \ No newline at end of file +} diff --git a/HttpClient.Caching/InMemory/InMemoryCacheHandler.cs b/HttpClient.Caching/InMemory/InMemoryCacheHandler.cs index fc34694..fa003de 100644 --- a/HttpClient.Caching/InMemory/InMemoryCacheHandler.cs +++ b/HttpClient.Caching/InMemory/InMemoryCacheHandler.cs @@ -3,6 +3,7 @@ using System.Net.Http; using System.Net.Http.Headers; using Microsoft.Extensions.Caching.Abstractions; +using Microsoft.Extensions.Caching.Memory; namespace Microsoft.Extensions.Caching.InMemory { @@ -264,4 +265,4 @@ private bool TryGetCachedHttpResponseMessage(HttpRequestMessage request, string return false; } } -} \ No newline at end of file +} diff --git a/HttpClient.Caching/InMemory/Internal/ISystemClock.cs b/HttpClient.Caching/InMemory/Internal/ISystemClock.cs deleted file mode 100644 index 9b3e320..0000000 --- a/HttpClient.Caching/InMemory/Internal/ISystemClock.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System; - -namespace Microsoft.Extensions.Caching.InMemory.Internal -{ - /// Abstracts the system clock to facilitate testing. - public interface ISystemClock - { - /// Retrieves the current system time in UTC. - DateTimeOffset UtcNow { get; } - } -} \ No newline at end of file diff --git a/HttpClient.Caching/InMemory/Internal/SystemClock.cs b/HttpClient.Caching/InMemory/Internal/SystemClock.cs deleted file mode 100644 index 3133403..0000000 --- a/HttpClient.Caching/InMemory/Internal/SystemClock.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.ComponentModel; - -namespace Microsoft.Extensions.Caching.InMemory.Internal -{ - /// Provides access to the normal system clock. - [EditorBrowsable(EditorBrowsableState.Never)] - public class SystemClock : ISystemClock - { - /// Retrieves the current system time in UTC. - public DateTimeOffset UtcNow => DateTimeOffset.UtcNow; - } -} \ No newline at end of file diff --git a/HttpClient.Caching/InMemory/MemoryCache.cs b/HttpClient.Caching/InMemory/MemoryCache.cs deleted file mode 100644 index 37c277f..0000000 --- a/HttpClient.Caching/InMemory/MemoryCache.cs +++ /dev/null @@ -1,333 +0,0 @@ -using System.Collections.Concurrent; -using System.Diagnostics.CodeAnalysis; -using Microsoft.Extensions.Caching.Abstractions; -using Microsoft.Extensions.Caching.InMemory.Internal; - -namespace Microsoft.Extensions.Caching.InMemory -{ - public class MemoryCache : IMemoryCache - { - private readonly ConcurrentDictionary entries; - private readonly Action setEntry; - private readonly Action entryExpirationNotification; - private readonly ISystemClock clock; - private readonly TimeSpan expirationScanFrequency; - - private DateTimeOffset lastExpirationScan; - private bool disposed; - - public int Count => this.entries.Count; - - private ICollection> EntriesCollection => this.entries; - - public MemoryCache() : this(new MemoryCacheOptions()) - { - } - - public MemoryCache(MemoryCacheOptions memoryCacheOptions) - { - if (memoryCacheOptions == null) - { - throw new ArgumentNullException(nameof(memoryCacheOptions)); - } - - this.entries = new ConcurrentDictionary(); - this.setEntry = this.SetEntry; - this.entryExpirationNotification = this.EntryExpired; - this.clock = memoryCacheOptions.Clock ?? new SystemClock(); - this.expirationScanFrequency = memoryCacheOptions.ExpirationScanFrequency; - this.lastExpirationScan = this.clock.UtcNow; - } - - ~MemoryCache() - { - this.Dispose(false); - } - - public ICacheEntry CreateEntry(object key) - { - this.CheckDisposed(); - return new CacheEntry(key, this.setEntry, this.entryExpirationNotification); - } - - private void SetEntry(CacheEntry entry) - { - if (this.disposed) - { - return; - } - - var utcNow = this.clock.UtcNow; - var nullable = new DateTimeOffset?(); - if (entry.absoluteExpirationRelativeToNow.HasValue) - { - var dateTimeOffset = utcNow; - var expirationRelativeToNow = entry.absoluteExpirationRelativeToNow; - nullable = expirationRelativeToNow.HasValue ? dateTimeOffset + expirationRelativeToNow.GetValueOrDefault() : new DateTimeOffset?(); - } - else if (entry.absoluteExpiration.HasValue) - { - nullable = entry.absoluteExpiration; - } - - if (nullable.HasValue && (!entry.absoluteExpiration.HasValue || nullable.Value < entry.absoluteExpiration.Value)) - { - entry.absoluteExpiration = nullable; - } - - entry.LastAccessed = utcNow; - if (this.entries.TryGetValue(entry.Key, out var cacheEntry)) - { - cacheEntry.SetExpired(EvictionReason.Replaced); - } - - if (!entry.CheckExpired(utcNow)) - { - bool flag; - if (cacheEntry == null) - { - flag = this.entries.TryAdd(entry.Key, entry); - } - else - { - flag = this.entries.TryUpdate(entry.Key, entry, cacheEntry); - if (!flag) - { - flag = this.entries.TryAdd(entry.Key, entry); - } - } - - if (flag) - { - entry.AttachTokens(); - } - else - { - entry.SetExpired((EvictionReason)2); - entry.InvokeEvictionCallbacks(); - } - - cacheEntry?.InvokeEvictionCallbacks(); - } - else - { - entry.InvokeEvictionCallbacks(); - if (cacheEntry != null) - { - this.RemoveEntry(cacheEntry); - } - } - - this.StartScanForExpiredItems(); - } - - public bool TryGetValue(object key, [NotNullWhen(true)] out object? result) - { - if (key == null) - { - throw new ArgumentNullException(nameof(key)); - } - - this.CheckDisposed(); - result = null; - var utcNow = this.clock.UtcNow; - var flag = false; - if (this.entries.TryGetValue(key, out var entry)) - { - if (entry.CheckExpired(utcNow) && entry.EvictionReason != EvictionReason.Replaced) - { - this.RemoveEntry(entry); - } - else - { - flag = true; - entry.LastAccessed = utcNow; - result = entry.Value; - //entry.PropagateOptions(CacheEntryHelper.Current); - } - } - - this.StartScanForExpiredItems(); - return flag; - } - - public void Remove(object key) - { - if (key == null) - { - throw new ArgumentNullException(nameof(key)); - } - - this.CheckDisposed(); - if (this.entries.TryRemove(key, out var cacheEntry)) - { - cacheEntry.SetExpired(EvictionReason.Removed); - cacheEntry.InvokeEvictionCallbacks(); - } - - this.StartScanForExpiredItems(); - } - - public void Clear() - { - this.CheckDisposed(); - var keys = this.entries.Keys.ToList(); - foreach (var key in keys) - { - if (this.entries.TryRemove(key, out var cacheEntry)) - { - cacheEntry.SetExpired(EvictionReason.Removed); - cacheEntry.InvokeEvictionCallbacks(); - } - } - - this.StartScanForExpiredItems(); - } - - private void RemoveEntry(CacheEntry entry) - { - if (!this.EntriesCollection.Remove(new KeyValuePair(entry.Key, entry))) - { - return; - } - - entry.InvokeEvictionCallbacks(); - } - - private void EntryExpired(CacheEntry entry) - { - this.RemoveEntry(entry); - this.StartScanForExpiredItems(); - } - - private void StartScanForExpiredItems() - { - var utcNow = this.clock.UtcNow; - if (!(this.expirationScanFrequency < utcNow - this.lastExpirationScan)) - { - return; - } - - this.lastExpirationScan = utcNow; - var factory = Task.Factory; - var none = CancellationToken.None; - var scheduler = TaskScheduler.Default; - factory.StartNew(state => ScanForExpiredItems((MemoryCache)state!), this, none, TaskCreationOptions.DenyChildAttach, scheduler); - } - - private static void ScanForExpiredItems(MemoryCache cache) - { - var utcNow = cache.clock.UtcNow; - foreach (var entry in cache.entries.Values) - { - if (entry.CheckExpired(utcNow)) - { - cache.RemoveEntry(entry); - } - } - } - - public void Compact(double percentage) - { - var entriesToRemove = new List(); - var priorityEntries1 = new List(); - var priorityEntries2 = new List(); - var priorityEntries3 = new List(); - var utcNow = this.clock.UtcNow; - - foreach (var cacheEntry in this.entries.Values) - { - if (cacheEntry.CheckExpired(utcNow)) - { - entriesToRemove.Add(cacheEntry); - } - else - { - switch ((int)cacheEntry.Priority) - { - case 0: - priorityEntries1.Add(cacheEntry); - continue; - case 1: - priorityEntries2.Add(cacheEntry); - continue; - case 2: - priorityEntries3.Add(cacheEntry); - continue; - case 3: - continue; - default: - throw new NotSupportedException("Not implemented: " + cacheEntry.Priority); - } - } - } - - var removalCountTarget = (int)(this.entries.Count * percentage); - this.ExpirePriorityBucket(removalCountTarget, entriesToRemove, priorityEntries1); - this.ExpirePriorityBucket(removalCountTarget, entriesToRemove, priorityEntries2); - this.ExpirePriorityBucket(removalCountTarget, entriesToRemove, priorityEntries3); - foreach (var entry in entriesToRemove) - { - this.RemoveEntry(entry); - } - } - - private void ExpirePriorityBucket(int removalCountTarget, List entriesToRemove, List priorityEntries) - { - if (removalCountTarget <= entriesToRemove.Count) - { - return; - } - - if (entriesToRemove.Count + priorityEntries.Count <= removalCountTarget) - { - foreach (var priorityEntry in priorityEntries) - { - priorityEntry.SetExpired(EvictionReason.Capacity); - } - - entriesToRemove.AddRange(priorityEntries); - } - else - { - foreach (var cacheEntry in priorityEntries.OrderBy(entry => entry.LastAccessed)) - { - cacheEntry.SetExpired(EvictionReason.Capacity); - entriesToRemove.Add(cacheEntry); - if (removalCountTarget <= entriesToRemove.Count) - { - break; - } - } - } - } - - public void Dispose() - { - this.Dispose(true); - } - - protected virtual void Dispose(bool disposing) - { - if (this.disposed) - { - return; - } - - if (disposing) - { - GC.SuppressFinalize(this); - } - - this.disposed = true; - } - - private void CheckDisposed() - { - if (this.disposed) - { - throw new ObjectDisposedException(typeof(MemoryCache).FullName); - } - } - } -} \ No newline at end of file diff --git a/HttpClient.Caching/InMemory/MemoryCacheEntryOptions.cs b/HttpClient.Caching/InMemory/MemoryCacheEntryOptions.cs deleted file mode 100644 index 6f05f24..0000000 --- a/HttpClient.Caching/InMemory/MemoryCacheEntryOptions.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using System.Collections.Generic; -using Microsoft.Extensions.Caching.Abstractions; - -namespace Microsoft.Extensions.Caching.InMemory -{ - public class MemoryCacheEntryOptions - { - private TimeSpan? absoluteExpirationRelativeToNow; - private TimeSpan? slidingExpiration; - - /// - /// Gets the instances which cause the cache - /// entry to - /// expire. - /// - public IList ExpirationTokens { get; } = new List(); - - /// - /// Gets or sets the callbacks will be fired after the cache entry is evicted from the cache. - /// - public IList PostEvictionCallbacks { get; } = new List(); - - /// - /// Gets or sets the priority for keeping the cache entry in the cache during a - /// memory pressure triggered cleanup. The default is - /// . - /// - public CacheItemPriority Priority { get; set; } = CacheItemPriority.Normal; - - /// - /// Gets or sets an absolute expiration date for the cache entry. - /// - public DateTimeOffset? AbsoluteExpiration { get; set; } - - /// - /// Gets or sets an absolute expiration time, relative to now. - /// - public TimeSpan? AbsoluteExpirationRelativeToNow - { - get { return this.absoluteExpirationRelativeToNow; } - set - { - var nullable = value; - var zero = TimeSpan.Zero; - if ((nullable.HasValue ? (nullable.GetValueOrDefault() <= zero ? 1 : 0) : 0) != 0) - { - throw new ArgumentOutOfRangeException(nameof(this.AbsoluteExpirationRelativeToNow), value, "The relative expiration value must be positive."); - } - - this.absoluteExpirationRelativeToNow = value; - } - } - - /// - /// Gets or sets how long a cache entry can be inactive (e.g. not accessed) before it will be removed. - /// This will not extend the entry lifetime beyond the absolute expiration (if set). - /// - public TimeSpan? SlidingExpiration - { - get => this.slidingExpiration; - set - { - var nullable = value; - var zero = TimeSpan.Zero; - if ((nullable.HasValue ? (nullable.GetValueOrDefault() <= zero ? 1 : 0) : 0) != 0) - { - throw new ArgumentOutOfRangeException(nameof(this.SlidingExpiration), value, "The sliding expiration value must be positive."); - } - - this.slidingExpiration = value; - } - } - } -} \ No newline at end of file diff --git a/HttpClient.Caching/InMemory/MemoryCacheOptions.cs b/HttpClient.Caching/InMemory/MemoryCacheOptions.cs deleted file mode 100644 index 94c646b..0000000 --- a/HttpClient.Caching/InMemory/MemoryCacheOptions.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Microsoft.Extensions.Caching.InMemory.Internal; - -namespace Microsoft.Extensions.Caching.InMemory -{ - public class MemoryCacheOptions - { - public TimeSpan ExpirationScanFrequency { get; set; } = TimeSpan.FromMinutes(1.0); - - public ISystemClock? Clock { get; set; } - } -} \ No newline at end of file diff --git a/HttpClient.Caching/Internals/Nullable.cs b/HttpClient.Caching/Internals/Nullable.cs index 8954a29..10a8f66 100644 --- a/HttpClient.Caching/Internals/Nullable.cs +++ b/HttpClient.Caching/Internals/Nullable.cs @@ -1,4 +1,4 @@ -#if NET48_OR_GREATER || NETSTANDARD1_2 || NETSTANDARD2_0 +#if NET462_OR_GREATER || NETSTANDARD1_2 || NETSTANDARD2_0 // ReSharper disable once CheckNamespace namespace System.Diagnostics.CodeAnalysis { diff --git a/README.md b/README.md index 07bcb27..2f00baf 100644 --- a/README.md +++ b/README.md @@ -7,19 +7,19 @@ Use the following command to install HttpClient.Caching using NuGet package mana PM> Install-Package HttpClient.Caching -You can use this library in any .Net project which is compatible to .Net Framework 4.5+ and .Net Standard 1.2+ (e.g. Xamarin Android, iOS, Universal Windows Platform, etc.) +You can use this library in projects targeting `.NET Framework 4.6.2`, `.NET Standard 2.0`, `.NET 8.0` and later. ### The Purpose of HTTP Caching HTTP Caching affects both involved communication peers, the client and the server. On the server-side, caching is appropriate for improving throughput (scalability). HTTP caching doesn't make a single HTTP call faster but it can lead to better response performance in high-load scenarios. On the client-side, caching is used to avoid unnecessarily repetitiv HTTP calls. This leads to less waiting time on the client-side since cache reads have naturally a much better response performance than HTTP calls over relatively slow network links. ### API Usage #### Using MemoryCache -Declare IMemoryCache in your API service, either by creating an instance manually or by injecting IMemoryCache into your API service class. +Declare `IMemoryCache` in your API service, either by creating an instance manually or by injecting `IMemoryCache` into your API service class. ```C# -private readonly IMemoryCache memoryCache = new MemoryCache(); +private readonly IMemoryCache memoryCache = new MemoryCache(new MemoryCacheOptions()); ``` -Following example show how IMemoryCache can be used to store an HTTP GET result in memory for a given time span (cacheExpirection): +Following example shows how `IMemoryCache` can be used to store an HTTP GET result in memory for a given time span (`cacheExpiration`): ```C# public async Task GetAsync(string uri, TimeSpan? cacheExpiration = null) { @@ -55,7 +55,7 @@ public async Task GetAsync(string uri, TimeSpan? cacheExpirati ``` #### Using InMemoryCacheHandler -HttpClient allows to inject a custom http handler. In the follwing example, we inject an HttpClientHandler which is nested into an InMemoryCacheHandler where the InMemoryCacheHandler is responsible for maintaining and reading the cache. +`HttpClient` allows injecting a custom HTTP handler. In the following example, an `HttpClientHandler` is nested into an `InMemoryCacheHandler`, and the `InMemoryCacheHandler` is responsible for maintaining and reading the cache. ```C# static void Main(string[] args) { @@ -110,9 +110,9 @@ TotalRequests: 5 ### Cache keys By default, requests will be cached by using a key which is composed with http method and url (only HEAD and GET http methods are supported). -If this default behavior isn't enough **you can implement your own ICacheKeyProvider** wich provides **cache key** starting **from HttpRequestMessage**. +If this default behavior isn't enough, you can implement your own `ICacheKeysProvider`, which builds a cache key from an `HttpRequestMessage`. -The following example show how use a cache provider of type MethodUriHeadersCacheKeysProvider. +The following example shows how to use a cache provider of type `MethodUriHeadersCacheKeysProvider`. This cache key provider is already implemented and evaluates http method, specified headers and url to compose a cache key. with InMemoryCacheHandler. ```C# @@ -122,7 +122,7 @@ static void Main(string[] args) var httpClientHandler = new HttpClientHandler(); var cacheExpirationPerHttpResponseCode = CacheExpirationProvider.CreateSimple(TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(5)); - // this is a CacheKeyProvider which evaluates http method, specified headers and url to compose a key + // this cache key provider evaluates http method, specified headers and url to compose a key var cacheKeyProvider = new MethodUriHeadersCacheKeysProvider(new string[] { "FIRST-HEADER", "SECOND-HEADER" }); var handler = new InMemoryCacheHandler( innerHandler: httpClientHandler, diff --git a/ReleaseNotes.txt b/ReleaseNotes.txt index 2cf4915..d35f4af 100644 --- a/ReleaseNotes.txt +++ b/ReleaseNotes.txt @@ -1,5 +1,6 @@ 2.0 - Replace Newtonsoft.Json with System.Text.Json. +- Replace MemoryCache implementation with Microsoft.Extensions.Caching.Memory. - Drop support for netstandard 1.2. - Maintenance updates. diff --git a/Samples/ConsoleAppSample/Program.cs b/Samples/ConsoleAppSample/Program.cs index 4b25a2b..de58ab4 100644 --- a/Samples/ConsoleAppSample/Program.cs +++ b/Samples/ConsoleAppSample/Program.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Microsoft.Extensions.Caching.Abstractions; using Microsoft.Extensions.Caching.InMemory; +using Microsoft.Extensions.Caching.Memory; namespace ConsoleAppSample { @@ -59,4 +60,4 @@ private static async Task Main(string[] args) Console.ReadKey(); } } -} \ No newline at end of file +} diff --git a/Tests/HttpClient.Caching.Tests/HttpClient.Caching.Tests.csproj b/Tests/HttpClient.Caching.Tests/HttpClient.Caching.Tests.csproj index 42e8e0f..ab7d04f 100644 --- a/Tests/HttpClient.Caching.Tests/HttpClient.Caching.Tests.csproj +++ b/Tests/HttpClient.Caching.Tests/HttpClient.Caching.Tests.csproj @@ -1,7 +1,7 @@  - net48;net10.0 + net462;net10.0 enable latest enable @@ -15,7 +15,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -23,7 +23,7 @@ - + diff --git a/Tests/HttpClient.Caching.Tests/InMemory/InMemoryCacheFallbackHandlerTests.cs b/Tests/HttpClient.Caching.Tests/InMemory/InMemoryCacheFallbackHandlerTests.cs index 9483887..c029de8 100644 --- a/Tests/HttpClient.Caching.Tests/InMemory/InMemoryCacheFallbackHandlerTests.cs +++ b/Tests/HttpClient.Caching.Tests/InMemory/InMemoryCacheFallbackHandlerTests.cs @@ -1,16 +1,16 @@ -using System; -using System.Net; +using System.Net; using System.Net.Http; -using System.Threading.Tasks; using FluentAssertions; using HttpClient.Caching.Tests.TestData; -using Microsoft.Extensions.Caching.Abstractions; using Microsoft.Extensions.Caching.InMemory; +using Microsoft.Extensions.Caching.Memory; using Moq; using Xunit; namespace HttpClient.Caching.Tests.InMemory { + using HttpClient = System.Net.Http.HttpClient; + public class InMemoryCacheFallbackHandlerTests { private readonly string url = "http://unittest/"; @@ -18,31 +18,31 @@ public class InMemoryCacheFallbackHandlerTests [Fact] public async Task AlwaysCallsTheHttpHandler() { - // setup + // Arrange var testMessageHandler = new TestMessageHandler(); var cache = new MemoryCache(new MemoryCacheOptions()); - var client = new System.Net.Http.HttpClient(new InMemoryCacheFallbackHandler(testMessageHandler, TimeSpan.FromDays(1), TimeSpan.FromDays(1), null, cache)); + var client = new HttpClient(new InMemoryCacheFallbackHandler(testMessageHandler, TimeSpan.FromDays(1), TimeSpan.FromDays(1), null, cache)); - // execute twice + // Act twice await client.GetAsync(this.url); cache.Get(InMemoryCacheFallbackHandler.CacheFallbackKeyPrefix + HttpMethod.Get + this.url).Should().NotBeNull(); // ensure it's cached before the 2nd call await client.GetAsync(this.url); - // validate + // Assert testMessageHandler.NumberOfCalls.Should().Be(2); } [Fact] public async Task AlwaysUpdatesTheCacheOnSuccess() { - // setup + // Arrange var testMessageHandler = new TestMessageHandler(); var cache = new Mock(MockBehavior.Strict); var cacheTime = TimeSpan.FromSeconds(123); cache.Setup(c => c.CreateEntry(InMemoryCacheFallbackHandler.CacheFallbackKeyPrefix + HttpMethod.Get + this.url)); - var client = new System.Net.Http.HttpClient(new InMemoryCacheFallbackHandler(testMessageHandler, TimeSpan.FromDays(1), cacheTime, null, cache.Object)); + var client = new HttpClient(new InMemoryCacheFallbackHandler(testMessageHandler, TimeSpan.FromDays(1), cacheTime, null, cache.Object)); - // execute twice, validate cache is called each time + // Act twice, validate cache is called each time await client.GetAsync(this.url); cache.Verify(c => c.CreateEntry(InMemoryCacheFallbackHandler.CacheFallbackKeyPrefix + HttpMethod.Get + this.url), Times.Once); await client.GetAsync(this.url); @@ -52,15 +52,15 @@ public async Task AlwaysUpdatesTheCacheOnSuccess() [Fact] public async Task UpdatesTheCacheForHeadAndGetIndependently() { - // setup + // Arrange var testMessageHandler = new TestMessageHandler(); var cache = new Mock(MockBehavior.Strict); var cacheTime = TimeSpan.FromSeconds(123); cache.Setup(c => c.CreateEntry(InMemoryCacheFallbackHandler.CacheFallbackKeyPrefix + HttpMethod.Get + this.url)); cache.Setup(c => c.CreateEntry(InMemoryCacheFallbackHandler.CacheFallbackKeyPrefix + HttpMethod.Head + this.url)); - var client = new System.Net.Http.HttpClient(new InMemoryCacheFallbackHandler(testMessageHandler, TimeSpan.FromDays(1), cacheTime, null, cache.Object)); + var client = new HttpClient(new InMemoryCacheFallbackHandler(testMessageHandler, TimeSpan.FromDays(1), cacheTime, null, cache.Object)); - // execute twice, validate cache is called each time + // Act twice, validate cache is called each time await client.SendAsync(new HttpRequestMessage(HttpMethod.Head, this.url)); await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, this.url)); cache.Verify(c => c.CreateEntry(InMemoryCacheFallbackHandler.CacheFallbackKeyPrefix + HttpMethod.Head + this.url), Times.Once); @@ -70,56 +70,56 @@ public async Task UpdatesTheCacheForHeadAndGetIndependently() [Fact] public async Task NeverUpdatesTheCacheOnFailure() { - // setup + // Arrange var testMessageHandler = new TestMessageHandler(HttpStatusCode.InternalServerError); var cache = new Mock(MockBehavior.Strict); var cacheTime = TimeSpan.FromSeconds(123); - object expectedValue; + object? expectedValue; cache.Setup(c => c.CreateEntry(It.IsAny())); cache.Setup(c => c.TryGetValue(this.url, out expectedValue)).Returns(false); - var client = new System.Net.Http.HttpClient(new InMemoryCacheFallbackHandler(testMessageHandler, TimeSpan.FromDays(1), cacheTime, null, cache.Object)); + var client = new HttpClient(new InMemoryCacheFallbackHandler(testMessageHandler, TimeSpan.FromDays(1), cacheTime, null, cache.Object)); - // execute + // Act await client.GetAsync(this.url); - // validate + // Assert cache.Verify(c => c.CreateEntry(It.IsAny()), Times.Never); } [Fact] public async Task TriesToAccessCacheOnFailureButReturnsErrorIfNotInCache() { - // setup + // Arrange var testMessageHandler = new TestMessageHandler(HttpStatusCode.InternalServerError); var cache = new Mock(MockBehavior.Strict); var cacheTime = TimeSpan.FromSeconds(123); - object expectedValue; + object? expectedValue; cache.Setup(c => c.TryGetValue(this.url, out expectedValue)).Returns(false); - var client = new System.Net.Http.HttpClient(new InMemoryCacheFallbackHandler(testMessageHandler, TimeSpan.FromDays(1), cacheTime, null, cache.Object)); + var client = new HttpClient(new InMemoryCacheFallbackHandler(testMessageHandler, TimeSpan.FromDays(1), cacheTime, null, cache.Object)); - // execute + // Act var result = await client.GetAsync(this.url); - // validate + // Assert result.StatusCode.Should().Be(HttpStatusCode.InternalServerError); } [Fact] public async Task GetsItFromTheHttpCallAfterBeingInCache() { - // setup + // Arrange var testMessageHandler1 = new TestMessageHandler(content: "message-1", delay: TimeSpan.FromMilliseconds(100)); var testMessageHandler2 = new TestMessageHandler(content: "message-2"); var cache = new MemoryCache(new MemoryCacheOptions()); - var client1 = new System.Net.Http.HttpClient(new InMemoryCacheFallbackHandler(testMessageHandler1, TimeSpan.FromMilliseconds(1), TimeSpan.FromDays(1), null, cache)); - var client2 = new System.Net.Http.HttpClient(new InMemoryCacheFallbackHandler(testMessageHandler2, TimeSpan.FromMilliseconds(1), TimeSpan.FromDays(1), null, cache)); + var client1 = new HttpClient(new InMemoryCacheFallbackHandler(testMessageHandler1, TimeSpan.FromMilliseconds(1), TimeSpan.FromDays(1), null, cache)); + var client2 = new HttpClient(new InMemoryCacheFallbackHandler(testMessageHandler2, TimeSpan.FromMilliseconds(1), TimeSpan.FromDays(1), null, cache)); - // execute twice + // Act twice var result1 = await client1.GetAsync(this.url); cache.Get(InMemoryCacheFallbackHandler.CacheFallbackKeyPrefix + HttpMethod.Get + this.url).Should().NotBeNull(); var result2 = await client2.GetAsync(this.url); - // validate + // Assert // - that each message handler got called testMessageHandler1.NumberOfCalls.Should().Be(1); testMessageHandler2.NumberOfCalls.Should().Be(1); @@ -134,18 +134,18 @@ public async Task GetsItFromTheHttpCallAfterBeingInCache() [Fact] public async Task GetsItFromTheCacheWhenUnsuccessful() { - // setup + // Arrange var testMessageHandler1 = new TestMessageHandler(HttpStatusCode.OK, "message-1"); var testMessageHandler2 = new TestMessageHandler(HttpStatusCode.InternalServerError, "message-2"); var cache = new MemoryCache(new MemoryCacheOptions()); - var client1 = new System.Net.Http.HttpClient(new InMemoryCacheFallbackHandler(testMessageHandler1, TimeSpan.FromDays(1), TimeSpan.FromDays(1), null, cache)); - var client2 = new System.Net.Http.HttpClient(new InMemoryCacheFallbackHandler(testMessageHandler2, TimeSpan.FromDays(1), TimeSpan.FromDays(1), null, cache)); + var client1 = new HttpClient(new InMemoryCacheFallbackHandler(testMessageHandler1, TimeSpan.FromDays(1), TimeSpan.FromDays(1), null, cache)); + var client2 = new HttpClient(new InMemoryCacheFallbackHandler(testMessageHandler2, TimeSpan.FromDays(1), TimeSpan.FromDays(1), null, cache)); - // execute twice + // Act twice var result1 = await client1.GetAsync(this.url); var result2 = await client2.GetAsync(this.url); - // validate + // Assert // - that each message handler got called testMessageHandler1.NumberOfCalls.Should().Be(1); testMessageHandler2.NumberOfCalls.Should().Be(1); @@ -157,4 +157,4 @@ public async Task GetsItFromTheCacheWhenUnsuccessful() data2.Should().BeEquivalentTo(data1); } } -} \ No newline at end of file +} diff --git a/Tests/HttpClient.Caching.Tests/InMemory/InMemoryCacheHandlerTests.cs b/Tests/HttpClient.Caching.Tests/InMemory/InMemoryCacheHandlerTests.cs index 1e61731..94e3b2e 100644 --- a/Tests/HttpClient.Caching.Tests/InMemory/InMemoryCacheHandlerTests.cs +++ b/Tests/HttpClient.Caching.Tests/InMemory/InMemoryCacheHandlerTests.cs @@ -8,6 +8,7 @@ using FluentAssertions; using HttpClient.Caching.Tests.TestData; using Microsoft.Extensions.Caching.InMemory; +using Microsoft.Extensions.Caching.Memory; using Xunit; namespace HttpClient.Caching.Tests.InMemory @@ -414,6 +415,8 @@ public async Task ReturnsResponseHeader() var response = await client.GetAsync("http://unittest"); // Assert + response.Content.Headers.Should().NotBeNull(); + response.Content.Headers.ContentType.Should().NotBeNull(); response.Content.Headers.ContentType.MediaType.Should().Be("text/plain"); response.Content.Headers.ContentType.CharSet.Should().Be("utf-8"); } @@ -481,4 +484,4 @@ public async Task InvalidatesCachePerMethod() testMessageHandler.NumberOfCalls.Should().Be(3); } } -} \ No newline at end of file +} diff --git a/Tests/HttpClient.Caching.Tests/MemoryCacheTests.cs b/Tests/HttpClient.Caching.Tests/MemoryCacheTests.cs index 0b04984..234cc37 100644 --- a/Tests/HttpClient.Caching.Tests/MemoryCacheTests.cs +++ b/Tests/HttpClient.Caching.Tests/MemoryCacheTests.cs @@ -1,8 +1,8 @@ using System; using FluentAssertions; using HttpClient.Caching.Tests.TestData; -using Microsoft.Extensions.Caching.Abstractions; using Microsoft.Extensions.Caching.InMemory; +using Microsoft.Extensions.Caching.Memory; using Xunit; using Xunit.Abstractions; @@ -39,4 +39,4 @@ public void ShouldSetCache() cache.Count.Should().Be(10); } } -} \ No newline at end of file +} diff --git a/Tests/HttpClient.Caching.Tests/Testdata/TestMessageHandler.cs b/Tests/HttpClient.Caching.Tests/Testdata/TestMessageHandler.cs index 74797e6..5eec06c 100644 --- a/Tests/HttpClient.Caching.Tests/Testdata/TestMessageHandler.cs +++ b/Tests/HttpClient.Caching.Tests/Testdata/TestMessageHandler.cs @@ -1,10 +1,7 @@ -using System; -using System.Net; +using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Text; -using System.Threading; -using System.Threading.Tasks; namespace HttpClient.Caching.Tests.TestData { @@ -17,8 +14,8 @@ internal class TestMessageHandler : HttpMessageHandler private readonly HttpStatusCode responseStatusCode; private readonly string content; private readonly string contentType; - private readonly TimeSpan delay; - private readonly CacheControlHeaderValue cacheControl; + private readonly TimeSpan? delay; + private readonly CacheControlHeaderValue? cacheControl; private readonly Encoding encoding; public int NumberOfCalls { get; set; } @@ -27,9 +24,9 @@ public TestMessageHandler( HttpStatusCode responseStatusCode = DefaultResponseStatusCode, string content = DefaultContent, string contentType = DefaultContentType, - Encoding encoding = null, - TimeSpan delay = default, - CacheControlHeaderValue cacheControl = null) + Encoding? encoding = null, + TimeSpan? delay = null, + CacheControlHeaderValue? cacheControl = null) { this.responseStatusCode = responseStatusCode; this.content = content; @@ -59,9 +56,9 @@ protected override async Task SendAsync(HttpRequestMessage { this.NumberOfCalls++; - if (this.delay != default) + if (this.delay is TimeSpan delay) { - await Task.Delay(this.delay, cancellationToken); + await Task.Delay(delay, cancellationToken); } return this.CreateHttpResponseMessage(); @@ -72,9 +69,9 @@ protected override HttpResponseMessage Send(HttpRequestMessage request, Cancella { this.NumberOfCalls++; - if (this.delay != default) + if (this.delay is TimeSpan delay) { - Thread.Sleep(this.delay); + Thread.Sleep(delay); } return this.CreateHttpResponseMessage(); From b1e773502457ab9b4682bc4c720332304f7d235a Mon Sep 17 00:00:00 2001 From: Thomas Galliker Date: Mon, 6 Apr 2026 17:20:11 +0200 Subject: [PATCH 15/19] Fix namespace names --- .../DefaultCacheKeysProvider.cs | 5 ++--- .../{InMemory => Memory}/IMemoryCacheExtensions.cs | 13 +++++-------- .../InMemoryCacheFallbackHandler.cs | 8 ++------ .../{InMemory => Memory}/InMemoryCacheHandler.cs | 3 +-- .../MethodUriHeadersCacheKeysProvider.cs | 2 +- Samples/ConsoleAppSample/Program.cs | 1 - .../InMemory/DefaultCacheKeysProviderTests.cs | 2 +- .../InMemory/InMemoryCacheFallbackHandlerTests.cs | 1 - .../InMemory/InMemoryCacheHandlerTests.cs | 6 +----- .../MethodUriHeadersCacheKeysProviderTests.cs | 2 +- Tests/HttpClient.Caching.Tests/MemoryCacheTests.cs | 4 +--- 11 files changed, 15 insertions(+), 32 deletions(-) rename HttpClient.Caching/{InMemory => Memory}/DefaultCacheKeysProvider.cs (92%) rename HttpClient.Caching/{InMemory => Memory}/IMemoryCacheExtensions.cs (82%) rename HttpClient.Caching/{InMemory => Memory}/InMemoryCacheFallbackHandler.cs (97%) rename HttpClient.Caching/{InMemory => Memory}/InMemoryCacheHandler.cs (99%) rename HttpClient.Caching/{InMemory => }/MethodUriHeadersCacheKeysProvider.cs (98%) diff --git a/HttpClient.Caching/InMemory/DefaultCacheKeysProvider.cs b/HttpClient.Caching/Memory/DefaultCacheKeysProvider.cs similarity index 92% rename from HttpClient.Caching/InMemory/DefaultCacheKeysProvider.cs rename to HttpClient.Caching/Memory/DefaultCacheKeysProvider.cs index 73c471b..d74997c 100644 --- a/HttpClient.Caching/InMemory/DefaultCacheKeysProvider.cs +++ b/HttpClient.Caching/Memory/DefaultCacheKeysProvider.cs @@ -1,9 +1,8 @@ -using System; -using System.Net.Http; +using System.Net.Http; using System.Text; using Microsoft.Extensions.Caching.Abstractions; -namespace Microsoft.Extensions.Caching.InMemory +namespace Microsoft.Extensions.Caching.Memory { /// /// Provides keys to store or retrieve data in the cache in the default way (http method + http request Uri) diff --git a/HttpClient.Caching/InMemory/IMemoryCacheExtensions.cs b/HttpClient.Caching/Memory/IMemoryCacheExtensions.cs similarity index 82% rename from HttpClient.Caching/InMemory/IMemoryCacheExtensions.cs rename to HttpClient.Caching/Memory/IMemoryCacheExtensions.cs index fce2eb0..495a009 100644 --- a/HttpClient.Caching/InMemory/IMemoryCacheExtensions.cs +++ b/HttpClient.Caching/Memory/IMemoryCacheExtensions.cs @@ -1,26 +1,23 @@ -using System; -using System.Threading.Tasks; -using System.Diagnostics.CodeAnalysis; +using System.Diagnostics.CodeAnalysis; using Microsoft.Extensions.Caching.Abstractions; -using Microsoft.Extensions.Caching.Memory; -namespace Microsoft.Extensions.Caching.InMemory +namespace Microsoft.Extensions.Caching.Memory { /// /// Extension methods for an . /// internal static class IMemoryCacheExtensions { - public static bool TryGetCacheData(this IMemoryCache cache, string key, [NotNullWhen(true)] out CacheData? cacheData) + public static bool TryGetCacheData(this IMemoryCache memoryCache, string key, [NotNullWhen(true)] out CacheData? cacheData) { var result = false; cacheData = null; try { - if (cache.TryGetValue(key, out var binaryData)) + if (memoryCache.TryGetValue(key, out var binaryData)) { - cacheData = binaryData.Deserialize(); + cacheData = binaryData!.Deserialize(); result = true; } } diff --git a/HttpClient.Caching/InMemory/InMemoryCacheFallbackHandler.cs b/HttpClient.Caching/Memory/InMemoryCacheFallbackHandler.cs similarity index 97% rename from HttpClient.Caching/InMemory/InMemoryCacheFallbackHandler.cs rename to HttpClient.Caching/Memory/InMemoryCacheFallbackHandler.cs index 552b900..155446b 100644 --- a/HttpClient.Caching/InMemory/InMemoryCacheFallbackHandler.cs +++ b/HttpClient.Caching/Memory/InMemoryCacheFallbackHandler.cs @@ -1,11 +1,7 @@ -using System; -using System.Net.Http; -using System.Threading; -using System.Threading.Tasks; +using System.Net.Http; using Microsoft.Extensions.Caching.Abstractions; -using Microsoft.Extensions.Caching.Memory; -namespace Microsoft.Extensions.Caching.InMemory +namespace Microsoft.Extensions.Caching.Memory { /// /// Tries to retrieve the result from the HTTP call, and if it times out or results in an unsuccessful status code, diff --git a/HttpClient.Caching/InMemory/InMemoryCacheHandler.cs b/HttpClient.Caching/Memory/InMemoryCacheHandler.cs similarity index 99% rename from HttpClient.Caching/InMemory/InMemoryCacheHandler.cs rename to HttpClient.Caching/Memory/InMemoryCacheHandler.cs index fa003de..ba2e6be 100644 --- a/HttpClient.Caching/InMemory/InMemoryCacheHandler.cs +++ b/HttpClient.Caching/Memory/InMemoryCacheHandler.cs @@ -3,9 +3,8 @@ using System.Net.Http; using System.Net.Http.Headers; using Microsoft.Extensions.Caching.Abstractions; -using Microsoft.Extensions.Caching.Memory; -namespace Microsoft.Extensions.Caching.InMemory +namespace Microsoft.Extensions.Caching.Memory { /// /// Tries to retrieve the result from an InMemory cache, and if that's not available, gets the value from the diff --git a/HttpClient.Caching/InMemory/MethodUriHeadersCacheKeysProvider.cs b/HttpClient.Caching/MethodUriHeadersCacheKeysProvider.cs similarity index 98% rename from HttpClient.Caching/InMemory/MethodUriHeadersCacheKeysProvider.cs rename to HttpClient.Caching/MethodUriHeadersCacheKeysProvider.cs index 9efce4d..a4fa827 100644 --- a/HttpClient.Caching/InMemory/MethodUriHeadersCacheKeysProvider.cs +++ b/HttpClient.Caching/MethodUriHeadersCacheKeysProvider.cs @@ -4,7 +4,7 @@ using System.Text; using Microsoft.Extensions.Caching.Abstractions; -namespace Microsoft.Extensions.Caching.InMemory +namespace Microsoft.Extensions.Caching.Memory { /// /// Provides keys to store or retrieve data in the cache by using http method, specific headers and Uri diff --git a/Samples/ConsoleAppSample/Program.cs b/Samples/ConsoleAppSample/Program.cs index de58ab4..2ac76fc 100644 --- a/Samples/ConsoleAppSample/Program.cs +++ b/Samples/ConsoleAppSample/Program.cs @@ -4,7 +4,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Caching.Abstractions; -using Microsoft.Extensions.Caching.InMemory; using Microsoft.Extensions.Caching.Memory; namespace ConsoleAppSample diff --git a/Tests/HttpClient.Caching.Tests/InMemory/DefaultCacheKeysProviderTests.cs b/Tests/HttpClient.Caching.Tests/InMemory/DefaultCacheKeysProviderTests.cs index 6be1772..6fda380 100644 --- a/Tests/HttpClient.Caching.Tests/InMemory/DefaultCacheKeysProviderTests.cs +++ b/Tests/HttpClient.Caching.Tests/InMemory/DefaultCacheKeysProviderTests.cs @@ -1,7 +1,7 @@ using System; using System.Net.Http; using FluentAssertions; -using Microsoft.Extensions.Caching.InMemory; +using Microsoft.Extensions.Caching.Memory; using Xunit; namespace HttpClient.Caching.Tests.InMemory diff --git a/Tests/HttpClient.Caching.Tests/InMemory/InMemoryCacheFallbackHandlerTests.cs b/Tests/HttpClient.Caching.Tests/InMemory/InMemoryCacheFallbackHandlerTests.cs index c029de8..beb0a7d 100644 --- a/Tests/HttpClient.Caching.Tests/InMemory/InMemoryCacheFallbackHandlerTests.cs +++ b/Tests/HttpClient.Caching.Tests/InMemory/InMemoryCacheFallbackHandlerTests.cs @@ -2,7 +2,6 @@ using System.Net.Http; using FluentAssertions; using HttpClient.Caching.Tests.TestData; -using Microsoft.Extensions.Caching.InMemory; using Microsoft.Extensions.Caching.Memory; using Moq; using Xunit; diff --git a/Tests/HttpClient.Caching.Tests/InMemory/InMemoryCacheHandlerTests.cs b/Tests/HttpClient.Caching.Tests/InMemory/InMemoryCacheHandlerTests.cs index 94e3b2e..ca81267 100644 --- a/Tests/HttpClient.Caching.Tests/InMemory/InMemoryCacheHandlerTests.cs +++ b/Tests/HttpClient.Caching.Tests/InMemory/InMemoryCacheHandlerTests.cs @@ -1,13 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Net; +using System.Net; using System.Net.Http; using System.Net.Http.Headers; using System.Text; -using System.Threading.Tasks; using FluentAssertions; using HttpClient.Caching.Tests.TestData; -using Microsoft.Extensions.Caching.InMemory; using Microsoft.Extensions.Caching.Memory; using Xunit; diff --git a/Tests/HttpClient.Caching.Tests/InMemory/MethodUriHeadersCacheKeysProviderTests.cs b/Tests/HttpClient.Caching.Tests/InMemory/MethodUriHeadersCacheKeysProviderTests.cs index 8dc5a3f..644c272 100644 --- a/Tests/HttpClient.Caching.Tests/InMemory/MethodUriHeadersCacheKeysProviderTests.cs +++ b/Tests/HttpClient.Caching.Tests/InMemory/MethodUriHeadersCacheKeysProviderTests.cs @@ -1,7 +1,7 @@ using System; using System.Net.Http; using FluentAssertions; -using Microsoft.Extensions.Caching.InMemory; +using Microsoft.Extensions.Caching.Memory; using Xunit; namespace HttpClient.Caching.Tests.InMemory diff --git a/Tests/HttpClient.Caching.Tests/MemoryCacheTests.cs b/Tests/HttpClient.Caching.Tests/MemoryCacheTests.cs index 234cc37..08bc32f 100644 --- a/Tests/HttpClient.Caching.Tests/MemoryCacheTests.cs +++ b/Tests/HttpClient.Caching.Tests/MemoryCacheTests.cs @@ -1,7 +1,5 @@ -using System; -using FluentAssertions; +using FluentAssertions; using HttpClient.Caching.Tests.TestData; -using Microsoft.Extensions.Caching.InMemory; using Microsoft.Extensions.Caching.Memory; using Xunit; using Xunit.Abstractions; From e362a85a80713e06810271622be46e2e99d4d1b8 Mon Sep 17 00:00:00 2001 From: thomasgalliker Date: Tue, 21 Apr 2026 08:41:38 +0200 Subject: [PATCH 16/19] Add Clear method as extension method for IMemoryCache --- .../Memory/IMemoryCacheExtensions.cs | 9 ++--- .../Memory/MemoryCacheExtensions.cs | 27 +++++++++++++++ .../MemoryCacheTests.cs | 34 ++++++++++++++++--- 3 files changed, 59 insertions(+), 11 deletions(-) create mode 100644 HttpClient.Caching/Memory/MemoryCacheExtensions.cs diff --git a/HttpClient.Caching/Memory/IMemoryCacheExtensions.cs b/HttpClient.Caching/Memory/IMemoryCacheExtensions.cs index 495a009..002bf6d 100644 --- a/HttpClient.Caching/Memory/IMemoryCacheExtensions.cs +++ b/HttpClient.Caching/Memory/IMemoryCacheExtensions.cs @@ -3,12 +3,9 @@ namespace Microsoft.Extensions.Caching.Memory { - /// - /// Extension methods for an . - /// internal static class IMemoryCacheExtensions { - public static bool TryGetCacheData(this IMemoryCache memoryCache, string key, [NotNullWhen(true)] out CacheData? cacheData) + internal static bool TryGetCacheData(this IMemoryCache memoryCache, string key, [NotNullWhen(true)] out CacheData? cacheData) { var result = false; cacheData = null; @@ -37,7 +34,7 @@ public static bool TryGetCacheData(this IMemoryCache memoryCache, string key, [N /// The value of this cache entry. /// Expiration relative to now. /// A task, when completed, has tried to put the entry into the cache. - public static Task TrySetAsync(this IMemoryCache cache, string key, CacheData cacheData, TimeSpan absoluteExpirationRelativeToNow) + internal static Task TrySetAsync(this IMemoryCache cache, string key, CacheData cacheData, TimeSpan absoluteExpirationRelativeToNow) { try { @@ -51,7 +48,7 @@ public static Task TrySetAsync(this IMemoryCache cache, string key, CacheD } } - public static bool TrySetCacheData(this IMemoryCache cache, string key, CacheData value, TimeSpan absoluteExpirationRelativeToNow) + internal static bool TrySetCacheData(this IMemoryCache cache, string key, CacheData value, TimeSpan absoluteExpirationRelativeToNow) { bool result; diff --git a/HttpClient.Caching/Memory/MemoryCacheExtensions.cs b/HttpClient.Caching/Memory/MemoryCacheExtensions.cs new file mode 100644 index 0000000..fac57d8 --- /dev/null +++ b/HttpClient.Caching/Memory/MemoryCacheExtensions.cs @@ -0,0 +1,27 @@ +namespace Microsoft.Extensions.Caching.Memory +{ + public static class MemoryCacheExtensions + { + /// + /// Clears all entries from the cache. + /// + /// The cache to clear. + /// Thrown when is null. + /// Thrown when the cache implementation cannot be cleared wholesale. + public static void Clear(this IMemoryCache memoryCache) + { + if (memoryCache is null) + { + throw new ArgumentNullException(nameof(memoryCache)); + } + + if (memoryCache is MemoryCache m) + { + m.Clear(); + return; + } + + throw new NotSupportedException($"Clear is not supported for cache type '{memoryCache.GetType().FullName}'."); + } + } +} diff --git a/Tests/HttpClient.Caching.Tests/MemoryCacheTests.cs b/Tests/HttpClient.Caching.Tests/MemoryCacheTests.cs index 08bc32f..3f4f22d 100644 --- a/Tests/HttpClient.Caching.Tests/MemoryCacheTests.cs +++ b/Tests/HttpClient.Caching.Tests/MemoryCacheTests.cs @@ -21,20 +21,44 @@ public void ShouldSetCache() // Arrange var expirationTimeSpan = TimeSpan.FromHours(1); var options = new MemoryCacheOptions(); - var cache = new MemoryCache(options); - var cacheEntryOptions = new MemoryCacheEntryOptions { SlidingExpiration = expirationTimeSpan }; + var memoryCache = new MemoryCache(options); + var entryOptions = new MemoryCacheEntryOptions { SlidingExpiration = expirationTimeSpan }; // Act for (var i = 1; i <= 10; i++) { - cache.Set($"{i}", new TestPayload(i), cacheEntryOptions); + memoryCache.Set($"{i}", new TestPayload(i), entryOptions); } // Assert - cache.TryGetValue("1", out var result1); + memoryCache.TryGetValue("1", out var result1); result1.Should().NotBeNull(); result1.Should().BeOfType().Which.Id.Should().Be(1); - cache.Count.Should().Be(10); + memoryCache.Count.Should().Be(10); + } + + [Fact] + public void ShouldClearCache() + { + // Arrange + var options = new MemoryCacheEntryOptions + { + SlidingExpiration = TimeSpan.FromHours(1) + }; + var memoryCache = new MemoryCache(new MemoryCacheOptions()); + + for (var i = 1; i <= 10; i++) + { + memoryCache.Set($"{i}", new TestPayload(i), options); + } + + // Act + ((IMemoryCache)memoryCache).Clear(); + + // Assert + memoryCache.Count.Should().Be(0); + memoryCache.TryGetValue("1", out var result1).Should().BeFalse(); + result1.Should().BeNull(); } } } From 2e822b9d1c8c666c6db3992de15138b9052a8b0d Mon Sep 17 00:00:00 2001 From: thomasgalliker Date: Tue, 21 Apr 2026 08:42:21 +0200 Subject: [PATCH 17/19] Merge extension methods --- .../Memory/IMemoryCacheExtensions.cs | 69 ------------------- .../Memory/MemoryCacheExtensions.cs | 66 +++++++++++++++++- 2 files changed, 65 insertions(+), 70 deletions(-) delete mode 100644 HttpClient.Caching/Memory/IMemoryCacheExtensions.cs diff --git a/HttpClient.Caching/Memory/IMemoryCacheExtensions.cs b/HttpClient.Caching/Memory/IMemoryCacheExtensions.cs deleted file mode 100644 index 002bf6d..0000000 --- a/HttpClient.Caching/Memory/IMemoryCacheExtensions.cs +++ /dev/null @@ -1,69 +0,0 @@ -using System.Diagnostics.CodeAnalysis; -using Microsoft.Extensions.Caching.Abstractions; - -namespace Microsoft.Extensions.Caching.Memory -{ - internal static class IMemoryCacheExtensions - { - internal static bool TryGetCacheData(this IMemoryCache memoryCache, string key, [NotNullWhen(true)] out CacheData? cacheData) - { - var result = false; - cacheData = null; - - try - { - if (memoryCache.TryGetValue(key, out var binaryData)) - { - cacheData = binaryData!.Deserialize(); - result = true; - } - } - catch - { - // Ignore exception - } - - return result; - } - - /// - /// Tries to set a new value to the cache, that is, ignoring all exceptions. - /// - /// The in memory cache. - /// The key for this cache entry. - /// The value of this cache entry. - /// Expiration relative to now. - /// A task, when completed, has tried to put the entry into the cache. - internal static Task TrySetAsync(this IMemoryCache cache, string key, CacheData cacheData, TimeSpan absoluteExpirationRelativeToNow) - { - try - { - cache.Set(key, cacheData.Serialize(), absoluteExpirationRelativeToNow); - return Task.FromResult(true); - } - catch (Exception) - { - // ignore all exceptions - return Task.FromResult(false); - } - } - - internal static bool TrySetCacheData(this IMemoryCache cache, string key, CacheData value, TimeSpan absoluteExpirationRelativeToNow) - { - bool result; - - try - { - cache.Set(key, value.Serialize(), absoluteExpirationRelativeToNow); - result = true; - } - catch - { - // Ignore exceptions - result = false; - } - - return result; - } - } -} diff --git a/HttpClient.Caching/Memory/MemoryCacheExtensions.cs b/HttpClient.Caching/Memory/MemoryCacheExtensions.cs index fac57d8..4612cc1 100644 --- a/HttpClient.Caching/Memory/MemoryCacheExtensions.cs +++ b/HttpClient.Caching/Memory/MemoryCacheExtensions.cs @@ -1,4 +1,7 @@ -namespace Microsoft.Extensions.Caching.Memory +using System.Diagnostics.CodeAnalysis; +using Microsoft.Extensions.Caching.Abstractions; + +namespace Microsoft.Extensions.Caching.Memory { public static class MemoryCacheExtensions { @@ -23,5 +26,66 @@ public static void Clear(this IMemoryCache memoryCache) throw new NotSupportedException($"Clear is not supported for cache type '{memoryCache.GetType().FullName}'."); } + + internal static bool TryGetCacheData(this IMemoryCache memoryCache, string key, [NotNullWhen(true)] out CacheData? cacheData) + { + var result = false; + cacheData = null; + + try + { + if (memoryCache.TryGetValue(key, out var binaryData)) + { + cacheData = binaryData!.Deserialize(); + result = true; + } + } + catch + { + // Ignore exception + } + + return result; + } + + /// + /// Tries to set a new value to the cache, that is, ignoring all exceptions. + /// + /// The in memory cache. + /// The key for this cache entry. + /// The value of this cache entry. + /// Expiration relative to now. + /// A task, when completed, has tried to put the entry into the cache. + internal static Task TrySetAsync(this IMemoryCache cache, string key, CacheData cacheData, TimeSpan absoluteExpirationRelativeToNow) + { + try + { + cache.Set(key, cacheData.Serialize(), absoluteExpirationRelativeToNow); + return Task.FromResult(true); + } + catch (Exception) + { + // ignore all exceptions + return Task.FromResult(false); + } + } + + internal static bool TrySetCacheData(this IMemoryCache cache, string key, CacheData value, TimeSpan absoluteExpirationRelativeToNow) + { + bool result; + + try + { + cache.Set(key, value.Serialize(), absoluteExpirationRelativeToNow); + result = true; + } + catch + { + // Ignore exceptions + result = false; + } + + return result; + } } } From 1b24731455d092963b25f298d1e3054534aa8a4c Mon Sep 17 00:00:00 2001 From: thomasgalliker Date: Mon, 11 May 2026 15:09:05 +0200 Subject: [PATCH 18/19] Add TryGetValue with NotNullWhen(true) for value --- .../Memory/MemoryCacheExtensions.cs | 27 ++++++- .../Extensions/MemoryCacheExtensionsTests.cs | 73 +++++++++++++++++++ .../MemoryCacheTests.cs | 64 ---------------- 3 files changed, 98 insertions(+), 66 deletions(-) create mode 100644 Tests/HttpClient.Caching.Tests/Extensions/MemoryCacheExtensionsTests.cs delete mode 100644 Tests/HttpClient.Caching.Tests/MemoryCacheTests.cs diff --git a/HttpClient.Caching/Memory/MemoryCacheExtensions.cs b/HttpClient.Caching/Memory/MemoryCacheExtensions.cs index 4612cc1..8ebd99c 100644 --- a/HttpClient.Caching/Memory/MemoryCacheExtensions.cs +++ b/HttpClient.Caching/Memory/MemoryCacheExtensions.cs @@ -5,6 +5,29 @@ namespace Microsoft.Extensions.Caching.Memory { public static class MemoryCacheExtensions { + /// + /// Tries to get the value associated with the given key. + /// + /// The type of the object to get. + /// The instance this method extends. + /// The key of the value to get. + /// The value associated with the given key. + /// true if the key was found; false otherwise. + public static bool TryGetValue(this IMemoryCache cache, object key, [NotNullWhen(true)] out TItem? value) + { + if (cache.TryGetValue(key, out var result)) + { + if (result is TItem item) + { + value = item; + return true; + } + } + + value = default; + return false; + } + /// /// Clears all entries from the cache. /// @@ -34,9 +57,9 @@ internal static bool TryGetCacheData(this IMemoryCache memoryCache, string key, try { - if (memoryCache.TryGetValue(key, out var binaryData)) + if (TryGetValue(memoryCache, key, out var binaryData)) { - cacheData = binaryData!.Deserialize(); + cacheData = binaryData.Deserialize(); result = true; } } diff --git a/Tests/HttpClient.Caching.Tests/Extensions/MemoryCacheExtensionsTests.cs b/Tests/HttpClient.Caching.Tests/Extensions/MemoryCacheExtensionsTests.cs new file mode 100644 index 0000000..8365816 --- /dev/null +++ b/Tests/HttpClient.Caching.Tests/Extensions/MemoryCacheExtensionsTests.cs @@ -0,0 +1,73 @@ +using FluentAssertions; +using HttpClient.Caching.Tests.TestData; +using Microsoft.Extensions.Caching.Memory; +using Xunit; +using Xunit.Abstractions; + +namespace HttpClient.Caching.Tests.Extensions +{ + public class MemoryCacheExtensionsTests + { + private readonly ITestOutputHelper testOutputHelper; + + public MemoryCacheExtensionsTests(ITestOutputHelper testOutputHelper) + { + this.testOutputHelper = testOutputHelper; + } + + [Fact] + public void Clear_RemovesAllEntries() + { + // Arrange + var options = new MemoryCacheEntryOptions + { + SlidingExpiration = TimeSpan.FromHours(1) + }; + var memoryCache = new MemoryCache(new MemoryCacheOptions()); + + for (var i = 1; i <= 10; i++) + { + memoryCache.Set($"{i}", new TestPayload(i), options); + } + + // Act + ((IMemoryCache)memoryCache).Clear(); + + // Assert + memoryCache.Count.Should().Be(0); + memoryCache.TryGetValue("1", out var result1).Should().BeFalse(); + result1.Should().BeNull(); + } + + [Fact] + public void TryGetValue_ReturnsTrueAndTypedValue() + { + // Arrange + using var memoryCache = new MemoryCache(new MemoryCacheOptions()); + memoryCache.Set("1", new TestPayload(1)); + + // Act + var result = MemoryCacheExtensions.TryGetValue(memoryCache, "1", out var value); + + // Assert + result.Should().BeTrue(); + value.Should().NotBeNull(); + value!.Id.Should().Be(1); + } + + [Fact] + public void TryGetValue_ReturnFalseAndNull() + { + // Arrange + using var memoryCache = new MemoryCache(new MemoryCacheOptions()); + memoryCache.Set("1", null); + + // Act + var result = MemoryCacheExtensions.TryGetValue(memoryCache, "1", out var value); + + // Assert + result.Should().BeFalse(); + value.Should().BeNull(); + } + } +} diff --git a/Tests/HttpClient.Caching.Tests/MemoryCacheTests.cs b/Tests/HttpClient.Caching.Tests/MemoryCacheTests.cs deleted file mode 100644 index 3f4f22d..0000000 --- a/Tests/HttpClient.Caching.Tests/MemoryCacheTests.cs +++ /dev/null @@ -1,64 +0,0 @@ -using FluentAssertions; -using HttpClient.Caching.Tests.TestData; -using Microsoft.Extensions.Caching.Memory; -using Xunit; -using Xunit.Abstractions; - -namespace HttpClient.Caching.Tests -{ - public class MemoryCacheTests - { - private readonly ITestOutputHelper testOutputHelper; - - public MemoryCacheTests(ITestOutputHelper testOutputHelper) - { - this.testOutputHelper = testOutputHelper; - } - - [Fact] - public void ShouldSetCache() - { - // Arrange - var expirationTimeSpan = TimeSpan.FromHours(1); - var options = new MemoryCacheOptions(); - var memoryCache = new MemoryCache(options); - var entryOptions = new MemoryCacheEntryOptions { SlidingExpiration = expirationTimeSpan }; - - // Act - for (var i = 1; i <= 10; i++) - { - memoryCache.Set($"{i}", new TestPayload(i), entryOptions); - } - - // Assert - memoryCache.TryGetValue("1", out var result1); - result1.Should().NotBeNull(); - result1.Should().BeOfType().Which.Id.Should().Be(1); - memoryCache.Count.Should().Be(10); - } - - [Fact] - public void ShouldClearCache() - { - // Arrange - var options = new MemoryCacheEntryOptions - { - SlidingExpiration = TimeSpan.FromHours(1) - }; - var memoryCache = new MemoryCache(new MemoryCacheOptions()); - - for (var i = 1; i <= 10; i++) - { - memoryCache.Set($"{i}", new TestPayload(i), options); - } - - // Act - ((IMemoryCache)memoryCache).Clear(); - - // Assert - memoryCache.Count.Should().Be(0); - memoryCache.TryGetValue("1", out var result1).Should().BeFalse(); - result1.Should().BeNull(); - } - } -} From e4d285ff96109a1011deee24c9de0e239f796f64 Mon Sep 17 00:00:00 2001 From: Thomas Galliker Date: Thu, 28 May 2026 10:21:31 +0200 Subject: [PATCH 19/19] Replace FluentAssertions with AwesomeAssertions --- .../Abstractions/CacheDataExtensionsTests.cs | 8 --- .../CacheExpirationProviderTests.cs | 8 +-- .../Abstractions/StatsProviderTests.cs | 5 -- .../Abstractions/StatsResultTests.cs | 5 -- .../Abstractions/StatsValueTests.cs | 4 -- .../Abstractions/StatusCodeExtensionsTests.cs | 9 +-- .../Extensions/MemoryCacheExtensionsTests.cs | 8 +-- .../HttpClient.Caching.Tests/GlobalUsings.cs | 14 +++++ .../HttpClient.Caching.Tests.csproj | 2 +- .../InMemory/DefaultCacheKeysProviderTests.cs | 12 +--- .../InMemoryCacheFallbackHandlerTests.cs | 58 ++++++++----------- .../InMemory/InMemoryCacheHandlerTests.cs | 11 +--- .../MethodUriHeadersCacheKeysProviderTests.cs | 16 ++--- .../Testdata/TestMessageHandler.cs | 7 +-- .../Testdata/TestPayload.cs | 5 +- 15 files changed, 54 insertions(+), 118 deletions(-) create mode 100644 Tests/HttpClient.Caching.Tests/GlobalUsings.cs diff --git a/Tests/HttpClient.Caching.Tests/Abstractions/CacheDataExtensionsTests.cs b/Tests/HttpClient.Caching.Tests/Abstractions/CacheDataExtensionsTests.cs index 8907820..d9fea33 100644 --- a/Tests/HttpClient.Caching.Tests/Abstractions/CacheDataExtensionsTests.cs +++ b/Tests/HttpClient.Caching.Tests/Abstractions/CacheDataExtensionsTests.cs @@ -1,11 +1,3 @@ -using System; -using System.Collections.Generic; -using System.Net; -using System.Net.Http; -using FluentAssertions; -using Microsoft.Extensions.Caching.Abstractions; -using Xunit; - namespace HttpClient.Caching.Tests.Abstractions { public class CacheDataExtensionsTests diff --git a/Tests/HttpClient.Caching.Tests/Abstractions/CacheExpirationProviderTests.cs b/Tests/HttpClient.Caching.Tests/Abstractions/CacheExpirationProviderTests.cs index 04109f4..628bcca 100644 --- a/Tests/HttpClient.Caching.Tests/Abstractions/CacheExpirationProviderTests.cs +++ b/Tests/HttpClient.Caching.Tests/Abstractions/CacheExpirationProviderTests.cs @@ -1,10 +1,4 @@ -using System; -using System.Net; -using FluentAssertions; -using Microsoft.Extensions.Caching.Abstractions; -using Xunit; - -namespace HttpClient.Caching.Tests.Abstractions +namespace HttpClient.Caching.Tests.Abstractions { public class CacheExpirationProviderTests { diff --git a/Tests/HttpClient.Caching.Tests/Abstractions/StatsProviderTests.cs b/Tests/HttpClient.Caching.Tests/Abstractions/StatsProviderTests.cs index a35a488..c16f974 100644 --- a/Tests/HttpClient.Caching.Tests/Abstractions/StatsProviderTests.cs +++ b/Tests/HttpClient.Caching.Tests/Abstractions/StatsProviderTests.cs @@ -1,8 +1,3 @@ -using System.Net; -using FluentAssertions; -using Microsoft.Extensions.Caching.Abstractions; -using Xunit; - namespace HttpClient.Caching.Tests.Abstractions { public class StatsProviderTests diff --git a/Tests/HttpClient.Caching.Tests/Abstractions/StatsResultTests.cs b/Tests/HttpClient.Caching.Tests/Abstractions/StatsResultTests.cs index 7dc2a5a..1ed809b 100644 --- a/Tests/HttpClient.Caching.Tests/Abstractions/StatsResultTests.cs +++ b/Tests/HttpClient.Caching.Tests/Abstractions/StatsResultTests.cs @@ -1,8 +1,3 @@ -using System.Net; -using FluentAssertions; -using Microsoft.Extensions.Caching.Abstractions; -using Xunit; - namespace HttpClient.Caching.Tests.Abstractions { public class StatsResultTests diff --git a/Tests/HttpClient.Caching.Tests/Abstractions/StatsValueTests.cs b/Tests/HttpClient.Caching.Tests/Abstractions/StatsValueTests.cs index d1252f0..8bdd76b 100644 --- a/Tests/HttpClient.Caching.Tests/Abstractions/StatsValueTests.cs +++ b/Tests/HttpClient.Caching.Tests/Abstractions/StatsValueTests.cs @@ -1,7 +1,3 @@ -using FluentAssertions; -using Microsoft.Extensions.Caching.Abstractions; -using Xunit; - namespace HttpClient.Caching.Tests.Abstractions { public class StatsValueTests diff --git a/Tests/HttpClient.Caching.Tests/Abstractions/StatusCodeExtensionsTests.cs b/Tests/HttpClient.Caching.Tests/Abstractions/StatusCodeExtensionsTests.cs index dc1170e..73f1fa5 100644 --- a/Tests/HttpClient.Caching.Tests/Abstractions/StatusCodeExtensionsTests.cs +++ b/Tests/HttpClient.Caching.Tests/Abstractions/StatusCodeExtensionsTests.cs @@ -1,11 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Net; -using FluentAssertions; -using Microsoft.Extensions.Caching.Abstractions; -using Xunit; - -namespace HttpClient.Caching.Tests.Abstractions +namespace HttpClient.Caching.Tests.Abstractions { public class StatusCodeExtensionsTests { diff --git a/Tests/HttpClient.Caching.Tests/Extensions/MemoryCacheExtensionsTests.cs b/Tests/HttpClient.Caching.Tests/Extensions/MemoryCacheExtensionsTests.cs index 8365816..4d29dbb 100644 --- a/Tests/HttpClient.Caching.Tests/Extensions/MemoryCacheExtensionsTests.cs +++ b/Tests/HttpClient.Caching.Tests/Extensions/MemoryCacheExtensionsTests.cs @@ -1,10 +1,4 @@ -using FluentAssertions; -using HttpClient.Caching.Tests.TestData; -using Microsoft.Extensions.Caching.Memory; -using Xunit; -using Xunit.Abstractions; - -namespace HttpClient.Caching.Tests.Extensions +namespace HttpClient.Caching.Tests.Extensions { public class MemoryCacheExtensionsTests { diff --git a/Tests/HttpClient.Caching.Tests/GlobalUsings.cs b/Tests/HttpClient.Caching.Tests/GlobalUsings.cs new file mode 100644 index 0000000..4916b11 --- /dev/null +++ b/Tests/HttpClient.Caching.Tests/GlobalUsings.cs @@ -0,0 +1,14 @@ +global using System; +global using System.Collections.Generic; +global using System.Diagnostics; +global using System.Net; +global using System.Net.Http; +global using System.Net.Http.Headers; +global using System.Text; +global using AwesomeAssertions; +global using HttpClient.Caching.Tests.TestData; +global using Microsoft.Extensions.Caching.Abstractions; +global using Microsoft.Extensions.Caching.Memory; +global using Moq; +global using Xunit; +global using Xunit.Abstractions; \ No newline at end of file diff --git a/Tests/HttpClient.Caching.Tests/HttpClient.Caching.Tests.csproj b/Tests/HttpClient.Caching.Tests/HttpClient.Caching.Tests.csproj index ab7d04f..1c013ec 100644 --- a/Tests/HttpClient.Caching.Tests/HttpClient.Caching.Tests.csproj +++ b/Tests/HttpClient.Caching.Tests/HttpClient.Caching.Tests.csproj @@ -20,7 +20,7 @@ all - + diff --git a/Tests/HttpClient.Caching.Tests/InMemory/DefaultCacheKeysProviderTests.cs b/Tests/HttpClient.Caching.Tests/InMemory/DefaultCacheKeysProviderTests.cs index 6fda380..f7045f6 100644 --- a/Tests/HttpClient.Caching.Tests/InMemory/DefaultCacheKeysProviderTests.cs +++ b/Tests/HttpClient.Caching.Tests/InMemory/DefaultCacheKeysProviderTests.cs @@ -1,14 +1,8 @@ -using System; -using System.Net.Http; -using FluentAssertions; -using Microsoft.Extensions.Caching.Memory; -using Xunit; - -namespace HttpClient.Caching.Tests.InMemory +namespace HttpClient.Caching.Tests.InMemory { public class DefaultCacheKeysProviderTests { - private readonly string url = "http://unittest/"; + private const string TestUrl = "http://unittest/"; [Fact] public void ShouldGetKey() @@ -17,7 +11,7 @@ public void ShouldGetKey() var cacheKeysProvider = new DefaultCacheKeysProvider(); var request = new HttpRequestMessage { - RequestUri = new Uri(this.url), + RequestUri = new Uri(TestUrl), Method = HttpMethod.Get, }; diff --git a/Tests/HttpClient.Caching.Tests/InMemory/InMemoryCacheFallbackHandlerTests.cs b/Tests/HttpClient.Caching.Tests/InMemory/InMemoryCacheFallbackHandlerTests.cs index beb0a7d..5cbe604 100644 --- a/Tests/HttpClient.Caching.Tests/InMemory/InMemoryCacheFallbackHandlerTests.cs +++ b/Tests/HttpClient.Caching.Tests/InMemory/InMemoryCacheFallbackHandlerTests.cs @@ -1,18 +1,10 @@ -using System.Net; -using System.Net.Http; -using FluentAssertions; -using HttpClient.Caching.Tests.TestData; -using Microsoft.Extensions.Caching.Memory; -using Moq; -using Xunit; - -namespace HttpClient.Caching.Tests.InMemory +namespace HttpClient.Caching.Tests.InMemory { using HttpClient = System.Net.Http.HttpClient; public class InMemoryCacheFallbackHandlerTests { - private readonly string url = "http://unittest/"; + private const string TestUrl = "http://unittest/"; [Fact] public async Task AlwaysCallsTheHttpHandler() @@ -23,9 +15,9 @@ public async Task AlwaysCallsTheHttpHandler() var client = new HttpClient(new InMemoryCacheFallbackHandler(testMessageHandler, TimeSpan.FromDays(1), TimeSpan.FromDays(1), null, cache)); // Act twice - await client.GetAsync(this.url); - cache.Get(InMemoryCacheFallbackHandler.CacheFallbackKeyPrefix + HttpMethod.Get + this.url).Should().NotBeNull(); // ensure it's cached before the 2nd call - await client.GetAsync(this.url); + await client.GetAsync(TestUrl); + cache.Get(InMemoryCacheFallbackHandler.CacheFallbackKeyPrefix + HttpMethod.Get + TestUrl).Should().NotBeNull(); // ensure it's cached before the 2nd call + await client.GetAsync(TestUrl); // Assert testMessageHandler.NumberOfCalls.Should().Be(2); @@ -38,14 +30,14 @@ public async Task AlwaysUpdatesTheCacheOnSuccess() var testMessageHandler = new TestMessageHandler(); var cache = new Mock(MockBehavior.Strict); var cacheTime = TimeSpan.FromSeconds(123); - cache.Setup(c => c.CreateEntry(InMemoryCacheFallbackHandler.CacheFallbackKeyPrefix + HttpMethod.Get + this.url)); + cache.Setup(c => c.CreateEntry(InMemoryCacheFallbackHandler.CacheFallbackKeyPrefix + HttpMethod.Get + TestUrl)); var client = new HttpClient(new InMemoryCacheFallbackHandler(testMessageHandler, TimeSpan.FromDays(1), cacheTime, null, cache.Object)); // Act twice, validate cache is called each time - await client.GetAsync(this.url); - cache.Verify(c => c.CreateEntry(InMemoryCacheFallbackHandler.CacheFallbackKeyPrefix + HttpMethod.Get + this.url), Times.Once); - await client.GetAsync(this.url); - cache.Verify(c => c.CreateEntry(InMemoryCacheFallbackHandler.CacheFallbackKeyPrefix + HttpMethod.Get + this.url), Times.Exactly(2)); + await client.GetAsync(TestUrl); + cache.Verify(c => c.CreateEntry(InMemoryCacheFallbackHandler.CacheFallbackKeyPrefix + HttpMethod.Get + TestUrl), Times.Once); + await client.GetAsync(TestUrl); + cache.Verify(c => c.CreateEntry(InMemoryCacheFallbackHandler.CacheFallbackKeyPrefix + HttpMethod.Get + TestUrl), Times.Exactly(2)); } [Fact] @@ -55,15 +47,15 @@ public async Task UpdatesTheCacheForHeadAndGetIndependently() var testMessageHandler = new TestMessageHandler(); var cache = new Mock(MockBehavior.Strict); var cacheTime = TimeSpan.FromSeconds(123); - cache.Setup(c => c.CreateEntry(InMemoryCacheFallbackHandler.CacheFallbackKeyPrefix + HttpMethod.Get + this.url)); - cache.Setup(c => c.CreateEntry(InMemoryCacheFallbackHandler.CacheFallbackKeyPrefix + HttpMethod.Head + this.url)); + cache.Setup(c => c.CreateEntry(InMemoryCacheFallbackHandler.CacheFallbackKeyPrefix + HttpMethod.Get + TestUrl)); + cache.Setup(c => c.CreateEntry(InMemoryCacheFallbackHandler.CacheFallbackKeyPrefix + HttpMethod.Head + TestUrl)); var client = new HttpClient(new InMemoryCacheFallbackHandler(testMessageHandler, TimeSpan.FromDays(1), cacheTime, null, cache.Object)); // Act twice, validate cache is called each time - await client.SendAsync(new HttpRequestMessage(HttpMethod.Head, this.url)); - await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, this.url)); - cache.Verify(c => c.CreateEntry(InMemoryCacheFallbackHandler.CacheFallbackKeyPrefix + HttpMethod.Head + this.url), Times.Once); - cache.Verify(c => c.CreateEntry(InMemoryCacheFallbackHandler.CacheFallbackKeyPrefix + HttpMethod.Get + this.url), Times.Once); + await client.SendAsync(new HttpRequestMessage(HttpMethod.Head, TestUrl)); + await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, TestUrl)); + cache.Verify(c => c.CreateEntry(InMemoryCacheFallbackHandler.CacheFallbackKeyPrefix + HttpMethod.Head + TestUrl), Times.Once); + cache.Verify(c => c.CreateEntry(InMemoryCacheFallbackHandler.CacheFallbackKeyPrefix + HttpMethod.Get + TestUrl), Times.Once); } [Fact] @@ -75,11 +67,11 @@ public async Task NeverUpdatesTheCacheOnFailure() var cacheTime = TimeSpan.FromSeconds(123); object? expectedValue; cache.Setup(c => c.CreateEntry(It.IsAny())); - cache.Setup(c => c.TryGetValue(this.url, out expectedValue)).Returns(false); + cache.Setup(c => c.TryGetValue(TestUrl, out expectedValue)).Returns(false); var client = new HttpClient(new InMemoryCacheFallbackHandler(testMessageHandler, TimeSpan.FromDays(1), cacheTime, null, cache.Object)); // Act - await client.GetAsync(this.url); + await client.GetAsync(TestUrl); // Assert cache.Verify(c => c.CreateEntry(It.IsAny()), Times.Never); @@ -93,11 +85,11 @@ public async Task TriesToAccessCacheOnFailureButReturnsErrorIfNotInCache() var cache = new Mock(MockBehavior.Strict); var cacheTime = TimeSpan.FromSeconds(123); object? expectedValue; - cache.Setup(c => c.TryGetValue(this.url, out expectedValue)).Returns(false); + cache.Setup(c => c.TryGetValue(TestUrl, out expectedValue)).Returns(false); var client = new HttpClient(new InMemoryCacheFallbackHandler(testMessageHandler, TimeSpan.FromDays(1), cacheTime, null, cache.Object)); // Act - var result = await client.GetAsync(this.url); + var result = await client.GetAsync(TestUrl); // Assert result.StatusCode.Should().Be(HttpStatusCode.InternalServerError); @@ -114,9 +106,9 @@ public async Task GetsItFromTheHttpCallAfterBeingInCache() var client2 = new HttpClient(new InMemoryCacheFallbackHandler(testMessageHandler2, TimeSpan.FromMilliseconds(1), TimeSpan.FromDays(1), null, cache)); // Act twice - var result1 = await client1.GetAsync(this.url); - cache.Get(InMemoryCacheFallbackHandler.CacheFallbackKeyPrefix + HttpMethod.Get + this.url).Should().NotBeNull(); - var result2 = await client2.GetAsync(this.url); + var result1 = await client1.GetAsync(TestUrl); + cache.Get(InMemoryCacheFallbackHandler.CacheFallbackKeyPrefix + HttpMethod.Get + TestUrl).Should().NotBeNull(); + var result2 = await client2.GetAsync(TestUrl); // Assert // - that each message handler got called @@ -141,8 +133,8 @@ public async Task GetsItFromTheCacheWhenUnsuccessful() var client2 = new HttpClient(new InMemoryCacheFallbackHandler(testMessageHandler2, TimeSpan.FromDays(1), TimeSpan.FromDays(1), null, cache)); // Act twice - var result1 = await client1.GetAsync(this.url); - var result2 = await client2.GetAsync(this.url); + var result1 = await client1.GetAsync(TestUrl); + var result2 = await client2.GetAsync(TestUrl); // Assert // - that each message handler got called diff --git a/Tests/HttpClient.Caching.Tests/InMemory/InMemoryCacheHandlerTests.cs b/Tests/HttpClient.Caching.Tests/InMemory/InMemoryCacheHandlerTests.cs index ca81267..5829447 100644 --- a/Tests/HttpClient.Caching.Tests/InMemory/InMemoryCacheHandlerTests.cs +++ b/Tests/HttpClient.Caching.Tests/InMemory/InMemoryCacheHandlerTests.cs @@ -1,13 +1,4 @@ -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Text; -using FluentAssertions; -using HttpClient.Caching.Tests.TestData; -using Microsoft.Extensions.Caching.Memory; -using Xunit; - -namespace HttpClient.Caching.Tests.InMemory +namespace HttpClient.Caching.Tests.InMemory { using HttpClient = System.Net.Http.HttpClient; diff --git a/Tests/HttpClient.Caching.Tests/InMemory/MethodUriHeadersCacheKeysProviderTests.cs b/Tests/HttpClient.Caching.Tests/InMemory/MethodUriHeadersCacheKeysProviderTests.cs index 644c272..a6ee006 100644 --- a/Tests/HttpClient.Caching.Tests/InMemory/MethodUriHeadersCacheKeysProviderTests.cs +++ b/Tests/HttpClient.Caching.Tests/InMemory/MethodUriHeadersCacheKeysProviderTests.cs @@ -1,14 +1,8 @@ -using System; -using System.Net.Http; -using FluentAssertions; -using Microsoft.Extensions.Caching.Memory; -using Xunit; - -namespace HttpClient.Caching.Tests.InMemory +namespace HttpClient.Caching.Tests.InMemory { public class MethodUriHeadersCacheKeysProviderTests { - private readonly string url = "http://unittest/"; + private const string TestUrl = "http://unittest/"; [Fact] public void ShouldGetKey_EmptyHeaderNames() @@ -18,7 +12,7 @@ public void ShouldGetKey_EmptyHeaderNames() var cacheKeysProvider = new MethodUriHeadersCacheKeysProvider(headersNames); var request = new HttpRequestMessage { - RequestUri = new Uri(this.url), + RequestUri = new Uri(TestUrl), Method = HttpMethod.Get }; request.Headers.Add("X-HEADER-1", "Value1"); @@ -38,7 +32,7 @@ public void ShouldGetKey_WithMatchingHeader() var cacheKeysProvider = new MethodUriHeadersCacheKeysProvider(headersNames); var request = new HttpRequestMessage { - RequestUri = new Uri(this.url), + RequestUri = new Uri(TestUrl), Method = HttpMethod.Get }; request.Headers.Add("X-HEADER-1", "Value1"); @@ -58,7 +52,7 @@ public void ShouldGetKey_WithoutMatchingHeader() var cacheKeysProvider = new MethodUriHeadersCacheKeysProvider(headersNames); var request = new HttpRequestMessage { - RequestUri = new Uri(this.url), + RequestUri = new Uri(TestUrl), Method = HttpMethod.Get }; request.Headers.Add("X-HEADER-3", "Value3"); diff --git a/Tests/HttpClient.Caching.Tests/Testdata/TestMessageHandler.cs b/Tests/HttpClient.Caching.Tests/Testdata/TestMessageHandler.cs index 5eec06c..b9460f2 100644 --- a/Tests/HttpClient.Caching.Tests/Testdata/TestMessageHandler.cs +++ b/Tests/HttpClient.Caching.Tests/Testdata/TestMessageHandler.cs @@ -1,9 +1,4 @@ -using System.Net; -using System.Net.Http; -using System.Net.Http.Headers; -using System.Text; - -namespace HttpClient.Caching.Tests.TestData +namespace HttpClient.Caching.Tests.TestData { internal class TestMessageHandler : HttpMessageHandler { diff --git a/Tests/HttpClient.Caching.Tests/Testdata/TestPayload.cs b/Tests/HttpClient.Caching.Tests/Testdata/TestPayload.cs index d9a6bc9..ce2feb8 100644 --- a/Tests/HttpClient.Caching.Tests/Testdata/TestPayload.cs +++ b/Tests/HttpClient.Caching.Tests/Testdata/TestPayload.cs @@ -1,7 +1,4 @@ -using System; -using System.Diagnostics; - -namespace HttpClient.Caching.Tests.TestData +namespace HttpClient.Caching.Tests.TestData { [DebuggerDisplay("{this.Id}")] public class TestPayload