Skip to content

Commit fb6b27a

Browse files
authored
Merge pull request #320 from DuendeSoftware/dh/timeprovider-memorycache
Add Duende.Extensions.Caching.Memory component
2 parents 16cb39f + 2cf67f8 commit fb6b27a

19 files changed

+855
-0
lines changed

.github/workflow-gen/Program.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,13 @@
4040
"intro",
4141
[GitHubHostedRunners.UbuntuLatest],
4242
["net10.0"]),
43+
44+
new("memory-cache",
45+
["Extensions.Caching.Memory"],
46+
["Extensions.Caching.Memory.Tests"],
47+
"ecm",
48+
[GitHubHostedRunners.UbuntuLatest],
49+
["net10.0"]),
4350
];
4451

4552
foreach (var component in components)

.github/workflows/generate-test-reports.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ on:
99
- 'identity-model/ci'
1010
- 'identity-model-oidc-client/ci'
1111
- 'introspection/ci'
12+
- 'memory-cache/ci'
1213
types:
1314
- completed
1415
jobs:
@@ -71,3 +72,13 @@ jobs:
7172
reporter: dotnet-trx
7273
fail-on-error: true
7374
fail-on-empty: true
75+
- name: Test report - memory-cache - Extensions.Caching.Memory.Tests-net10.0
76+
if: github.event.workflow.name == 'memory-cache/ci'
77+
uses: dorny/test-reporter@31a54ee7ebcacc03a09ea97a7e5465a47b84aea5
78+
with:
79+
artifact: test-results
80+
name: Test Report - Extensions.Caching.Memory.Tests-net10.0
81+
path: Extensions.Caching.Memory.Tests-net10.0.trx
82+
reporter: dotnet-trx
83+
fail-on-error: true
84+
fail-on-empty: true
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# This was generated by tool. Edits will be overwritten.
2+
3+
name: memory-cache/ci
4+
on:
5+
workflow_dispatch:
6+
push:
7+
paths:
8+
- .github/workflows/memory-cache-**
9+
- memory-cache/**
10+
- .editorconfig
11+
- Directory.Packages.props
12+
- global.json
13+
- src.props
14+
- test.props
15+
pull_request:
16+
paths:
17+
- .github/workflows/memory-cache-**
18+
- memory-cache/**
19+
- .editorconfig
20+
- Directory.Packages.props
21+
- global.json
22+
- src.props
23+
- test.props
24+
env:
25+
DOTNET_NOLOGO: true
26+
DOTNET_CLI_TELEMETRY_OPTOUT: true
27+
jobs:
28+
build:
29+
name: Build
30+
runs-on: ubuntu-latest
31+
permissions:
32+
actions: read
33+
checks: write
34+
contents: read
35+
packages: write
36+
defaults:
37+
run:
38+
shell: bash
39+
working-directory: memory-cache
40+
timeout-minutes: 15
41+
steps:
42+
- name: Checkout
43+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
44+
with:
45+
fetch-depth: 0
46+
- name: Setup Dotnet
47+
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d
48+
with:
49+
dotnet-version: 10.0.100
50+
- name: Restore
51+
run: dotnet restore *.slnf
52+
- name: Verify Formatting
53+
run: dotnet format *.slnf --verify-no-changes --no-restore
54+
- name: Build - Extensions.Caching.Memory.Tests
55+
run: dotnet build -c Release test/Extensions.Caching.Memory.Tests
56+
- name: Test - Extensions.Caching.Memory.Tests
57+
run: >-
58+
for tfm in net10.0; do
59+
dotnet run --project test/Extensions.Caching.Memory.Tests -c Release --no-build -f $tfm -- \
60+
--report-xunit-trx --report-xunit-trx-filename Extensions.Caching.Memory.Tests-$tfm.trx \
61+
--coverage --coverage-output-format cobertura \
62+
--coverage-output Extensions.Caching.Memory.Tests-$tfm.cobertura.xml
63+
done
64+
- name: Test report
65+
if: success() || failure()
66+
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882
67+
with:
68+
name: test-results
69+
path: memory-cache/test/Extensions.Caching.Memory.Tests/TestResults/Extensions.Caching.Memory.Tests-net10.0.trx
70+
retention-days: 5
71+
- name: Tool restore
72+
run: dotnet tool restore
73+
- name: Pack Extensions.Caching.Memory
74+
run: dotnet pack -c Release src/Extensions.Caching.Memory -o artifacts
75+
- name: Sign packages
76+
if: github.event_name == 'push'
77+
run: |-
78+
for file in artifacts/*.nupkg; do
79+
dotnet NuGetKeyVaultSignTool sign "$file" --file-digest sha256 --timestamp-rfc3161 http://timestamp.digicert.com --azure-key-vault-url https://duendecodesigninghsm.vault.azure.net/ --azure-key-vault-client-id 18e3de68-2556-4345-8076-a46fad79e474 --azure-key-vault-tenant-id ed3089f0-5401-4758-90eb-066124e2d907 --azure-key-vault-client-secret ${{ secrets.SignClientSecret }} --azure-key-vault-certificate NuGetPackageSigning
80+
done
81+
- name: Push packages to GitHub
82+
if: github.ref == 'refs/heads/main'
83+
run: dotnet nuget push artifacts/*.nupkg --source https://nuget.pkg.github.com/DuendeSoftware/index.json --api-key ${{ secrets.GITHUB_TOKEN }} --skip-duplicate
84+
env:
85+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
86+
NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
87+
- name: Upload Artifacts
88+
if: github.event_name == 'push'
89+
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882
90+
with:
91+
name: artifacts
92+
path: memory-cache/artifacts/*.nupkg
93+
overwrite: true
94+
retention-days: 15
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# This was generated by tool. Edits will be overwritten.
2+
3+
name: memory-cache/release
4+
on:
5+
workflow_dispatch:
6+
inputs:
7+
version:
8+
description: 'Version in format X.Y.Z, X.Y.Z-preview.N, or X.Y.Z-rc.N'
9+
type: string
10+
required: true
11+
default: '0.0.0'
12+
branch:
13+
description: '(Optional) the name of the branch to release from'
14+
type: string
15+
required: false
16+
default: 'main'
17+
remove-tag-if-exists:
18+
description: 'If set, will remove the existing tag. Use this if you have issues with the previous release action'
19+
type: boolean
20+
required: false
21+
default: false
22+
env:
23+
DOTNET_NOLOGO: true
24+
DOTNET_CLI_TELEMETRY_OPTOUT: true
25+
jobs:
26+
tag:
27+
name: Tag and Pack
28+
runs-on: ubuntu-latest
29+
permissions:
30+
contents: write
31+
packages: write
32+
defaults:
33+
run:
34+
shell: bash
35+
working-directory: memory-cache
36+
steps:
37+
- name: Checkout
38+
uses: actions/checkout@v4
39+
with:
40+
fetch-depth: 0
41+
- name: Validate Version Input
42+
run: echo '${{ github.event.inputs.version }}' | grep -P '^\d+\.\d+\.\d+(-preview\.\d+|-rc\.\d+)?$' || (echo 'Invalid version format' && exit 1)
43+
- name: Checkout target branch
44+
if: github.event.inputs.branch != 'main'
45+
run: git checkout ${{ github.event.inputs.branch }}
46+
- name: Setup Dotnet
47+
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d
48+
with:
49+
dotnet-version: 10.0.100
50+
- name: Git Config
51+
run: |-
52+
git config --global user.email "github-bot@duendesoftware.com"
53+
git config --global user.name "Duende Software GitHub Bot"
54+
- name: Remove previous git tag
55+
if: github.event.inputs['remove-tag-if-exists'] == 'true'
56+
run: |-
57+
if git rev-parse ecm-${{ github.event.inputs.version }} >/dev/null 2>&1; then
58+
git tag -d ecm-${{ github.event.inputs.version }}
59+
git push --delete origin ecm-${{ github.event.inputs.version }}
60+
else
61+
echo 'Tag ecm-${{ github.event.inputs.version }} does not exist.'
62+
fi
63+
- name: Git tag
64+
run: |-
65+
git tag -a ecm-${{ github.event.inputs.version }} -m "Release v${{ github.event.inputs.version }}"
66+
git push origin ecm-${{ github.event.inputs.version }}
67+
- name: Pack Extensions.Caching.Memory
68+
run: dotnet pack -c Release src/Extensions.Caching.Memory -o artifacts
69+
- name: Tool restore
70+
run: dotnet tool restore
71+
- name: Sign packages
72+
run: |-
73+
for file in artifacts/*.nupkg; do
74+
dotnet NuGetKeyVaultSignTool sign "$file" --file-digest sha256 --timestamp-rfc3161 http://timestamp.digicert.com --azure-key-vault-url https://duendecodesigninghsm.vault.azure.net/ --azure-key-vault-client-id 18e3de68-2556-4345-8076-a46fad79e474 --azure-key-vault-tenant-id ed3089f0-5401-4758-90eb-066124e2d907 --azure-key-vault-client-secret ${{ secrets.SignClientSecret }} --azure-key-vault-certificate NuGetPackageSigning
75+
done
76+
- name: Push packages to GitHub
77+
run: dotnet nuget push artifacts/*.nupkg --source https://nuget.pkg.github.com/DuendeSoftware/index.json --api-key ${{ secrets.GITHUB_TOKEN }} --skip-duplicate
78+
env:
79+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
80+
NUGET_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
81+
- name: Upload Artifacts
82+
uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882
83+
with:
84+
name: artifacts
85+
path: memory-cache/artifacts/*.nupkg
86+
overwrite: true
87+
retention-days: 15
88+
publish:
89+
name: Publish to nuget.org
90+
needs:
91+
- tag
92+
runs-on: ubuntu-latest
93+
environment:
94+
name: nuget.org
95+
steps:
96+
- uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16
97+
with:
98+
name: artifacts
99+
path: artifacts
100+
- name: Setup Dotnet
101+
uses: actions/setup-dotnet@d4c94342e560b34958eacfc5d055d21461ed1c5d
102+
with:
103+
dotnet-version: 10.0.100
104+
- name: List files
105+
run: tree
106+
shell: bash
107+
- name: Push packages to nuget.org
108+
run: dotnet nuget push artifacts/*.nupkg --source https://api.nuget.org/v3/index.json --api-key ${{ secrets.NUGET_ORG_API_KEY }} --skip-duplicate

Directory.Packages.props

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
<PackageVersion Include="Microsoft.AspNetCore.WebUtilities" Version="$(FrameworkVersion)" />
1717
<PackageVersion Include="Microsoft.BCL.Memory" Version="$(ExtensionsVersion)" />
1818
<PackageVersion Include="Microsoft.Extensions.Caching.Hybrid" Version="10.0.0" />
19+
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="$(ExtensionsVersion)" />
20+
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="$(ExtensionsVersion)" />
1921
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="$(ExtensionsVersion)" />
2022
<PackageVersion Include="Microsoft.Extensions.Http" Version="$(ExtensionsVersion)" />
2123
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="10.0.0" />

foss.slnx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,4 +82,13 @@
8282
<File Path="introspection/test/Directory.Build.props" />
8383
<Project Path="introspection/test/AspNetCore.Authentication.OAuth2Introspection.Tests/AspNetCore.Authentication.OAuth2Introspection.Tests.csproj" />
8484
</Folder>
85+
<Folder Name="/memory-cache/" />
86+
<Folder Name="/memory-cache/src/">
87+
<File Path="memory-cache/src/Directory.Build.props" />
88+
<Project Path="memory-cache/src/Extensions.Caching.Memory/Extensions.Caching.Memory.csproj" Id="848d971c-3f76-463f-83fd-4b584a4b7573" />
89+
</Folder>
90+
<Folder Name="/memory-cache/test/">
91+
<File Path="memory-cache/test/Directory.Build.props" />
92+
<Project Path="memory-cache/test/Extensions.Caching.Memory.Tests/Extensions.Caching.Memory.Tests.csproj" />
93+
</Folder>
8594
</Solution>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"version": 1,
3+
"isRoot": true,
4+
"tools": {
5+
"NuGetKeyVaultSignTool": {
6+
"version": "3.2.3",
7+
"commands": [
8+
"NuGetKeyVaultSignTool"
9+
]
10+
}
11+
}
12+
}

memory-cache/README.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# Duende.Extensions.Caching.Memory
2+
3+
A `TimeProvider`-based implementation of `IMemoryCache` that enables testable time-dependent caching. This exists because
4+
of [this outstanding issue](https://github.com/dotnet/runtime/issues/114011). This library will be obsolete once that issue is resolved
5+
in some future version of .NET SDK.
6+
7+
## Overview
8+
9+
This library provides a wrapper around `Microsoft.Extensions.Caching.Memory.MemoryCache` that integrates with .NET's `TimeProvider` abstraction. This allows you to:
10+
11+
- Write unit tests that manipulate time using `FakeTimeProvider`
12+
- Verify cache expiration behavior without real delays
13+
- Maintain full compatibility with the standard `IMemoryCache` interface
14+
15+
## Usage
16+
17+
### Direct Instantiation
18+
19+
```csharp
20+
var timeProvider = TimeProvider.System; // or FakeTimeProvider in tests
21+
var options = Options.Create(new TimeProviderMemoryCacheOptions
22+
{
23+
ExpirationScanFrequency = TimeSpan.FromMinutes(1),
24+
SizeLimit = 1024
25+
});
26+
27+
var cache = new TimeProviderMemoryCache(timeProvider, options);
28+
29+
// Use like any IMemoryCache
30+
cache.Set("key", "value", TimeSpan.FromMinutes(5));
31+
```
32+
33+
### Dependency Injection
34+
35+
```csharp
36+
// Basic registration (uses TimeProvider.System)
37+
services.AddTimeProviderMemoryCache();
38+
39+
// With configuration
40+
services.AddTimeProviderMemoryCache(options =>
41+
{
42+
options.ExpirationScanFrequency = TimeSpan.FromMinutes(1);
43+
options.SizeLimit = 1024;
44+
});
45+
```
46+
47+
## Testing Example
48+
49+
```csharp
50+
var fakeTime = new FakeTimeProvider();
51+
var cache = new TimeProviderMemoryCache(fakeTime, options);
52+
53+
cache.Set("key", "value", TimeSpan.FromMinutes(5));
54+
55+
// Advance time past expiration
56+
fakeTime.Advance(TimeSpan.FromMinutes(6));
57+
58+
// Entry should be expired
59+
cache.TryGetValue("key", out var result).ShouldBeFalse();
60+
```
61+
62+
## License
63+
64+
Apache 2.0 - see [LICENSE](https://github.com/DuendeSoftware/foss/blob/main/LICENSE) for details.

memory-cache/memory-cache.slnf

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"solution": {
3+
"path": "..\\foss.slnx",
4+
"projects": [
5+
"memory-cache\\src\\Extensions.Caching.Memory\\Extensions.Caching.Memory.csproj",
6+
"memory-cache\\test\\Extensions.Caching.Memory.Tests\\Extensions.Caching.Memory.Tests.csproj"
7+
]
8+
}
9+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<Project>
3+
4+
<Import Project="../../src.props" />
5+
6+
<PropertyGroup>
7+
<MinVerTagPrefix>ecm-</MinVerTagPrefix>
8+
<MinVerMinimumMajorMinor>10.0</MinVerMinimumMajorMinor>
9+
</PropertyGroup>
10+
11+
</Project>

0 commit comments

Comments
 (0)