Skip to content

The search box knows all the secrets -- try it!

Fisher is part of the Critter Stack ecosystem.

JasperFx Logo JasperFx provides formal support for Fisher and other Critter Stack libraries. Please check our Support Plans for more details.

Fisher as Event Store

Fisher provides a full event store on SQLite, following the same patterns as Marten's and Polecat's — and implementing the same JasperFx abstractions, so a projection ports between them unchanged.

Key Concepts

  • Event — an immutable record of something that happened
  • Stream — a sequence of events for one aggregate or entity
  • Aggregate — a domain object whose state is derived by replaying events
  • Projection — a read model built from events, inline, live or asynchronously

Event Store Tables

TablePurpose
fi_eventsEvery event: sequence, stream, version, type, JSON data
fi_streamsStream metadata: version, type, timestamps, archived flag
fi_event_progressionAsync daemon progress per shard
fi_dead_lettersEvents a shard could not apply and was told to skip
fi_event_tag_<suffix>One per registered DCB tag type
fi_natural_key_<alias>One per natural key definition

See Event Storage.

Stream Identity

cs
opts.Events.StreamIdentity = StreamIdentity.AsGuid;    // the default
opts.Events.StreamIdentity = StreamIdentity.AsString;

Quick Example

cs
public record InvoiceCreated(decimal Amount, string Customer);
public record InvoicePaid(decimal AmountPaid, DateTimeOffset PaidAt);

await using var session = store.LightweightSession();

// StartStream hands back a StreamAction; its Id is the stream's identity.
var stream = session.Events.StartStream<Invoice>(
    new InvoiceCreated(100m, "Acme Corp"),
    new InvoicePaid(100m, DateTimeOffset.UtcNow));

await session.SaveChangesAsync();

var invoice = await session.Events.AggregateStreamAsync<Invoice>(stream.Id);

See the Quick Start for a complete walkthrough.

Projection Strategies

StrategyWhen appliedUse case
InlineThe same transaction as the appendStrong consistency
LiveOn demand, by replayOccasional reads, always current
AsyncA background daemonEventually consistent read models

What is here

Topic
Appending EventsStartStream, Append, concurrency, FetchForWriting
Querying EventsStream reads, event metadata queries, body queries
MetadataCorrelation, causation, user name, headers
ArchivingArchive, unarchive, tombstone
SnapshotsSnapshot<T> across all three lifecycles
Natural KeysAddressing a stream by a business identifier
DCBTags, tagged appends, consistency boundaries
Rewriting EventsOverwrite, replace, masking, stream compacting
ProjectionsEvery shape, every lifecycle
SubscriptionsArbitrary code over each range of events

Two SQLite properties worth knowing up front

Committed sequence numbers are contiguous. One writer per file plus BEGIN IMMEDIATE means a transaction's sequences fully commit before the next writer allocates any, and a rollback returns the number. So the async daemon's high-water mark simply is max(seq_id) — Marten and Polecat must distinguish the highest sequence issued from the highest safe to read, and Fisher has no such distinction to draw.

fi_events.seq_id is AUTOINCREMENT, and that is load-bearing rather than decorative. A bare INTEGER PRIMARY KEY aliases the rowid, which SQLite reuses after a delete — and a reused sequence below the daemon's high-water mark is an event no async projection would ever see. That is what makes event deletion safe at all.

Released under the MIT License.