Skip to content

GH-4838: WolverineFx.InMemory - Wolverine over the in-memory prototyping store - #4844

Merged
jeremydmiller merged 2 commits into
mainfrom
gh-4838-in-memory-store
Oct 6, 2026
Merged

jeremydmiller merged 2 commits into
mainfrom
gh-4838-in-memory-store

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Closes #4838. Part of JasperFx/jasperfx#962.

This adds a new package, WolverineFx.InMemory (src/Persistence/Wolverine.InMemory). It runs Wolverine over the JasperFx.Events.InMemory prototyping store that shipped in JasperFx 2.81.0, so a stub-first application's handlers run before anyone has chosen Marten, Polecat or Fisher.

builder.Host.UseWolverine(opts =>
{
    opts.UseInMemoryStoreForPrototyping(store =>
    {
        store.ConfigureProjections(x => x.Snapshot<Account>(SnapshotLifecycle.Inline));
    });
});

Scope

This follows Jeremy's decisions on #4838 and #962.

Only Wolverine's store-agnostic usage is supported:

  • [WriteModel], [ReadModel] and [DeciderFunction];
  • Storage.StartStream and Storage.AppendEvents;
  • [Entity], IStorageAction<T>, and [All] / [FirstOrDefault] / [Queryable];
  • sagas;
  • handlers that take the shared contract types: IDocumentSessionOperations, IDocumentWriteOperations, IDocumentReadOperations, IEventStoreOperations and IEventOperations.

Deliberately left out:

  • an in-memory [WriteAggregate];
  • natural keys (the aggregate frame refuses them with a message);
  • durable inbox and outbox. Messaging runs on NullMessageStore; the session commits on its own and outgoing messages are flushed after it.

The JasperFx registration's guardrails carry through. The host logs a warning on start and refuses to start in Production unless you opt in.

Shape

The package is modelled on Wolverine.Fisher, but much smaller:

  • InMemoryPersistenceFrameProvider implements both IPersistenceFrameProvider and IEventSourcingFrameProvider on one class, registered through InsertFirstPersistenceStrategy.
    • It's a catch-all provider: EF Core still wins for the entities it maps.
    • CanApply matches the writable contract types and IEventStream<>, but not the read-only ones.
  • Codegen frames cover: opening the session (tenant-aware), the transactional middleware with the eager idempotency check, SaveChangesAsync, FetchForWriting / FetchForExclusiveWriting / FetchLatest, document load / store / delete, the storage-action applier, and the query frames.
  • InMemorySessionFactory opens the session for the message's tenant and copies the correlation and causation ids onto it.
  • UseInMemoryStoreForPrototyping(this WolverineOptions, Action<InMemoryStoreOptions>?) calls AddInMemoryStoreForPrototyping and includes the extension. The name is deliberately awkward, to match the JasperFx side.

JasperFx pins move 2.80.2 → 2.81.0 (JasperFx, JasperFx.Events, JasperFx.Events.SourceGenerator, JasperFx.SourceGenerator), and JasperFx.Events.InMemory 2.81.0 is added.

Tests: src/Persistence/InMemoryTests, 16 tests, all green

  • agnostic_model_attributes uses the same handlers as FisherTests, MartenTests and PolecatTests. It covers:
    • [WriteModel] appends;
    • the inline snapshot written by the handler's commit;
    • transaction support on the chain;
    • [DeciderFunction];
    • [ReadModel] not advancing the stream;
    • a stale FetchForWriting failing and appending nothing;
    • NullMessageStore.
  • storage_side_effects covers:
    • StartStream and AppendEvents;
    • [Entity] with Storage.Insert / Update / Delete;
    • a missing required entity stopping the handler;
    • [All];
    • a handler that takes IDocumentSessionOperations and is committed by AutoApplyTransactions.
  • saga_lifecycle covers start, update, and complete-then-delete.
  • production_guard checks that the Production refusal still fires through UseWolverine.

The source generator comes only through the package. The test project has no explicit JasperFx.Events.SourceGenerator reference: the generator arrives from JasperFx.Events.InMemory's bundled analyzer through Wolverine.InMemory, which is the path a real application takes.

The test project references Wolverine.RuntimeCompilation, because dynamic codegen needs it (GH-2876). An application prototyping on this store needs it too, and the docs will have to say so.

Wiring

  • The package and its tests are in wolverine.slnx under /Persistence/InMemory/.
  • Wolverine_InMemory is in NugetProjects, which ValidatePackList enforces.
  • CI: CIFisher also builds and runs InMemoryTests. Both are containerless, and the in-memory suite takes seconds, so it doesn't justify a runner of its own.

Not in this PR

  • Docs. A guide page needs Jeremy's voice.
  • The swap gate (ims-swap-gate on the Stoat plan): a stub-first sample green on the in-memory store, then on Marten, with no handler changes.

Local verification

  • dotnet build wolverine.slnx -c Release -f net9.0: succeeded: 229 projects, 0 errors, 0 warnings
  • InMemoryTests: 16/16 on net9.0.

🤖 Generated with Claude Code

jeremydmiller and others added 2 commits October 5, 2026 19:53
…Memory prototyping store

- New package src/Persistence/Wolverine.InMemory: one frame provider implementing both
  IPersistenceFrameProvider and IEventSourcingFrameProvider, registered via
  InsertFirstPersistenceStrategy; session / aggregate / document / query frames
- opts.UseInMemoryStoreForPrototyping(...) registers the store and the extension
- Store-agnostic usage only: [WriteModel]/[ReadModel]/[DeciderFunction], Storage.StartStream /
  AppendEvents, [Entity], IStorageAction<T>, [All]/[FirstOrDefault]/[Queryable], sagas; messaging
  on NullMessageStore (no durable inbox/outbox)
- JasperFx pins 2.80.2 -> 2.81.0, plus JasperFx.Events.InMemory 2.81.0
- InMemoryTests (16 tests) run in the CIFisher job; package added to the pack list

Closes #4838

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
JasperFx 2.81.0's SourceWriter no longer emits stray blank lines (#956), so the codegen-drift
gate saw 43 committed files out of date after the pin bump. Regenerated with `codegen write` from each
project's own directory; the diff is blank-line removals only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jeremydmiller
jeremydmiller merged commit 07155a7 into main Oct 6, 2026
83 of 84 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant