Skip to content

JasperFx 2.74.0

Choose a tag to compare

@jeremydmiller jeremydmiller released this 22 Sep 17:39

An exception-diagnostics release. Seven issues from the 2026-09-22 sweep, all of them about the same thing: a Critter Stack failure naming the fact and stopping, where it could have named the remedy — plus one duplicate type name that made a catch ambiguous.

Nothing here changes how any store reads or writes data.

Two exceptions lifted onto the shared surface

JasperFx.Events.ArchivedStreamException (#871). Three stores refused an append to an archived stream with three unrelated types — Marten's generic InvalidStreamOperationException, Polecat's InvalidStreamException, Fisher's own ArchivedStreamException — and none was on the shared surface. Fisher's shape is the canonical one, because only its message names the way back:

Event stream '{id}' is archived and cannot be appended to. Call UnArchiveStream to reopen it, or start a new stream.

JasperFx.MultiTenancy.DisabledTenantException (#875). A tenant that exists and was deliberately disabled is reported as unknown by Marten and Polecat, so the operator who just ran CritterWatch's disable_tenant — or the on-call engineer after them — reads "Unknown tenant id 'acme'" about a tenant that is still there:

Tenant '{tenantId}' is registered but disabled, so this store will not open a session for it. Re-enable it through the tenancy source that owns it; its data is untouched.

It derives from UnknownTenantIdException, so an existing catch (UnknownTenantIdException) keeps catching a disabled tenant and a store can adopt it without a breaking change.

Both carry a protected message-overriding constructor, the same seam the six exceptions lifted in #751 use, so a store whose wording diverged can subclass without breaking its own message assertions.

Messages that now say what to do

ExistingStreamIdCollisionException and NonExistentStreamException (#872). Polecat and Fisher inherit these through their subclasses, so both pick the remediation up without a change on their side; Marten keeps its own wording.

The second one is the costly case. Plain Append is deliberately start-or-append on every store, so the only way to reach the exception is AppendOptimistic / AppendExclusive — and the bare fact reads as "Append needs StartStream first", sending people to change code that was fine. The message now names the overloads that actually require an existing stream.

UnknownTenantIdException (#874). The canonical message gains a remedy sentence covering the three things it could mean — never registered, registered under a different casing, or disabled — and a new UnknownTenantIdException(string tenantId, IReadOnlyCollection<string>? knownTenantIds) overload lets a source that holds its tenants in memory list them: It knows: one, two. Sources that would have to hit the database pass null.

⚠ Behaviour change: StaticTenantSource<T>.FindAsync now throws UnknownTenantIdException instead of ArgumentOutOfRangeException. Every store already threw the shared type for this condition; the one tenancy source JasperFx ships itself did not, so a caller catching the shared type was missing the shared implementation. If you catch ArgumentOutOfRangeException around StaticTenantSource.FindAsync, change it.

One name, not two

JasperFx.RuntimeCompiler.CodeGenerationException now derives from JasperFx.CodeGeneration.CodeGenerationException (#877). Two public classes shared the simple name, so catch (CodeGenerationException) caught whichever one a file's using directives happened to resolve, and a stack trace naming it was ambiguous. Deriving — the RuntimeCompiler one is the same class of failure one layer down — makes a single catch cover both, with no rename and no obsoletion cycle. It also exposes Subject now.

Compliance

ComplianceExceptionKind.ArchivedStream joins the six existing categories, defaulting to ArchivedStreamException, and StreamArchivingCompliance.appending_to_an_archived_stream_is_rejected asserts the nominated type instead of "something threw".

⚠ For store maintainers: that assertion has teeth now. A store that neither subclasses ArchivedStreamException nor points ExceptionTypeFor(ComplianceExceptionKind.ArchivedStream) at its own type will fail that one compliance fact. Marten will always name its own type there — all_exceptions_should_derive_from_MartenException plus single inheritance — which is exactly what the seam exists for.

Documentation

  • AutoCreate XML comments now describe what Weasel actually does (#873). CreateOrUpdate was documented as purely additive; its Update delta drops columns, indexes and foreign keys the model no longer declares. None was documented as throwing on drift; it does not — the lazy path returns untouched, the explicit apply paths (db-apply, resources setup, ApplyAllConfiguredChangesToDatabaseAsync) coerce it to CreateOrUpdate, and only AssertDatabaseMatchesConfigurationAsync / db-assert reports drift. Both wordings came from Weasel's and Marten's own docs, so the same correction is queued there.
  • Tenant id casing, all four behaviours in one place (#876, docs). Marten and Wolverine honour TenantIdStyle (both defaulting to CaseSensitive); Polecat and Fisher do not have it and are split down the middle — the database-per-tenant lookup is case-insensitive while the conjoined tenant_id comparison is exact, so a mixed-case id finds the right database and then writes rows the other spelling cannot see. Until every store honours one knob, that table is the specification. Normalize tenant ids at the edge of your own system.
  • AOT: docs/codegen/aot.md now records that TypeLoadMode.Auto degrades to the static loader inside a Native AOT image while an explicit TypeLoadMode.Dynamic throws PlatformNotSupportedException. No code change — the behaviour was already deliberate.

Packages

Seven packages at 2.74.0: JasperFx, JasperFx.Events, JasperFx.Events.ComplianceTests, JasperFx.Events.SourceGenerator, JasperFx.SourceGenerator, JasperFx.Aspire, JasperFx.Events.MicrosoftExtensionsAI. JasperFx.RuntimeCompiler is versioned independently and stays at 5.0.0.

Note: 2.73.2 was published to NuGet without a tag or a GitHub release; this release does not restate it. Nothing shipped in 2.73.2 is missing from 2.74.0.