Skip to content

feat(events): durable inbox/outbox via Wolverine EF Core#192

Merged
antosubash merged 4 commits into
mainfrom
feature/event-system-7v91n
May 11, 2026
Merged

feat(events): durable inbox/outbox via Wolverine EF Core#192
antosubash merged 4 commits into
mainfrom
feature/event-system-7v91n

Conversation

@antosubash
Copy link
Copy Markdown
Owner

Summary

Replaces the in-process Wolverine event bus with a durable inbox/outbox implementation backed by the configured database (SQLite, PostgreSQL, or SQL Server). Closes the gap where events could be lost on a crash between publish and dispatch, and gives handlers at-most-once delivery via the inbox dedup.

  • WolverineFx.{Sqlite,Postgresql,SqlServer,EntityFrameworkCore} packages added (v5.31.0)
  • New WolverineConfiguration.Configure(...) helper shared between web host and worker — single source of truth for provider switch, EF transactional middleware, durable queue/inbox policies, and entity-event scraping
  • IEvent now requires EventId (Guid.CreateVersion7()) and OccurredAt; new DomainEvent record base supplies both. All 25 event records migrated
  • DomainEventInterceptor deleted, replaced by PublishDomainEventsFromEntityFrameworkCore<IHasDomainEvents>(x => x.Events)
  • 9 service-level publishes migrated to atomic outbox via IDbContextOutbox<T>.SaveChangesAndFlushMessagesAsyncTenantService (Update, ChangeStatus, AddHost, RemoveHost), EmailService.Templates (Update, Delete), SendEmailJob (Sent, Failed), RetryFailedEmailsJob
  • 5 publishes remain on bus.PublishAsync with documented carve-outs — events depending on DB-generated IDs, UserManager-driven flows, and SettingsService (DI cycle with AuditingMessageBus)
  • Auto-discovery of every module assembly for Wolverine handlers — per-module WolverineExtension shims removed
  • FakeDbContextOutbox<T> helper in SimpleModule.Tests.Shared/Fakes for unit-testing outbox-using services
  • SimpleModuleWebApplicationFactory uses a per-process temp SQLite file for Wolverine durability (Wolverine.Sqlite rejects in-memory)
  • CONSTITUTION.md Events section rewritten to describe the new guarantees and the remaining carve-outs

Verification

  • App boots cleanly via dotnet run --project template/SimpleModule.Host; Wolverine creates the message-store schema (wolverine_outgoing_envelopes, wolverine_incoming_envelopes, wolverine_dead_letters, etc.) in app.db on startup
  • New tests: WolverineAssemblyDiscoveryTests, WolverineDurabilitySmokeTests, DomainEventScrapingTests
  • Legacy DomainEventInterceptorTests removed (replaced by the scraping integration test)
  • All local CI steps green: npm run check, npm run build, dotnet build, dotnet test --no-build (938 tests across 17 projects)
  • E2E npm run test:smoke fails locally at auth setup; verified against main (no event-system changes) — fails identically there. Pre-existing macOS-only environment issue (port 5000 held by ControlCenter; smoke tests assume a CI environment). Should pass on the GitHub Actions runner.

Test plan

  • CI green on PR (in particular: e2e smoke tests, which fail locally on macOS for pre-existing environmental reasons)
  • Reviewer spot-checks WolverineConfiguration.Configure and the publisher migrations in TenantService / EmailService.Templates / Email jobs
  • Reviewer confirms the documented carve-outs in CONSTITUTION.md match the surviving bus.PublishAsync call sites

Replaces in-process Wolverine with durable messaging backed by the
configured database (SQLite, PostgreSQL, or SQL Server). Adds:

- WolverineFx.{Sqlite,Postgresql,SqlServer,EntityFrameworkCore} packages
- IEvent now requires EventId + OccurredAt; new DomainEvent record base
  supplies both via Guid.CreateVersion7() / DateTimeOffset.UtcNow. All
  25 event records migrated to inherit from DomainEvent
- DomainEventInterceptor deleted; replaced by Wolverine's
  PublishDomainEventsFromEntityFrameworkCore<IHasDomainEvents>(x => x.Events)
- WolverineConfiguration.Configure() helper shared by web host and worker
- Auto-discovery of every module assembly for handler scanning; per-module
  WolverineExtension shims removed
- 9 service-level publishes migrated to atomic outbox via
  IDbContextOutbox<T>.SaveChangesAndFlushMessagesAsync (Tenants: Update,
  ChangeStatus, AddHost, RemoveHost; Email templates: Update, Delete;
  SendEmailJob; RetryFailedEmailsJob)
- FakeDbContextOutbox<T> test helper for unit-testing outbox-using services
- Test factory uses per-process temp SQLite file for Wolverine durability
  (in-memory SQLite is rejected by Wolverine.Sqlite)
- CONSTITUTION.md documents the new guarantees and the small carve-out for
  publishes that stay on bus.PublishAsync (DB-generated IDs, UserManager,
  SettingsService DI cycle)
- New tests: WolverineAssemblyDiscoveryTests, WolverineDurabilitySmokeTests,
  DomainEventScrapingTests; legacy DomainEventInterceptorTests removed
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented May 11, 2026

Deploying simplemodule-website with  Cloudflare Pages  Cloudflare Pages

Latest commit: bfc7928
Status: ✅  Deploy successful!
Preview URL: https://4d6ebebc.simplemodule-website.pages.dev
Branch Preview URL: https://feature-event-system-7v91n.simplemodule-website.pages.dev

View logs

Adds three end-to-end integration tests in SimpleModule.Core.Tests
that exercise the durable event pipeline through the real host:

- PublishAsync_Routes_Event_Through_Durable_Wolverine_Pipeline:
  publishes SettingChangedEvent via IMessageBus, uses Wolverine's
  TrackedSession to wait for processing, asserts wolverine_outgoing/
  incoming/dead_letters tables exist on disk.
- AuditConfigCacheInvalidatorHandler_Runs_For_SettingChangedEvent:
  seeds the audit-config cache, publishes a SettingChangedEvent for
  an "auditlogs.*" key, and asserts the cache entry was drained by
  the handler — proves a real handler executes end-to-end.
- PublishedEvent_Survives_The_Durable_Pipeline_End_To_End:
  real HTTP PUT /api/settings against an authenticated client; the
  ASP.NET -> service -> Wolverine outbox path returns 2xx.

Also fixes a real pre-existing bug surfaced by this verification:
AuditConfigCacheInvalidator was never being discovered as a Wolverine
handler because its class name didn't end in a recognized convention
suffix (Handler/Consumer/Subscriber). Renamed to
AuditConfigCacheInvalidatorHandler so default discovery picks it up
and auditlogs.* setting changes now actually invalidate the request
config cache as intended.

Exposes SimpleModuleWebApplicationFactory.WolverineDbPath publicly
so tests can query the durability tables directly.
xUnit treats any exception from a fixture's Dispose/DisposeAsync as a
test-pipeline failure that fails the process even when every assertion
passes. On Linux CI runners Wolverine's host shutdown intermittently
throws during cleanup because the durability tables/connection are torn
down out from under its polling agents — exit code 1 despite "Passed!"
for every test project.

Wrap base.Dispose / base.DisposeAsync and the kept-alive SqliteConnection
disposal in try/catch on the test factory, and add the DisposeAsync
override (xUnit.v3 prefers async disposal) so the shutdown race is
silenced uniformly across local + CI.
PublishAsync is fire-and-forget; the assertion checking the handler's
side effect raced the in-memory dispatcher and failed on Linux CI
(passed on macOS where the dispatcher returned faster). Wrap the
publish in Wolverine.TrackActivity so the assertion only runs after
the handler has actually executed.
@antosubash antosubash merged commit 8d3795c into main May 11, 2026
6 checks passed
@antosubash antosubash deleted the feature/event-system-7v91n branch May 11, 2026 15:23
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.

1 participant