Skip to content

Marten batch: JasperFx.Events 1.29.0 + GH-4267 + GH-4268 reproducer attempt#4269

Merged
jeremydmiller merged 3 commits intomasterfrom
feature/projectionbatch-metadata-overload
Apr 21, 2026
Merged

Marten batch: JasperFx.Events 1.29.0 + GH-4267 + GH-4268 reproducer attempt#4269
jeremydmiller merged 3 commits intomasterfrom
feature/projectionbatch-metadata-overload

Conversation

@jeremydmiller
Copy link
Copy Markdown
Member

Summary

Batched work against Marten's master:

  1. JasperFx.Events 1.29.0 uptakeDirectory.Packages.props bumps, plus a new ProjectionBatch.PublishMessageAsync(object, MessageMetadata) overload that forwards to the IMessageSink metadata path. Enables the Wolverine-side work in JasperFx/wolverine#2545.
  2. Feature request: AddProjectionWithServices usable without builder chain #4267AddProjectionWithServices extensions on IServiceCollection, so modular-monolith modules that only have IServiceCollection can register projections without reaching for undiscoverable JasperFx internals.
  3. Changing projection from async to inline gives DDL execution exception(s) #4268 — reproducer attempt for the async → inline projection migration failure. Does not reproduce. Left in as a baseline regression guard.

1. JasperFx.Events 1.29.0

  • JasperFx.Events 1.28.1 → 1.29.0
  • JasperFx 1.24.1 → 1.26.0 (transitive floor required by 1.29.0)
  • Marten.Events.Daemon.Internals.ProjectionBatch: new PublishMessageAsync(object message, MessageMetadata metadata) overload that calls IMessageSink.PublishAsync(message, metadata), letting downstream sinks (Wolverine) map metadata to their native delivery options.

2. GH-4267AddProjectionWithServices on IServiceCollection

Closes #4267.

services.AddProjectionWithServices<MyProjection>(
    ProjectionLifecycle.Inline, ServiceLifetime.Singleton);

services.AddProjectionWithServices<MyProjection, IMyStore>(
    ProjectionLifecycle.Async, ServiceLifetime.Scoped);

Thin wrappers over TProjection.Register<…>(services, …). Covered by two new integration tests in ContainerScopedProjectionTests/projections_registered_directly_on_IServiceCollection.cs — both paths (default store and ancillary AddMartenStore<T>) green end-to-end against Postgres.

3. GH-4268 — reproducer attempt

The reporter flipped three async projections to inline and hit a migration exception:
unique constraint on partitioned table must include all partitioning columns while Weasel was dropping tenant_id from mt_doc_envelope (Wolverine's outbox table).

Added the closest approximation of the reporter's setup (conjoined tenancy + archived-stream partitioning + envelope metadata enabled + EnableSideEffectsOnInlineProjections + TenantIdStyle.ForceLowerCase + AppendMode.Quick + store A async → store B inline on the same schema).

Result: test passes. The reporter's failure almost certainly involves Wolverine-specific outbox integration that can't be exercised from Marten alone. More information needed from the reporter — not enough here to repro. Leaving the test in as a regression guard.

Test plan

  • dotnet test src/EventSourcingTests --filter "FullyQualifiedName~SideEffect|FullyQualifiedName~publish_message|FullyQualifiedName~Projection" — 223/223 pass (post-1.29.0 bump)
  • dotnet test src/ContainerScopedProjectionTests --filter "projections_registered_directly_on_IServiceCollection" — 2/2 pass
  • dotnet test --filter "Bug_4268" — 1/1 pass (baseline regression)
  • Full Marten CI matrix

Downstream

Once this merges and Marten releases, JasperFx/wolverine#2545 can land its MartenToWolverineMessageBatch metadata-aware overload and close the user-facing story.

🤖 Generated with Claude Code

jeremydmiller and others added 2 commits April 20, 2026 19:04
## JasperFx.Events 1.29.0 plumbing (supports wolverine#2545)

- Bump Directory.Packages.props: JasperFx.Events 1.28.1 → 1.29.0 and
  JasperFx 1.24.1 → 1.26.0 (transitive floor from the 1.29.0 bump).
- ProjectionBatch.PublishMessageAsync(object, MessageMetadata)
  overload forwarding to the underlying IMessageSink with full metadata,
  so downstream sinks (e.g. Wolverine) can map the metadata onto their
  native delivery options (correlation id, causation id, headers,
  user name).

## GH-4267 — AddProjectionWithServices on IServiceCollection

AddProjectionWithServices was only available on
MartenConfigurationExpression / MartenStoreExpression<T>, which means
callers need the builder chain returned by AddMarten(). In
modular-monolith setups the builder is consumed once at composition
root and individual modules only have IServiceCollection, forcing
users into the undiscoverable
TProjection.Register<TProjection>(services, ...) JasperFx internal.

Adds two new extensions on IServiceCollection mirroring the builder
versions exactly:

  services.AddProjectionWithServices<TProjection>(
      lifecycle, lifetime, configure);

  services.AddProjectionWithServices<TProjection, TStore>(
      lifecycle, lifetime, configure);

Both are thin wrappers over TProjection.Register<...>(services, ...).
Covered by two new integration tests
(ContainerScopedProjectionTests/projections_registered_directly_on_IServiceCollection)
that exercise the default-store and ancillary-store paths end-to-end
against Postgres.

## GH-4268 — reproducer attempt, does not reproduce

The reporter flipped three async projections to inline and hit a
migration exception involving mt_doc_envelope (Wolverine's outbox
table) and partitioned-table constraint handling.

This change adds the closest approximation I can build from the
reported setup:

  - Conjoined tenancy (events + all documents)
  - UseArchivedStreamPartitioning
  - Envelope metadata + Headers + Version enabled on all docs
  - EnableSideEffectsOnInlineProjections
  - TenantIdStyle.ForceLowerCase
  - Advanced.DefaultTenantUsageEnabled = false
  - AppendMode = Quick
  - Store A creates the schema + commits one event with the
    projection as Async
  - Store B points at the same schema with the projection flipped
    to Inline and then commits another event

Result: test passes cleanly. The reporter's error is almost
certainly tied to Wolverine-specific envelope integration that is
outside Marten, and cannot be reproduced from Marten alone without
more information.

Left in place as a baseline regression guard so this specific
direct async → inline flip on a conjoined + partitioned schema stays
green going forward. The test docstring spells out what we did and
did not reproduce.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Follow-up attempt: instead of letting SaveChanges lazily materialize
only the per-document storage, explicitly call
ApplyAllConfiguredChangesToDatabaseAsync on the separate store after
flipping to Inline. That makes Weasel diff the full model against the
existing schema — the widest possible surface the reporter's running
app could hit on the first deploy.

Result: still passes. The async → inline flip on a conjoined +
partitioned schema with every relevant option toggled does not trigger
the "unique constraint on partitioned table must include all
partitioning columns" failure, even when the full migration is
forced. This strengthens the earlier finding that the async → inline
flip alone isn't the driver. Something else in the reporter's
deployment history is making Marten's model describe the mt_doc_envelope
user document as single-tenant while the live table is conjoined +
partitioned, and we can't synthesize that condition without more
information.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
jeremydmiller added a commit to JasperFx/weasel that referenced this pull request Apr 21, 2026
…I break

Upgrades the Weasel graph onto the latest JasperFx (1.24.0 → 1.26.0),
which transitively upgrades Spectre.Console from 0.53 to 0.55.

Spectre.Console 0.55 removed the StringExtensions.EscapeMarkup(string)
extension (the static Spectre.Console.Markup.Escape(string) is the
replacement). Weasel.Core.CommandLine.AssertCommand used the old
extension form in two places, which surfaced downstream in Marten as
a MissingMethodException on every db-assert / resources command.
Patched AssertCommand to call Markup.Escape directly.

Unblocks JasperFx/marten#4269.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CoreTests.jasper_fx_mechanics.* were failing on PR CI with:

    System.MissingMethodException: Method not found:
      'System.String Spectre.Console.StringExtensions.EscapeMarkup(System.String)'
        at Weasel.Core.CommandLine.AssertCommand.Execute(WeaselInput input)

Root cause: Spectre.Console 0.55 (pulled in transitively by the
JasperFx 1.26 bump earlier on this branch) removed the
StringExtensions.EscapeMarkup(string) extension. The replacement
Spectre.Console.Markup.Escape(string) is the supported API. Weasel
8.14.1 switches to it; see JasperFx/weasel#252.

Local CoreTests.jasper_fx_mechanics subset: 13/13 pass (was 6/13).

Co-Authored-By: Claude Opus 4.7 (1M context) <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.

Feature request: AddProjectionWithServices usable without builder chain

1 participant