Fisher is Marten and Polecat for SQLite — event sourcing and document storage with the same Critter Stack API, in a database that is a file inside your own process. No server to install, nothing to provision, nothing to keep running. Backup is cp. Tests need no fixture.
Documentation: fisher.jasperfx.net
dotnet add package FisherWhat 1.0 means
The API is stable and the semantics below are settled. It is not a claim of feature parity with Marten — the gaps that remain are decisions with reasons, not a backlog, and they are listed at the bottom of these notes rather than filed as issues.
Fisher passes all 37 suites and 319 tests of JasperFx.Events.ComplianceTests, the shared cross-store suite Marten and Polecat also enroll in, alongside its own 1,320. Both target frameworks, net9.0 and net10.0.
What's in it
Event store — streams under both identity styles, optimistic concurrency, archiving and tombstoning, FetchForWriting / WriteToAggregate / FetchLatest with an opt-in second-level aggregate write cache, natural keys, DCB tags, and event rewriting: OverwriteEvent, GDPR-style data masking, and one-way stream compacting.
Projections — all five shapes across all three lifecycles: single-stream, multi-stream, event projections, flat-table, and composite. Live aggregation, inline, and an async daemon with dead letters, side-effect seams, and event-emitting projections.
Documents — all four identity types plus strong-typed wrappers, hierarchies, soft delete, numeric revisions, duplicated fields as SQLite VIRTUAL generated columns, user-declared expression indexes, foreign keys, metadata mapping, patching, and bulk insert.
LINQ — Where, ordering, paging, projections, grouping with HAVING, the scalar aggregates, and joins, chained across any number of tables. The surface refuses rather than falling back to client-side evaluation; anything unsupported is refused by name, with the alternative.
Multi-tenancy — conjoined and database-per-tenant, with tenants that appear, suspend and resume at runtime. On SQLite database-per-tenant is arguably the better story: a tenant is a file, and N tenants write concurrently instead of queueing behind one write lock.
Hosting — AddFisher(...), multi-store registration, transaction participants, tracing, and two companion packages: Fisher.AspNetCore (streaming IResult types, ETag/304 handling, a high-water health check) and Fisher.EntityFrameworkCore (a DbContext saving inside Fisher's transaction, and projections whose documents are EF entities).
Since 0.9.2
The documentation site now deploys with the release. docs.yml was workflow_dispatch-only and had never once run, so fisher.jasperfx.net had drifted behind the packages that point at it. It now fires on the same v* tag publish.yml does, so the packages and the documentation for them are cut from one commit.
The README's compliance scoreboard was two waves stale and is now counted off an actual run.
Deliberate gaps
Each is a decision with a reason:
- No message bus. The projection side-effect seam is real and both commit paths bracket their transaction, but the default outbox drops every message. Delivery is a bus integration's job here as it is on both siblings —
Wolverine.Fisheris built in the Wolverine repo. - Exclusive appends are the optimistic ones. SQLite has no row lock, so the faithful equivalent would hold
BEGIN IMMEDIATEfrom fetch to commit and block every other writer for as long as a caller holds a session. Safety is unchanged — the version guard still runs inside the write transaction — but a loser fails instead of waiting. - No hot-cold daemon coordination.
AddAsyncDaemon(DaemonMode.HotCold)refuses rather than quietly running Solo: failover means several nodes competing for a lease through the database, and a Fisher store is a file SQLite does not make safe to share across nodes. - Event rewriting does not reach anything derived from the events. The high-water mark is a sequence and a rewrite does not move it, so an async projection that already passed the event keeps what it derived until it is rebuilt. Marten is the same; it is why masking is a data-at-rest operation and compacting is one-way.
- No ordering on a string-stored enum. The stored form is the member's name, so it would sort alphabetically rather than by declared order. Refused rather than answered wrongly.
- No table partitioning, permanently — SQLite has no equivalent worth having.
Packages
| Package | |
|---|---|
Fisher |
the store |
Fisher.AspNetCore |
streaming results, ETags, health check |
Fisher.EntityFrameworkCore |
EF Core in Fisher's transaction |