WopiHost.AzureLockProvider 8.0.0

dotnet add package WopiHost.AzureLockProvider --version 8.0.0
                    
NuGet\Install-Package WopiHost.AzureLockProvider -Version 8.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="WopiHost.AzureLockProvider" Version="8.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="WopiHost.AzureLockProvider" Version="8.0.0" />
                    
Directory.Packages.props
<PackageReference Include="WopiHost.AzureLockProvider" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add WopiHost.AzureLockProvider --version 8.0.0
                    
#r "nuget: WopiHost.AzureLockProvider, 8.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package WopiHost.AzureLockProvider@8.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=WopiHost.AzureLockProvider&version=8.0.0
                    
Install as a Cake Addin
#tool nuget:?package=WopiHost.AzureLockProvider&version=8.0.0
                    
Install as a Cake Tool

WopiHost.AzureLockProvider

NuGet NuGet

Distributed IWopiLockProvider backed by Azure Blob leases. Use this in place of WopiHost.MemoryLockProvider when you run more than one WopiHost instance and need them to agree on who is currently editing a file.

Install

dotnet add package WopiHost.AzureLockProvider

Configure

"Wopi": {
  "LockProvider": {
    "ConnectionString": "UseDevelopmentStorage=true",
    "ContainerName": "wopi-locks"
  }
}
"Wopi": {
  "LockProvider": {
    "ServiceUri": "https://my-account.blob.core.windows.net",
    "ContainerName": "wopi-locks"
  }
}

The lock container is dedicated and separate from your content blobs — that keeps lock churn out of the hot data path and lets you put locks in a different storage account if you want.

Register

builder.Services.AddAzureLockProvider(builder.Configuration);
builder.Services.AddWopi(o =>
{
    o.ClientUrl = new Uri("https://your-office-online-server.com");
});

The runnable sample exposes a small Sample:LockProvider discriminator (Memory / Azure / Redis) and dispatches to the chosen provider's typed extension — see sample/WopiHost/ServiceCollectionExtensions.cs.

How it works

For every fileId, the provider holds a placeholder blob named SHA256(fileId) in the configured container. Two pieces of state coexist:

  1. An infinite-duration Azure blob lease — provides true cross-instance mutual exclusion. Only one WopiHost instance can hold the lease at a time.
  2. Blob metadata — carries the WOPI-level state visible to any instance that can read the blob:
Metadata key Meaning
wopi_lock_id The client-supplied WOPI lock id (any string).
wopi_lease_id The Azure lease GUID — needed by remote instances to renew or release the lease.
wopi_created ISO-8601 timestamp; honours the WOPI 30-minute auto-expiry.

Why both? The lease provides "is this lock physically held right now"; the metadata provides "who claims it and when did the claim start". An instance handling a RefreshLock or Unlock reads the metadata to recover the lease GUID stored by whichever instance originally created the lock.

Crash recovery

If the WopiHost instance that created a lock dies without releasing, the infinite lease persists. The next GetLock or AddLock call against that fileId notices the metadata indicates a >30-minute-old claim, breaks the lease, and either evicts (GetLock) or takes over (AddLock). This matches the WOPI specification's 30-minute lock auto-expiry without requiring a background sweeper.

UnlockAndRelock atomicity

TryUnlockAndRelockAsync reads the current blob metadata + ETag, validates the caller's expectedExistingLockId against the stored value (through the configured IWopiLockComparer), and writes the new metadata under both an IfMatch=etag precondition and the existing lease. The ETag changes on every metadata mutation, so a concurrent UnlockAndRelock from a different instance landing first turns this call into a 412 Precondition Failed and returns false instead of silently overwriting the other instance's lock.

Lock-id comparison

The provider takes an optional IWopiLockComparer constructor parameter, defaulting to OrdinalWopiLockComparer.Instance (byte-exact). If your WOPI client mutates lock ids between round-trips (the canonical case is OOS / M365-for-the-Web's JSON-format locks), wire JsonShapedWopiLockComparer (or your own implementation) via DI:

services.Replace(ServiceDescriptor.Singleton<IWopiLockComparer, JsonShapedWopiLockComparer>());

See the WopiHost.Abstractions README for the trade-offs.

Caveats

  • Latency: every WOPI lock op is one or two round-trips to Azure Blob. For high-volume editing scenarios, measure carefully.
  • Costs: each lock acquire/refresh/release is a billable Azure Storage operation.
  • Storage cleanup: RemoveLock deletes the placeholder blob; expired locks are also evicted on observation. There is no separate cleanup process.

Local development

Works against Azurite. The repo's tests use Testcontainers to spin up Azurite per test run; production hosts will typically use Azurite via Aspire's AddAzureStorage().RunAsEmulator() integration (see WopiHost.AppHost).

License

See the repo README.

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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 198 5/14/2026
7.0.0 238 5/6/2026
6.0.0 141 5/3/2026