From 81db0b67b6a6d198377b7cb259978e6a2072fce2 Mon Sep 17 00:00:00 2001 From: Jacob Atchley <10537213+JacobAtchley@users.noreply.github.com> Date: Wed, 7 Oct 2026 13:24:29 -0500 Subject: [PATCH 1/7] feat!: target net9.0 and net10.0 with configurable frameworks Directory.Build.props defines FirebendTargetFrameworks (net9.0;net10.0) for the library and tests and FirebendAppTargetFramework (net10.0) for the sample. Central package management in Directory.Packages.props gives the Microsoft.Extensions.* packages per-framework versions (9.0.x for net9.0, 10.0.x for net10.0). Packages are updated, including StackExchange.Redis 2.9.11 to 3.3.1, and in-box System.Text.Json and System.ComponentModel.TypeConverter references are removed. CI installs the 9 and 10 SDKs. BREAKING CHANGE: packages now target only net9.0 and net10.0; the netstandard2.1 target is dropped. StackExchange.Redis is now 3.x, so consumers must be on StackExchange.Redis 3.x. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JMzvDp6hZ4dnMSGobxdo98 --- .editorconfig | 4 +- .github/workflows/build-and-test.yml | 8 ++-- .github/workflows/build-test-and-release.yml | 8 ++-- Directory.Build.props | 10 +++++ Directory.Packages.props | 43 +++++++++++++++++++ .../Implementations/LitRedisCacheStore.cs | 3 +- LitRedis.Core/LitRedis.Core.csproj | 11 +++-- LitRedis.Sample/LitRedis.Sample.csproj | 6 +-- LitRedis.Tests/LitRedis.Tests.csproj | 17 ++++---- nuget.config | 8 ++++ 10 files changed, 91 insertions(+), 27 deletions(-) create mode 100644 Directory.Build.props create mode 100644 Directory.Packages.props create mode 100644 nuget.config diff --git a/.editorconfig b/.editorconfig index 79f8198..ee5ab19 100644 --- a/.editorconfig +++ b/.editorconfig @@ -95,7 +95,9 @@ dotnet_style_predefined_type_for_locals_parameters_members = true:warning dotnet_style_predefined_type_for_member_access = true:warning # Modifier preferences # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#normalize-modifiers -dotnet_style_require_accessibility_modifiers = always:warning +# for_non_interface_members matches what the .NET 9 SDK analyzers enforced; the .NET 10 SDK +# also flags interface members under "always". +dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:warning visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:warning dotnet_style_readonly_field = true:warning diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index b57be4c..b68a019 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -13,12 +13,14 @@ jobs: steps: - name : Checkout Repo - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Setup .NET Core - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v4 with: - dotnet-version: 8.0.101 + dotnet-version: | + 9.0.x + 10.0.x - name: Run dotnet format run: dotnet format lit-redis.sln --verify-no-changes diff --git a/.github/workflows/build-test-and-release.yml b/.github/workflows/build-test-and-release.yml index 00251ac..dbc84e0 100644 --- a/.github/workflows/build-test-and-release.yml +++ b/.github/workflows/build-test-and-release.yml @@ -12,7 +12,7 @@ jobs: steps: - name: Setup Git - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: token : ${{ secrets.FIREBEND_GIT_PUSH_PAT}} fetch-depth: 0 @@ -36,9 +36,11 @@ jobs: - name: Setup .NET Core if: steps.versionize.outcome == 'success' - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v4 with: - dotnet-version: 8.0.101 + dotnet-version: | + 9.0.x + 10.0.x - name: Install dependencies if: steps.versionize.outcome == 'success' diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..43afb4b --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,10 @@ + + + + + net9.0;net10.0 + + net10.0 + + + diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 0000000..e0ce642 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,43 @@ + + + + true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LitRedis.Core/Implementations/LitRedisCacheStore.cs b/LitRedis.Core/Implementations/LitRedisCacheStore.cs index 8673be0..bd0d301 100644 --- a/LitRedis.Core/Implementations/LitRedisCacheStore.cs +++ b/LitRedis.Core/Implementations/LitRedisCacheStore.cs @@ -6,6 +6,7 @@ using LitRedis.Core.Interfaces; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Primitives; +using StackExchange.Redis; namespace LitRedis.Core.Implementations; @@ -48,7 +49,7 @@ public async Task PutAsync(string key, T model, TimeSpan? expiry, Cancellatio var json = _jsonSerializer.Serialize(model); - await _litRedisConnectionService.UseDbAsync((db, _) => db.StringSetAsync(key, json, expiry), cancellationToken); + await _litRedisConnectionService.UseDbAsync((db, _) => db.StringSetAsync(key, json, expiry.HasValue ? new Expiration(expiry.Value) : Expiration.Default), cancellationToken); } /// diff --git a/LitRedis.Core/LitRedis.Core.csproj b/LitRedis.Core/LitRedis.Core.csproj index 2ddfa57..be2fca8 100644 --- a/LitRedis.Core/LitRedis.Core.csproj +++ b/LitRedis.Core/LitRedis.Core.csproj @@ -1,7 +1,7 @@ - netstandard2.1 + $(FirebendTargetFrameworks) 5.0.0 latestmajor 4 @@ -19,11 +19,10 @@ - - - - - + + + + diff --git a/LitRedis.Sample/LitRedis.Sample.csproj b/LitRedis.Sample/LitRedis.Sample.csproj index 5435458..ad71dec 100644 --- a/LitRedis.Sample/LitRedis.Sample.csproj +++ b/LitRedis.Sample/LitRedis.Sample.csproj @@ -2,13 +2,11 @@ Exe - net8.0 + $(FirebendAppTargetFramework) - - - + diff --git a/LitRedis.Tests/LitRedis.Tests.csproj b/LitRedis.Tests/LitRedis.Tests.csproj index 05780dd..358fad4 100644 --- a/LitRedis.Tests/LitRedis.Tests.csproj +++ b/LitRedis.Tests/LitRedis.Tests.csproj @@ -1,26 +1,25 @@ - net8.0 + $(FirebendTargetFrameworks) false - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - - - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - diff --git a/nuget.config b/nuget.config new file mode 100644 index 0000000..c6ad81a --- /dev/null +++ b/nuget.config @@ -0,0 +1,8 @@ + + + + + + + + From 665dab3963f622fdcbfca079586525c616323ba4 Mon Sep 17 00:00:00 2001 From: Jacob Atchley <10537213+JacobAtchley@users.noreply.github.com> Date: Wed, 7 Oct 2026 14:06:40 -0500 Subject: [PATCH 2/7] test: pin FluentAssertions to the Apache-licensed 7.x line FluentAssertions 8 requires a commercial license. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JMzvDp6hZ4dnMSGobxdo98 --- .editorconfig | 4 +--- Directory.Packages.props | 3 ++- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.editorconfig b/.editorconfig index ee5ab19..79f8198 100644 --- a/.editorconfig +++ b/.editorconfig @@ -95,9 +95,7 @@ dotnet_style_predefined_type_for_locals_parameters_members = true:warning dotnet_style_predefined_type_for_member_access = true:warning # Modifier preferences # https://docs.microsoft.com/visualstudio/ide/editorconfig-language-conventions#normalize-modifiers -# for_non_interface_members matches what the .NET 9 SDK analyzers enforced; the .NET 10 SDK -# also flags interface members under "always". -dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning +dotnet_style_require_accessibility_modifiers = always:warning csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:warning visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:warning dotnet_style_readonly_field = true:warning diff --git a/Directory.Packages.props b/Directory.Packages.props index e0ce642..9a5743e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -33,7 +33,8 @@ - + + From 93389830d45101244ca6f8d40faa07a4650ee418 Mon Sep 17 00:00:00 2001 From: Jacob Atchley <10537213+JacobAtchley@users.noreply.github.com> Date: Wed, 7 Oct 2026 14:06:40 -0500 Subject: [PATCH 3/7] style: require accessibility modifiers on interface members Restores the always rule for dotnet_style_require_accessibility_modifiers and adds the explicit public modifiers the .NET 10 SDK now flags (IDE0040). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JMzvDp6hZ4dnMSGobxdo98 --- LitRedis.Core/Interfaces/ILitRedisCacheStore.cs | 14 +++++++------- LitRedis.Core/Interfaces/ILitRedisConnection.cs | 4 ++-- .../Interfaces/ILitRedisConnectionService.cs | 6 +++--- .../Interfaces/ILitRedisDistributedLock.cs | 6 +++--- .../Interfaces/ILitRedisDistributedLockService.cs | 2 +- .../Interfaces/ILitRedisJsonSerializer.cs | 4 ++-- .../ILitRedisSystemTextJsonOptionsProvider.cs | 2 +- 7 files changed, 19 insertions(+), 19 deletions(-) diff --git a/LitRedis.Core/Interfaces/ILitRedisCacheStore.cs b/LitRedis.Core/Interfaces/ILitRedisCacheStore.cs index a5102b9..a679b14 100644 --- a/LitRedis.Core/Interfaces/ILitRedisCacheStore.cs +++ b/LitRedis.Core/Interfaces/ILitRedisCacheStore.cs @@ -7,17 +7,17 @@ namespace LitRedis.Core.Interfaces; public interface ILitRedisCacheStore { - Task PutAsync(string key, T model, TimeSpan? expiry, CancellationToken cancellationToken); + public Task PutAsync(string key, T model, TimeSpan? expiry, CancellationToken cancellationToken); - Task GetAsync(string key, CancellationToken cancellationToken); + public Task GetAsync(string key, CancellationToken cancellationToken); - Task GetAsync(string key, CancellationToken cancellationToken); + public Task GetAsync(string key, CancellationToken cancellationToken); - Task ClearAsync(string key, CancellationToken cancellationToken); + public Task ClearAsync(string key, CancellationToken cancellationToken); - Task> GetAllKeys(CancellationToken cancellationToken); + public Task> GetAllKeys(CancellationToken cancellationToken); - Task ClearAllAsync(CancellationToken cancellationToken); + public Task ClearAllAsync(CancellationToken cancellationToken); - Task SetExpiryAsync(string key, TimeSpan span, CancellationToken cancellationToken); + public Task SetExpiryAsync(string key, TimeSpan span, CancellationToken cancellationToken); } diff --git a/LitRedis.Core/Interfaces/ILitRedisConnection.cs b/LitRedis.Core/Interfaces/ILitRedisConnection.cs index c0ab22a..b83ee66 100644 --- a/LitRedis.Core/Interfaces/ILitRedisConnection.cs +++ b/LitRedis.Core/Interfaces/ILitRedisConnection.cs @@ -5,7 +5,7 @@ namespace LitRedis.Core.Interfaces; public interface ILitRedisConnection { - void ForceReconnect(); + public void ForceReconnect(); - Task GetConnectionMultiplexer(); + public Task GetConnectionMultiplexer(); } diff --git a/LitRedis.Core/Interfaces/ILitRedisConnectionService.cs b/LitRedis.Core/Interfaces/ILitRedisConnectionService.cs index 874e8bd..e6cc464 100644 --- a/LitRedis.Core/Interfaces/ILitRedisConnectionService.cs +++ b/LitRedis.Core/Interfaces/ILitRedisConnectionService.cs @@ -7,9 +7,9 @@ namespace LitRedis.Core.Interfaces; public interface ILitRedisConnectionService { - Task UseDbAsync(Func> fn, CancellationToken cancellationToken); + public Task UseDbAsync(Func> fn, CancellationToken cancellationToken); - Task UseServerAsync(Func> fn, CancellationToken cancellationToken); + public Task UseServerAsync(Func> fn, CancellationToken cancellationToken); - Task UseServerAsync(Func fn, CancellationToken cancellationToken); + public Task UseServerAsync(Func fn, CancellationToken cancellationToken); } diff --git a/LitRedis.Core/Interfaces/ILitRedisDistributedLock.cs b/LitRedis.Core/Interfaces/ILitRedisDistributedLock.cs index 38cc352..1f7a6a5 100644 --- a/LitRedis.Core/Interfaces/ILitRedisDistributedLock.cs +++ b/LitRedis.Core/Interfaces/ILitRedisDistributedLock.cs @@ -6,18 +6,18 @@ namespace LitRedis.Core.Interfaces; public interface ILitRedisDistributedLock { - Task TakeLockAsync( + public Task TakeLockAsync( string key, string token, TimeSpan expiryTime, CancellationToken cancellationToken); - Task ReleaseLockAsync( + public Task ReleaseLockAsync( string key, string token, CancellationToken cancellationToken); - Task ExtendLockAsync( + public Task ExtendLockAsync( string key, string token, TimeSpan expiryTime, diff --git a/LitRedis.Core/Interfaces/ILitRedisDistributedLockService.cs b/LitRedis.Core/Interfaces/ILitRedisDistributedLockService.cs index 3feaacb..faa2b3a 100644 --- a/LitRedis.Core/Interfaces/ILitRedisDistributedLockService.cs +++ b/LitRedis.Core/Interfaces/ILitRedisDistributedLockService.cs @@ -6,5 +6,5 @@ namespace LitRedis.Core.Interfaces; public interface ILitRedisDistributedLockService { - Task AcquireLockAsync(RequestLockModel model, CancellationToken cancellationToken); + public Task AcquireLockAsync(RequestLockModel model, CancellationToken cancellationToken); } diff --git a/LitRedis.Core/Interfaces/ILitRedisJsonSerializer.cs b/LitRedis.Core/Interfaces/ILitRedisJsonSerializer.cs index ff37d22..87466f4 100644 --- a/LitRedis.Core/Interfaces/ILitRedisJsonSerializer.cs +++ b/LitRedis.Core/Interfaces/ILitRedisJsonSerializer.cs @@ -2,7 +2,7 @@ namespace LitRedis.Core.Interfaces; public interface ILitRedisJsonSerializer { - string Serialize(T value); + public string Serialize(T value); - T Deserialize(string value); + public T Deserialize(string value); } diff --git a/LitRedis.Core/Interfaces/ILitRedisSystemTextJsonOptionsProvider.cs b/LitRedis.Core/Interfaces/ILitRedisSystemTextJsonOptionsProvider.cs index 27dbfeb..dec0529 100644 --- a/LitRedis.Core/Interfaces/ILitRedisSystemTextJsonOptionsProvider.cs +++ b/LitRedis.Core/Interfaces/ILitRedisSystemTextJsonOptionsProvider.cs @@ -4,5 +4,5 @@ namespace LitRedis.Core.Interfaces; public interface ILitRedisSystemTextJsonOptionsProvider { - JsonSerializerOptions GetOptions(); + public JsonSerializerOptions GetOptions(); } From ce3de25e13e30ef4f03841ca4e0e304447f8936e Mon Sep 17 00:00:00 2001 From: Jacob Atchley <10537213+JacobAtchley@users.noreply.github.com> Date: Wed, 7 Oct 2026 14:06:40 -0500 Subject: [PATCH 4/7] chore: embed package icon and use MIT license expression Replaces the deprecated PackageLicenseUrl and PackageIconUrl (NU5125, NU5048) with PackageLicenseExpression and an embedded nuget-image.png, packed from Directory.Build.props for every project that sets PackageIcon. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JMzvDp6hZ4dnMSGobxdo98 --- Directory.Build.props | 5 +++++ LitRedis.Core/LitRedis.Core.csproj | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 43afb4b..5db5b8c 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -7,4 +7,9 @@ net10.0 + + + + + diff --git a/LitRedis.Core/LitRedis.Core.csproj b/LitRedis.Core/LitRedis.Core.csproj index be2fca8..6456189 100644 --- a/LitRedis.Core/LitRedis.Core.csproj +++ b/LitRedis.Core/LitRedis.Core.csproj @@ -13,9 +13,9 @@ firebend;redis;cache;caching;lock;distributed;locking Firebend https://github.com/firebend/lit-redis - https://github.com/firebend/lit-redis/blob/main/LICENSE + MIT https://github.com/firebend/lit-redis - https://github.com/firebend/lit-redis/blob/main/nuget-image.png?raw=true + nuget-image.png From dddab83ba91ac1fa5adf7525b69808b3b5090799 Mon Sep 17 00:00:00 2001 From: Jacob Atchley <10537213+JacobAtchley@users.noreply.github.com> Date: Wed, 7 Oct 2026 14:26:56 -0500 Subject: [PATCH 5/7] build: pin the .NET SDK with global.json global.json pins SDK 10.0.401 with rollForward latestPatch, so local builds and CI run the same analyzer rules and a new SDK feature band can no longer break dotnet format without a code change. CI reads global.json through setup-dotnet and still installs the 9.0 SDK for the net9.0 test runtime. The release workflow now sets up .NET before installing Versionize, so the dotnet tool install runs on the pinned SDK. Dependabot opens monthly dotnet-sdk updates for global.json. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JMzvDp6hZ4dnMSGobxdo98 --- .github/dependabot.yml | 7 +++++++ .github/workflows/build-and-test.yml | 6 +++--- .github/workflows/build-test-and-release.yml | 15 +++++++-------- global.json | 6 ++++++ 4 files changed, 23 insertions(+), 11 deletions(-) create mode 100644 global.json diff --git a/.github/dependabot.yml b/.github/dependabot.yml index eb80fbb..ab79c21 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -12,3 +12,10 @@ updates: commit-message: include: scope prefix: "fix: " + - package-ecosystem: "dotnet-sdk" + directory: "/" + schedule: + interval: "monthly" + commit-message: + include: scope + prefix: "chore: " diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index b68a019..0ef5697 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -18,9 +18,9 @@ jobs: - name: Setup .NET Core uses: actions/setup-dotnet@v4 with: - dotnet-version: | - 9.0.x - 10.0.x + # The .NET 9 SDK supplies the net9.0 runtime for tests; global.json pins the SDK that builds. + dotnet-version: 9.0.x + global-json-file: global.json - name: Run dotnet format run: dotnet format lit-redis.sln --verify-no-changes diff --git a/.github/workflows/build-test-and-release.yml b/.github/workflows/build-test-and-release.yml index dbc84e0..54b7886 100644 --- a/.github/workflows/build-test-and-release.yml +++ b/.github/workflows/build-test-and-release.yml @@ -17,6 +17,13 @@ jobs: token : ${{ secrets.FIREBEND_GIT_PUSH_PAT}} fetch-depth: 0 + - name: Setup .NET Core + uses: actions/setup-dotnet@v4 + with: + # The .NET 9 SDK supplies the net9.0 runtime for tests; global.json pins the SDK that builds. + dotnet-version: 9.0.x + global-json-file: global.json + - name: Install Versionize run: dotnet tool install --global Versionize @@ -34,14 +41,6 @@ jobs: if: steps.versionize.outcome != 'success' run: echo "Skipping publishing. No release required." - - name: Setup .NET Core - if: steps.versionize.outcome == 'success' - uses: actions/setup-dotnet@v4 - with: - dotnet-version: | - 9.0.x - 10.0.x - - name: Install dependencies if: steps.versionize.outcome == 'success' run: dotnet restore lit-redis.sln diff --git a/global.json b/global.json new file mode 100644 index 0000000..f3e8020 --- /dev/null +++ b/global.json @@ -0,0 +1,6 @@ +{ + "sdk": { + "version": "10.0.401", + "rollForward": "latestPatch" + } +} From d2767551cefe035fa60199b8eedb24f29d5d6cf2 Mon Sep 17 00:00:00 2001 From: Jacob Atchley <10537213+JacobAtchley@users.noreply.github.com> Date: Wed, 7 Oct 2026 16:15:14 -0500 Subject: [PATCH 6/7] docs: document the SDK pin and framework configuration Adds a Building from source section covering global.json and the FirebendTargetFrameworks and FirebendAppTargetFramework properties. Dependabot now ignores major updates to the framework-aligned Microsoft packages, which are versioned per target framework in Directory.Packages.props. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JMzvDp6hZ4dnMSGobxdo98 --- .github/dependabot.yml | 13 +++++++++++++ README.md | 6 ++++++ 2 files changed, 19 insertions(+) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index ab79c21..84e52b8 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -12,6 +12,19 @@ updates: commit-message: include: scope prefix: "fix: " + # Framework-aligned packages are versioned per target framework in Directory.Packages.props. + # A new major version means a new target framework block, which is added by hand. + ignore: + - dependency-name: "Microsoft.Extensions.*" + update-types: ["version-update:semver-major"] + - dependency-name: "Microsoft.EntityFrameworkCore*" + update-types: ["version-update:semver-major"] + - dependency-name: "Microsoft.AspNetCore.Authentication.JwtBearer" + update-types: ["version-update:semver-major"] + - dependency-name: "Microsoft.AspNetCore.JsonPatch" + update-types: ["version-update:semver-major"] + - dependency-name: "Microsoft.AspNetCore.Mvc.NewtonsoftJson" + update-types: ["version-update:semver-major"] - package-ecosystem: "dotnet-sdk" directory: "/" schedule: diff --git a/README.md b/README.md index 5df454e..6ba3f98 100644 --- a/README.md +++ b/README.md @@ -251,3 +251,9 @@ When the lock is lost, the release callback will _not_ attempt to release the ke If the background renewal loop repeatedly fails to extend the lock the library will mark the lock as lost. The number of consecutive extension failures tolerated before marking the lock lost is configurable via `RequestLockModel.MaxExtendRetries` (default: 3). You can set it fluently with `WithMaxExtendRetries(int)` on the request model. When a lock is marked lost the `Status` becomes `Lost` and `LockLostToken` will be cancelled so callers can react promptly. + +## Building from source + +- Building requires the .NET SDK version in [`global.json`](global.json) (10.0.401 or a later 10.0.4xx patch). Running the tests also needs the .NET 9 runtime. +- Target frameworks are set in [`Directory.Build.props`](Directory.Build.props). `FirebendTargetFrameworks` lists the frameworks every library and test project builds for (currently `net9.0;net10.0`). `FirebendAppTargetFramework` is the single framework for samples and other runnable apps. +- Package versions are managed centrally in [`Directory.Packages.props`](Directory.Packages.props). Microsoft framework packages have one version block per target framework, so adding a framework means adding it to `FirebendTargetFrameworks` and adding a matching block there. From 73a4e39e70feab4e0f8986776360ff4cb46b1cff Mon Sep 17 00:00:00 2001 From: Jacob Atchley <10537213+JacobAtchley@users.noreply.github.com> Date: Wed, 7 Oct 2026 16:18:21 -0500 Subject: [PATCH 7/7] test: assert the expiration passed to StringSetAsync Pin that a null expiry maps to Expiration.Default (a plain SET that clears any existing TTL, matching the 2.x expiry: null behavior) and not Expiration.KeepTtl, and that a set expiry maps to a relative Expiration of the same span. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01JMzvDp6hZ4dnMSGobxdo98 --- .../LitRedisCacheStoreTests.cs | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/LitRedis.Tests/Core/Implementations/LitRedisCacheStoreTests.cs b/LitRedis.Tests/Core/Implementations/LitRedisCacheStoreTests.cs index c768ca4..7d11b9e 100644 --- a/LitRedis.Tests/Core/Implementations/LitRedisCacheStoreTests.cs +++ b/LitRedis.Tests/Core/Implementations/LitRedisCacheStoreTests.cs @@ -100,4 +100,59 @@ public async Task Lit_Redis_Cache_Store_Should_Put_Value_Object() mockRedisConnectionService.Verify(x => x.UseDbAsync(It.IsAny>>(), It.IsAny()), Times.Once); } + + [TestMethod] + public async Task Lit_Redis_Cache_Store_Should_Put_Without_Expiry_When_Expiry_Is_Null() + { + //act + var expiration = await PutAndCaptureExpirationAsync(null); + + //assert + // Expiration.Default sends a plain SET, which clears any existing TTL (it is not KEEPTTL). + expiration.Should().Be(Expiration.Default); + expiration.Should().NotBe(Expiration.KeepTtl); + } + + [TestMethod] + public async Task Lit_Redis_Cache_Store_Should_Put_With_Relative_Expiry_When_Expiry_Is_Set() + { + //arrange + var expiry = TimeSpan.FromMinutes(5); + + //act + var expiration = await PutAndCaptureExpirationAsync(expiry); + + //assert + expiration.Should().Be(new Expiration(expiry)); + } + + private static async Task PutAndCaptureExpirationAsync(TimeSpan? expiry) + { + var fixture = new Fixture(); + fixture.Customize(new AutoMoqCustomization()); + + var mockRedisConnectionService = fixture.Freeze>(); + var mockDatabase = new Mock(); + Expiration? captured = null; + + mockDatabase.Setup(x => x.StringSetAsync( + It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) + .Callback((_, _, expiration, _, _) => captured = expiration) + .ReturnsAsync(true); + + mockRedisConnectionService.Setup(x => + x.UseDbAsync(It.IsAny>>(), It.IsAny())) + .Returns>, CancellationToken>((func, ct) => func(mockDatabase.Object, ct)); + + var cacheStore = fixture.Create(); + + await cacheStore.PutAsync("fake-key", new FakeCacheObject { Value = "Fakey Fake" }, expiry, default); + + mockDatabase.Verify(x => x.StringSetAsync( + "fake-key", It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()), Times.Once); + + captured.Should().NotBeNull(); + + return captured.Value; + } }