Describe the bug
With a Redis distributed locker and L2 configured, a GetOrSetAsync factory succeeds but its result cannot be stored in a size-limited memory cache because the entry has no Size.
The expected InvalidOperationException is thrown, but the distributed lock acquired before the factory remains held after the call completes.
To Reproduce
Requires the .NET 10 SDK and a running Docker-compatible engine. Testcontainers starts an isolated Redis instance on a random host port and removes it after the run. No manually started Redis or connection string is needed.
Save these two files in an empty directory.
Repro.csproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="ZiggyCreatures.FusionCache" Version="2.8.0" />
<PackageReference Include="ZiggyCreatures.FusionCache.Locking.Distributed.Redis" Version="2.8.0" />
<PackageReference Include="ZiggyCreatures.FusionCache.Serialization.SystemTextJson" Version="2.8.0" />
<PackageReference Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="10.0.11" />
<PackageReference Include="Testcontainers.Redis" Version="4.14.0" />
</ItemGroup>
</Project>
Program.cs:
using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Caching.StackExchangeRedis;
using StackExchange.Redis;
using Testcontainers.Redis;
using ZiggyCreatures.Caching.Fusion;
using ZiggyCreatures.Caching.Fusion.Locking.Distributed.Redis;
using ZiggyCreatures.Caching.Fusion.Serialization.SystemTextJson;
await using var redis = new RedisBuilder("redis:7-alpine").Build();
await redis.StartAsync();
using var connection = await ConnectionMultiplexer.ConnectAsync(redis.GetConnectionString());
using var locker = new RedisDistributedLocker(new RedisDistributedLockerOptions
{
ConnectionMultiplexerFactory = () => Task.FromResult<IConnectionMultiplexer>(connection)
}, null);
using var memory = new MemoryCache(new MemoryCacheOptions { SizeLimit = 100 });
using var l2 = new RedisCache(new RedisCacheOptions
{
ConnectionMultiplexerFactory = () => Task.FromResult<IConnectionMultiplexer>(connection)
});
using var cache = new FusionCache(new FusionCacheOptions(), memory);
cache.SetupDistributedLocker(locker);
cache.SetupDistributedCache(l2, new FusionCacheSystemTextJsonSerializer());
// Run with --control to supply the required entry size.
var options = new FusionCacheEntryOptions();
if (args.Contains("--control")) options.Size = 1;
var key = $"memory-write-{Guid.NewGuid():N}";
var server = connection.GetServer(connection.GetEndPoints()[0]);
RedisKey lockKey = default;
try
{
await cache.GetOrSetAsync<int>(key, (_, _) =>
{
// Confirm acquisition for this key before the memory write.
lockKey = server.Keys(pattern: $"*{key}*:lock").Single();
return Task.FromResult(42);
}, options: options);
Console.WriteLine("Memory write succeeded");
}
catch (InvalidOperationException)
{
Console.WriteLine("Memory write failed: entry size is required");
}
bool held = await connection.GetDatabase().KeyExistsAsync(lockKey);
Console.WriteLine($"Distributed lock still held: {held} (expected False)");
Environment.ExitCode = held ? 1 : 0;
Run:
dotnet run --project Repro.csproj
Observed output, excluding Testcontainers lifecycle logs:
Memory write failed: entry size is required
Distributed lock still held: True (expected False)
The reproduction exits with code 1 when it detects the bug. The same program should exit with code 0 once the behavior is corrected.
Run the control:
dotnet run --project Repro.csproj -- --control
The control changes only the entry options by setting Size = 1, which lets the memory write succeed. It runs in a fresh container.
Observed control output, exit code 0:
Memory write succeeded
Distributed lock still held: False (expected False)
Versions
- FusionCache: 2.8.0, official unmodified package.
- Redis distributed locker: 2.8.0.
- .NET: 10.0.
- Host OS: macOS 26.6.2.
- Redis: redis:7-alpine, running locally in Docker.
- Integration tests use Redis Testcontainers.
Describe the bug
With a Redis distributed locker and L2 configured, a
GetOrSetAsyncfactory succeeds but its result cannot be stored in a size-limited memory cache because the entry has noSize.The expected
InvalidOperationExceptionis thrown, but the distributed lock acquired before the factory remains held after the call completes.To Reproduce
Requires the .NET 10 SDK and a running Docker-compatible engine. Testcontainers starts an isolated Redis instance on a random host port and removes it after the run. No manually started Redis or connection string is needed.
Save these two files in an empty directory.
Repro.csproj:Program.cs:Run:
Observed output, excluding Testcontainers lifecycle logs:
The reproduction exits with code
1when it detects the bug. The same program should exit with code0once the behavior is corrected.Run the control:
The control changes only the entry options by setting
Size = 1, which lets the memory write succeed. It runs in a fresh container.Observed control output, exit code
0:Versions