9.0.0
9.0.0
Highlights
This is the Minimal-API release. The entire WOPI surface has moved off MVC controllers onto ASP.NET Core Minimal APIs (#438) — wired with a single app.MapWopiEndpoints(), with the override-multiplexed POST {id} routes dispatched through a custom matcher policy so each X-WOPI-Override keeps its own auth policy. Both sample frontends were rebuilt on Razor Components (#470, #479).
Around that core sits a big automation & supply-chain investment — CodeQL scanning, an agentic Claude PR-reviewer, a fork-friendly conditional Cobalt build, grouped Dependabot PRs, and a self-improving codebase-audit skill — plus cross-platform IWopiFile.Owner, a path-traversal hardening, and a Redis lock-eviction fix.
net8.0/net9.0 multitargeting — the libraries are net10.0-only (#432). The breaking surface is smaller than 8.0 and concentrated in the host wiring (controllers → endpoints), the host-customization seam (no more HttpContext), and PascalCase header/auth constants. See Migration guide below.
✨ New
- WOPI endpoints as Minimal APIs (#438, closes #430) — controllers retired in favour of
app.MapWopiEndpoints(); faster startup, trimmer-friendlier, and eachX-WOPI-Overrideroute gets its own authorization policy viaWopiOverrideMatcherPolicy. A minimal-API idiom audit followed in #470. - Razor Components sample frontends (#470, #479) —
WopiHost.WebandWopiHost.Web.Oidcmigrated from MVC/Razor Pages to Razor Components, with full OIDC sign-in / sign-out flow coverage (#481). IWopiFile.Owneron every platform (#499) — owner resolution is no longer Windows-only, with a macOSpasswd-struct SIGSEGV fixed along the way (#506).- Storage-provider conformance suite (#512) —
WopiHost.Abstractions.Testingnow shipsStorageProviderConformanceTestsalongside the existing lock-provider suite, so everyIWopiStorageProviderruns the same battery. Adding a provider = one sealed subclass. - Collabora end-to-end test suite (#435, closes #357 Approach B) — real-browser editing against a live Collabora container via
Aspire.Hosting.Testing+ Playwright. - CI / automation overhaul —
- PR auto-labeler, CodeQL analysis, and a conditional Cobalt build so forks without the private feed build the rest of the solution cleanly (#489).
- Agentic PR review via the Claude Code action (#486, #524, #525) — internal PRs reviewed as
claude[bot],@claudeon-demand for forks. - Dependabot minor/patch updates grouped into bulk PRs (#523) — far less PR noise.
codebase-auditskill (#516, #518, #528) — a repeatable, self-improving health-check sweeping duplication, architectural drift, .NET design-guideline conformance, security and library-hygiene vectors, and WOPI-spec compliance.
🐛 Fixes
- Path-traversal gap in file/container creation (#530) —
CreateWopiChildFile/CreateWopiChildContainernow run the same single-segment name validation as the rename/suggested-name paths, rejecting../and separator characters. PutRelativeFilereturns 501 on unauthorized overwrite (#472) — spec-correct response when the target exists and overwrite isn't permitted.- Redis lock provider proactively evicts expired locks on the CAS read path (#491).
BlobIdMapthread-safety race fixed (#521).- Collabora domain regex narrowed from
.*tohost.docker.internal.*(#490). - FileSystem storage provider aligned on Singleton DI lifetime (#507); storage providers register with
TryAddfor a clean host-override path (#509). - Audit-driven correctness batches from #456 / #457 — #464, #474, #484, with added proof-validation integration coverage.
🧹 Refactors & internals
HttpContextdropped from customization seams (#476) —IWopiHostExtensionshooks now receive a typed context record carrying theClaimsPrincipaland resource, not the rawHttpContext. (See breaking changes.)- Shared sample code extracted to
WopiHost.Web.Shared(#487) — view models +WopiOptionsdeduplicated across the frontends. - Provider config/options consistency (#510) and AppHost Redis switched to Session lifetime (#478).
- Spec-helper extractions —
EnsureExactlyOneOffor mutex headers (#477),ValidatePutFileLock(#513), bounded-bodyReadBytesAsyncoverload (#492), inlined token-issuance helpers to keep Infer# clean (#483). - Codebase-wide comment cleanup + enforced comment-style rules (#508); documented ProblemDetails /
UseStatusCodePagesstrategy (#482).
🔧 Maintenance, CI & dependencies
- Target frameworks: dropped
net8.0/net9.0multitargeting — libraries arenet10.0-only (#432). - Package-validation baseline restored to 7.0.0 for the 8.0.0 cut, then bumped to 8.0.0 with the post-release auto-bump hardened against a race (#431, #434).
- Test-package management centralized into the root
Directory.Packages.props(#465); test-package bumps Dependabot wrongly auto-closed were re-applied (#445, #453, #463). src/line coverage raised 95.7% → 97.7% (#433).- Dependency bumps (consolidated): Aspire.Hosting.*
13.3.2 → 13.4.2, StackExchange.Redis2.9.32 → 2.13.17, Azure.Storage.Blobs12.28.0 → 12.29.0, Microsoft.IdentityModel.Tokens / System.IdentityModel.Tokens.Jwt8.18 → 8.19.1, Microsoft.Playwright1.59 → 1.60, Microsoft.NET.Test.Sdk18.5.1 → 18.6.0, Scalar.AspNetCore2.14.11 → 2.14.14,actions/labeler 5 → 6,actions/checkout 4 → 6.
💥 Breaking changes
| Before (8.x) | After (9.0) | PR | |
|---|---|---|---|
| Target framework | net8.0 / net9.0 / net10.0 |
net10.0 only |
#432 |
| WOPI route registration | MVC controllers (app.MapControllers()) |
Minimal APIs (app.MapWopiEndpoints()) |
#438 |
| Host-customization seam | hooks received HttpContext |
hooks receive a context record (ClaimsPrincipal? User, resource, payload) |
#476 |
| Header constants | WopiHeaders.WOPI_OVERRIDE, ITEM_VERSION, PROOF, TIMESTAMP, … (SCREAMING_SNAKE) |
WopiOverride, ItemVersion, Proof, Timestamp, … (PascalCase) — values unchanged |
#529 |
| Auth constants | AccessTokenDefaults.AUTHENTICATION_SCHEME, ACCESS_TOKEN_QUERY_NAME |
AuthenticationScheme, AccessTokenQueryName |
#529 |
| FileSystem provider DI lifetime | as-registered | Singleton | #507 |
Migration guide
1. Target framework
WopiHost 9.x runs on .NET 10 only. Multitargeting back to net8.0/net9.0 is gone — stay on the 8.x line if you can't move to net10 yet.
2. Endpoint registration (controllers → Minimal APIs)
The WOPI surface is no longer MVC controllers. Map the endpoints instead, and drop any AddControllers() / AddMvc() you only added for WopiHost:
var builder = WebApplication.CreateBuilder(args);
-builder.Services.AddControllers();
builder.Services.AddWopi();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
-app.MapControllers();
+app.MapWopiEndpoints();3. Host-customization seam (no more HttpContext)
IWopiHostExtensions hooks now take a typed context record exposing the authenticated ClaimsPrincipal and the resource — not the raw HttpContext:
public sealed class MyHostExtensions : IWopiHostExtensions
{
- public Task<WopiCheckFileInfo> OnCheckFileInfoAsync(WopiCheckFileInfo cfi, HttpContext ctx, CancellationToken ct)
- => ApplyTenantOverrides(cfi, ctx.User, ct);
+ public Task<WopiCheckFileInfo> OnCheckFileInfoAsync(WopiCheckFileInfoContext context, CancellationToken ct = default)
+ => ApplyTenantOverrides(context.CheckFileInfo, context.User, ct);
}WopiCheckFileInfoContext is (ClaimsPrincipal? User, IWopiFile File, WopiCheckFileInfo CheckFileInfo); the container/folder/ecosystem hooks follow the same shape. If you relied on something only reachable from HttpContext, surface it through DI instead.
4. Renamed constants (PascalCase)
Public header/auth constants were renamed to satisfy CA1707. Values are identical — only the symbol names changed, so it's a recompile, not a behavior change:
-context.Request.Headers[WopiHeaders.WOPI_OVERRIDE]
+context.Request.Headers[WopiHeaders.WopiOverride]
-AccessTokenDefaults.AUTHENTICATION_SCHEME
+AccessTokenDefaults.AuthenticationSchemeThe same SCREAMING_SNAKE → PascalCase mapping applies across all of WopiHeaders (e.g. ITEM_VERSION → ItemVersion, PROOF → Proof, TIMESTAMP → Timestamp) and AccessTokenDefaults (ACCESS_TOKEN_QUERY_NAME → AccessTokenQueryName).
5. FileSystem provider lifetime
AddFileSystemStorageProvider now registers the provider as a Singleton via TryAdd. If you pre-register your own IWopiStorageProvider, yours still wins; if you depended on per-request (scoped) state inside the default provider, move that state out — the instance is now shared process-wide.
Full Changelog: 8.0.0...9.0.0