Skip to content

Let applications register their own exception transforms (#5682) - #5750

Merged
jeremydmiller merged 1 commit into
masterfrom
gh-5682-exception-transforms
Oct 23, 2026
Merged

jeremydmiller merged 1 commit into
masterfrom
gh-5682-exception-transforms

Conversation

@jeremydmiller

Copy link
Copy Markdown
Member

Closes #5682.

opts.Advanced.ExceptionTransforms.IfExceptionIs<PostgresException>()
    .If(e => e.SqlState == "CT001")
    .ThenTransformTo(e => new MyDomainException(e));

This is the API the issue proposed, verbatim. ExceptionTransforms is JasperFx's existing JasperFx.Core.Exceptions.ExceptionTransforms, the same type MartenExceptionTransformer already uses.

Order

  1. The failing operations' own transforms, as today: stream-version and similar.
  2. The application's, from StoreOptions.Advanced.ExceptionTransforms.
  3. Marten's built-ins, ending with the NpgsqlException → MartenCommandException catch-all.

An error the application doesn't claim falls through unchanged. One it claims that Marten also translates, such as 40001, uses the application's translation for that store, which the XML docs say.

Per store, not static

MartenExceptionTransformer.Transforms is a process-wide static, so registering there would have leaked between stores. The application's transforms are carried from StoreOptions through SessionOptions into ConnectionLifetimeBase, and each lifetime picks them up in its constructor: TransactionalConnection, AutoClosingLifetime, AmbientTransactionLifetime, ExternalTransaction and their read-only and RLS subclasses. Every place that ran the built-ins now runs them in that order:

  • the per-command and per-batch failure handlers;
  • the five batch-commit catch blocks;
  • DocumentSessionBase.SaveChanges;
  • the LINQ provider;
  • the document cleaner.

Binary compatibility: the only public change is one new get-only property on AdvancedOptions. Everything else is internal.

Tests

CoreTests.application_exception_transforms creates a plpgsql guard that raises CT001:

  • queued with QueueSqlCommand in the committing batch (the issue's case) → GuardRefusedException;
  • queued from a session listener's BeforeSaveChangesAsync → GuardRefusedException;
  • a raw QueryAsync<int> → GuardRefusedException;
  • a LINQ query through MatchesSql → GuardRefusedException;
  • an error the application doesn't claim (42703) → still MartenCommandException;
  • a store without the registration → still MartenCommandException, so nothing leaks between stores.

A/B: with the property present but the wiring stashed, the first four fail and the last two pass. With the change, all six pass. Regression runs (net10.0, PG 17):

  • CoreTests exception and retry classes: 81 passed;
  • DocumentDbTests.Concurrency: 52 passed.

Docs: a new "Translating Database Errors into Your Own Exceptions" section in docs/configuration/storeoptions.md; markdownlint and cspell are clean.

🤖 Generated with Claude Code

StoreOptions.Advanced.ExceptionTransforms (JasperFx.Core.Exceptions.ExceptionTransforms) is consulted for
every database failure the store reports, after the failing operations' own transforms and BEFORE Marten's
built-ins, whose NpgsqlException -> MartenCommandException catch-all would otherwise swallow it. Per store,
never process-wide. Wired through SessionOptions into every connection lifetime (command, batch, ambient,
external, auto-closing, RLS), the SaveChanges batch, the LINQ provider and the document cleaner.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

Let applications register their own exception transforms

1 participant