Changelog
All notable changes to this project are documented in this file, grouped by date.
The format is based on Keep a Changelog.
[2026-07-20]
Changed
- Infra: PostgreSQL bumped 17 → 18 across compose, the Aspire AppHost, and the Testcontainers integration fixture. PostgreSQL 18's official image moves the cluster from
/var/lib/postgresql/datato/var/lib/postgresql/{major}/docker, so the compose volume now mounts the whole/var/lib/postgresqltree instead of.../data(this also enables future major upgrades withpg_upgrade --link). Action required for local dev: an existingpostgres_datavolume holds a PG17 cluster that PG18 will not start against — rundocker compose down -vbefore the firstup. Local data is disposable; Liquibase recreates the schema. - Deps:
WolverineFx/.Kafka/.Postgresql/.RuntimeCompilation6.20.0→ 6.21.0. Headline is durable-inbox listener batching, which directly benefits the durable inbox enabled yesterday (upstream measured a 2,000 msg/s Kafka stream going from unbounded backlog to a steady 32ms delivery p50, +83% sustained durable throughput), plus a sender-batching flush fix and a faster/lower-allocation Kafka mapping hot path. Two upstream behavior changes to note: the per-message "success" log now defaults toDebug(wasInformation), andwolverine-execution-timebecame a floating-point histogram (same name/unit, different point type — check dashboards built on it). - Deps:
CloudNative.CloudEvents.Kafka2.8.0→ 3.9.0 andCloudNative.CloudEvents.SystemTextJson2.8.0→ 2.9.0. This resolves a latent version mismatch: the 2.8.0 Kafka package declaresConfluent.Kafka 1.9.3whileWolverineFx.Kafkapulls2.14.x, soCloudEventMapperwas compiled against a five-major-versions-old client API and unified upward at runtime. 3.9.0 targetsConfluent.Kafka 2.14.2, matching Wolverine exactly. (The 2.x → 3.x jump is the Kafka package's own version line; coreCloudNative.CloudEventsremains 2.9.0.) - Deps: OpenTelemetry family → 1.17.0 (exporter, hosting, and the AspNetCore/Http/Runtime instrumentation packages, now all driven by one property);
Microsoft.Extensions.Http.Resilience,.ServiceDiscovery,.Diagnostics.Testing→ 10.8.0;Microsoft.NET.Test.Sdk→ 18.8.1;SonarAnalyzer.CSharp→ 10.29.0.143774;Microsoft.SourceLink.GitHub→ 10.0.301;Scalar.AspNetCore→ 2.16.15;Microsoft.CodeAnalysis.Analyzers→ 5.6.0 (the 5.0.0 pin was already being overridden transitively to 5.3.0). - Infra: Kafka image
confluentinc/cp-kafka7.6.0→ 7.9.8 in compose and the integration fixture. 8.x was evaluated and rejected for now: Testcontainers 4.13'sKafkaBuilderdoes not setKAFKA_PROCESS_ROLES, which the 8.x entrypoint requires, so every integration test fails to start the broker. Revisit when Testcontainers adds 8.x support.
Fixed
- Tests: the integration fixture set
Aspire:Confluent:Kafka:Messaging:Consumer:Config:EnableAutoCommit=true, the same setting removed fromappsettingsyesterday — it suppressed Wolverine's at-least-once offset management, so the tests were not exercising the delivery guarantee the template ships. Removed. - Template: gated the AppHost's
Google.ProtobufandGrpc.AspNetCorePackageReferences behind#if (INCLUDE_GRPC)to match theirPackageVersion(alreadyINCLUDE_GRPC-gated inDirectory.Packages.props). Without this, gRPC-less configurations (--grpc false,--api false, aspire-only) generated an AppHost referencingGoogle.Protobufwith no corresponding version under Central Package Management and failed to build withNU1010. The AppHost uses only Aspire endpoint types (GrpcExtensions), so it needs neither package when gRPC is excluded.
Notes
Refitter.MSBuildis held at 2.0.0: 2.1.0's generator throwsMethod not found: System.Text.ValueStringBuilder.AsSpan()against the current runtime and fails the E2E client generation at build time.- Still outstanding (unchanged):
OpenTelemetry.Instrumentation.GrpcCore 1.0.0-beta.13is abandoned upstream and tied to the end-of-lifeGrpc.Corelibrary; it should be removed if the API only uses grpc-dotnet.dpage/pgadmin4:latestandazurite:lateststill float their tags, and there is noglobal.jsonpinning the SDK.
[2026-07-19]
Changed
- Messaging topology: Wolverine message persistence (inbox/outbox) and the PostgreSQL queue transport now live in the application database (
app_domain) instead of a separateservice_busdatabase. Wolverine persistence reuses the application's registeredNpgsqlDataSource(WolverineNpgsqlExtensionsresolves it from DI and passes the instance toPersistMessagesWithPostgresql) — the same data sourceTransactionalOutboxMiddlewareopens its transaction on — so the outbox tables are structurally guaranteed to share the application database and its connection. There is no longer a separateServiceBusconnection string that could be pointed at a different database and silently break outbox atomicity; the deadServiceBusconnection strings (appsettings, compose), the AppHostWithReference(database, connectionName: "ServiceBus")references, the keyedServiceBusdata source, and the integration fixture'sServiceBusentry were all removed. The library keeps aConnectionStrings:ServiceBusfallback for standalone use (no application data source registered). Wolverine does not support splitting the transport from the message store, and a separate database made the outbox non-atomic with business writes; co-locating them (per-servicesvcbus_{service}persistence schema +svcbus_queuesschema alongsidemain) removes the crash window in which committed business data could lose its outgoing events. Theservice_busdatabase, its Liquibase changelog, andliquibase.servicebus.propertieswere removed; thesvcbus_queuesschema is now pre-created by theapp_domainchangelog. - Messaging routing: removed
ConventionalLocalRoutingIsAdditive(). An integration event that a service both publishes and handles was previously processed twice — once via local routing at publish time and again when consumed back from its own Kafka subscription. With the default (non-additive) routing, such events go only to the external transport and are processed exactly once on consumption; messages without explicit external routes (commands, queries, local events) still route locally as before. - ServiceDefaults/Messaging: Wolverine schema names now carry a
svcbus_prefix so messaging infrastructure is clearly distinguishable from application schemas in the shared database — the per-service persistence schema issvcbus_{service}(e.g.svcbus_appdomain_api) and the PostgreSQL transport schema issvcbus_queues(wasqueues). - ServiceDefaults/Messaging:
Envelope.GetMessageName(fullName: true)caches the sanitized full type name per message type (previously fourstring.Replaceallocations per processed message via the OTel and performance middlewares).
Added
Template: new
TransactionalOutboxMiddleware(applied to allICommand<>handler chains via the AppDomain Wolverine extension) makes command handling fully atomic: it opens a single transaction on the application database, exposes it ambiently (TransactionalOutbox.CurrentTransaction),AppDomainDb(LinqToDB) instances resolved during the message execution attach to that transaction, and Wolverine's outbox is enlisted in it (MessageContext.EnlistInOutboxAsync+DatabaseEnvelopeTransaction), so cascaded integration events are persisted to the outgoing envelope table in the same transaction as the business writes — one commit covers both, and failures roll back both. Nested command invocations (the DbCommand pattern) join the ambient transaction instead of opening their own.ServiceDefaults/Messaging:
ConfigureReliableMessagingnow installs a default failure policy — transientNpgsqlExceptions andTimeoutExceptions get two quick in-process retries (50ms/250ms), then two durable scheduled retries (5s/30s) that release the listener instead of blocking it (so a cooling-down message never stalls a Kafka partition), then the dead letter queue. Previously no retry rules existed anywhere, so any transient failure dead-lettered on first attempt despite the docs claiming retry support. Application failure rules (added via theAddServiceBusconfigure callback) still take precedence.ServiceDefaults/Messaging: reliable messaging now also enables the durable inbox on all listening endpoints (
UseDurableInboxOnAllListeners) — incoming Kafka messages are persisted before processing, giving at-least-once delivery with duplicate detection by message id, and failed messages land in the replayable database dead-letter table.Docs: new dead-letter operations guide in
guide/messaging/wolverine.md— inspect (storage counts), replay (storage replay [--exception-type ...]), and programmaticIDeadLetterAdminServiceusage — closing the "no DLQ recovery story" gap.
Fixed
Kafka at-least-once delivery: removed
EnableAutoCommit: true/AutoCommitIntervalMsfrom the template's Kafka consumer configuration. Under Wolverine 6, an explicitEnableAutoCommit=truesuppresses Wolverine's commit management (KafkaOffsetCommitter.ResolveStrategy) and falls back to librdkafka storing offsets at consume time — a crash during message processing lost the message (the failure mode JasperFx/wolverine#2114 was opened against). With the keys removed, Wolverine's defaultCommitMode.StoreThenAutoFlushapplies:EnableAutoOffsetStore=false, offsets stored only after successful processing, and the commit watermark never advances past an in-flight message.KafkaWolverineExtensionsnow logs a startup warning if configuration reintroduces the unsafe combination.ServiceDefaults/Messaging:
AutoProvision/AutoBuildMessageStorageOnStartupare now read from theServiceBus:Wolverinesection — the same pathappsettings.jsonand the Kafka extension already use. PreviouslyWolverineSetupExtensionsreadServiceBus:AutoProvision, a key nothing sets, soAutoBuildMessageStorageOnStartupwas forced toNonein every environment.ServiceDefaults/Messaging: a
ServiceNameset explicitly in theAddServiceBusconfigure callback is no longer silently overwritten by theServiceBus:PublicServiceName-derived default (which also feeds the Postgres persistence schema name).ServiceDefaults/Messaging: integration-event publisher discovery now unions the explicitly marked
[DomainAssembly]assemblies (force-loaded via their type markers) into the loaded-assembly sweep, and prefix-matches on full namespace segments (NameorName.) instead of rawStartsWith. Fixes events being missed when a referenced contracts assembly had not been lazily loaded yet, and accidental matches of unrelated assemblies sharing a name prefix.Kafka: the "Configured Kafka subscriptions" log line now reports the actual consumer group id from the Aspire consumer config instead of Wolverine's
ServiceName, which is not the group id.ServiceDefaults/Messaging: FluentValidation middleware now flows the message
CancellationTokenintoValidateAsync, so async validators cancel with the request.Docs:
AddWolverineWithDefaultsXML remarks no longer overstate delivery guarantees — they now document that the outbox is only atomic with business data when both share the same database connection/transaction (not the case with the default separateservice_busdatabase + LinqToDB topology).
[2026-07-18]
Changed
- Deps: bumped
WolverineFx/WolverineFx.Kafka/WolverineFx.Postgresqlfrom5.39.xto6.20.0(major). Two runtime-default changes needed code fixes, both in the generated project template:- Core
WolverineFxno longer ships the Roslyn runtime compiler;TypeLoadMode.Dynamic(used locally whenCodegenEnabled: true, seeappsettings.Local.json) now needs the newWolverineFx.RuntimeCompilationpackage. Added it as an unconditional dependency ofMomentum.ServiceDefaults(every host type uses Dynamic mode locally) and callopts.UseRuntimeCompilation()explicitly inWolverineSetupExtensions— the package's usual[WolverineModule]auto-registration relies on Wolverine's assembly-discovery scan, which this codebase already disables viaExtensionDiscovery.ManualOnly. WolverineOptions.ServiceLocationPolicynow defaults toNotAllowed(wasAllowedButWarn). LinqToDB'sAddLinqToDBContext<T>registersDataOptions<T>behind an opaque lambda factory Wolverine's codegen can't statically resolve, which broke every command handler touching the database. Fixed with a scoped allow-list (opts.CodeGeneration.AlwaysUseServiceLocationFor<DataOptions<AppDomainDb>>()) registered viaIWolverineExtensionin the generated project'sDependencyInjection.cs, rather than disabling the new policy repo-wide.- Verified:
dotnet new mmt --local(default flags) restores, builds, and passes all 47 integration tests (Testcontainers Postgres + Liquibase); this repo's ownAppDomain.slnxpasses 350 unit + 209 integration/arch tests including all 49 real Wolverine-handler integration tests.
- Core
Fixed
EventMarkdownGenerator: GitHub source links for generated events no longer guess wrong when a project folder's name itself contains a dot (e.g.
AppDomain.BackOfficewas being split intoAppDomain/BackOffice/...) — the compiled assembly name is now used as the (unsplit) root segment. New optional--source-rootCLI flag additionally verifies the guessed file exists before emitting a link, falling back to#otherwise (catches cases reflection can never resolve, e.g. a type colocated in a file named after something else entirely).EventMarkdownGenerator:
Pluralize()no longer mangles past-tense/participle words (e.g."reservation-created"→"reservation-createds") — words ending in-edare now left unchanged, except for a small set of genuine-ednouns (bed,seed,speed, ...).EventMarkdownGenerator: topic fallback (when
Topicisn't set) now derives from the pluralized entity name instead of the event name.EventMarkdownGenerator: 22 event-doc scenario baselines regenerated to drop the bad pluralization they had baked in (
payment-processeds→payment-processed, etc);docs/events/events-sidebar.jsonduplicate entries removed as a byproduct of regeneration.Template docs:
docs/events/domain_events.mdandintegration_events.mdused HTML-comment-style<!--#if (INCLUDE_SAMPLE) -->conditional markers, but.template.config/template.jsonconfigures.mdconditional processing for shell-style# #if/# #endifsyntax (matching the rest of the repo's docs, e.g.docs/index.md). The mismatch meant the directives were never recognized, so every default-generated project shipped these two overview pages with the raw markers leaking into the rendered output. Both files now use the# #ifstyle already used elsewhere.Template README: same
<!--#if -->vs# #ifmismatch as above also brokeProjectREADME.md(the generated project's rootREADME.md) andsrc/AppDomain.Contracts/README.md— every generated project's README shipped with ~100 raw conditional-marker lines instead of the intended content. Converted both to# #ifstyle; verified default,--no-sample, and--orleansgenerations all render correctly with no leftover markers.
Added
- EventTopicAttribute: new
CollapseTopicOnDomainproperty (defaulttrue) — drops the topic segment from the fully-qualified topic name when it exactly duplicates the domain[.subdomain] path (e.g. domain"orders"+ topic"orders"→"orders.v1"instead of"orders.orders.v1"); set tofalseto always keep both segments.
Changed
- EventMarkdownGenerator: generated event docs now nest under
integration_events//domain_events/subfolders instead of sitting flat indocs/events/, classified by the sameIsInternalflag the sidebar already groups "Domain Events" by (not by namespace text, which can disagree with it — e.g. an event under anIntegrationEventsnamespace but markedInternal = true). Sidebar links, the VitePress fallback sidebar generator, and the staticintegration_events.mdoverview page all updated to match; realdocs/events/regenerated onto the new layout.
[2026-07-17]
Changed
- Deps: bumped
NSubstitutefrom5.3.0to6.0.0(major). Fixed three test files (CreateCashierCommandHandlerTests,UpdateCashierCommandHandlerTests,CreateInvoiceCommandHandlerTests) that castCallInfo's indexer directly ((T)x[0]), which no longer compiles under 6.0's nullable-annotated public API — switched to the null-safex.ArgAt<T>(0)extension instead.
Skipped
- Deps:
Refitter.MSBuild2.0.0→2.1.0was left in place — 2.1.0 refactored the MSBuild task itself (upstream decoupled the CLI binary from the task) and fails code generation locally withMissingMethodException: System.Text.ValueStringBuilder.AsSpan(). Needs an upstream fix before retrying.
[2026-07-16]
Changed
- Docs: widened both VitePress sites (
docs/,libs/Momentum/docs/) — layout frame now maxes out at 1640px (--vp-layout-max-width) and the doc content column at 900px, up from the VitePress defaults. - Docs: nav "Changelog" link now renders
CHANGELOG.mdinline as a docs page (/changelog) instead of linking out to GitHub.
Fixed
- Docs: Mermaid diagrams now render with the registered ELK layout (
layout: "elk"was never passed tomermaid.render, so the loader was registered but unused). - Docs: fixed 115 Mermaid diagrams across both sites using the invalid arrow
-/->, which failed to parse and silently dropped every affected diagram (most notably all ofdocs/arch/*.md).
Changed
- Docs: Mermaid diagrams now render with the
neolook (was unset, defaulting toclassic). - Docs: the architecture/system diagrams in
docs/arch/*.md(index.md,eda.md,events.md,background-processing.md) now use a consistent C4-style palette — blue for containers, light blue for domain/component-level nodes, grey for external systems (Kafka, third-party services) — replacing the ad-hoc pastelstyleoverrides that only existed on one of the four diagrams. - Deps: bumped
mermaidfrom^11.10.1to^11.16.0(latest) in bothdocs/andlibs/Momentum/docs/. - Docs: Mermaid subgraph/cluster backgrounds are now white instead of the default pale-yellow theme color (light theme only, via
themeVariables.clusterBkg).
[2026-07-15]
Added
- EventMarkdownGenerator:
TopicandFullyQualifiedTopicNameare now exposed separately onEventMetadataandEventViewModel.Topicis the plain topic / event hub name;FullyQualifiedTopicNameis the composed{env}.{domain}.{visibility}.{topic}.{version}convention string. - EventMarkdownGenerator:
--partition-key-attributeoption to discover partition keys by attribute name (or name prefix), mirroring--event-attribute. Partition key discovery and itsOrderare now resolved generically via reflection instead of a hardcodedPartitionKeyAttributetype. - EventMarkdownGenerator: event attributes may expose an optional
EventNameproperty to override the documented event name (falls back to the CLR type name when absent). - Docs: new VitePress guide page for the Event Documentation Generator (
libs/Momentum/docs/guide/messaging/event-documentation.md), covering MSBuild/CLI usage, template customization, and generated output structure.
Changed
- EventMarkdownGenerator:
EventMetadata.TopicName/EventViewModel.TopicNamereplaced byFullyQualifiedTopicName. The defaultevent.liquidtemplate now renders bothTopicandFully Qualified Topic. - EventMarkdownGenerator: multi-line XML doc property descriptions are flattened to a single line (whitespace collapsed) so they no longer break the generated markdown payload table.
Added
- EventMarkdownGenerator:
EventMetadata.EventTypeNameexposes the event's CLR type name separately fromEventName(which may be overridden via the topic attribute'sEventNameproperty). Rendered in generated docs as "Type Name". - EventMarkdownGenerator:
EventMetadata.AttributePropertiescaptures every public property of the discovered topic attribute (via reflection) into a name/value dictionary, so custom attribute properties the generator has no dedicated field for still surface in generated docs, under "Attribute Properties".
Changed
- EventMarkdownGenerator: extracted
EventMetadataBuilderfromAssemblyEventDiscoveryso assembly/type scanning stays independent from reflecting a single event type and its topic attribute intoEventMetadata. - EventMarkdownGenerator: extracted
EventPropertyMetadataBuilderfromEventMetadataBuilderso reflecting an event type's own properties/partition keys stays independent from resolving its topic attribute and computed fields (topic, domain, fully-qualified topic name, etc). - EventMarkdownGenerator: moved generic reflection helpers (
FindAttributeByName,GetPropertyValue<T>,MapConstructorParametersToProperties) that carry no event-metadata-specific meaning fromEventMetadataBuilderintoTypeUtils. Also removed a private kebab-case fallback that duplicated theMomentum.Extensions.Abstractions.Extensions.ToKebabCase()extension already imported in the same file.
Tests
- EventMarkdownGenerator: added an
all-properties-showcasescenario (realTestEventsfixture and XML doc input, checked intoIntegrationTestScenarios/) that exercises every renderableEventMetadataproperty at once — obsolete marker, explicit domain/topic/version, internal visibility, multiple partition keys, a complex nested property, a collection of a complex type, and real Summary/Remarks/Example/param documentation — as a durable, reviewable example alongside the existing marker-value completeness unit test. - EventMarkdownGenerator:
ScenarioBasedIntegrationTestsnow supports a per-scenariotemplates/event.liquidoverride (falling back to the default embedded template when absent). Used byall-properties-showcase, whose override renders every singleEventViewModelfield verbatim — including every entry ofProperties,PartitionKeys, andAttributeProperties— as a raw, exhaustive dump distinct from the polished default rendering.
Added
- EventMarkdownGenerator:
EventMetadata.Entityis now computed once at metadata-build time (moved offEventViewModelFactory). When the topic attribute isn't generic (noTEntityto reflect), it falls back to stripping a common event-verb suffix (Created,Updated,Deleted,Completed,Processed, etc.) off the event type's own name, e.g.WidgetCreated→widget. - EventMarkdownGenerator:
EventMetadata.EventNameKebabexposes a kebab-cased form ofEventName(respecting the sameEventNameattribute override) for topic-adjacent/URL-safe uses, without kebab-casing the human-facingEventName/heading itself. Rendered in generated docs as "Event Slug".
Changed
- EventMarkdownGenerator:
AssemblyEventDiscovery.DiscoverEventsnow returnsIEnumerable<EventWithDocumentation>(metadata paired with its XML documentation) instead of bareEventMetadata, doing thexmlParser.GetEventDocumentation(...)lookup internally. Removes the repeatedevents.Select(m => new EventWithDocumentation { ... })boilerplate every caller (GenerateCommand, tests) previously had to write itself. - EventMarkdownGenerator: removed
EventMetadata.EventType— the raw CLRTypewas only ever used to re-look-up XML documentation or extractEntity, both of which now happen once during metadata construction instead of being deferred to callers. - EventMarkdownGenerator:
FullyQualifiedTopicNameno longer includes a leading{env}.placeholder segment. It was never substituted by the doc generator (a design-time tool with no concept of a deployment environment), so it only ever showed up as a literal, unresolved token in generated docs. Now composed as{domain}.{visibility}.{topic}.{version}. - Docs: synced the
event.liquidvariable table in the new VitePress guide page (libs/Momentum/docs/guide/messaging/event-documentation.md) with the fields and behavior described above — dropped the stale{env}prefix, and addedEventNameKebab,EventTypeName,Domain,Summary, andAttributeProperties.
Fixed
- EventMarkdownGenerator:
FullyQualifiedTopicNameused the assembly-level default domain instead of the event's actually-resolved domain (explicit attributeDomainoverride, then namespace-derived, then the default) — so an event could renderDomain: comprehensive-domainin one field while its fully-qualified topic string still showed the unrelated default domain segment. Both fields now agree. - EventMarkdownGenerator: the domain segment of
FullyQualifiedTopicNamewas only lowercased (ToLowerInvariant()), not kebab-cased, so a multi-word domain likeTestEventsproduced the gluedtesteventsinstead oftest-events. Now usesToKebabCase(), consistent with howTopicandEntityare derived. - EventMarkdownGenerator: property descriptions whose XML doc summary wrapped onto multiple lines previously spilled out of their table cell as an orphaned line.
- EventMarkdownGenerator:
EventMetadata.Domainwas computed but never mapped onto the Liquid view model, so it never appeared in generated docs. Now rendered as "Domain". - EventMarkdownGenerator: the
TopicAttributefield onEventMetadatacould point at a different attribute instance than the one topic/domain/version were actually parsed from (its own lookup usedGetCustomAttributes<Attribute>().FirstOrDefault(), which matches any attribute, not specifically the topic one). It now reuses the single correctly-matched instance throughout. - DummyInvoiceGenerator (sample BackOffice service): resolved a DI lifetime crash on startup — the singleton
BackgroundServicewas constructor-injecting the scopedWolverine.IMessageBusdirectly, which fails ASP.NET Core's DI validation. It now resolvesIMessageBusfrom a newIServiceScopeFactory-created scope on each publish iteration. - AppDomain.BackOffice.Orleans: fixed a startup crash where resolving
GrainDirectory:Defaultwrote an incomplete provider section (ServiceKeywith noProviderType) intoIConfiguration, which Orleans' own configuration-driven provider discovery then tried and failed to auto-register. Grain directory service-name resolution is now read-only, since it's configured purely through the fluentAddAzureTableGrainDirectorycall. - AppDomain.BackOffice.Orleans: fixed Azure Table clustering failing with "No credentials specified" under Docker Compose — the
Clustering:ServiceKey/GrainStorage:Default:ServiceKeyvalues must be written back intoIConfigurationfor Orleans to bind its declarative provider to the correct keyed Azure client, which a prior fix attempt had inadvertently removed. - compose.yml: Azurite now starts with
--skipApiVersionCheck— the Azure SDK clients request the latest Azure Storage service API version, which is routinely ahead of what Azurite has added support for, breaking local Orleans clustering/grain-storage/grain-directory calls out of the box.
Security
- docs-dotnet.ts: the
docfx metadatabuild step now resolves thedocfxexecutable from the fixed.NETglobal-tools directory instead of an unpinnedPATHlookup, closing a SonarCloud security hotspot (typescript:S4036).