0.7.0
Six issues, and three of them are the kind that do not throw. If you are upgrading, read the first two.
Fixed: IEvent.StreamKey was empty inside an inline projection (#72)
The stream key was persisted correctly and arrived blank on the events handed to an inline projection. Reading e.StreamKey is the normal way a string-identified projection learns which entity it is projecting, so a projection doing that wrote a document with an empty key field — and nothing threw. The first visible symptom was a query returning nothing, which reads as "the projection did not run" rather than "one field was blank".
The cause is upstream and asymmetric: StreamAction.AddEvent stamps StreamId/StreamKey/TenantId, and StreamAction.Append(graph, Guid, …) goes through it — but StreamAction.Append(graph, string, …) appends straight to the backing list and does not. PrepareEvents does not close the gap either; it sets TenantId, Timestamp, Version and Sequence, never the stream identity. Filed as JasperFx/jasperfx#663.
Fisher now stamps the identity in its own append planner, which is idempotent and stays correct when that ships. The async daemon was never affected — it hydrates events from the row.
Fixed: the nupkg did not carry the projection dispatcher generator (#73)
Projection dispatch is source-generated with no runtime fallback, and the generator ships as a development dependency that does not flow transitively. Marten and Polecat both bundle it inside their own package; Fisher did not. So a consumer referencing only Fisher never ran it.
The absence was not a build failure. Removing the generator removes generated partials that nothing hand-written references, so the consuming assembly compiled clean and then threw on its first projected event:
JasperFx.Events.Projections.InvalidProjectionException :
No source-generated dispatcher found for EventProjection ...
On a deployment that is a service that boots and dies on its first message. The package now carries analyzers/dotnet/cs/JasperFx.Events.SourceGenerator.dll, so you can drop the explicit analyzer reference if you added one as a workaround.
The generator still runs in the assembly that defines the aggregate or projection, so that assembly is the one that has to reference Fisher.
Fixed: a read before the first write threw no such table (#74)
Query<T>() or LoadAsync<T> against a document type nothing had ever written failed with a raw SQLite error, where Marten and Polecat provision the table and return an empty result. Table creation was reached from the write path only.
This is what every cold start does — resolving a cache before anything has populated it, listing a collection on a fresh install — and it is asymmetric in the worst direction: it works on a warm database and fails on a fresh one, so it passes in development and fails on first deploy. The workaround was a hand-maintained list of Schema.For<T>() lines kept in sync across the live registration and every test fixture.
Reads now provision exactly as the first write does. Query<T>(), the scalar terminals, joins, LoadAsync, LoadManyAsync, CheckExistsAsync, LoadJsonAsync and MetadataForAsync are all covered.
Two things unchanged: an enlisted session still asserts rather than creating, on reads as on writes — running a migration on a second connection from inside your transaction would deadlock against your own write lock, so it throws by name instead. And a type with registered projection storage is skipped, since its rows are not in a Fisher document table.
Fixed: Projections.Add<T>(lifecycle) under-registered (#76)
The issue reads as "the overload is missing". It was not — it was inherited from ProjectionGraph, compiled, and was worse than missing: it went straight to All.Add, bypassing registration, so it registered neither the projection's event types nor its published document type. The table was never created, and both failures were silent at registration.
opts.Projections.Add<OrdersByCustomer>(ProjectionLifecycle.Async);
opts.Projections.Add<OrdersByCustomer>(ProjectionLifecycle.Async, o => o.BatchSize = 1000);now means exactly what the instance form means. Its constraint is deliberately weaker than the inherited one, so a bare IProjection can be registered by type too.
Added: StoreOptions.RegisterValueType<T>() (#75)
Fisher discovers strong-typed identifiers from their shape, so this is never required. It exists so store configuration is portable — the same block should read identically whichever store it is pointed at, rather than dropping one line for Fisher and explaining the omission in a comment. Same argument as polecat#459.
opts.ConfigureSerialization(EnumStorage.AsString, Casing.CamelCase);
opts.Events.StreamIdentity = StreamIdentity.AsString;
opts.RegisterValueType<AlertId>();It is not an accepted no-op, and that is the point of using it. Discovery has to treat "not a wrapper" as the ordinary answer; naming a type here is an assertion that it is one, so the same answer becomes a configuration error — reported with the type named, rather than surfacing later as has no identity member.
Added: per-tenant dead letter counts (#77)
IEventDatabase.FetchDeadLetterCountsAsync(tenantId) was not overridden, so it landed on JasperFx's default and threw NotSupportedException for a non-null tenant while the store-global overload beside it worked. A monitoring console meets that as soon as it renders per-tenant badges per shard.
var forBlue = await store.Database.FetchDeadLetterCountsAsync("blue");A null tenant stays store-global and leaves TenantId null, so a consumer keying by {ProjectionName}:{ShardKey} can tell "every tenant" from "the default tenant".
Behaviour change: session.Store(deadLetterEvent) now throws
On Marten and Polecat a DeadLetterEvent is also an ordinary document, so that call lands it in the very table QueryDeadLetterEventsAsync reads. In Fisher it is event store infrastructure with its own table and its own write path — so the same call compiled, succeeded, wrote a fi_doc_deadletterevent row, and the dead-letter query never saw it.
Fisher's arrangement is the better one, and the divergence was still silent in the direction that hurts: ported code kept working and quietly stopped recording anything. It now throws, naming StoreDeadLetterEventAsync — which is what the daemon does and what ports back to either sibling unchanged.
Also
- Docs corrected across nine pages for the two behaviour changes above — the five that told you to reference the source generator yourself, and the four that warned a query against a never-written type would fail.
Wolverine.Http.Fishermoved to the wolverine repo, where the code would live. It is unblocked —Wolverine.Fisheralready carries every base class it needs.- #81 filed: the on-demand table path does not honour
AutoCreate.None, where the Hi-Lo path does. Pre-existing on the write path; the read path added in #74 inherits it, and the current behaviour is pinned so either resolution is deliberate.
Full changelog: v0.6.0...v0.7.0