Repository navigation
JasperFx 2.74.0
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
AutoCreateXML comments now describe what Weasel actually does (#873).CreateOrUpdatewas documented as purely additive; itsUpdatedelta drops columns, indexes and foreign keys the model no longer declares.Nonewas documented as throwing on drift; it does not — the lazy path returns untouched, the explicit apply paths (db-apply,resources setup,ApplyAllConfiguredChangesToDatabaseAsync) coerce it toCreateOrUpdate, and onlyAssertDatabaseMatchesConfigurationAsync/db-assertreports 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 toCaseSensitive); Polecat and Fisher do not have it and are split down the middle — the database-per-tenant lookup is case-insensitive while the conjoinedtenant_idcomparison 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.mdnow records thatTypeLoadMode.Autodegrades to the static loader inside a Native AOT image while an explicitTypeLoadMode.DynamicthrowsPlatformNotSupportedException. 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.