NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
NuGet · #1580 most downloaded on NuGet
Foundational helpers and command line support used by JasperFx and the Critter Stack projects
Last release 2 days ago
05 Oct 2026
Ships on a steady schedule
a new release about every 9 days
Most releases are documented
notes for 40 of the last 60 stable releases
3 versions withdrawn
withdrawn after publishing
127 years old
244 releases · first in 1900
⚠️ Behaviour change: daemon drains are bounded by StopAndDrainTimeout
StopAndDrainTimeoutStopAndDrainAsync now honours DaemonSettings.StopAndDrainTimeout (default 5 seconds) for both event subscriptions (#953) and async projection shards (#980).
What this means for Marten, Polecat and Fisher users:
ProgressionProgressOutOfOrderException and a Stopped shard.StopAndDrainTimeout is cancelled on every shutdown or rebalance and then run again. Delivery was always at-least-once, but duplicates will now happen more often for slow pages. Side effects outside the store session aren't rolled back, such as HTTP calls or sends that bypass the outbox. If your pages are slow, raise StopAndDrainTimeout.ProcessRangeAsync directly are unaffected.Downstream adoption: JasperFx/marten#5610, JasperFx/polecat#744, JasperFx/fisher#434.
JasperFx.Events.InMemory, a store for prototyping (#962)A new package with an in-memory document and event store, meant for short-lived prototyping, before you pick Marten, Polecat or Fisher. It is not meant for production use.
SaveChangesAsync is now all-or-nothing.StartStream/append, FetchForWriting/FetchLatest, live aggregation, and inline single- and multi-stream projections. It is validated against the event compliance suites.AddInMemoryStoreForPrototyping(...) with ConfigureDocuments, ConfigureEvents and ConfigureProjections.NotSupportedException.Command<T>(), Automation(..), View<T>(), Translation(..), Slice(name) and InChapter (#957).Origin on its slices (#959).[Domain] on handlers and endpoints (#960).SourceWriter output: no blank line before }, else, catch or the end of the file. END is only recognised as a whole directive, and there is now a way to emit a backtick (#956).ShardName construction is ~3–8x faster and TryParse ~2–4x faster, both with roughly half the allocations, which matters on stores with thousands of tenant shards (#952).BlockIdleWakeupTests inline-execution assertion (#973), and one in the BlueGreen gate test harness (#983).main and actually builds the docs (#794).Full Changelog: V2.80.1...V2.81.0
One column per quarter.
Nothing published for this version
ResilientEventLoader now wraps load failures in EventLoaderException ( #948 , #949 ). LoadAsync returned the Polly pipeline's task without awaiting it
ResilientEventLoader now wraps load failures in EventLoaderException (#948, #949). LoadAsync returned the Polly pipeline's task without awaiting it, and Polly captures even a synchronous throw into that task, so the wrap never ran at all. Shards saw the raw provider exception, without the shard name and database identifier. The method now awaits inside its try.
OperationCanceledException, so daemon shutdown paths keep treating it as shutdown, not as a shard failure.EventLoaderException (e.g. Marten's read policy and WriteRetryClassifier) will now see it on this path for the first time.Full Changelog: V2.80.0...V2.80.1
ValueTypeInfo strong-typed id wrappers now work under Native AOT ( #942 , #946 ). CreateWrapper<TOuter, TInner>() and UnWrapper<TOuter, TInner>() prev
ValueTypeInfo strong-typed id wrappers now work under Native AOT (#942, #946). CreateWrapper<TOuter, TInner>() and UnWrapper<TOuter, TInner>() previously always compiled via FastExpressionCompiler and threw PlatformNotSupportedException in a native image. They now branch on RuntimeFeature.IsDynamicCodeSupported, like LambdaBuilder, and fall back to invoking the resolved constructor, static builder, or value property through reflection. JIT behaviour is unchanged.JasperFx.Events.ComplianceTests)DocumentSearchCompliance gains hierarchy facts (#944, #945). Two new ComplianceMemory sub-classes and four facts, gated on SupportsDocumentHierarchies plus the search flags: sub-class vector search, root vector search, sub-class hybrid search, and a sub-class search filtering on the sub-class's own member. The facts assert a member only the requested sub-class carries, not just a count. ⚠️ Stores whose search path has no doc_type predicate will fail these (see polecat#723, marten#5440).GuidOptimisticConcurrencyCompliance covers the mapped version member (#943). It now covers a version mapped through the store's metadata (Metadata(m => m.Version.MapTo(...))) as well as the IVersioned marker interface. The mapped route is a separate code path that has broken four times across stores.Full Changelog: V2.79.2...V2.80.0
Native AOT: source-generated evolver constructors were trimmed, so PublishAot apps failed at startup with MissingMethodException . GeneratedEvolverAtt
Fix incorrect disposal of composite projection batches ( #939 , @Hawxy )
New policy: in a minor release, members added to existing interfaces get default implementations. A ratchet test now enforces this for every public in
IDocumentStoreDiagnostics.Subject and LoadDocumentAsync became abstract, so Marten 9.42 (and Polecat < 5.34, Fisher < 1.15) threw TypeLoadException at startup when resolved alongside JasperFx 2.77 or 2.78. Both members now have default implementations. On an older store, LoadDocumentAsync handles default-tenant loads through the store's own LoadDocumentJsonAsync, and anything it can't answer throws NotSupportedException with an upgrade message (#935).IDocumentReadOperations.LoadManyAsync<T> (Guid / string ids) and IEventStoreOperations.FetchManyForWriting<T> (Guid / string ids: one handle per id, in order, each with its own version check). Both have default implementations that make one call per id; stores override them for a single round trip. Compliance coverage is included (#934).DocumentTypesAsync lists sub-classes. New DocumentTypeRef.RootTypeName / IsSubClass (#936).marten#5550, polecat#712, fisher#374 (native batch reads); marten#5551, fisher#375, polecat#713 (sub-class listing); CritterWatch#1376, #1377.
GH-928 : DocumentQueryOptions.AllTenants , an explicit all-tenants scope for IDocumentStoreDiagnostics , plus AssertValidTenantScope() and compliance
IDocumentStoreDiagnostics grows up ( #870 , #927 )
IDocumentStoreDiagnostics grows up (#870, #927)The store-agnostic document diagnostics contract that CritterWatch uses:
DocumentQueryOptions gains Where, OrderBy, Arguments (Dynamic LINQ text, per #869) and IncludeSoftDeleted. If a store can't apply a filter, it must throw the new DocumentCriteriaNotSupportedException instead of returning an unfiltered page.Subject URI, which pairs with IDocumentStoreUsageSource.Subject so a console can target the main store or an ancillary one.DocumentQueryOptions.NormalizeTenantId).LoadDocumentAsync(type, id, tenantId) returns a StoredDocument. LoadDocumentJsonAsync now forwards to it.StoredDocument rows (version, last modified, created, tenant, deleted and deleted-at, .NET type) are on DocumentQueryResult.Documents.IDocumentStoreDiagnosticsWriter saves from JSON and deletes, with an expected-version check and a tenant.DocumentStoreUsage.SerializerCasing, plus structured Indexes and DuplicatedFields on DocumentMappingDescriptor.DocumentStoreDiagnosticsCompliance suite, with SoftDeleted<T>() and AddSubClass<TRoot, TSub>() on DocumentComplianceConfig.⚠️ Breaking for implementers:
IDocumentStoreDiagnostics.SubjectandLoadDocumentAsyncare abstract on purpose, so Marten, Polecat and Fisher all adopt the contract in the same wave. Tracking issues: JasperFx/marten#5543, JasperFx/polecat#706, JasperFx/fisher#364.
ShardState.LastUpdated (#924, #926): the progression row's last_updated now flows through AllProjectionProgress, which gives a signal for whether a shard is still alive.ProjectionDeleted on the async-daemon path (#917, #925). This is the deferred half of #893.EventTypes (marten#3942, #923).All seven are at 2.77.0. JasperFx.RuntimeCompiler is versioned independently and is unchanged.
Full Changelog: V2.76.1...V2.77.0
DatabaseUri() no longer throws on SQL Server named instances, LocalDB, or SQL Express ( #918 , #919 ). Server names such as db-host\MSSQL2017 , .\SQLE
DatabaseUri() no longer throws on SQL Server named instances, LocalDB, or SQL Express (#918, #919). Server names such as db-host\MSSQL2017, .\SQLEXPRESS, and (localdb)\MSSQLLocalDB used to throw Invalid URI: The hostname could not be parsed. That stopped Wolverine hosts with a SQL Server outbox from starting (wolverine#4685). The host is now sanitized to the characters a URI host allows, instead of being escaped.resources -t is the type filter again (#921, #922). --timeout and --type both claimed -t, so resources setup -t Wolverine was read as a timeout and threw. --timeout is now long-form only.resources command is documented (#920, #922). The new Stateful Resources page covers the command's actions and flags, startup setup with AddResourceSetupOnStartup and StartupAction, the IHost test helpers, and writing your own IStatefulResource.IEventStore.CompactStreamAsync is tenant-aware ( #910 ) — a compaction policy selecting streams in a tenant scope can now compact them, including when
IEventStore.CompactStreamAsync is tenant-aware (#910) — a compaction policy selecting streams in a tenant scope can now compact them, including when the default tenant is disabled.IEventStore.HasEventStore (#914) — a store-agnostic signal that a store actually has an event store, so monitoring can skip document-only stores.ShardStartException carries a classified reason (#912) — transient vs configuration vs paused, instead of prose only; the constructor is now public.IAdvisoryLock.FindHolderAsync (#913) — ask who holds a projection distribution lock. The default throws NotSupportedException; Weasel implements it for Postgres and SQL Server (weasel#650).Store implementations: marten#5527, polecat#698, fisher#354, weasel#650.
A patch release. One user-facing diagnostic fix that neither 2.75.0 nor 2.75.1 helps with , plus a new build warning.
A patch release. One user-facing diagnostic fix that neither 2.75.0 nor 2.75.1 helps with, plus a new build warning.
If you hit that exception on a projection registered as Snapshot<T>(), SingleStreamProjection<T, TId> or AggregateStream<T>, the message said:
The JasperFx.Events source generator DID run over assemblies YourApp and Marten, so it saw the aggregate YourApp.MyAggregate and declined it — this is a code-shape problem, not a build configuration one.
For that registration shape the verdict was inverted. The projection type is the store's own generic, so its assembly is Marten.dll — and Marten's build runs the generator, so that assembly always carries the marker #887 introduced. The verdict was therefore pinned to "the generator ran" for the commonest registration shape in the product, and you were sent to audit your aggregate's method shapes while being explicitly told that the cause you actually had was not the cause.
marten#5495 is exactly this shape. #887 was filed out of it, so as shipped it would have misdirected the very reporter it was written for — and for this shape it was worse than the nine-line hedge it replaced, which at least named the csproj cause among the others.
Neither earlier release helps: AggregateApplication.cs is byte-identical across 2.75.0 and 2.75.1. If you are debugging a missing dispatcher, this is the bump you want.
Fixed by excluding constructed generic projection types from the evidence. Deliberately not by requiring confirmation everywhere: a user-declared MyProjection : SingleStreamProjection<Agg, Guid> in its own assembly really does get a projection-specific evolver emitted there, so confirmation in that assembly stays genuine evidence — a fact now guards that case explicitly. #906.
Two copies of JasperFx.Events.SourceGenerator in one compilation emit byte-identical file paths, and the file-scoped evolvers mangle to the same name, so the build fails with
error CS0433: The type 'XEvolver' exists in both 'YourAssembly' and 'YourAssembly'
— one assembly named twice, which is the fingerprint. Nothing in that message mentions the generator, the two copies, or the remedy.
2.75.0's MSBuild target already prevented this by keeping one copy, but the states that actually produce the CS0433 are the ones where that target did not run — and there it said nothing. Detection is now separate from the fix: JasperFxEventsDedupeSourceGeneratorAnalyzers=false opts out of the dedupe, not the diagnosis, and a duplicate now warns JFXEVT902 naming both paths, the error to expect, and the remedy (a store package bundles the analyzer, so dropping a direct PackageReference on JasperFx.Events.SourceGenerator usually resolves it).
Stated plainly, because it is a real limit: that covers MSBuild builds only. A consumer on a JasperFx.Events predating the target, or a compilation running no MSBuild targets, reaches the same CS0433 with nothing of ours present to warn. So the generated marker file now carries a header explaining the error — which reaches those cases, because the generator running twice is what a double load is, so that file exists in exactly the states where the CS0433 happens. #902.
The shipped JasperFx.Events.targets is now imported and exercised by ./build.sh Test in six arrangements (#908). It ships as buildTransitive, so it reaches every consumer of the stack transitively — but as <None> rather than an <Import>, so our own build never loaded it and could not tell whether it was even well-formed XML. A malformed version passed the entire suite here while breaking every consumer's build, which is not hypothetical: it happened once while writing the JFXEVT902 warning above, and was caught by hand. Now it is caught by CI. No packaged content changed.
Unaffected by this patch: JasperFx/marten#5517, JasperFx/polecat#682, JasperFx/fisher#336 — enrollment of 2.75.0's DocumentConjoinedTenancyCompliance, plus replaying 2.75.1's new ComplianceStoreConfig.AddCommitListener in each event fixture.
A patch release. One runtime fix, and one correction to a compliance fact that 2.75.0 shipped in a state no store could pass — read the first section
A patch release. One runtime fix, and one correction to a compliance fact that 2.75.0 shipped in a state no store could pass — read the first section if you enrolled the new document tenancy suite.
DocumentConjoinedTenancyCompliance on 2.75.0, one of its ten facts could not passoptimistic_concurrency_is_scoped_to_the_tenant_for_a_shared_id asserted a refusal no correct store can produce. It advanced tenant A's row by storing a loaded instance, then re-stored that same instance and expected a ConcurrencyException — but a committed Store writes the landed version back onto the instance, so the guard matched the current version and the write was admitted.
That write-back is not incidental, it is required: GuidOptimisticConcurrencyCompliance.a_successful_write_moves_the_instances_own_version_on mandates it, so that a long-lived instance stays usable. The two facts contradicted each other, and no store could be green on both. The only way to pass the tenancy fact was to fail the concurrency suite it is a tenanted special case of.
This was our bug, not yours. A red fact there needed no change on your side, and no store behaviour was wrong. Because JasperFx.Events.ComplianceTests ships as source, the fix arrives by bumping to 2.75.1 — nothing else to do.
Fixed by making "stale" mean a separately loaded instance, read before the winning write, which is what stale has to mean on a store with write-back. The arrangement that gives the fact its teeth — one document id shared across two tenants — is unchanged.
The fact skipped in this repository, because the in-memory reference store left SupportsOptimisticConcurrency false. A fact that skips everywhere it could run is a fact nobody has checked, and this one reached three stores that way.
So the reference store now implements the IVersioned guard and enrolls GuidOptimisticConcurrencyCompliance alongside the tenancy suite. It had to be enrolled in both, because the contradiction is between two suites — nothing enrolled in only one can see it. Reverting the suite to its 2.75.0 text now fails in this repo with the same symptom reported against Fisher.
That work also exposed a second defect in the reference store: LoadAsync handed back the stored object reference, so two independent reads were one object and could never disagree about a version. Documents now round-trip through JSON on read and write, as a real store does.
#903.
Block<T> workers no longer inherit the activity current when the block was builtBlock<T> starts its workers with Task.Run in the constructor, and Task.Run flows the ExecutionContext — so every worker captured whatever Activity.Current was set at construction and kept it for the block's whole lifetime, long after that activity had ended. Every span the block's action started became a child of it.
The reported consequence: a Marten async daemon whose projection agents were rebuilt from inside a Wolverine HTTP handler parented every subsequent projection page span under that one request — 3,943 spans in ten minutes, on a trace already reported as finished. Anyone restarting projection agents from a request-scoped context was affected.
The ambient activity is now cleared per item, so each posted item's work is a trace root. Per item rather than once per worker because an action that starts an activity and does not dispose it would otherwise leak into the next item's parentage.
ExecutionContext.SuppressFlow() would have fixed the symptom too and was deliberately not used: it stops every AsyncLocal from reaching the workers, logging scopes and the current culture included, and a caller relying on those would lose them silently. Clearing one ambient value changes nothing else a worker inherits, and nothing at all for the caller.
Reported with a complete repro, the correct diagnosis and the fix taken here by @smoqmilus. #900.
Two facts were passing regardless of whether the behaviour they name was correct. Both are replaced or joined by versions that fail on a broken runtime — verified by reverting the relevant fix and re-running, not by inspection.
creating_and_deleting_within_one_batch_reports_no_deletion (#893) asserts what a commit reported rather than what it stored. Its predecessor passed either way, because inline the phantom delete targets a row that does not exist and is a no-op in SQL — but the deletion still appears in IDocumentChangeSet.Deleted, which is how #886's reporter found it. Confirmed to fail with #889's runtime fix reverted, while the old fact still passed.a_project_with_no_aggregates_builds_clean_under_the_double_load (#887) pins JasperFxSourceGeneratorAppliedAttribute's AllowMultiple = true in the one topology where the marker is the only thing emitted twice. The existing fact asserted "no CS0579" inside a build already broken by CS0433, so it would have passed if the rule regressed.ComplianceStoreConfig.AddCommitListener(...) — the event-store twin of the document config's listener slot. Each store's event fixture needs to replay it (options.AddCommitListener(listener) on Marten, one call identical to what each document fixture already makes). Until a store does, the new fact fails rather than skips, by the #672 rule.
Still open, and unaffected by this patch: JasperFx/marten#5517, JasperFx/polecat#682, JasperFx/fisher#336. The Polecat one is not just enrollment — document tenancy there is taken from the event store's, store-wide, with no per-type opt-in.
Conjoined document tenancy, and tenancy past the event read path ( #898 , #899 )
JasperFx.Events.ComplianceTests covered conjoined events well — twelve facts, enrolled by all three stores — and the document side had no tenancy seam at all. This release closes that, and extends the event suite past reads.
DocumentConjoinedTenancyCompliance (10 facts)Every fact checks both directions and reuses one document id across two tenants. The second is load-bearing: a store keying documents on id alone does not fail loudly, it folds the two writes into one row — so the second silently overwrites the first and both tenants then read the same document. Distinct ids per tenant, which every tenanted fact used before, passes cleanly on exactly that store.
Covers every read shape with no Where (the fisher#51 leak), LoadAsync on all three identity overloads, insert and update of a shared id, all three Delete routes, tenant-scoped optimistic concurrency and numeric revisions, and the AnyTenant / TenantIsOneOf escapes.
ConjoinedEventTenancyComplianceinline_and_async_snapshots_of_a_shared_stream_id_stay_isolated_per_tenant — no fact anywhere had registered a projection and written one stream id in two tenants, so a store projecting into a single-tenanted snapshot table passed everything. Both lifecycles, since the inline write knows its tenant and the async write must reconstruct it.async_rebuild_across_many_tenants_produces_one_document_per_tenant_stream — 200 tenants share one stream id, making the count an assertion: 200 documents on a correct store, exactly 1 on a broken one.with_the_default_tenant_disabled_every_read_route_is_reachable_through_a_tenant_scope — the first assertion anywhere of ComplianceExceptionKind.DefaultTenantUsageDisabled, declared and never used until now.projection_side_effect_messages_carry_the_event_tenant — the shared RecordingMessageOutbox was receiving the tenant id and discarding it.IDocumentSessionFactoryGains LightweightSession(string tenantId) and QuerySession(string tenantId), with throwing defaults, so nothing breaks on the bump.
A store implementing IDocumentSessionFactory<TOperations, TQuerySession> must write the explicit non-generic forwarders, exactly as it already does for the parameterless pair:
IDocumentSessionOperations IDocumentSessionFactory.LightweightSession(string tenantId)
=> LightweightSession(tenantId);C# interface implementation is not return-type covariant, so a product-typed member satisfies only the generic interface and leaves the contract member on the throwing default — on tenancy that is otherwise perfectly correct. Same near-miss as IDocumentReadOperations.Events. DocumentSessionFactoryDefaultsTests pins the shape.
DetermineAction's action per batch, not per eventIEventStore.OpenReadOnlyEventStore(tenantId)JasperFx/marten#5517, JasperFx/polecat#682, JasperFx/fisher#336. The Polecat one is not just enrollment — document tenancy there is taken from the event store's, store-wide, with no per-type opt-in.
…tenant and a store can adopt it without a breaking change.
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.
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.
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.
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.
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.
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.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.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.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.
Backfilled on 2026-09-22. 2.73.2 was published to NuGet on 2026-09-18 without a tag or a release entry. This tag and these notes were reconstructed af
Backfilled on 2026-09-22. 2.73.2 was published to NuGet on 2026-09-18 without a tag or a release entry. This tag and these notes were reconstructed afterwards from the commits between V2.73.1 and
3fb8de4, so that the version on NuGet has something to point at. The packages themselves are untouched and are the ones published on the 18th.
A single-fix patch over 2.73.1.
codegen test no longer fails on an app with message handlers (#867)Fixes wolverine#4486.
codegen test compiles every ICodeFile into its own in-memory assembly. When an AotRoots companion rooted a sibling generated type by name (AttributeArg.TypeNamed), that name lived in a different assembly and the compile failed with CS0234. Since Wolverine 6.37, HandlerRegistryCodeFile roots every generated message handler this way — so codegen test failed for any app with message handlers, while codegen write plus a build worked fine.
The fix adds a GeneratedAssembly.CompiledInIsolation flag that only TryBuildAndCompileAll sets; when it is set, AddAotRoots drops the TypeNamed roots that do not resolve within the assembly being compiled. Roots to existing Types and to the file's own generated types are kept.
The write paths are deliberately untouched — codegen write and Auto-mode CompileAndAttach write what they compile, so they still emit every root into the file the trimmer sees. Verified against four real services on Wolverine 6.39 / Marten 9.37: codegen test went from 19 × CS0234 to passing, and codegen write output was byte-identical, all 20 generated-type roots present.
Wolverine needs no change of its own — only this version.
Three other commits landed in this range and change nothing a NuGet consumer sees: the move of @jasperfx/event-model-vue into this repo (#864), and two documentation corrections to that package's publish workflow about npm trusted publishing (#865, #866 — the cause turned out to be the capitalisation of the GitHub owner in the npm binding).
Seven packages at 2.73.2: 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.
A binary-compatibility patch over 2.73.0 . 2.73.0 has been unlisted on NuGet — use this instead. No functional change beyond the constructor described
A binary-compatibility patch over 2.73.0. 2.73.0 has been unlisted on NuGet — use this instead. No functional change beyond the constructor described below; everything 2.73.0 shipped is here.
EventModelClaim gained a third, defaulted parameter (Source, from #861). That is a source-compatible change and a binary-breaking one: a record emits a single constructor, so the two-argument signature 2.72.0 published stopped existing. An assembly compiled against 2.72.0 that constructs a claim got a MissingMethodException rather than picking up the default.
An explicit two-argument constructor delegating to the primary one, restoring the 2.72.0 signature:
public EventModelClaim(EventModelProvenance provenance, string value)
: this(provenance, value, null) { }⚠ If you are tempted to make the same change elsewhere, it needs one more thing. A second parameterized constructor makes System.Text.Json refuse the type outright:
NotSupportedException : Deserialization of types without a parameterless constructor, a singular parameterized constructor, or a parameterized constructor annotated with 'JsonConstructorAttribute' is not supported. Type 'EventModelClaim'.
Every descriptor carrying a SourceDisagreement would have failed coming back off the wire — the path into CritterWatch and Bobcat. The primary constructor is therefore marked [method: JsonConstructor], the same pairing SpecificationDescriptor has carried in that folder for the same reason.
Deconstruct's arity changed the same way and is left alone. A two-output overload would silently drop Source for anyone destructuring a claim in new code, which is the loss #859 exists to stop, and nothing in the Critter Stack deconstructs a claim. If you do, recompile.
So: the constructor surface is restored, not the whole 2.72.0 surface.
Seven packages at 2.73.1: 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.
Full Changelog: V2.73.0...V2.73.1
⚠ Unlisted — use 2.73.1 instead
⚠ Unlisted — use 2.73.1 instead
This release is unlisted on NuGet.
EventModelClaim's two-argument constructor stopped
existing here (a defaulted third parameter on a record's primary constructor is binary-breaking),
so an assembly compiled against 2.72.0 that constructs one gets aMissingMethodException.
2.73.1 restores it and is otherwise
identical to what is described below. See the binary-compatibility note at the end of these notes.
Three Event Modeling / event sourcing changes, all additive.
StubEventStream<T> (#858, #862)JasperFx.Events.StubEventStream<T> is a stand-in for IEventStream<T> for unit testing a handler that takes a stream handle directly — Wolverine's [WriteAggregate] IEventStream<Account> account. Construct one with the aggregate state the handler should see, call the handler, assert on EventsAppended. No database, and no mocking library.
The stand-in previously existed only as Marten.Events.StubEventStream<T>, built on Marten's StoreOptions / EventGraph, so Polecat and Fisher users wrote their own or mocked the interface — and Received(1).AppendOne(...) proves only that a method was called, leaving which event and what it carried unchecked. EventRegistry builds IEvent envelopes with no store behind it, which is what made this liftable.
Id, Key, StartingVersion, CurrentVersion, Cancellation and AlwaysEnforceConsistency are settable for multi-stream and version-sensitive handlers; TryFastForwardVersion() is inert. Marten.Events.StubEventStream<T> is untouched and keeps compiling.
New docs page: Event Sourcing → Unit Testing Handlers.
Pattern: Declared claims Automation; Declared claims Command named neither party and read as one source contradicting itself. EventModelProvenance is a rung, and a rung holds several sources — spec-first work has a declared model file and specs, both Declared by construction.
EventModelClaim now carries a Source beside its Provenance, populated by the merge from the contributing slice's Origin, and Claimant renders the source where there is one and the rung otherwise:
Pattern: file://CritterCrush.emodel.yaml claims Automation; suite://CritterCrush.Specs claims Command
Two anonymous sources on one rung now say so (two Declared sources disagree — kept X, dropped Y) rather than naming the rung twice, and one source genuinely contradicting itself is the only case that reads that way. A source that never stamped Origin renders exactly as before.
ModelCollapse carries its model names as data (#853, #860)HotspotDescriptor.ModelCollapse recorded which Event Models were folded only inside Text, so a consumer that wanted to act on it — offer a picker, route per model, count them — had to regex a sentence. ServiceName and CollapsedModelNames now sit beside an unchanged Text.
HotspotDescriptor's equality is hand-written as a result, comparing CollapsedModelNames element-wise: a record compares a collection member by reference, which would have broken JSON round-trip equality and hotspot de-duplication. The merge itself de-duplicates on Origin + Text, never on equality.
EventModelClaim is a positional record and gained a third parameter (Source, defaulted), so its primary constructor's signature changed. Source-compatible — new EventModelClaim(rung, value) still compiles — but an assembly compiled against 2.72.0 that constructs one will need a recompile rather than a drop-in swap. Construction is almost entirely internal to JasperFx; reading the properties is unaffected.
Seven packages at 2.73.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.
Full Changelog: V2.72.0...V2.73.0
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Event Modeling attribution and multi-model hosting.
Event Modeling attribution and multi-model hosting.
EventModelSliceDescriptor.Origin), alongside which provenance rung it sits on. ProjectionEventModelSource stamps it, and its dedupe records a SourceDisagreement hotspot when it drops a duplicate document name from a different store instead of discarding it silently.EventModelSetDescriptor pairs a service with every Event Model it hosts, so a modular monolith no longer has to lose a model name to fit a wire that carries one. EventModelDiscovery.AssembleSetAsync returns the pair; Find/Sole let a single-canvas consumer choose a model, and Collapse() is the explicit fold that records a ModelCollapse hotspot naming what went in.Both new enum values are appended and Origin is additive on the wire, so existing payloads and consumers are unaffected.
Two fixes and a documentation correction.
Two fixes and a documentation correction.
Thanks to @AllainPL for the diagnosis, the provenance and a deterministic repro.
MethodFrameArranger.compileFrames step 3 enumerated DependencyGatherer.Dependencies, an ImHashMap keyed by Frame. Frame does not override GetHashCode, so that walk followed identity hash codes and differed between two arrangements of the same logical method — and step 4's topological sort preserves the relative order of independent frames, so the emitted body carried the variation. #196 fixed exactly this for DependencyGatherer.Variables, which is why constructor parameters and fields have been stable and the statements inside the method were not.
Measured on a real application with committed generated code, running codegen write twice in the same environment: 15 of 60 files differed at 1.24.1, 6 of 60 still differed at 1.31.0 (the first release carrying #196). What was left moving is statement order. This also closes out JasperFx/wolverine#3059, which reported the same drift as a macOS-vs-Linux difference and guessed at the cause without a repro.
The fix walks frames and indexes the cache rather than enumerating the cache. The cache's keys are exactly the frames passed in — DependencyGatherer's constructor fills one entry per frame and nothing adds a key before step 3 — so the set gathered is unchanged and only the order becomes deterministic.
⚠️ Expect a one-time diff. If you have generated code committed, regenerating on 2.69.2 may reorder statements within a method once as they settle into a stable order. That is this fix landing, not a semantic change. Subsequent regenerations are byte-identical — which is the point: a drift check in CI, a code review, or a reproducible build can now tell a real change from hash-order noise.
SupportsMultipleDatabasesThe remarks said "Fisher is legitimately false — one file, one database." Fisher has had database-per-tenant since fisher#47 and runtime tenants since fisher#58, and both arms of MultiDatabaseExplorerCompliance are green there. Replaced with the store-neutral condition, plus the part that makes it matter: a Fisher tenant is a file, so this is the one suite in the set whose precondition is nearly free there and expensive on the other two — the opposite of what the old sentence implied.
WaitForNonStaleProjectionDataAsync's contract is now stated on the fixture seam: wait on every configured shard, not every shard that has reported. An implementation that gates on the rows it finds is satisfied by a store where one projection reached the head and another never ran — the wait returns and the next read sees a document that was never written. polecat#602 shipped that defect.
Found while wiring ProjectionEventModelSource into Fisher. JasperFxSingleStreamProjectionBase.determineEventTypes() concatenates both stream lifecycle
Two fixes on top of 2.69.0.
ConsumedEvents carried Archived and Compacted<T>Found while wiring ProjectionEventModelSource into Fisher. JasperFxSingleStreamProjectionBase.determineEventTypes() concatenates both stream lifecycle events onto every non-empty apply set, so an aggregate declaring exactly two Apply methods yielded a View slice consuming four event types. Honest about what the projection handles; not what an Event Model canvas means by the events a read model consumes — they render as stickies for events the application never wrote and no command slice emits, so they link to nothing.
Filtered in ProjectionEventModelSource.ToSlice, and deliberately not in the reader that fills AppliedEvents: Compacted<T> is a real stored event, the same reader feeds AggregateDescriptor.AppliedEvents, and a monitoring console asking "what does this projection handle" wants both. The narrow question is the canvas one, so the judgement sits with the thing drawing it. SubscriptionDescriptor.AppliedEvents is unchanged.
Matched on type identity — typeof(Archived).Assembly and typeof(Compacted<>).Name — so an application type called Archived keeps its sticky, and a rename upstream cannot leave a stale string behind.
Downstream: Fisher's event_model_source.a_registered_projection_becomes_a_view_slice asserts Archived is present, so that test moves when Fisher takes this version (JasperFx/fisher#251).
JasperFxSubscriptionBase's ILoggerFactory BuildExecution overload handed SubscriptionExecution<T> the database where its sibling hands the store — and the store is what implements ISubscriptionRunner<T>. That overload threw on construction, unconditionally.
JasperFxAsyncDaemon.buildAgentForShard picks it whenever the daemon was built with a logger factory — the hosted / projection-coordinator path — so on Marten, Polecat and Fisher alike a subscription registered that way never ran. It surfaced as a logged shard start failure rather than a crash: the app came up and the subscription silently did nothing.
Every other ISubscriptionFactory in the tree already passed store from both overloads; this one was the lone outlier.
Validated before release by packing locally and building against Marten, Polecat and Fisher.
#818 — ProjectionStatusCompliance, and a ruling on what ShardStatus means. Three stores had independently decided what the same five fields meant. ShardStatus.State is now a fact about the running daemon: a store that can reach one reports what it says, a store that cannot reports Unknown, which is distinct from Stopped. The inventory is projections, not shards. Reading statuses never starts a daemon, and EventStoreSequence is the head of the store rather than the high-water row. New ShardStatusState vocabulary. Downstream: marten#5383, polecat#589, fisher#243.
#819 — GuidOptimisticConcurrencyCompliance. There was no shared suite for Guid optimistic concurrency on any store, and two stores shipped the same field broken in two different ways (fisher#245, marten#5372). Five paired facts, plus DocumentComplianceConfig.UseOptimisticConcurrency<T>() / UseNumericRevisions<T>().
#810 — multi-database arms for the explorer reads. DatabasePerTenantExplorerCompliance pins that a store-global read on a multi-database store is not a silent partial answer; ShardedTenancyExplorerCompliance pins the tenant_id predicate on a store where many tenants share a database. New seams: ComplianceStoreConfig.TenantDatabases, SupportsMultipleDatabases, DatabaseForTenantAsync.
#823 — EventModelDescriptor.Links. Cross-slice cause→effect relationships, computed on read from the roles slices already stamp and never accepted as input. The join is public and pure (EventModelLinks.Compute) so consumers share one rule rather than keeping copies.
#824 — ConsumedEvents, ReadsFrom, Chapter. The vocabulary recorded what a slice produced and never what it read, so the State View arrow and the Automation input edge could not be drawn at all.
#825 — ProjectionEventModelSource. The store-derived rung: one View slice per registered projection, read out of IEventStore — so a View slice no longer appears on a canvas only when a human wrote one down. Store-agnostic; merges by document-type name with a spec-declared slice.
Validated before release by packing locally and building against Marten, Polecat and Fisher.
Two additions to the store-agnostic IEventStore surface, both driven by what a monitoring console that ships one assembly against Marten, Polecat and
Two additions to the store-agnostic IEventStore surface, both driven by what a monitoring console that ships one assembly against Marten, Polecat and Fisher alike cannot reach today. Additive and non-breaking — every member is a default interface implementation.
IEventStore.RegisteredShardNames() (#815, #816)AllShards() is declared only on IEventStore<TOperations, TQuerySession>, so reaching it means naming a closed generic per store. RegisteredShardNames() is the non-generic way in, and it supplies the expected half of the expected-versus-observed correlation that IEventDatabase.FetchProjectionLagAsync already performs against one database's progression rows — the half that lets ProjectionLag.HasProgressionRow tell "registered here, never started here" from "at zero".
The generic interface satisfies it from AllShards(), so no store needs to change. The non-generic default throws rather than returning an empty list: an empty registry is a meaningful answer, and handing one back is indistinguishable from "nothing is registered".
The explorer reads were scoped by tenant only. On a store whose DatabaseCardinality is not Single, a store-global read answers from whichever database the default session resolved and looks exactly like a complete answer. New overloads take an IEventDatabase, in the shape FetchProjectionLagAsync and DeleteProjectionProgressAsync already use:
GetRecentStreamsAsync(IEventDatabase, int, string?, CancellationToken)ReadStreamAsync(IEventDatabase, string, string?, CancellationToken)GetStreamMetadataAsync(IEventDatabase, string, string?, CancellationToken)QueryByTagsAsync(IEventDatabase, IReadOnlyDictionary<string, string>, string?, CancellationToken)GetProjectionStatusesAsync(IEventDatabase, string?, CancellationToken)A tool enumerates AllDatabases() and reads each one, attributing the answer to the database it came from. The default delegates on a single-database store (so those are correct with no implementation work), refuses on a multi-database one rather than answering from whichever database it happened to open, and rejects a null database rather than letting it read as "store-global".
Store-side follow-ups — deciding the tenant predicate by tenancy style rather than cardinality, and making a store-global read on a multi-database store fan out or throw — are tracked in JasperFx/marten#5383, JasperFx/polecat#584 and JasperFx/fisher#240.
EventStoreExplorerCompliance gained single-database coverage of the new overloads (skipped unless a store reports exactly one database through AllDatabases()), and AsyncDaemonCompliance asserts RegisteredShardNames() off the non-generic interface.
Partial descriptors are the intended shape — Merge folds several sources' slices together by name, and a source (Wolverine's host-side event-model exp
Fix release.
#807 — a partial EventModelDescriptor could not round-trip. System.Text.Json leaves any constructor parameter the JSON does not carry at its default, so an EventModelSliceDescriptor sent without emittedEvents / projectionTypes / readModelTypes arrived with those three null despite being declared non-nullable, and buildGraph() threw ArgumentNullException on the way back out, through the computed Elements getter.
Partial descriptors are the intended shape — Merge folds several sources' slices together by name, and a source (Wolverine's host-side event-model export, or a Bobcat spec assembly) is by definition partial. These members are now normalized at construction the way the init-only collections already default. The same trap was fixed on EventModelDescriptor.Slices, AggregateDescriptor.AppliedEvents, SpecificationDescriptor.ResolvedTypes and HandlerRelationshipDescriptor.EmittedEvents.
Source- and binary-compatible: the wire shape, the positional constructors and record equality are unchanged.
Full changelog: V2.67.0...V2.67.1
Event query tags, made composable — plus two consistency fixes found by store-parity work.
Event query tags, made composable — plus two consistency fixes found by store-parity work.
EventQuery can now AND a tag filter with everything else (#801)EventQuery composed every filter it carried with AND — event types, time and sequence windows, stream id, tenant — except tags. The dictionary form of a tag query lived only on the non-composable IEventStore.QueryByTagsAsync(IReadOnlyDictionary<string,string>, …), so a caller filtered by tags or by everything else, never both. Taking that path also gave up paging and TotalCount.
New EventQuery.TagValues (Dictionary<string,string>) sits beside the rich TagConditions:
TagConditions, whose conditions OR.PagedEvents path, so a tag query keeps paging and a truthful TotalCount of distinct matching events.EventQueryFilters.TagValues bit, so the guard rail refuses it by name on a store that hasn't implemented it.ArgumentException from the new EventQuery.AssertIsWellFormed(), which AssertFiltersAreSupported now calls first thing.Tag names are contract now, not an implementation detail. New TagTypeRegistrationExtensions.FindByTagName / RequireByTagName match a name against the tag type's CLR simple name ("StudentId") or its registered table suffix ("student"), case-insensitively, refusing an unknown name loudly rather than answering empty. This moved into the abstraction because the stores had already drifted — Marten matched only the CLR name and compared values case-sensitively while Polecat accepted either spelling case-insensitively — under a surface with no compliance coverage at all. A caller holding a store descriptor cannot discover which spelling an engine takes.
event-query --tags was broken for every namespaced tag type (#803)The flag turned each JSON property into a condition whose TypeDescriptor.FullName was the bare property name. EventTagQuerySpec.ResolverFor matches on full name and nothing else, so the lookup missed for any tag type in a namespace and the flag threw UnknownTagQueryTypeException against every real store. It was wrong twice over: even with resolution, the documented '{"StudentId":"s-1"}' could not deserialize into a wrapper record, because a tag value is serialized under its runtime type and a record id round-trips as {"Value":"s-1"}.
Both failures are the same mistake — a name is not a type — so --tags now builds TagValues, where the store resolves the name against its own graph. Consequently entries AND rather than OR (what an operator naming two tags means), and a composite JSON value is refused rather than stringified into something that matches nothing.
ResolverFor's doc comment claimed a simple-name fallback the code never had; it now describes the exact match it performs and why guessing would be worse.
CompactStreamAsync<T> refusal said "Use Marten or Polecat" — stale in both directions, since Fisher implements compacting and Polecat implements the typed overload while refusing the untyped one. Support is per-overload, so no store list is safe; the message now names the capability.T (Marten refused, Fisher inferred the fold and got it right). The compliance suite pinned neither, so the same policy declaration worked on one store and was refused on another, invisibly until runtime. The suite now states inference: the typed overload names T outright, and requiring a registration on top would mean a compaction policy could only ever target an aggregate the application already snapshots.EventQueryCompliance gains a TagValues section — both name spellings, case-insensitive names and values, AND-across-entries with no duplication of a doubly-tagged event, composition with type and sequence windows, paging with interleaved noise, the empty/refused/malformed cases, a kitchen-sink query whose decoy fails only the second tag entry, and an end-to-end fact running the event-query command's own output through the store. StreamCompactingCompliance gains the inference fact, asserting the whole round trip rather than that the call didn't throw.
EventQueryFilters.TagValues joined EventQueryFilters.All. A store that declares All and takes this upgrade silently claims support for a filter it has not written — the exact failure the guard rail exists to prevent. Implement it, or subtract it (EventQueryFilters.All & ~EventQueryFilters.TagValues) until you do.
Tracking issues: marten#5365, marten#5366, polecat#575, fisher#230.
Full changelog: V2.66.1...V2.67.0
This is for several previous releases. JsperFx is churning very hard because of downstream work including the storage compliance tests, CritterWatch,
This is for several previous releases. JsperFx is churning very hard because of downstream work including the storage compliance tests, CritterWatch, Bobcat event modeling, and Stoat code generation
Full Changelog: V2.63.2...v2.66.1
Nothing published for this version
Nothing published for this version
Nothing published for this version
Two fixes since 2.63.1, both in the Native AOT chain that wolverine#4232 and wolverine#4287 reported:
Two fixes since 2.63.1, both in the Native AOT chain that wolverine#4232 and
wolverine#4287 reported:
Co-authored-by: Claude Opus 5 noreply@anthropic.com
Co-authored-by: Claude Fable 5 noreply@anthropic.com
Co-authored-by: Claude Fable 5 noreply@anthropic.com
Nothing published for this version
Nothing published for this version
A self-aggregating type may declare its Create handler as an event-shaped constructor, public Foo(FooCreated e) , instead of a named static Create . T
#733 — a self-aggregating type's constructor-based Create was dropped when ShouldDelete was present. (#735)
A self-aggregating type may declare its Create handler as an event-shaped constructor, public Foo(FooCreated e), instead of a named static Create. That worked on its own. Adding a ShouldDelete method to the same aggregate switched the source generator to a different emitter — one that built its dispatch switch from named conventional methods only — so the constructor's event type got no case arm at all.
Nothing failed loudly. The generated code compiled, the constructor never ran, and the next Apply-only event built the aggregate through RuntimeHelpers.GetUninitializedObject, skipping every field initializer. The reporter saw an ApplyEventException wrapping a NullReferenceException out of an Apply that appended to a collection property; the quieter symptom is a silently blank aggregate.
Both workarounds from the issue — converting the constructor to a static Create, or registering the delete through DeleteEvent<T>() instead of ShouldDelete — are unnecessary on this release.
The same omission was also in the generated EventTypes property on every self-aggregating path, including the ones whose dispatch was already correct. An aggregate with a constructor Create and no ShouldDelete gains its creating event in that list here too.
Verified end-to-end against Marten (JasperFx/marten#5322): the reporter's shape fails on 2.60.x across inline, async and live aggregation, and passes on 2.61.0, with EventSourcingTests (2002) and DaemonTests (319) green.
Thanks to @bugs-wkettlitz for an unusually complete report — root cause, emitted code, and a minimal repro.
gh-728 : a projection-run CLI command by @jeremydmiller in #729
Full Changelog: v2.59.0...V2.60.0
Closes the tenancy-slicing gap opened by 2.58.0, and adds two opt-in compliance suites.
Closes the tenancy-slicing gap opened by 2.58.0, and adds two opt-in compliance suites.
Nothing here changes behavior for a store that does nothing. The fix ships inert — it activates only when a store adopts a new one-line seam — and both new suites are opt-in, so neither runs until a store enrolls it.
JasperFxSingleStreamProjectionBase now resolves ForceSingleTenancy itself (#723), plus SingleTenantedEventSlicingCompliance (#724) and CompositeProjectionCompliance (#725).2.58.0 made ForceSingleTenancy take effect on the async daemon for the first time. Only Marten ever set it, by overriding BuildSlicer; Polecat's and Fisher's SingleStreamProjection<TDoc,TId> are empty class bodies. So 2.58.0 fixed wolverine#2053 / marten#4085 on one store out of three and left the other two carrying a live async-projection correctness bug.
The base now resolves it from a new IEventTenancySource on the session — the only thing BuildSlicer is handed. A session that does not implement it yields false, exactly the previous behavior, so upgrading changes nothing until a store opts in:
public TenancyStyle EventTenancyStyle => Options.Events.TenancyStyle;The member is EventTenancyStyle rather than TenancyStyle on purpose: all three stores already declare a TenancyStyle somewhere in their options graph (Marten on EventGraph, Polecat and Fisher on EventStoreOptions), so the shorter name would bind implicitly on some and not others — silently correct in one store and silently absent in the next, which is the failure mode being ended.
Adopting this is what closes polecat#526 and fisher#139. Once adopted, Marten's own BuildSlicer override is redundant.
SingleTenantedEventSlicingCompliance (#724) — on a single-tenanted store, events whose tenant_id values disagree must still fold into one aggregate. Drives the async daemon, since that is the only path the bug ever reached. It guards its own precondition: if a store normalizes the stamped tenant ids away on write, the fact skips with a message rather than passing vacuously.
CompositeProjectionCompliance (#725) — composite staging, one-shard identity, and member teardown on rebuild. Needs the new IComplianceStoreRegistrar.AddCompositeProjection seam, because a composite cannot be constructed by a suite: every product keeps its subclass's constructor internal. Implementations are a forward plus a small adapter, roughly three lines. Its rebuild fact is the load-bearing one, and members are additive so a store that replayed over surviving rows instead of tearing them down reads back exactly doubled.
Both are documented in the package README, along with the new seam.
Minor bump: two of the three changes are things a consumer will notice on upgrade.
Minor bump: two of the three changes are things a consumer will notice on upgrade.
VariableSource.Existing, a lookup that never manufactures (wolverine#4198). New API, additive.TenantedEventSlicer.SliceAsync(EventRange) now honors ForceSingleTenancy (#721). Behavior change.[BoundaryAggregate] (#718).#722 changes async projection grouping on Marten. ForceSingleTenancy was honored by SliceAsync(IReadOnlyList<IEvent>) but ignored by SliceAsync(EventRange) — and the async daemon reaches only the latter. Marten has been setting the flag since the wolverine#2053 / marten#4085 fix, so on this bump that fix takes effect on the daemon for the first time.
Concretely: on a single-tenanted store whose event rows carry mixed tenant_id values, an async single-stream projection now folds the stream into one aggregate instead of several partial ones. Conjoined stores are unaffected, and single-tenanted stores with clean rows already produced a single group. See marten#5305 for the Marten-side follow-up.
Polecat and Fisher do not pick this up — their SingleStreamProjection<TDoc,TId> subclasses are empty class bodies and never set the flag (polecat#526, fisher#139). #723 tracks hoisting the determination into the shared base so all three stores get it.
#720 adds two compliance facts that fold an identity-less [BoundaryAggregate] with events present — coverage that did not exist because every DCB aggregate in the suite happened to carry an Id, so a store could require a single-stream identity for a boundary aggregate and pass the whole suite. Both stores that had that bug have since fixed it — Fisher in 1.0.5 (fisher#135) and Polecat in 5.21.0 (polecat#521) — so on current versions these facts should pass everywhere. They are a regression guard, not a new demand.
wolverine#4167: a Block never runs its action on the publisher's thread by @jeremydmiller in #714
Full Changelog: V2.56.0...V2.57.2
Nothing published for this version
Nothing published for this version
GitHub Actions pins moved off the deprecated Node 20 runtime ( #699 ).
An async-daemon reliability release, plus provenance for the Event Model.
EventModelDescriptor.Merge no longer resolves competing claims by registration order. Sources now sit on a three-rung ladder of authority and the higher rung wins:
Declared < Derived (from code) < Observed (in production)
This inverts the previous arrangement, where WolverineEventModelSource was registered at index 0 specifically so derived roles would beat overlays. Registration order is now only a tie-breaker between sources on the same rung.
Nothing breaks on upgrade. IEventModelDefinitionSource.Provenance is a default interface member returning Declared, so every existing source ties, every tie still resolves on order, and an application that has not stamped its sources merges exactly as it did on 2.55.0. To adopt the ladder, a source overrides Provenance — one line.
Precedence is per claimed role, not wholesale: a role is claimed when a slice carries a value for it, and a source that does not claim a role never overrides one that does. Slice names, domains, trigger labels and specification links therefore keep coming from declarations and keep winning by default, because nothing else claims them.
A tenant agent seeded below its own committed position no longer dies permanently (#702, thanks @erdtsieck). SubscriptionAgent.Apply's Start branch threw on LastCommitted > HighWaterMark, which funnelled into ReportCriticalFailureAsync → Paused — and nothing restarts a paused agent under a node-distributed daemon, so one momentarily missing ceiling took a tenant's projection out until the next deployment. The seed is now clamped to the committed position with a warning. Nothing is replayed or skipped: loading only ever starts above LastCommitted, so the agent idles until the real mark is routed to it. PolledTenantSet also gained a reference-counted Pin/Unpin that a wholesale SetTenants cannot drop, held across an agent start's priming poll.
High water is primed exactly once, and concurrent starts wait for it (#709). HighWaterAgent.IsRunning was set before the Detect() it exists to gate, so a second concurrent StartAgentAsync skipped the priming and read Tracker.HighWaterMark while it was still 0. The flag now means "detection has completed", a failed detection leaves the agent not running instead of permanently claiming otherwise, and both start paths go through a single-flight rendezvous — so 25-way parallel agent starts prime once rather than launching 25 concurrent max(seq_id) scans. This was reachable on plain non-tenanted stores, not only under sharded tenancy.
Per-tenant catch-up and rebuild hold their tenants across the priming poll (#710). Both still used Activate, which a concurrent reconciliation could drop — leaving catch-up to skip that tenant's work in total silence. Catch-up now also distinguishes "never polled" (logged) from "polled, no events yet" (the benign skip), and PolledTenantSet.Deactivate no longer removes a pinned tenant.
EventModelProvenance, EventModelRole, per-role attribution via EventModelSliceDescriptor.ProvenanceFor(role) and ClaimedBy, the same answer stamped onto every rendered EventModelElement, and IEventModelDefinitionSource.Provenance so a source declares its rung once. A higher rung replaces a list rather than unioning with it — unioning derived {A, C} with observed {A, B} invents a slice emitting three events nobody claimed.HotspotOrigin.SourceDisagreement, recorded whenever both sides claim a role and the merged answer drops one of them, naming the role, both claims and the rung each came from via Role / WinningClaim / LosingClaim. New semantics on shipped machinery — HotspotDescriptor already renders in both viewers. Purely additive: nothing is recorded when nothing is lost, so a model with no disagreements is identical to one produced on 2.55.0.RetryBlock<T>.ShouldRetry and OnTerminalFailure (#701). A caller can now classify a failure as terminal instead of swallowing an exception inside its own handler purely to stop the retry loop. OnTerminalFailure is the capability swallowing cannot provide — the block's owner can meter or dead-letter a give-up that is not a success. Both default to null, so existing blocks are unchanged.EventStoreExplorerCompliance now requires the usage descriptor to describe registered projections (#700). It previously asserted on usage.Events and nothing else, so a store could fill that one list and leave every other slot on EventStoreUsage empty while passing — which is exactly what happened for several releases in JasperFx/fisher#120.
⚠️ This is a new requirement on every store that runs the suite, covering an Inline registration specifically. It will go red anywhere TryCreateUsage returns a usage without calling Projections.Describe. Marten and Polecat both call it.
Full changelog: V2.55.0...V2.56.0
One additive feature completing the Event Modeling hotspot story, one defect fix in application-assembly detection, and the first documentation for th
One additive feature completing the Event Modeling hotspot story, one defect fix in application-assembly detection, and the first documentation for the Event Model. Minor rather than patch because the overlay and EventModelDescriptor both gain public surface; nothing is removed, renamed or obsoleted, and every existing constructor and JSON payload round-trips unchanged.
HotspotOrigin.Prose has existed since #687 as a reserved form — a hotspot a source could emit but nobody could author. #689 made a pending specification is a hotspot the primary mechanism and deliberately deferred this one; this adds the authoring surface.
EventModelSliceBuilder.Hotspot(text) attaches a HotspotDescriptor.Prose to that slice. It renders as a Hotspot element in the wireframe lane in the canonical magenta, exactly like a pending-spec hotspot — and a slice that is nothing but a hotspot still renders one element, which is the point for a slice you have thought about but not built.EventModelBuilder.Hotspot(text) attaches to the model instead, for a question that is not about any one slice, through a new additive EventModelDescriptor.Hotspots.Both fold the way everything else in the model does: unioned across sources, deduplicated on origin plus text, order preserved. Prose and a pending specification that happen to share a string stay two distinct hotspots, because they mean two different things.
Prose is the escape valve, not the default, and the XML docs on both methods say so. A pending-specification hotspot is evidence: it appears because a real spec is failing or unbound, and it retires itself the day that stops being true. Prose has no lifecycle — nothing retires it but you. Once a question is sharp enough to name a scenario, write the pending spec instead.
Ports the one fix from JasperFx/wolverine#4024 that JasperFx needed.
DetermineCallingAssembly skipped System* / Microsoft* / test runners / Critter Stack assemblies and nothing else. But AssemblyGenerator names its output with Path.GetRandomFileName() and loads it from a stream, so runtime-compiled assemblies carry a random 8.3-style name and an empty Location — and a walk running during code generation would find one sitting immediately outside the JasperFx frames and adopt it.
The noisy symptom is a false ApplicationAssemblyReuseWarning: instrumenting a fully green 2495-test Wolverine suite produced 44 of them, 41 blamed on generated assemblies, and every one a false positive. The serious symptom is silent — the same walk picks the assembly used for type discovery, so adopting a generated assembly means scanning one that holds none of the application's types, and the only evidence is a handler, document or projection mysteriously not being found.
The walk now skips IsDynamic and empty-Location assemblies. The Location half is switched off under a single-file publish, where every bundled assembly reports an empty Location including the real application assembly; there the walk falls through to Assembly.GetEntryAssembly(), which is the correct answer for a single-file app anyway.
The other two fixes in that Wolverine PR were checked and do not apply here: AddJasperFx already captures the registration assembly eagerly before deferring into optionsBuilder.Configure, and establishApplicationAssembly already runs the divergence check on the branch that adopts RememberedApplicationAssembly. That matters beyond this release — #697 was the stated blocker for consolidating Wolverine onto the shared JasperFxOptions.ApplicationAssemblyReuseWarning, and it is now clear.
The semantic model shipped across #687, #689 and #690 with nothing but XML docs behind it. There is now a documentation section — overview, the overlay API, hotspots, and the wire descriptors — modelling Wolverine's IncidentService sample end to end.
Every snippet is compile-checked out of DocSamples, which had silently stopped compiling: it used Microsoft.NET.Sdk.Web, whose OutputType defaults to Exe, and there is no Program.Main in it. It is back on Microsoft.NET.Sdk and now in jasperfx.slnx, so the samples are built on every CI run and a sample that drifts from the API fails the build.
JasperFx, JasperFx.Events, JasperFx.Events.ComplianceTests, JasperFx.Events.SourceGenerator, JasperFx.SourceGenerator and JasperFx.Aspire go to 2.55.0. JasperFx.RuntimeCompiler is on its own version track and is unchanged at 5.0.0.
Two additive features for the Spec Driven Development track (master: JasperFx/stoat#9 ). Minor rather than patch because both add public surface; noth
Two additive features for the Spec Driven Development track (master: JasperFx/stoat#9). Minor rather than patch because both add public surface; nothing existing is removed, renamed or obsoleted, and every existing constructor and JSON payload round-trips unchanged.
JasperFx.Events.EventModeling is now the one slice vocabulary every source writes into and every viewer reads from:
EventModelSliceDescriptor keeps its positional shape and gains Pattern (SlicePattern: Command / View / Automation / Translation), TriggerKind (Http / Grpc / MessageHandler / JobScheduler / Human / External), TriggerOrigin, AggregateTypes (a list — one handler may load several projected models), PublishedMessages, ExternalSystems, Hotspots, Specifications (typed: {Feature}/{Scenario} identity + resolved types, stamped by sources), Domain, plus the computed rendering contract Elements (stable id, kind → canonical colour, lane) and Edges (directed, by id), and Merge(other).EventModelDescriptor gains Aggregates and a static Merge(name, descriptors).HandlerRelationshipDescriptor.ToSliceDescriptor() folds the older handler-relationship vocabulary into a slice.EventModelDefinition / EventModelBuilder / EventModelSliceBuilder name, group (InDomain), annotate (TriggeredBy(label)) and link (LinksToSpecification) — roles are derived by the sources that can see them. The role-declaring methods live behind one documented escape hatch, ForFlowNotOwnedHere(...).EventModelDefinitionSource, services.AddEventModel<T>() / AddEventModel(type | instance | name, lambda) / AddEventModelsFromAssembly / AddEventModelSource, and EventModelDiscovery.DiscoverAsync / AssembleAsync. IEventModelDefinitionSource finally has implementations.HotspotDescriptor.PendingSpecification(specIdentity) — a pending specification renders as a hotspot on the slice it binds to.ProjectionScenario exposes its plan and observes each step (#688 — PR #692)ProjectionScenario<TOperations, TQuerySession> gains PlannedSteps (number, kind, description — readable before ExecuteAsync) and an Observer (IProjectionScenarioObserver: StepStarted / StepSucceeded / StepFailed / StepSkipped), numbered as the exception report numbers them. Marten, Polecat and Fisher subclasses need no change.
One fix and one documentation change. Minor rather than patch because the fix adds a member to a public interface — additive, with a default, so nothi
One fix and one documentation change. Minor rather than patch because the fix adds a member to a public interface — additive, with a default, so nothing has to change to keep compiling.
IProjectionStorage.IsThreadSafe (#683, #685)AggregationRunner applies every slice in a range through a fixed 10-wide block, and every one of them gets the same IProjectionStorage instance. That is what the products' own document storage is built for. It is not what an EF Core storage can take: it wraps one DbContext per tenant/batch, and a DbContext is not thread-safe. A multi-stream projection with custom grouping fans one event out into many slices, so up to ten concurrently call Entry() / FindAsync and mutate the same change tracker — surfacing as InvalidOperationException out of Dictionary.TryInsert and NullReferenceException out of ChangeDetector.DetectChanges (JasperFx/marten#5266).
A storage can now say it cannot take that:
public bool IsThreadSafe => false;and the runner applies its slices one at a time. Resolved per tenant group, so a store may answer differently per tenant.
Defaults to true, so no existing storage changes. The declaration is on the storage rather than on AsyncOptions because the storage is the thing that is or is not safe and it already knows — a parallelism knob would work, but it would make correctness a configuration problem a user has to know they have.
⚠️ Serializing calls inside a storage implementation is not a substitute. A lock around each member still leaves the aggregation on one thread mutating entities while another thread's Entry() runs change detection over them, which is exactly the reported ChangeDetector failure. The fan-out itself has to stop, and nothing reachable from inside the storage can stop it.
Both routes run the same handler and collect into the same exception list, so MarkSliceAction, the single-vs-aggregate throw and ApplyPendingCacheUpdates are unchanged. The exception collector is now a ConcurrentQueue — it had one unguarded writer already, and the serial route adds a second.
Adopters: Marten JasperFx/marten#5266, Polecat JasperFx/polecat#489, Fisher JasperFx/fisher#108 — each returns false from its EF Core projection storage.
One trap worth knowing if you write tests around this: NSubstitute proxies a default interface member rather than inheriting it, so Substitute.For<IProjectionStorage<,>>().IsThreadSafe is false and silently takes the serial route. Harmless, since serial is always correct, but a substitute cannot exercise the concurrent route and cannot be trusted to report what a real storage would.
Documentation only, no behavior change.
JasperFxSingleStreamProjectionBase and JasperFxEventProjectionBase are the shared implementations behind each store's own SingleStreamProjection<TDoc,TId> / EventProjection — and deriving from them directly is not the same as deriving from the store's subclass. Nothing about it fails at compile time.
As of Marten 9.23, Marten.Events.Aggregation.SingleStreamProjection<TDoc,TId> adds two behaviors the base omits:
BuildSlicer returns a TenantedEventSlicer with ForceSingleTenancy from the store's TenancyStyle — the fix for JasperFx/wolverine#2053ConfigureAggregateMapping sets UseVersionFromMatchingStream = true, which changes how an aggregate's version metadata is persistedTake the base instead and you get a projection that builds, runs, and slices or versions differently. Polecat's subclass is an empty class body, so the divergence is Marten-shaped today — which is what makes it a trap: checking one store tells you nothing.
The XML docs now say this on the types themselves, and record the two routes that do work for a projection meant to compile against several stores: a per-flavour alias bound to each store's own subclass, or — where the document owns its stream — a self-aggregating document registered with Snapshot<T>(), which sidesteps it entirely because the store then constructs its own subclass.
This is the cheap half of #649. Closing the gap properly — hoisting the behavior into the base, or a seam each store fills in — is still open.
Full Changelog: V2.52.1...V2.53.0
A single fix. Patch release — no API additions, no behavior change for anything that was not already broken.
A single fix. Patch release — no API additions, no behavior change for anything that was not already broken.
ShardState.Mode now reports a rebuild (#681, #682)ShardMode.rebuilding had no writer anywhere in the tree. Every ShardState a SubscriptionAgent published went out with the property's default of continuous — including the ones published during a replay — so a subscriber watching ShardStateTracker could not tell a projection catching up under a rebuild from one running normally. That is the only distinction the enum exists to draw.
The agent already knew which it was; it just knew it on a different enum (ShardExecutionMode) that was never copied onto what it published. So this is propagation, not new state.
What you can now key on. An IObserver<ShardState> sees Mode == ShardMode.rebuilding for every state a shard publishes while replaying — the start, per-batch progress, and a failure — and ShardMode.continuous otherwise. That is enough to tell an operator "this projection is rebuilding, so the sequence you are watching is catch-up progress toward the rebuild's ceiling, not lag".
ShardExecutionMode.CatchUp deliberately maps to continuous. A shard behind the high water mark under normal operation is still continuous: the number being watched is lag either way, and only a rebuild changes what it means.
A finished replay drops back to continuous before its teardown, so the Stopped state disposal publishes does not still claim to be rebuilding — otherwise a consumer tracking the last state it saw would latch on "rebuilding" permanently, from a rebuild that ended cleanly.
ShardMode is now documented, which it was not, because an enum with no writer had no stated meaning. Note in particular that none means "no mode was stored" — it is the default for a persisted progression row, not a running state.
Nothing to do. Marten, Polecat and Fisher all use JasperFx's SubscriptionAgent and ShardStateTracker directly — none of them implements ISubscriptionAgent — so all three pick this up on the package bump with no store-side change and no store release to wait for.
One thing this does not change: Marten's persisted mode column has no writer either, so a ShardState hydrated from the progression table still reports none. That is a separate path from the live tracker and out of scope here.
Full Changelog: V2.52.0...V2.52.1
Nothing published for this version
Three changes to the store-agnostic contracts, all additive. No store breaks on this bump.
Three changes to the store-agnostic contracts, all additive. No store breaks on this bump.
IDocumentSessionOperations.PendingStreams (#673, #675)A consumer holding the shared document session contract can now read the StreamActions a session has queued but not yet committed:
IReadOnlyList<StreamAction> PendingStreams { get; }All three stores already surfaced the same JasperFx.Events.StreamAction collection under three different names — Marten's PendingChanges.Streams(), Polecat's PendingChanges.Streams, Fisher's Events.PendingStreams — so this closes a naming gap, not a capability gap. It sits on the committable tier for the same reason Events does: a stream action can only be pending in a session that can append, and IDocumentWriteOperations (the tier a projection's RaiseSideEffects receives) cannot.
The member carries a throwing default rather than an empty one. An empty list is indistinguishable from a session with nothing pending, so a silent default would let a consumer's derived work be discarded with a clean build and green tests.
New opt-in PendingStreamActionsCompliance holds stores to the behavior.
IAggregateWriteCache — a shared second-level snapshot cache for FetchForWriting (#674, #676)The aggregate snapshot cache built and measured for Marten is now a shared contract in JasperFx.Events.Fetching, so Marten, Polecat and Fisher get one implementation of it rather than three.
IAggregateWriteCache, AggregateCacheKey, NulloAggregateWriteCacheRecentlyUsedAggregateWriteCache — the default, bounded and node-localAggregateWriteCacheOptions — opt-in per aggregate type, off by defaultEventRegistry.CacheAggregatesForWriting<T>() — inherited by every store's event optionsThe cached snapshot is a baseline only: the stream version and every event after the cached version are still read from the database on every call, and the optimistic concurrency assertion on append is untouched. A stale entry costs a larger delta query — never a wrong aggregate, never a suppressed concurrency failure. What it removes is the snapshot load, which on the measured workload was 4.5–4.9 ms of a 13.2 ms round.
The default implementation is backed by JasperFx.Core's existing RecentlyUsedCache, so this adds no new package dependency — a deliberate reversal of the prototype's Microsoft.Extensions.Caching.Memory reference, which would have pushed that onto every store.
New opt-in AggregateWriteCacheCompliance asserts that turning caching on is unobservable except in latency, including when the cached baseline is stale, ahead of the stream, or evicted.
DocumentComplianceConfig gains a nullable StreamIdentity, so a document compliance suite can declare the stream identity style it needs instead of leaving each fixture to guess. DocumentSessionEventsCompliance appends by stream key and had no way to say so, which failed three of its five facts on every store defaulting to Guid identity — a correct store failing an undocumented precondition, which is a suite bug by definition. Additive: null means "leave the store on its own default", so no existing fixture changes.
BinaryEventAttribute is no longer sealed. A store that shipped its own before this one was promoted cannot delete it and could not derive from a sealed one, leaving it checking two attribute types indefinitely. Unsealing lets it subclass instead and collapse back to a single lookup, since attribute lookup matches by assignability. Stores without a pre-existing attribute should keep using the promoted one directly.
Adoption is tracked per store: Marten JasperFx/marten#5248, #5249, #5250, #5251 · Polecat JasperFx/polecat#477, #478, #479 · Fisher JasperFx/fisher#96, #97, #98.
Full Changelog: V2.49.0...V2.51.0
Nothing published for this version
A by-id load for strong-typed-identity documents ( #665 , #666 )
JasperFx.Events.Documents.IDocumentReadOperations gains one member:
Task<T?> LoadAsync<T>(object id, CancellationToken token = default) where T : notnull;Before this, the read contract offered Guid and string only, so a document keyed by a strong-typed identifier could not be loaded by id through the abstraction at all. Passing the wrapped primitive was not an alternative — it compiles and then throws the store's id-type mismatch at runtime — so store-agnostic code had to fall back to a LINQ query on the identity standing in for a load.
The typed overloads stay preferred by overload resolution, so no existing call site moves; only an argument that fits neither lands on the new member.
The member ships with a default implementation. It unboxes a Guid or string and forwards to the overload that already exists — the only half of the member that is answerable identically for every store — and throws NotSupportedException, naming the implementing type, the document type and the identity type, for anything else. Resolving a strong-typed identifier needs the store's own value-type registration, which nothing generic can guess.
So a store takes this release cleanly and overrides the member when it is ready. What holds it to the real behavior is the compliance suite rather than the compiler.
DocumentLoadAndStoreCompliance gains a document keyed by a strong-typed id and three facts: the load, its miss case, and the one an override is most likely to get wrong — a boxed Guid or string reaching the object overload through an object-typed local must resolve exactly as the typed overload does. A store inheriting the default passes that third fact and fails the first two.
DocumentComplianceConfig gains ValueTypes / RegisterValueType<T>(), because those facts need the identity type registered with the store and the document contract does not carry identity configuration. Enrolling fixtures must replay it — every store spells it options.RegisterValueType(type). This is not a compile break either: a fixture builds fine without it and then fails three facts at runtime.
LoadAsync<T>(object) — no product change. Fixture work only: JasperFx/marten#5241LoadAsync<T, TId>(TId), which does not satisfy the member on arity: JasperFx/fisher#89Six projection-dispatch defects in the aggregate source generator and its runtime, plus a silent event-identity bug on string-keyed appends.
Six projection-dispatch defects in the aggregate source generator and its runtime, plus a silent event-identity bug on string-keyed appends.
JFXEVT003 is now an ErrorJFXEVT003 was Info — invisible in a CLI build at any verbosity — and its message promised a "fallback to runtime expression compilation" that does not exist. It is now an Error with an accurate message, and it fires in far fewer places.
A projection that needs partial and lacks it now fails the build instead of failing later at store construction with InvalidProjectionException: No source-generated dispatcher found. Every affected consumer is already broken at runtime today; this moves the failure earlier and names the fix. Suppressible with dotnet_diagnostic.JFXEVT003.severity if you need a staged upgrade.
partial is required in far fewer placesProjection subclasses were silently skipped unless declared partial — build clean, then a hard failure at DocumentStore construction. The gate predated #462, which moved aggregation dispatch to a standalone file-scoped evolver that needs nothing from the user's declaration. Rider's redundant-partial inspection would also strip the modifier back off, correctly by the language's rules, reintroducing the failure on the next cleanup-on-save.
After this release partial is required in exactly two places, both reported at build time:
EventProjection subclass using conventional methods — its dispatcher is an ApplyAsync override on your class;private/protected, which a file-scoped type cannot name (and then every containing type must be partial too).Everything else — including DI-activated projections and every ordinary SingleStreamProjection / MultiStreamProjection subclass — needs no modifier. Generic and non-visible projections previously emitted uncompilable code (CS0246 / CS0122) even with partial; they now work. (#650, #651)
new'd or GetUninitializedObject shadow — so injected dependencies are reachable and anything the real constructor set is present. This also removes the member-injection emission that had reintroduced the CS0111 double-load failure when the generator ships in two referenced packages (#653, #661)ShouldDelete and Create/Apply covering the same event type produced two identical switch arms and failed the consumer build with CS8120. They now compose into one arm — delete when the predicate says so, otherwise fold the event in. A ctor-registered DeleteEvent<T>() stays unconditional (#652, #660)Evolve is no longer treated as an override. Previously this either threw a bogus "can only use the override of 'Evolve' or conventional Apply/Create/ShouldDelete methods, but not both" at registration, or reached first-event dispatch and threw NotImplementedException. Resolution also no longer throws AmbiguousMatchException when the projection declares a same-named helper (#656, #659)EventProjection with an explicit ApplyAsync override silently dropped its published-type registration with no diagnostic at all. Now reported as JFXEVT006 (Warning) naming the projection and the unregistered document types (#654, #658)JFXEVT001 and JFXEVT004 are removed. Neither was ever reported, and the lambda-registration opt-out JFXEVT004 described could only produce false skips — the ProjectEvent/CreateEvent/DeleteEvent lambda APIs it matched were removed in JasperFx 2.0 / Marten 9.0, so it could only fire on a user-defined helper that happened to share a name (#655, #657)StreamAction.Append(graph, string streamKey, …) and StreamAction.Append(string streamKey, IEvent[]) appended straight to the backing list instead of going through AddEvents, so the envelopes never got StreamId/StreamKey/TenantId. PrepareEvents does not close the gap either. A store handing stream.Events to an inline projection gave it envelopes with an empty StreamKey — the normal way a string-identified projection learns which entity it is projecting — and the projection silently wrote a document with a blank field (#663, #664)The generator and the JasperFx.Events runtime now move together: an evolver that takes the projection through its constructor cannot be activated by a runtime without activateEvolver. Pin JasperFx.Events and JasperFx.Events.SourceGenerator to the same version. Evolvers generated before this release keep working — the parameterless constructor is still the fallback.
Packed locally and validated against both stores before publishing:
9ddfd0ac8 — EventSourcingTests 1850 passed / 0 failed, CoreTests 535 passed / 0 failed, 0 build errors and no JFXEVT diagnostics anywhere.70b4367 — Polecat.Tests 2151 passed / 0 failed, 0 build errors and no JFXEVT diagnostics.The JFXEVT003 escalation produces no diagnostics in either corpus, so the severity change should be inert for existing projections in both stores.
Full Changelog: V2.47.0...V2.48.0
Adds the JasperFx.Events.Documents persistence abstractions ( #647 ) — the document slice JasperFx.Events did not previously cover, so store-agnostic
Adds the JasperFx.Events.Documents persistence abstractions (#647) — the
document slice JasperFx.Events did not previously cover, so store-agnostic
consumers can read and write documents alongside the event store.
Purely additive: new namespace, no existing public type changed.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01Ri7jHRKhaFNBesQM1B5i1k
Nothing published for this version
Nothing published for this version
Nothing published for this version
Nothing published for this version
Your coding agent can read these notes before it upgrades. Set up the MCP server →