Skip to content

GH-3956: recognise the JasperFx.Events.Documents contracts in all three stores - #3962

Merged
jeremydmiller merged 1 commit into
mainfrom
gh-3956-store-agnostic-document-contracts
Aug 16, 2026
Merged

GH-3956: recognise the JasperFx.Events.Documents contracts in all three stores#3962
jeremydmiller merged 1 commit into
mainfrom
gh-3956-store-agnostic-document-contracts

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Closes #3956.

A handler could not take JasperFx.Events.Documents.IDocumentSessionOperations, IDocumentWriteOperations or IDocumentReadOperations as a parameter — which is the whole point of those contracts existing, since store-agnostic source has no other way to take a session without naming a concrete store type.

Two independent gaps, both of which had to close

  1. Variable matching. Codegen matches a variable by its exact type, so nothing satisfied the parameter from the IDocumentSession the chain had already created. Added a SharedDocumentOperationsSource per store, alongside the existing SharedEventOperationsSource — the document-side counterparts to the event ones.
  2. CanApply. The fixed probe list/predicate named none of them, so AutoApplyTransactions skipped the chain and no SaveChangesAsync postprocessor was attached.

Measured, not assumed

The issue describes a silent write loss. On a stock host it's actually harder than that — the handler fails codegen outright with UnResolvableVariableException, because the stores don't register the abstractions in DI either.

The silent-loss half is real and was confirmed directly: with the variable source in place but the predicate reverted, the handler binds, runs, and the write is queued into the session's unit of work and discarded with no exception — two of the new tests go red while the other three stay green. Both halves are load-bearing.

Design notes

  • All three contracts resolve from the chain's single IDocumentSession, including the read-only one. Resolving reads from a separate IQuerySession would hand a handler that takes read and write contracts together two different sessions, and its reads would not see its own pending writes. There's a test pinning that they're the same instance.
  • IDocumentReadOperations is probed but deliberately NOT matched in the predicate, exactly as IQuerySession has always been: a read-only parameter is not evidence that the chain writes anything.
  • DI registration delegates to IDocumentSession/IQuerySession rather than opening a fresh session, so Marten's Codegen: prime scoped IServiceProvider via IRequireScopingFrame so MessageContext and IDocumentSession aren't duplicated under service location #3001 scope priming still applies and a service-located contract gets the handler's outbox-enrolled session. IDocumentStore already implements IDocumentSessionFactory, so that one is a straight singleton alias — the same move Requests with Accept: */* is dropped with 406 #430 made for IProjectionCoordinator.

No package bump needed

The handoff notes assumed this would need JasperFx.Events 2.48.0. It doesn't — 2.47.0 already carries every type, and Marten 9.23.0 / Polecat 5.12.0 / Fisher 0.7.0 all implement them. Verified by reflecting on the pinned assemblies.

One thing worth flagging for adopters

Importing the JasperFx.Events.Documents namespace makes ToListAsync() ambiguous between DocumentQueryableExtensions and each store's own queryable extensions (CS0121). The tests alias the individual contracts instead of importing the namespace. That's upstream in JasperFx.Events, not something this PR can fix, but it will bite anyone converting a file to the store-agnostic contracts.

Verification

  • New store_agnostic_document_session_parameters (Marten): 5/5, confirmed red first
  • Polecat + Fisher document contract tests added to the existing all_queryable_and_event_store_operations class (that suite balances CI shards by test-class count): 7/7 each
  • Full FisherTests: 25/25

🤖 Generated with Claude Code

https://claude.ai/code/session_01JG8Un6iNeyXECKJk3jo5uC

…ee stores

A handler could not take JasperFx.Events.Documents.IDocumentSessionOperations,
IDocumentWriteOperations or IDocumentReadOperations as a parameter, which is the
whole point of those contracts existing: store-agnostic source has no other way to
take a session without naming a concrete store type.

Two independent gaps, both of which had to close:

1. Codegen matches a variable by its EXACT type, so nothing satisfied the parameter
   from the IDocumentSession the chain had already created. Added a
   SharedDocumentOperationsSource per store, alongside the existing
   SharedEventOperationsSource -- the document-side counterparts to the event ones.

2. PersistenceFrameProvider.CanApply matched against a fixed type list that named
   none of them, so AutoApplyTransactions skipped the chain and no SaveChangesAsync
   postprocessor was attached.

Measured, not assumed: on a stock host today the handler fails codegen outright with
UnResolvableVariableException. Add only the variable source and it binds, runs, and
the write is queued into the session's unit of work and SILENTLY DISCARDED -- the
failure mode the issue reports, and confirmed here by reverting just the predicate
and watching two of the new tests go red while the rest stay green.

All three contracts resolve from the chain's single IDocumentSession, including the
read-only one. Resolving reads from a separate IQuerySession would give a handler
that takes read and write contracts together two different sessions, and its reads
would not see its own pending writes.

IDocumentReadOperations is probed but deliberately NOT matched in the predicate,
exactly as IQuerySession has always been: a read-only parameter is not evidence that
the chain writes anything.

Also registers the contracts in DI -- the stores register only their own interfaces,
so a service depending on one could not be resolved at all -- delegating to
IDocumentSession/IQuerySession so Marten's GH-3001 scope priming still applies and a
service-located contract gets the handler's outbox-enrolled session. IDocumentStore
already implements IDocumentSessionFactory, so that is a straight singleton alias,
the same move #430 made for IProjectionCoordinator.

No package pin bump needed: JasperFx.Events 2.47.0 already carries every type, and
Marten 9.23.0 / Polecat 5.12.0 / Fisher 0.7.0 all implement them. Verified by
reflecting on the pinned assemblies rather than assuming.

Note for adopters: importing the JasperFx.Events.Documents NAMESPACE makes
ToListAsync() ambiguous between DocumentQueryableExtensions and each store's own
queryable extensions (CS0121). The tests alias the individual contracts instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JG8Un6iNeyXECKJk3jo5uC
@jeremydmiller

Copy link
Copy Markdown
Member Author

Full-suite regression results after the change:

Suite Result
MartenTests 605 / 605
PolecatTests 286 passed, 2 skipped
FisherTests 25 / 25

Plus the new tests: store_agnostic_document_session_parameters 5/5 (Marten), and the document-contract additions to all_queryable_and_event_store_operations 7/7 on both Polecat and Fisher.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Persistence frame providers don't recognise JasperFx.Events.Documents — a handler taking IDocumentSessionOperations silently loses its writes

1 participant