WopiHost.RedisLockProvider
8.0.0
dotnet add package WopiHost.RedisLockProvider --version 8.0.0
NuGet\Install-Package WopiHost.RedisLockProvider -Version 8.0.0
<PackageReference Include="WopiHost.RedisLockProvider" Version="8.0.0" />
<PackageVersion Include="WopiHost.RedisLockProvider" Version="8.0.0" />
<PackageReference Include="WopiHost.RedisLockProvider" />
paket add WopiHost.RedisLockProvider --version 8.0.0
#r "nuget: WopiHost.RedisLockProvider, 8.0.0"
#:package WopiHost.RedisLockProvider@8.0.0
#addin nuget:?package=WopiHost.RedisLockProvider&version=8.0.0
#tool nuget:?package=WopiHost.RedisLockProvider&version=8.0.0
WopiHost.RedisLockProvider
IWopiLockProvider implementation backed by Redis, with atomic compare-and-swap via Lua scripts and TTL-driven WOPI expiry.
When to pick this over the other lock providers
| Provider | Cross-process? | Cross-instance? | Operational complexity | When |
|---|---|---|---|---|
MemoryLockProvider |
❌ | ❌ | None | Single-process development; smoke tests. |
WopiAzureLockProvider |
✅ | ✅ (Azure-coordinated blob leases) | Azure Storage account | Multi-region Azure deployments; strongest exclusion. |
WopiRedisLockProvider |
✅ | ✅ (best-effort, single Redis) | Redis container | Most real-world cases — many shops already run Redis for session state / cache and would rather not adopt Azure Blob just for WOPI locks. |
Best-effort, not Redlock
This provider deliberately does not implement Redlock (lock acquired against a majority of independent Redis nodes). The reasoning:
- WOPI lock semantics are advisory — the 30-minute server-side TTL is the safety net, not the lock itself. The spec already expects that a lock can be "lost" via expiry without coordination.
- Redlock is operationally heavy (independent Redis nodes, clock-skew bounds, fencing tokens) and famously controversial. The cost isn't justified for advisory locks with a known expiry contract.
- For deployments that need stronger cross-region exclusion, prefer
WopiAzureLockProvider.
If your Redis instance fails over to a replica with stale state mid-WOPI-session, the worst-case outcome is the same as any other lock provider after the 30-minute window expires: the editor reports a lock conflict and the user can re-acquire. No data loss.
Atomicity
Each non-trivial operation is a Lua script evaluated on the Redis server with EVAL. Lua scripts run to completion without interleaving with other commands, so the "match-then-mutate" steps land as a single observable transaction. The conformance suite's RefreshLockAsync_ConcurrentSwapBetweenObservationAndCAS_DoesNotRefresh test exercises this path against this provider too — a stale caller's expected lock id no longer matches the Redis-resident value when the script runs.
Registration
services.AddRedisLockProvider(builder.Configuration);
The runnable sample's Sample:LockProvider discriminator (Memory / Azure / Redis) dispatches to the typed extension above — see sample/WopiHost/ServiceCollectionExtensions.cs.
Reads Wopi:LockProvider:ConnectionString for the StackExchange.Redis connection string. If an IConnectionMultiplexer is already registered in DI (e.g. via Aspire's builder.AddRedisClient("wopi-locks")), that wins so a single multiplexer is reused across the process.
// appsettings.json
"Wopi": {
"LockProvider": {
"ConnectionString": "localhost:6379",
"KeyPrefix": "wopi:lock:" // optional; default shown
}
}
Interface contract
IWopiLockProvider from WopiHost.Abstractions:
Task<WopiLockInfo?> GetLockAsync(string fileId, CancellationToken ct = default);
Task<WopiLockInfo?> AddLockAsync(string fileId, string lockId, CancellationToken ct = default);
Task<bool> RefreshLockAsync(string fileId, string expectedExistingLockId, CancellationToken ct = default);
Task<bool> RemoveLockAsync(string fileId, CancellationToken ct = default);
Task<bool> TryUnlockAndRelockAsync(string fileId, string newLockId, string expectedExistingLockId, CancellationToken ct = default);
Behaviour is validated against the shared LockProviderConformanceTests in WopiHost.Abstractions.Testing — the same harness MemoryLockProvider and WopiAzureLockProvider run through, so all three are bound to the same contract.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.Extensions.Configuration.Binder (>= 10.0.8)
- Microsoft.Extensions.DependencyInjection (>= 10.0.8)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.8)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.8)
- StackExchange.Redis (>= 2.9.32)
- WopiHost.Abstractions (>= 8.0.0)
-
net8.0
- Microsoft.Extensions.Configuration.Binder (>= 10.0.8)
- Microsoft.Extensions.DependencyInjection (>= 10.0.8)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.8)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.8)
- StackExchange.Redis (>= 2.9.32)
- WopiHost.Abstractions (>= 8.0.0)
-
net9.0
- Microsoft.Extensions.Configuration.Binder (>= 10.0.8)
- Microsoft.Extensions.DependencyInjection (>= 10.0.8)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.8)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.8)
- StackExchange.Redis (>= 2.9.32)
- WopiHost.Abstractions (>= 8.0.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 8.0.0 | 93 | 5/14/2026 |