From 4e787fa2ea5516855297b6c40661dccf5f2e59e2 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Mon, 24 Aug 2026 22:00:22 +0200 Subject: [PATCH 01/11] Document new diagnostics overloads --- Snippets/Core/Core_10/Core_10.csproj | 2 +- .../Core_10/Hosting/StartUpDiagnostics.cs | 39 +++++++++++++++++-- .../Hosting/StartUpDiagnosticsWriter.txt | 2 +- Snippets/Core/Core_10/prerelease.txt | 0 nservicebus/hosting/startup-diagnostics.md | 6 +-- ...tics_adding-sections_core_[,10).partial.md | 3 ++ ...tics_adding-sections_core_[10,).partial.md | 15 +++++++ 7 files changed, 58 insertions(+), 9 deletions(-) create mode 100644 Snippets/Core/Core_10/prerelease.txt create mode 100644 nservicebus/hosting/startup-diagnostics_adding-sections_core_[,10).partial.md create mode 100644 nservicebus/hosting/startup-diagnostics_adding-sections_core_[10,).partial.md diff --git a/Snippets/Core/Core_10/Core_10.csproj b/Snippets/Core/Core_10/Core_10.csproj index 5afcf9ccb47..f787329ad48 100644 --- a/Snippets/Core/Core_10/Core_10.csproj +++ b/Snippets/Core/Core_10/Core_10.csproj @@ -12,7 +12,7 @@ - + diff --git a/Snippets/Core/Core_10/Hosting/StartUpDiagnostics.cs b/Snippets/Core/Core_10/Hosting/StartUpDiagnostics.cs index 8844cdf8de9..c4ade47c200 100644 --- a/Snippets/Core/Core_10/Hosting/StartUpDiagnostics.cs +++ b/Snippets/Core/Core_10/Hosting/StartUpDiagnostics.cs @@ -1,5 +1,6 @@ namespace Core.Hosting; +using System.Text.Json.Serialization; using System.Threading.Tasks; using NServiceBus; using NServiceBus.Settings; @@ -44,12 +45,44 @@ void CustomDiagnosticsSection(IReadOnlySettings settings) settings.AddStartupDiagnosticsSection( sectionName: "MySection", - section: new + section: new MyDiagnostics { SomeSetting = "some data", SomeOtherSetting = 10 - }); + }, + typeInfo: DiagnosticsJsonContext.Default.MyDiagnostics); #endregion } -} \ No newline at end of file + + void CustomDiagnosticsSectionFactory(IReadOnlySettings settings) + { + #region CustomDiagnosticsSectionFactory + + settings.AddStartupDiagnosticsSectionFactory( + sectionName: "MySection", + sectionFactory: () => new MyDiagnostics + { + SomeSetting = "some data", + SomeOtherSetting = 10 + }, + typeInfo: DiagnosticsJsonContext.Default.MyDiagnostics); + + #endregion + } +} + +#region CustomDiagnosticsSectionTypes + +sealed class MyDiagnostics +{ + public required string SomeSetting { get; init; } + public required int SomeOtherSetting { get; init; } +} + +[JsonSerializable(typeof(MyDiagnostics))] +sealed partial class DiagnosticsJsonContext : JsonSerializerContext +{ +} + +#endregion diff --git a/Snippets/Core/Core_10/Hosting/StartUpDiagnosticsWriter.txt b/Snippets/Core/Core_10/Hosting/StartUpDiagnosticsWriter.txt index 30b90556c9b..d2cfd6425b4 100644 --- a/Snippets/Core/Core_10/Hosting/StartUpDiagnosticsWriter.txt +++ b/Snippets/Core/Core_10/Hosting/StartUpDiagnosticsWriter.txt @@ -1,5 +1,5 @@ Snippet is code generated by StartUpDiagnosticsWriter.cs startcode StartUpDiagnosticsWriter -{ "Container": { "Type": "external" }, "Endpoint": { "Name": "StartUpDiagnosticsWriter", "SendOnly": false, "NServiceBusVersion": "10.2.0" }, "Features": [ { "Name": "NServiceBus.ReceiveStatisticsFeature", "Enabled": false, "Active": true, "PrerequisiteStatus": { "IsSatisfied": true, "Reasons": [] }, "Dependencies": [], "Version": "10.2.0", +{ "Container": { "Type": "external" }, "Endpoint": { "Name": "StartUpDiagnosticsWriter", "SendOnly": false, "NServiceBusVersion": "10.3.0" }, "Features": [ { "Name": "NServiceBus.ReceiveStatisticsFeature", "Enabled": false, "Active": true, "PrerequisiteStatus": { "IsSatisfied": true, "Reasons": [] }, "Dependencies": [], "Version": "10.3.0", ... endcode \ No newline at end of file diff --git a/Snippets/Core/Core_10/prerelease.txt b/Snippets/Core/Core_10/prerelease.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/nservicebus/hosting/startup-diagnostics.md b/nservicebus/hosting/startup-diagnostics.md index 26788156789..2ce887fe795 100644 --- a/nservicebus/hosting/startup-diagnostics.md +++ b/nservicebus/hosting/startup-diagnostics.md @@ -3,7 +3,7 @@ title: Startup diagnostics summary: Describes the mechanism for gathering diagnostic information when endpoints start component: Core versions: '[7,)' -reviewed: 2026-05-07 +reviewed: 2026-08-24 --- > [!NOTE] @@ -41,8 +41,6 @@ snippet: CustomDiagnosticsWriter ## Adding startup diagnostics sections -To extend the startup diagnostics with custom sections: - -snippet: CustomDiagnosticsSection +partial: adding-sections Settings can be accessed from a [feature](/nservicebus/pipeline/features.md#feature-setup) or via the [endpoint configuration](/nservicebus/pipeline/features.md#feature-settings-endpointconfiguration). diff --git a/nservicebus/hosting/startup-diagnostics_adding-sections_core_[,10).partial.md b/nservicebus/hosting/startup-diagnostics_adding-sections_core_[,10).partial.md new file mode 100644 index 00000000000..3092539be42 --- /dev/null +++ b/nservicebus/hosting/startup-diagnostics_adding-sections_core_[,10).partial.md @@ -0,0 +1,3 @@ +To extend the startup diagnostics with custom sections: + +snippet: CustomDiagnosticsSection diff --git a/nservicebus/hosting/startup-diagnostics_adding-sections_core_[10,).partial.md b/nservicebus/hosting/startup-diagnostics_adding-sections_core_[10,).partial.md new file mode 100644 index 00000000000..3451e72f95f --- /dev/null +++ b/nservicebus/hosting/startup-diagnostics_adding-sections_core_[10,).partial.md @@ -0,0 +1,15 @@ +To extend the startup diagnostics with custom sections: + +snippet: CustomDiagnosticsSection + +Starting in version 10.3, custom sections can be registered with a strongly-typed value and its `JsonTypeInfo`. The section value must be a named type registered with a source-generated `JsonSerializerContext`: + +snippet: CustomDiagnosticsSectionTypes + +Registering sections with type information makes startup diagnostics serialization AOT-safe and trimming-safe. This is required when reflection-based serialization is disabled, such as in NativeAOT applications. In that case, a section registered with the object-based overload cannot be serialized. NServiceBus logs an error identifying the section, and the diagnostics document is not written. When reflection-based serialization is disabled, every section in the document must be registered with type information. A single legacy section prevents the complete document from being written. + +The object-based overload remains available and continues to work when reflection-based serialization is enabled. + +Use the factory overload when the diagnostics value is expensive to compute and should only be evaluated when the diagnostics are actually written. The factory is evaluated once, when the diagnostics document is written, and the resulting value is reused for every output target, including the log, the file, and any custom diagnostics writer. For cheap values, prefer the direct overload: + +snippet: CustomDiagnosticsSectionFactory From 634d784feb1c283f8d98d8b27189be1a314b855c Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Mon, 24 Aug 2026 22:09:51 +0200 Subject: [PATCH 02/11] ReplyToOriginator changes due to obsoletion and warnings as errors --- Snippets/Core/Core_10/Sagas/Reply/MySaga.cs | 2 +- Snippets/Core/Core_10/Sagas/Timeouts/MySaga.cs | 4 ++-- samples/azure/azure-table/table/ASTP_7/Client/Client.csproj | 2 +- samples/azure/azure-table/table/ASTP_7/Server/Server.csproj | 2 +- .../azure/azure-table/table/ASTP_7/Server/ShipOrderSaga.cs | 2 +- .../table/ASTP_7/SharedMessages/SharedMessages.csproj | 2 +- samples/cosmosdb/container/CosmosDB_4/Client/Client.csproj | 2 +- samples/cosmosdb/container/CosmosDB_4/Server/Server.csproj | 2 +- samples/cosmosdb/container/CosmosDB_4/Server/ShipOrderSaga.cs | 2 +- .../container/CosmosDB_4/SharedMessages/SharedMessages.csproj | 2 +- 10 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Snippets/Core/Core_10/Sagas/Reply/MySaga.cs b/Snippets/Core/Core_10/Sagas/Reply/MySaga.cs index 7ad5cf82dc8..ef54c5767af 100644 --- a/Snippets/Core/Core_10/Sagas/Reply/MySaga.cs +++ b/Snippets/Core/Core_10/Sagas/Reply/MySaga.cs @@ -16,7 +16,7 @@ public Task Handle(StartMessage message, IMessageHandlerContext context) { SomeId = Data.SomeId }; - return ReplyToOriginator(context, almostDoneMessage); + return ReplyToOriginator(context, almostDoneMessage); } #endregion diff --git a/Snippets/Core/Core_10/Sagas/Timeouts/MySaga.cs b/Snippets/Core/Core_10/Sagas/Timeouts/MySaga.cs index 4121f5dddd8..eed5bc00d2a 100644 --- a/Snippets/Core/Core_10/Sagas/Timeouts/MySaga.cs +++ b/Snippets/Core/Core_10/Sagas/Timeouts/MySaga.cs @@ -30,14 +30,14 @@ public Task Handle(Message2 message, IMessageHandlerContext context) { SomeId = Data.SomeId }; - return ReplyToOriginator(context, almostDoneMessage); + return ReplyToOriginator(context, almostDoneMessage); } public Task Timeout(MyCustomTimeout state, IMessageHandlerContext context) { if (!Data.Message2Arrived) { - return ReplyToOriginator(context, new TiredOfWaitingForMessage2()); + return ReplyToOriginator(context, new TiredOfWaitingForMessage2()); } return Task.CompletedTask; } diff --git a/samples/azure/azure-table/table/ASTP_7/Client/Client.csproj b/samples/azure/azure-table/table/ASTP_7/Client/Client.csproj index a3fd46ff7ec..5183c1e2c60 100644 --- a/samples/azure/azure-table/table/ASTP_7/Client/Client.csproj +++ b/samples/azure/azure-table/table/ASTP_7/Client/Client.csproj @@ -5,7 +5,7 @@ 14.0 - + diff --git a/samples/azure/azure-table/table/ASTP_7/Server/Server.csproj b/samples/azure/azure-table/table/ASTP_7/Server/Server.csproj index a341710ebb3..3a5e0324b0f 100644 --- a/samples/azure/azure-table/table/ASTP_7/Server/Server.csproj +++ b/samples/azure/azure-table/table/ASTP_7/Server/Server.csproj @@ -5,7 +5,7 @@ 14.0 - + diff --git a/samples/azure/azure-table/table/ASTP_7/Server/ShipOrderSaga.cs b/samples/azure/azure-table/table/ASTP_7/Server/ShipOrderSaga.cs index ae9623b7c13..5d91bbbb2bf 100644 --- a/samples/azure/azure-table/table/ASTP_7/Server/ShipOrderSaga.cs +++ b/samples/azure/azure-table/table/ASTP_7/Server/ShipOrderSaga.cs @@ -32,7 +32,7 @@ public Task Timeout(CompleteOrder state, IMessageHandlerContext context) state.OrderId = Data.OrderId; - return ReplyToOriginator(context, state); + return ReplyToOriginator(context, state); } } diff --git a/samples/azure/azure-table/table/ASTP_7/SharedMessages/SharedMessages.csproj b/samples/azure/azure-table/table/ASTP_7/SharedMessages/SharedMessages.csproj index 47b57e5d963..a5c6a96e372 100644 --- a/samples/azure/azure-table/table/ASTP_7/SharedMessages/SharedMessages.csproj +++ b/samples/azure/azure-table/table/ASTP_7/SharedMessages/SharedMessages.csproj @@ -4,6 +4,6 @@ 14.0 - + diff --git a/samples/cosmosdb/container/CosmosDB_4/Client/Client.csproj b/samples/cosmosdb/container/CosmosDB_4/Client/Client.csproj index 655606df4f6..e4257f45aba 100644 --- a/samples/cosmosdb/container/CosmosDB_4/Client/Client.csproj +++ b/samples/cosmosdb/container/CosmosDB_4/Client/Client.csproj @@ -5,7 +5,7 @@ 14.0 - + diff --git a/samples/cosmosdb/container/CosmosDB_4/Server/Server.csproj b/samples/cosmosdb/container/CosmosDB_4/Server/Server.csproj index 54318afe08a..6975961421a 100644 --- a/samples/cosmosdb/container/CosmosDB_4/Server/Server.csproj +++ b/samples/cosmosdb/container/CosmosDB_4/Server/Server.csproj @@ -5,7 +5,7 @@ 14.0 - + diff --git a/samples/cosmosdb/container/CosmosDB_4/Server/ShipOrderSaga.cs b/samples/cosmosdb/container/CosmosDB_4/Server/ShipOrderSaga.cs index a352ff6204e..e5603f9ec73 100644 --- a/samples/cosmosdb/container/CosmosDB_4/Server/ShipOrderSaga.cs +++ b/samples/cosmosdb/container/CosmosDB_4/Server/ShipOrderSaga.cs @@ -31,7 +31,7 @@ public Task Timeout(CompleteOrder state, IMessageHandlerContext context) state.OrderId = Data.OrderId; - return ReplyToOriginator(context, state); + return ReplyToOriginator(context, state); } } diff --git a/samples/cosmosdb/container/CosmosDB_4/SharedMessages/SharedMessages.csproj b/samples/cosmosdb/container/CosmosDB_4/SharedMessages/SharedMessages.csproj index a2f224dbcc9..342d4ae9cec 100644 --- a/samples/cosmosdb/container/CosmosDB_4/SharedMessages/SharedMessages.csproj +++ b/samples/cosmosdb/container/CosmosDB_4/SharedMessages/SharedMessages.csproj @@ -4,6 +4,6 @@ 14.0 - + \ No newline at end of file From 57196597f8131470513412e2081f5789417fa61a Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Mon, 24 Aug 2026 22:59:32 +0200 Subject: [PATCH 03/11] Trimming documentation and guidance --- menu/menu.yaml | 2 + .../messaging/messages-events-commands.md | 3 +- .../trimming-safe-messaging-overloads.md | 129 ++++++++++++++++++ 3 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 nservicebus/messaging/trimming-safe-messaging-overloads.md diff --git a/menu/menu.yaml b/menu/menu.yaml index b18744b291a..94ed64f7f78 100644 --- a/menu/menu.yaml +++ b/menu/menu.yaml @@ -397,6 +397,8 @@ Title: Non-Durable Messaging - Url: nservicebus/messaging/forwarding Title: Forwarding + - Url: nservicebus/messaging/trimming-safe-messaging-overloads + Title: Trimming-safe messaging overloads - Url: nservicebus/messaging/delayed-delivery Title: Delayed Delivery Articles: diff --git a/nservicebus/messaging/messages-events-commands.md b/nservicebus/messaging/messages-events-commands.md index b86897527ed..ae09e34ccc9 100644 --- a/nservicebus/messaging/messages-events-commands.md +++ b/nservicebus/messaging/messages-events-commands.md @@ -2,10 +2,11 @@ title: Messages, events, and commands summary: Messages as commands or events are the the unit of communication for message-based distributed systems. NServiceBus ensures they are used correctly. component: Core -reviewed: 2025-02-19 +reviewed: 2026-08-24 related: - nservicebus/messaging/conventions - nservicebus/messaging/unobtrusive-mode + - nservicebus/messaging/trimming-safe-messaging-overloads - samples/message-assembly-sharing redirects: - nservicebus/introducing-ievent-and-icommand diff --git a/nservicebus/messaging/trimming-safe-messaging-overloads.md b/nservicebus/messaging/trimming-safe-messaging-overloads.md new file mode 100644 index 00000000000..8615aed907c --- /dev/null +++ b/nservicebus/messaging/trimming-safe-messaging-overloads.md @@ -0,0 +1,129 @@ +--- +title: Trimming-safe messaging overloads +summary: Use the strongly-typed messaging overloads and the migration analyzer to make messaging code trimming-safe and AOT-safe +component: Core +versions: '[10,)' +reviewed: 2026-08-24 +related: + - nservicebus/messaging/send-a-message + - nservicebus/messaging/reply-to-a-message + - nservicebus/operations/nservicebus-analyzer +--- + +Starting in NServiceBus version 10.3.0, the messaging APIs provide strongly-typed overloads that are safe to use with [trimming](https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/) and [NativeAOT](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/). A migration analyzer included in the NServiceBus package guides existing applications to these overloads. + +## What does trimming-safe mean + +Trimming removes unreferenced code and metadata from an application at publish time, and NativeAOT compiles the application ahead of time. In both scenarios, the runtime type information that reflection-based code depends on may no longer be available. + +Code is trimming-safe when the types it needs are known at compile time and preserved in the published application. Code that discovers types at runtime, such as `message.GetType()`, is not trimming-safe because the metadata for the type may have been removed. + +## Why messaging needs strongly-typed overloads + +The object-based overloads, such as `Send(message, options)` and `Publish(message, options)`, determine the message type at runtime by calling `message.GetType()`. When trimming or NativeAOT is enabled, the runtime type information required to route the message may no longer be available, so these overloads cannot be analyzed statically and are annotated with `RequiresUnreferencedCode`. + +Strongly-typed overloads carry the message type either in the generic type argument, as in `Send(message, options)`, or as an explicit `Type` parameter, as in `Send(message, messageType, options)`. Because the type is known at compile time, these overloads are trimming-safe and AOT-safe. + +## Strongly-typed overloads + +The following messaging operations provide generic overloads that are trimming-safe: + +| Operation | Generic overload | +| -- | -- | +| Send | `Send(T message, SendOptions options)` | +| Publish | `Publish(T message, PublishOptions options)` | +| Reply | `Reply(T message)` | +| SendLocal | `SendLocal(T message)` | +| UpdateMessage | `UpdateMessage(T newInstance)` | +| ReplyToOriginator | `ReplyToOriginator(T message)` | + +## Explicit-type overloads + +Middleware and platform code often receives messages as `object` after the compile-time type has been erased. In this case, the generic overload cannot be used, but the logical message type is still known. The explicit-type overloads accept that type directly: + +```csharp +object message = CreateMessage(); // compile-time type is erased +Type messageType = typeof(MyMessage); // logical message type is still known + +await session.Send(message, messageType, new SendOptions()); +``` + +This is a common pattern for type-erased scenarios. Explicit-type overloads exist for `Send`, `Publish`, `Reply`, `SendLocal`, and `UpdateMessage`. + +## Route and publisher registration + +Routes and publishers can be registered by message type, by assembly, or by namespace. Registration by assembly or namespace requires scanning assemblies at startup, which is not trimming-safe. Register routes and publishers by message type instead; see [routing](/nservicebus/messaging/routing.md) for details. + +## Compatibility + +The object-based overloads remain available, and existing calls continue to select them. Recompiling an existing application does not change its routing behavior: messages are still routed using their runtime type until the code is migrated explicitly. + +## Migration analyzer + +The NServiceBus package ships a migration analyzer that reports three diagnostics: + +| Rule ID | Title | Severity | Active by default | Code fix | +| -- | -- | -- | -- | -- | +| NSB0039 | Use the strongly typed message overload | Info | No | Yes | +| NSB0040 | Message routing uses the runtime type | Warning | No | No | +| NSB0041 | The message type must not be System.Object | Warning | Yes | No | + +### NSB0039 — Use the strongly typed message overload + +This diagnostic fires when the analyzer can prove that the runtime type of the message matches its static type, for example when the message is created directly: + +```csharp +await session.Send(new MyMessage(), new SendOptions()); +``` + +The provided code fix rewrites the call to use the generic overload: + +```csharp +await session.Send(new MyMessage(), new SendOptions()); +``` + +### NSB0040 — Message routing uses the runtime type + +This diagnostic fires when the runtime type can differ from the static type, for example when the message is passed through an interface, a base class, or a method return value. Changing such a call to a strongly-typed overload can change routing, subscriptions, or logical message identity, so this diagnostic intentionally has no code fix. Treat it as a routing decision rather than a mechanical migration. + +### NSB0041 — The message type must not be System.Object + +This diagnostic fires when the generic overload is called with `System.Object` as the explicit type argument, for example `Send(message, options)`. The strongly-typed overload would route the message as `System.Object`, which is never the intent. Specify the actual message type instead. + +## When the migration diagnostics are active + +NSB0039 and NSB0040 are quiet by default in ordinary builds. They activate automatically when a project enables trimming or AOT compatibility: + +* `PublishTrimmed` +* `PublishAot` +* `IsAotCompatible` +* `IsTrimmable` +* `EnableTrimAnalyzer` + +The diagnostics can also be enabled per rule in `.editorconfig`: + +```ini +[*.cs] +dotnet_diagnostic.NSB0039.severity = suggestion +dotnet_diagnostic.NSB0040.severity = warning +``` + +An explicit per-rule severity takes precedence over automatic activation, so `none` deliberately suppresses that rule for the matching files. NSB0041 is always active because calling the generic overload with `System.Object` is always incorrect. + +## Migration path + +The object-only overloads are removed in a single major version transition: + +| Version | Experience | +| -- | -- | +| 10.x | Object-only overloads remain available and continue to win overload resolution. The migration diagnostics are quiet by default and activate for trimming, AOT, or the explicit migration audit. | +| 11 | Object-only overloads are removed. Calls that preserve the runtime-type routing compile unchanged, and calls where generic inference could change routing are flagged by a new diagnostic, enabled as an error by default. Those calls must choose explicitly between a generic argument and an explicit-`Type` overload. | + +Users who migrate early may add explicit generic type arguments, such as `Send(new MyMessage(), options)`. After the object-only overloads are removed, the IDE may flag those type arguments as redundant, because `Send(new MyMessage(), options)` infers the same message type. Whether to keep or remove the explicit type argument is a choice: keeping it routes the message using the explicit type at the call site, while removing it relies on type inference. Both are valid as long as the inferred type matches the intended logical message type. + +## Related trimming guidance + +Strongly-typed messaging is one part of running an NServiceBus endpoint with trimming or NativeAOT: + +* [Startup diagnostics](/nservicebus/hosting/startup-diagnostics.md#adding-startup-diagnostics-sections) — register diagnostics sections with type information so the diagnostics document can be serialized without reflection. +* [Registering handlers and sagas](/nservicebus/handlers-and-sagas-registration.md) — source-generated registration is trimming and AOT-friendly; discovery by [assembly scanning](/nservicebus/hosting/assembly-scanning.md) relies on runtime type information. From 827c22c3a5c5624b8db02a22215f9b4aaa7338f7 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Tue, 25 Aug 2026 14:44:29 +0200 Subject: [PATCH 04/11] Another saga edge case --- Snippets/Testing/Testing_10/Saga/MySaga.cs | 2 +- Snippets/Testing/Testing_10/Testing_10.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Snippets/Testing/Testing_10/Saga/MySaga.cs b/Snippets/Testing/Testing_10/Saga/MySaga.cs index e008f84c608..fa80d08c804 100644 --- a/Snippets/Testing/Testing_10/Saga/MySaga.cs +++ b/Snippets/Testing/Testing_10/Saga/MySaga.cs @@ -9,7 +9,7 @@ public class MySaga : { public async Task Handle(StartsSaga message, IMessageHandlerContext context) { - await ReplyToOriginator(context, new MyResponse()); + await ReplyToOriginator(context, new MyResponse()); await context.Publish(new MyEvent()); await context.Send(new MyCommand()); await RequestTimeout(context, TimeSpan.FromDays(7), message); diff --git a/Snippets/Testing/Testing_10/Testing_10.csproj b/Snippets/Testing/Testing_10/Testing_10.csproj index 0872eeda178..e2053ac85fd 100644 --- a/Snippets/Testing/Testing_10/Testing_10.csproj +++ b/Snippets/Testing/Testing_10/Testing_10.csproj @@ -4,7 +4,7 @@ - + From 139a619c25bb77d1bfe613ef542a74e2b46dbf4c Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Tue, 25 Aug 2026 14:53:13 +0200 Subject: [PATCH 05/11] Cross link analyzer --- nservicebus/operations/nservicebus-analyzer.md | 4 +++- ...rvicebus-analyzer_migration-analyzer_core_[10,).partial.md | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 nservicebus/operations/nservicebus-analyzer_migration-analyzer_core_[10,).partial.md diff --git a/nservicebus/operations/nservicebus-analyzer.md b/nservicebus/operations/nservicebus-analyzer.md index fde7a9b447d..7f053f73208 100644 --- a/nservicebus/operations/nservicebus-analyzer.md +++ b/nservicebus/operations/nservicebus-analyzer.md @@ -1,7 +1,7 @@ --- title: NServiceBus Analyzer summary: How to use the NServiceBus analyzer to avoid missing awaits -reviewed: 2025-03-31 +reviewed: 2026-08-24 component: Core versions: '[6,]' --- @@ -64,3 +64,5 @@ Add a `NSB0001` element to the csproj file. ### Treat warnings as errors The C# compiler already contains a set of inspections which can warn about incorrect usage of `async` and `Task`-based APIs. It is recommended to treat these warnings as errors to ensure they are not missed accidentally. This feature can be enabled by the project settings or by adding `true` to the `csproj` file directly. + +partial: migration-analyzer diff --git a/nservicebus/operations/nservicebus-analyzer_migration-analyzer_core_[10,).partial.md b/nservicebus/operations/nservicebus-analyzer_migration-analyzer_core_[10,).partial.md new file mode 100644 index 00000000000..2fc5369f273 --- /dev/null +++ b/nservicebus/operations/nservicebus-analyzer_migration-analyzer_core_[10,).partial.md @@ -0,0 +1,3 @@ +## Migration analyzer + +The NServiceBus package also includes a migration analyzer for the messaging APIs. It reports the NSB0039, NSB0040, and NSB0041 diagnostics when messages are sent, published, or replied to using the object-based overloads that are not trimming-safe. See [Trimming-safe messaging overloads](/nservicebus/messaging/trimming-safe-messaging-overloads.md) for details. From 5c7cbc70afa36d0c31547ebfe31622e279da6ea3 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Thu, 3 Sep 2026 10:01:42 +0200 Subject: [PATCH 06/11] Update to alpha.2 --- Snippets/Core/Core_10/Core_10.csproj | 2 +- .../Core_10/Scanning/ManualRegistration.cs | 9 ++++++++ Snippets/Testing/Testing_10/Testing_10.csproj | 2 +- .../handlers-and-sagas-registration.md | 2 +- ...registration-content_core_[10,).partial.md | 2 ++ .../trimming-safe-messaging-overloads.md | 21 ++++++++++++++++++- nservicebus/serialization/index.md | 2 +- .../index_security_core_[10,).partial.md | 14 +++++++++++++ ... => index_security_core_[7,10).partial.md} | 0 .../table/ASTP_7/Client/Client.csproj | 2 +- .../table/ASTP_7/Server/Server.csproj | 2 +- .../SharedMessages/SharedMessages.csproj | 2 +- .../container/CosmosDB_4/Client/Client.csproj | 2 +- .../container/CosmosDB_4/Server/Server.csproj | 2 +- .../SharedMessages/SharedMessages.csproj | 2 +- 15 files changed, 55 insertions(+), 11 deletions(-) create mode 100644 nservicebus/serialization/index_security_core_[10,).partial.md rename nservicebus/serialization/{index_security_core_[7,].partial.md => index_security_core_[7,10).partial.md} (100%) diff --git a/Snippets/Core/Core_10/Core_10.csproj b/Snippets/Core/Core_10/Core_10.csproj index f787329ad48..f6178d184ab 100644 --- a/Snippets/Core/Core_10/Core_10.csproj +++ b/Snippets/Core/Core_10/Core_10.csproj @@ -12,7 +12,7 @@ - + diff --git a/Snippets/Core/Core_10/Scanning/ManualRegistration.cs b/Snippets/Core/Core_10/Scanning/ManualRegistration.cs index c561997184a..da02e4a8b93 100644 --- a/Snippets/Core/Core_10/Scanning/ManualRegistration.cs +++ b/Snippets/Core/Core_10/Scanning/ManualRegistration.cs @@ -43,6 +43,15 @@ void RegisterInstallerManually(EndpointConfiguration endpointConfiguration) #endregion } + + void RegisterMessageTypeManually(EndpointConfiguration endpointConfiguration) + { + #region RegisterMessageTypeManually + + endpointConfiguration.AddMessageType(); + + #endregion + } } public class PlaceOrderHandler : IHandleMessages diff --git a/Snippets/Testing/Testing_10/Testing_10.csproj b/Snippets/Testing/Testing_10/Testing_10.csproj index e2053ac85fd..cddcc52f328 100644 --- a/Snippets/Testing/Testing_10/Testing_10.csproj +++ b/Snippets/Testing/Testing_10/Testing_10.csproj @@ -4,7 +4,7 @@ - + diff --git a/nservicebus/handlers-and-sagas-registration.md b/nservicebus/handlers-and-sagas-registration.md index 18d73715de6..2cd19fc13ab 100644 --- a/nservicebus/handlers-and-sagas-registration.md +++ b/nservicebus/handlers-and-sagas-registration.md @@ -2,7 +2,7 @@ title: Registering Handlers and Sagas summary: How to register message handlers and sagas with an NServiceBus endpoint. component: Core -reviewed: 2026-04-27 +reviewed: 2026-09-03 --- Registration tells an endpoint which message handlers and sagas to include. NServiceBus supports explicit registration (which is recommended for new endpoints) and automatic discovery via assembly scanning (which is useful for plugin or dynamic discovery scenarios). diff --git a/nservicebus/handlers-and-sagas-registration_registration-content_core_[10,).partial.md b/nservicebus/handlers-and-sagas-registration_registration-content_core_[10,).partial.md index bc020d49c19..114186c1399 100644 --- a/nservicebus/handlers-and-sagas-registration_registration-content_core_[10,).partial.md +++ b/nservicebus/handlers-and-sagas-registration_registration-content_core_[10,).partial.md @@ -100,6 +100,8 @@ snippet: DisableAssemblyScanning When assembly scanning is disabled, message handlers, sagas, features, and installers must be explicitly registered. Messages received without a registered handler or saga [will fail and be moved to the error queue](/nservicebus/handlers/?version=core_10#no-handler-for-a-message). +In trimmed or NativeAOT deployments, message types that are only sent, published, or replied to, and that no registered handler or saga handles, must also be registered explicitly with `AddMessageType()`, available starting in NServiceBus version 10.3. See [registering message types](/nservicebus/messaging/trimming-safe-messaging-overloads.md#registering-message-types). + ### Fine-grained scanning configuration Scanning can be configured with exclusions, additional paths, nested directories, and exception handling. See [Assembly scanning](/nservicebus/hosting/assembly-scanning.md) for all configuration options. diff --git a/nservicebus/messaging/trimming-safe-messaging-overloads.md b/nservicebus/messaging/trimming-safe-messaging-overloads.md index 8615aed907c..821b85bdc36 100644 --- a/nservicebus/messaging/trimming-safe-messaging-overloads.md +++ b/nservicebus/messaging/trimming-safe-messaging-overloads.md @@ -3,7 +3,7 @@ title: Trimming-safe messaging overloads summary: Use the strongly-typed messaging overloads and the migration analyzer to make messaging code trimming-safe and AOT-safe component: Core versions: '[10,)' -reviewed: 2026-08-24 +reviewed: 2026-09-03 related: - nservicebus/messaging/send-a-message - nservicebus/messaging/reply-to-a-message @@ -121,6 +121,25 @@ The object-only overloads are removed in a single major version transition: Users who migrate early may add explicit generic type arguments, such as `Send(new MyMessage(), options)`. After the object-only overloads are removed, the IDE may flag those type arguments as redundant, because `Send(new MyMessage(), options)` infers the same message type. Whether to keep or remove the explicit type argument is a choice: keeping it routes the message using the explicit type at the call site, while removing it relies on type inference. Both are valid as long as the inferred type matches the intended logical message type. +## Trimming-safe transport and persistence + +Starting in NServiceBus version 10.3.0, an endpoint can be published as a trimmed or NativeAOT application when it uses a transport and persistence that keep all message state inside the endpoint process or its local environment: + +* The [Learning transport](/transports/learning/) and [Learning persistence](/persistence/learning/) ship with the NServiceBus package and are designed for development and testing. +* The [Non-Durable transport](/transports/non-durable/) and [Non-durable persistence](/persistence/non-durable/) are production options when message loss can be tolerated. Messages are held in process memory and are lost when the process ends, but no external infrastructure is required. + +Trimmed and NativeAOT endpoints discover handler, saga, and message types at build time rather than by scanning assemblies at runtime. See [registering message types](#registering-message-types) for the required configuration. + +## Registering message types + +With assembly scanning disabled and trimming or NativeAOT enabled, NServiceBus resolves message metadata only from types registered up front. The source-generated [handler and saga registration](/nservicebus/handlers-and-sagas-registration.md) registers the message types handled by the handlers and sagas it adds. Message types that an endpoint only sends, publishes, or replies to, and that no local handler or saga handles, are not covered by that registration and must be registered explicitly: + +snippet: RegisterMessageTypeManually + +`AddMessageType()` registers the message type together with its hierarchy of base types and implemented interfaces. The type must already be identified as a message by the endpoint's conventions; the method does not classify arbitrary types as messages. In ordinary applications the hierarchy is inferred at runtime, while under trimming or NativeAOT the call is replaced by a source-generated, reflection-free registration. + +When a required message type is not registered, message processing fails with an exception that names the missing type and the registration to add. Message types that are known when the endpoint starts fail at startup; message types that appear only later fail on first use. + ## Related trimming guidance Strongly-typed messaging is one part of running an NServiceBus endpoint with trimming or NativeAOT: diff --git a/nservicebus/serialization/index.md b/nservicebus/serialization/index.md index 2e60531c498..ba4607d0d41 100644 --- a/nservicebus/serialization/index.md +++ b/nservicebus/serialization/index.md @@ -2,7 +2,7 @@ title: Serialization summary: .NET messaging systems require serialization and deserialization of objects sent/received over transports. NServiceBus achieves this using serializers. component: Core -reviewed: 2025-06-21 +reviewed: 2026-09-03 isLearningPath: true related: - samples/serializers diff --git a/nservicebus/serialization/index_security_core_[10,).partial.md b/nservicebus/serialization/index_security_core_[10,).partial.md new file mode 100644 index 00000000000..0823a567858 --- /dev/null +++ b/nservicebus/serialization/index_security_core_[10,).partial.md @@ -0,0 +1,14 @@ +### Dynamic type loading + +Incoming messages might refer to a message type that has not yet been loaded by the endpoint. In these cases, the endpoint will automatically try to load the specified message type at runtime. This behavior can be disabled: + +snippet: disable-dynamic-type-loading + +> [!NOTE] +> When disabling dynamic type loading, all expected message types must be known when the endpoint starts. Message types are typically detected by [assembly scanning](/nservicebus/hosting/assembly-scanning.md). When assembly scanning is disabled, message types can instead be registered explicitly: [source-generated or manual handler and saga registration](/nservicebus/handlers-and-sagas-registration.md) also registers the message types of the handlers and sagas it adds, and message types that are only sent, published, or replied to can be registered with `AddMessageType()` (starting in NServiceBus version 10.3; see [registering message types](/nservicebus/messaging/trimming-safe-messaging-overloads.md#registering-message-types)). In trimmed or NativeAOT deployments, assembly scanning cannot reliably discover message types, so registration must be explicit. + +### Message type inference + +When an incoming message does not provide message type information via the `NServiceBus.EnclosedMessageTypes` header, the serializer can attempt to determine the message type based on the message's content (e.g., using Json.NET's `TypeNameHandling` setting). The exact capabilities and behavior depends heavily on the specific serializer being used but might introduce unintended security vulnerabilities. The endpoint can be configured to fail message processing immediately when the `NServiceBus.EnclosedMessageTypes` header does not contain a valid message type without passing the message content to the serializer: + +snippet: disable-message-type-inference diff --git a/nservicebus/serialization/index_security_core_[7,].partial.md b/nservicebus/serialization/index_security_core_[7,10).partial.md similarity index 100% rename from nservicebus/serialization/index_security_core_[7,].partial.md rename to nservicebus/serialization/index_security_core_[7,10).partial.md diff --git a/samples/azure/azure-table/table/ASTP_7/Client/Client.csproj b/samples/azure/azure-table/table/ASTP_7/Client/Client.csproj index 5183c1e2c60..b9db9c92d83 100644 --- a/samples/azure/azure-table/table/ASTP_7/Client/Client.csproj +++ b/samples/azure/azure-table/table/ASTP_7/Client/Client.csproj @@ -5,7 +5,7 @@ 14.0 - + diff --git a/samples/azure/azure-table/table/ASTP_7/Server/Server.csproj b/samples/azure/azure-table/table/ASTP_7/Server/Server.csproj index 3a5e0324b0f..d9cdbe07010 100644 --- a/samples/azure/azure-table/table/ASTP_7/Server/Server.csproj +++ b/samples/azure/azure-table/table/ASTP_7/Server/Server.csproj @@ -5,7 +5,7 @@ 14.0 - + diff --git a/samples/azure/azure-table/table/ASTP_7/SharedMessages/SharedMessages.csproj b/samples/azure/azure-table/table/ASTP_7/SharedMessages/SharedMessages.csproj index a5c6a96e372..46fdf7f0baf 100644 --- a/samples/azure/azure-table/table/ASTP_7/SharedMessages/SharedMessages.csproj +++ b/samples/azure/azure-table/table/ASTP_7/SharedMessages/SharedMessages.csproj @@ -4,6 +4,6 @@ 14.0 - + diff --git a/samples/cosmosdb/container/CosmosDB_4/Client/Client.csproj b/samples/cosmosdb/container/CosmosDB_4/Client/Client.csproj index e4257f45aba..6b52061b634 100644 --- a/samples/cosmosdb/container/CosmosDB_4/Client/Client.csproj +++ b/samples/cosmosdb/container/CosmosDB_4/Client/Client.csproj @@ -5,7 +5,7 @@ 14.0 - + diff --git a/samples/cosmosdb/container/CosmosDB_4/Server/Server.csproj b/samples/cosmosdb/container/CosmosDB_4/Server/Server.csproj index 6975961421a..406f3d9e6d0 100644 --- a/samples/cosmosdb/container/CosmosDB_4/Server/Server.csproj +++ b/samples/cosmosdb/container/CosmosDB_4/Server/Server.csproj @@ -5,7 +5,7 @@ 14.0 - + diff --git a/samples/cosmosdb/container/CosmosDB_4/SharedMessages/SharedMessages.csproj b/samples/cosmosdb/container/CosmosDB_4/SharedMessages/SharedMessages.csproj index 342d4ae9cec..213355a163a 100644 --- a/samples/cosmosdb/container/CosmosDB_4/SharedMessages/SharedMessages.csproj +++ b/samples/cosmosdb/container/CosmosDB_4/SharedMessages/SharedMessages.csproj @@ -4,6 +4,6 @@ 14.0 - + \ No newline at end of file From f2e8e05a687335bd2e500156fc29d8ffeaca7a99 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Fri, 11 Sep 2026 10:35:46 +0200 Subject: [PATCH 07/11] Document trimming-safe message mutators --- Snippets/Core/Core_10/Core_10.csproj | 2 +- .../Headers/Writers/HeaderWriterSaga.cs | 2 +- .../Instance/MutateIncomingMessages.cs | 2 +- .../Instance/MutateOutgoingMessages.cs | 2 +- Snippets/Testing/Testing_10/Testing_10.csproj | 2 +- .../handlers-and-sagas-registration.md | 2 +- ...registration-content_core_[10,).partial.md | 3 + .../trimming-safe-messaging-overloads.md | 77 ++++++++++++++++--- .../operations/nservicebus-analyzer.md | 2 +- ...r_migration-analyzer_core_[10,).partial.md | 2 +- nservicebus/pipeline/message-mutators.md | 20 ++++- nservicebus/sagas/index.md | 2 +- nservicebus/sagas/timeouts.md | 2 +- .../table/ASTP_7/Client/Client.csproj | 2 +- .../table/ASTP_7/Server/Server.csproj | 2 +- .../SharedMessages/SharedMessages.csproj | 2 +- samples/azure/azure-table/table/sample.md | 2 +- .../container/CosmosDB_4/Client/Client.csproj | 2 +- .../container/CosmosDB_4/Server/Server.csproj | 2 +- .../SharedMessages/SharedMessages.csproj | 2 +- samples/cosmosdb/container/sample.md | 2 +- 21 files changed, 105 insertions(+), 31 deletions(-) diff --git a/Snippets/Core/Core_10/Core_10.csproj b/Snippets/Core/Core_10/Core_10.csproj index f6178d184ab..5749a5dc2dc 100644 --- a/Snippets/Core/Core_10/Core_10.csproj +++ b/Snippets/Core/Core_10/Core_10.csproj @@ -12,7 +12,7 @@ - + diff --git a/Snippets/Core/Core_10/Headers/Writers/HeaderWriterSaga.cs b/Snippets/Core/Core_10/Headers/Writers/HeaderWriterSaga.cs index fb76d88178f..d8a9f0500cc 100644 --- a/Snippets/Core/Core_10/Headers/Writers/HeaderWriterSaga.cs +++ b/Snippets/Core/Core_10/Headers/Writers/HeaderWriterSaga.cs @@ -102,7 +102,7 @@ public async Task Handle(SendFromSagaMessage message, IMessageHandlerContext con var replyFromSagaMessage = new ReplyFromSagaMessage(); await context.Reply(replyFromSagaMessage); var replyToOriginatorFromSagaMessage = new ReplyToOriginatorFromSagaMessage(); - await ReplyToOriginator(context, replyToOriginatorFromSagaMessage); + await ReplyToOriginator(context, replyToOriginatorFromSagaMessage); await RequestTimeout(context, TimeSpan.FromMilliseconds(1), new TimeoutFromSaga()); } diff --git a/Snippets/Core/Core_10/Mutators/Instance/MutateIncomingMessages.cs b/Snippets/Core/Core_10/Mutators/Instance/MutateIncomingMessages.cs index c5045adc3e8..bed3d954e20 100644 --- a/Snippets/Core/Core_10/Mutators/Instance/MutateIncomingMessages.cs +++ b/Snippets/Core/Core_10/Mutators/Instance/MutateIncomingMessages.cs @@ -13,7 +13,7 @@ public Task MutateIncoming(MutateIncomingMessageContext context) var headers = context.Headers; // the incoming message - // optionally replace the message instance by setting context.Message + // optionally replace the message instance with context.UpdateMessageInstance(...) var message = context.Message; return Task.CompletedTask; diff --git a/Snippets/Core/Core_10/Mutators/Instance/MutateOutgoingMessages.cs b/Snippets/Core/Core_10/Mutators/Instance/MutateOutgoingMessages.cs index b9987e30aa8..416ae273baf 100644 --- a/Snippets/Core/Core_10/Mutators/Instance/MutateOutgoingMessages.cs +++ b/Snippets/Core/Core_10/Mutators/Instance/MutateOutgoingMessages.cs @@ -23,7 +23,7 @@ public Task MutateOutgoing(MutateOutgoingMessageContext context) } // the outgoing message - // optionally replace the message instance by setting context.OutgoingMessage + // optionally replace the message instance with context.UpdateMessage(...) var outgoingMessage = context.OutgoingMessage; return Task.CompletedTask; diff --git a/Snippets/Testing/Testing_10/Testing_10.csproj b/Snippets/Testing/Testing_10/Testing_10.csproj index cddcc52f328..807564b9843 100644 --- a/Snippets/Testing/Testing_10/Testing_10.csproj +++ b/Snippets/Testing/Testing_10/Testing_10.csproj @@ -4,7 +4,7 @@ - + diff --git a/nservicebus/handlers-and-sagas-registration.md b/nservicebus/handlers-and-sagas-registration.md index 2cd19fc13ab..2551f27b43d 100644 --- a/nservicebus/handlers-and-sagas-registration.md +++ b/nservicebus/handlers-and-sagas-registration.md @@ -2,7 +2,7 @@ title: Registering Handlers and Sagas summary: How to register message handlers and sagas with an NServiceBus endpoint. component: Core -reviewed: 2026-09-03 +reviewed: 2026-09-11 --- Registration tells an endpoint which message handlers and sagas to include. NServiceBus supports explicit registration (which is recommended for new endpoints) and automatic discovery via assembly scanning (which is useful for plugin or dynamic discovery scenarios). diff --git a/nservicebus/handlers-and-sagas-registration_registration-content_core_[10,).partial.md b/nservicebus/handlers-and-sagas-registration_registration-content_core_[10,).partial.md index 114186c1399..b5b77746030 100644 --- a/nservicebus/handlers-and-sagas-registration_registration-content_core_[10,).partial.md +++ b/nservicebus/handlers-and-sagas-registration_registration-content_core_[10,).partial.md @@ -64,6 +64,9 @@ The source generator automatically matches the visibility of the generated exten When source generation is not available or when only a few components need to be registered, individual types can be added explicitly. +> [!NOTE] +> `AddHandler()`, `AddSaga()`, and `AddMessageType()` use reflection unless the NServiceBus source generator replaces the call at build time. The source generator can only replace calls that name a concrete type, so pass handler, saga, and message types directly instead of forwarding a type parameter through generic helper code. Without a replacement, the call falls back to reflection and can report the trimming warning IL2026; `AddHandler()` and `AddSaga()` can also report the AOT warning IL3050. + ### Register a message handler Use `AddHandler()` to register a message handler: diff --git a/nservicebus/messaging/trimming-safe-messaging-overloads.md b/nservicebus/messaging/trimming-safe-messaging-overloads.md index 821b85bdc36..ef105aabc41 100644 --- a/nservicebus/messaging/trimming-safe-messaging-overloads.md +++ b/nservicebus/messaging/trimming-safe-messaging-overloads.md @@ -3,11 +3,12 @@ title: Trimming-safe messaging overloads summary: Use the strongly-typed messaging overloads and the migration analyzer to make messaging code trimming-safe and AOT-safe component: Core versions: '[10,)' -reviewed: 2026-09-03 +reviewed: 2026-09-11 related: - nservicebus/messaging/send-a-message - nservicebus/messaging/reply-to-a-message - nservicebus/operations/nservicebus-analyzer + - nservicebus/pipeline/message-mutators --- Starting in NServiceBus version 10.3.0, the messaging APIs provide strongly-typed overloads that are safe to use with [trimming](https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/) and [NativeAOT](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/). A migration analyzer included in the NServiceBus package guides existing applications to these overloads. @@ -22,7 +23,7 @@ Code is trimming-safe when the types it needs are known at compile time and pres The object-based overloads, such as `Send(message, options)` and `Publish(message, options)`, determine the message type at runtime by calling `message.GetType()`. When trimming or NativeAOT is enabled, the runtime type information required to route the message may no longer be available, so these overloads cannot be analyzed statically and are annotated with `RequiresUnreferencedCode`. -Strongly-typed overloads carry the message type either in the generic type argument, as in `Send(message, options)`, or as an explicit `Type` parameter, as in `Send(message, messageType, options)`. Because the type is known at compile time, these overloads are trimming-safe and AOT-safe. +Strongly-typed overloads carry the message type either in the generic type argument, as in `Send(message, options)`, or as an explicit `Type` parameter, as in `Send(message, messageType, options)`. Because the message type is supplied by the caller instead of being discovered from the message instance at runtime, the trimmer can analyze these overloads. When passing the type explicitly, use a value the trimmer can see through, such as `typeof(MyMessage)`. ## Strongly-typed overloads @@ -50,6 +51,22 @@ await session.Send(message, messageType, new SendOptions()); This is a common pattern for type-erased scenarios. Explicit-type overloads exist for `Send`, `Publish`, `Reply`, `SendLocal`, and `UpdateMessage`. +## Logical message mutators + +Logical message mutators can replace the message instance while it is processed in the pipeline. The replacement methods carry the logical message type, so mutators can be trimming-safe: + +| Context | Generic overload | Explicit-type overload | +| -- | -- | -- | +| `MutateIncomingMessageContext` | `UpdateMessageInstance(T newMessage)` | `UpdateMessageInstance(object newMessage, Type messageType)` | +| `MutateOutgoingMessageContext` | `UpdateMessage(T newMessage)` | `UpdateMessage(object newMessage, Type messageType)` | +| `IIncomingLogicalMessageContext` | `UpdateMessageInstance(T newInstance)` | `UpdateMessageInstance(object newInstance, Type messageType)` | + +The declared type is the logical message type. For outgoing messages, it also determines how the message is routed and which message type is recorded on the message. It can differ from the runtime type of the instance as long as the instance is assignable to the declared type. + +On `IIncomingLogicalMessageContext`, the object-based `UpdateMessageInstance(object)` overload is still available and wins overload resolution for a call without a type argument, so pipeline behaviors must pass the type argument explicitly: `context.UpdateMessageInstance(message)`. The mutator contexts have no competing overload, so there the type argument can be inferred from the message instance. + +Starting in NServiceBus version 10.3, prefer the strongly-typed methods above. The object-based `MutateIncomingMessageContext.Message` and `MutateOutgoingMessageContext.OutgoingMessage` setters are obsolete, will be treated as errors from version 11, and will be removed in version 12; the object-only `IIncomingLogicalMessageContext.UpdateMessageInstance(object)` method is annotated with `RequiresUnreferencedCode` and is planned for obsoletion. All of them determine the logical message type from the runtime type of the instance, which trimming cannot analyze. + ## Route and publisher registration Routes and publishers can be registered by message type, by assembly, or by namespace. Registration by assembly or namespace requires scanning assemblies at startup, which is not trimming-safe. Register routes and publishers by message type instead; see [routing](/nservicebus/messaging/routing.md) for details. @@ -90,15 +107,37 @@ This diagnostic fires when the runtime type can differ from the static type, for This diagnostic fires when the generic overload is called with `System.Object` as the explicit type argument, for example `Send(message, options)`. The strongly-typed overload would route the message as `System.Object`, which is never the intent. Specify the actual message type instead. +The diagnostics also cover logical message mutators. Assigning to `MutateIncomingMessageContext.Message` or `MutateOutgoingMessageContext.OutgoingMessage` is analyzed like the other messaging operations, and the code fix rewrites the assignment to the typed replacement method: + +```csharp +// MutateIncomingMessageContext +context.Message = new MyMessage(); + +// MutateOutgoingMessageContext +context.OutgoingMessage = new MyEvent(); +``` + +becomes + +```csharp +// MutateIncomingMessageContext +context.UpdateMessageInstance(new MyMessage()); + +// MutateOutgoingMessageContext +context.UpdateMessage(new MyEvent()); +``` + +Calling `UpdateMessageInstance` on an incoming logical message context is analyzed in the same way. The code fix keeps the explicit type argument because `IIncomingLogicalMessageContext` also has an object-based `UpdateMessageInstance(object)` overload; for the mutator contexts the type argument can be inferred from the message instance. + ## When the migration diagnostics are active NSB0039 and NSB0040 are quiet by default in ordinary builds. They activate automatically when a project enables trimming or AOT compatibility: -* `PublishTrimmed` -* `PublishAot` -* `IsAotCompatible` -* `IsTrimmable` -* `EnableTrimAnalyzer` +- `PublishTrimmed` +- `PublishAot` +- `IsAotCompatible` +- `IsTrimmable` +- `EnableTrimAnalyzer` The diagnostics can also be enabled per rule in `.editorconfig`: @@ -125,18 +164,32 @@ Users who migrate early may add explicit generic type arguments, such as `Send() + .Options(new JsonSerializerOptions + { + TypeInfoResolver = MyMessagesJsonContext.Default + }); +``` + +The [XML serializer](/nservicebus/serialization/xml.md) is not supported with trimming or NativeAOT because it relies on runtime type information and dynamic code generation. + ## Registering message types With assembly scanning disabled and trimming or NativeAOT enabled, NServiceBus resolves message metadata only from types registered up front. The source-generated [handler and saga registration](/nservicebus/handlers-and-sagas-registration.md) registers the message types handled by the handlers and sagas it adds. Message types that an endpoint only sends, publishes, or replies to, and that no local handler or saga handles, are not covered by that registration and must be registered explicitly: snippet: RegisterMessageTypeManually -`AddMessageType()` registers the message type together with its hierarchy of base types and implemented interfaces. The type must already be identified as a message by the endpoint's conventions; the method does not classify arbitrary types as messages. In ordinary applications the hierarchy is inferred at runtime, while under trimming or NativeAOT the call is replaced by a source-generated, reflection-free registration. +`AddMessageType()` registers the message type together with its hierarchy of base types and implemented interfaces. The type must already be identified as a message by the endpoint's conventions; the method does not classify arbitrary types as messages. In ordinary applications the hierarchy is inferred at runtime, while under trimming or NativeAOT the call is replaced by a source-generated, reflection-free registration. The call must name a concrete message type: the source generator cannot replace a call that forwards a type parameter through a generic helper method. When a required message type is not registered, message processing fails with an exception that names the missing type and the registration to add. Message types that are known when the endpoint starts fail at startup; message types that appear only later fail on first use. @@ -144,5 +197,5 @@ When a required message type is not registered, message processing fails with an Strongly-typed messaging is one part of running an NServiceBus endpoint with trimming or NativeAOT: -* [Startup diagnostics](/nservicebus/hosting/startup-diagnostics.md#adding-startup-diagnostics-sections) — register diagnostics sections with type information so the diagnostics document can be serialized without reflection. -* [Registering handlers and sagas](/nservicebus/handlers-and-sagas-registration.md) — source-generated registration is trimming and AOT-friendly; discovery by [assembly scanning](/nservicebus/hosting/assembly-scanning.md) relies on runtime type information. +- [Startup diagnostics](/nservicebus/hosting/startup-diagnostics.md#adding-startup-diagnostics-sections) — register diagnostics sections with type information so the diagnostics document can be serialized without reflection. +- [Registering handlers and sagas](/nservicebus/handlers-and-sagas-registration.md) — source-generated registration is trimming and AOT-friendly; discovery by [assembly scanning](/nservicebus/hosting/assembly-scanning.md) relies on runtime type information. diff --git a/nservicebus/operations/nservicebus-analyzer.md b/nservicebus/operations/nservicebus-analyzer.md index 7f053f73208..8f34de769f6 100644 --- a/nservicebus/operations/nservicebus-analyzer.md +++ b/nservicebus/operations/nservicebus-analyzer.md @@ -1,7 +1,7 @@ --- title: NServiceBus Analyzer summary: How to use the NServiceBus analyzer to avoid missing awaits -reviewed: 2026-08-24 +reviewed: 2026-09-11 component: Core versions: '[6,]' --- diff --git a/nservicebus/operations/nservicebus-analyzer_migration-analyzer_core_[10,).partial.md b/nservicebus/operations/nservicebus-analyzer_migration-analyzer_core_[10,).partial.md index 2fc5369f273..ff0093bf07d 100644 --- a/nservicebus/operations/nservicebus-analyzer_migration-analyzer_core_[10,).partial.md +++ b/nservicebus/operations/nservicebus-analyzer_migration-analyzer_core_[10,).partial.md @@ -1,3 +1,3 @@ ## Migration analyzer -The NServiceBus package also includes a migration analyzer for the messaging APIs. It reports the NSB0039, NSB0040, and NSB0041 diagnostics when messages are sent, published, or replied to using the object-based overloads that are not trimming-safe. See [Trimming-safe messaging overloads](/nservicebus/messaging/trimming-safe-messaging-overloads.md) for details. +The NServiceBus package also includes a migration analyzer for the messaging APIs. It reports the NSB0039, NSB0040, and NSB0041 diagnostics when messages are sent, published, replied to, or updated using the object-based overloads that are not trimming-safe, and when logical message mutators replace a message using its runtime type. See [Trimming-safe messaging overloads](/nservicebus/messaging/trimming-safe-messaging-overloads.md) for details. diff --git a/nservicebus/pipeline/message-mutators.md b/nservicebus/pipeline/message-mutators.md index 2f3528aecfe..a5ca80615ac 100644 --- a/nservicebus/pipeline/message-mutators.md +++ b/nservicebus/pipeline/message-mutators.md @@ -2,12 +2,13 @@ title: Message Mutators summary: Message Mutators allow mutation of messages in the pipeline component: Core -reviewed: 2026-06-01 +reviewed: 2026-09-11 redirects: - nservicebus/pipeline-management-using-message-mutators related: - samples/messagemutators - nservicebus/messaging/headers + - nservicebus/messaging/trimming-safe-messaging-overloads --- Message mutators allow mutation of messages in the pipeline. @@ -32,6 +33,23 @@ snippet: IMutateIncomingMessages snippet: IMutateOutgoingMessages +### Replacing the message instance + +Starting in NServiceBus version 10.3, logical message mutators can replace the message instance by calling a strongly-typed method. The typed method keeps the logical message type known at compile time, which is required for [trimming and NativeAOT](/nservicebus/messaging/trimming-safe-messaging-overloads.md#logical-message-mutators). + +| Mutator context | Replace the message instance | +| -- | -- | +| `MutateIncomingMessageContext` | `context.UpdateMessageInstance(newMessage)` | +| `MutateOutgoingMessageContext` | `context.UpdateMessage(newMessage)` | + +Both contexts also provide an overload that accepts the message instance and an explicit `Type` for scenarios where the message type is not known at compile time. + +The declared type is the logical message type. For outgoing messages, it also determines how the message is routed and which message type is recorded on the message. It can differ from the runtime type of the instance as long as the instance is assignable to the declared type. + +> [!NOTE] +> Assigning to `MutateIncomingMessageContext.Message` or `MutateOutgoingMessageContext.OutgoingMessage` determines the logical message type from the runtime type of the instance, which is not trimming-safe. Starting in version 10.3, these setters are obsolete and the compiler reports a warning when they are used. They will be treated as errors from version 11. The getters remain available and are the recommended way to read the current message. + + ## Transport message mutators Transport message mutators work on the serialized transport message and are useful for compression, header manipulation, etc. diff --git a/nservicebus/sagas/index.md b/nservicebus/sagas/index.md index ab7bb2eeffe..f83a3da82fc 100644 --- a/nservicebus/sagas/index.md +++ b/nservicebus/sagas/index.md @@ -2,7 +2,7 @@ title: Sagas summary: Master NServiceBus sagas to coordinate distributed workflows and ensure reliable long-running processes. component: Core -reviewed: 2026-04-27 +reviewed: 2026-09-11 redirects: - nservicebus/sagas-in-nservicebus related: diff --git a/nservicebus/sagas/timeouts.md b/nservicebus/sagas/timeouts.md index 58f9cf5c7ca..b26a0830041 100644 --- a/nservicebus/sagas/timeouts.md +++ b/nservicebus/sagas/timeouts.md @@ -1,7 +1,7 @@ --- title: Saga Timeouts summary: Configure saga timeouts in NServiceBus to specify an upper limit to the waiting period for messages that are handled by the saga -reviewed: 2026-01-12 +reviewed: 2026-09-11 component: Core related: - samples/saga diff --git a/samples/azure/azure-table/table/ASTP_7/Client/Client.csproj b/samples/azure/azure-table/table/ASTP_7/Client/Client.csproj index b9db9c92d83..bbe68f259fe 100644 --- a/samples/azure/azure-table/table/ASTP_7/Client/Client.csproj +++ b/samples/azure/azure-table/table/ASTP_7/Client/Client.csproj @@ -5,7 +5,7 @@ 14.0 - + diff --git a/samples/azure/azure-table/table/ASTP_7/Server/Server.csproj b/samples/azure/azure-table/table/ASTP_7/Server/Server.csproj index d9cdbe07010..9d91a7e554e 100644 --- a/samples/azure/azure-table/table/ASTP_7/Server/Server.csproj +++ b/samples/azure/azure-table/table/ASTP_7/Server/Server.csproj @@ -5,7 +5,7 @@ 14.0 - + diff --git a/samples/azure/azure-table/table/ASTP_7/SharedMessages/SharedMessages.csproj b/samples/azure/azure-table/table/ASTP_7/SharedMessages/SharedMessages.csproj index 46fdf7f0baf..17ed7ef49c7 100644 --- a/samples/azure/azure-table/table/ASTP_7/SharedMessages/SharedMessages.csproj +++ b/samples/azure/azure-table/table/ASTP_7/SharedMessages/SharedMessages.csproj @@ -4,6 +4,6 @@ 14.0 - + diff --git a/samples/azure/azure-table/table/sample.md b/samples/azure/azure-table/table/sample.md index be775611826..01337c76d54 100644 --- a/samples/azure/azure-table/table/sample.md +++ b/samples/azure/azure-table/table/sample.md @@ -1,7 +1,7 @@ --- title: AzureTable Persistence Usage with non-default table summary: Using Azure Table Persistence to store sagas providing a non-default table dynamically -reviewed: 2025-02-25 +reviewed: 2026-09-11 component: ASP related: - nservicebus/sagas diff --git a/samples/cosmosdb/container/CosmosDB_4/Client/Client.csproj b/samples/cosmosdb/container/CosmosDB_4/Client/Client.csproj index 6b52061b634..24356f07c8c 100644 --- a/samples/cosmosdb/container/CosmosDB_4/Client/Client.csproj +++ b/samples/cosmosdb/container/CosmosDB_4/Client/Client.csproj @@ -5,7 +5,7 @@ 14.0 - + diff --git a/samples/cosmosdb/container/CosmosDB_4/Server/Server.csproj b/samples/cosmosdb/container/CosmosDB_4/Server/Server.csproj index 406f3d9e6d0..36418096aae 100644 --- a/samples/cosmosdb/container/CosmosDB_4/Server/Server.csproj +++ b/samples/cosmosdb/container/CosmosDB_4/Server/Server.csproj @@ -5,7 +5,7 @@ 14.0 - + diff --git a/samples/cosmosdb/container/CosmosDB_4/SharedMessages/SharedMessages.csproj b/samples/cosmosdb/container/CosmosDB_4/SharedMessages/SharedMessages.csproj index 213355a163a..4a73db9f404 100644 --- a/samples/cosmosdb/container/CosmosDB_4/SharedMessages/SharedMessages.csproj +++ b/samples/cosmosdb/container/CosmosDB_4/SharedMessages/SharedMessages.csproj @@ -4,6 +4,6 @@ 14.0 - + \ No newline at end of file diff --git a/samples/cosmosdb/container/sample.md b/samples/cosmosdb/container/sample.md index d0824c387af..e460872af81 100644 --- a/samples/cosmosdb/container/sample.md +++ b/samples/cosmosdb/container/sample.md @@ -1,7 +1,7 @@ --- title: Cosmos DB Persistence Usage with non-default container summary: Using Cosmos DB Persistence to store sagas providing a non-default container dynamically -reviewed: 2026-06-01 +reviewed: 2026-09-11 component: CosmosDB related: - nservicebus/sagas From 3ed9f25336830d6cd51ac4552752c0a8d2796815 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Fri, 11 Sep 2026 12:30:50 +0200 Subject: [PATCH 08/11] Apply batched suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- nservicebus/messaging/trimming-safe-messaging-overloads.md | 2 +- nservicebus/serialization/index_security_core_[10,).partial.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/nservicebus/messaging/trimming-safe-messaging-overloads.md b/nservicebus/messaging/trimming-safe-messaging-overloads.md index ef105aabc41..9155627c5e9 100644 --- a/nservicebus/messaging/trimming-safe-messaging-overloads.md +++ b/nservicebus/messaging/trimming-safe-messaging-overloads.md @@ -36,7 +36,7 @@ The following messaging operations provide generic overloads that are trimming-s | Reply | `Reply(T message)` | | SendLocal | `SendLocal(T message)` | | UpdateMessage | `UpdateMessage(T newInstance)` | -| ReplyToOriginator | `ReplyToOriginator(T message)` | +| ReplyToOriginator | `ReplyToOriginator(IMessageHandlerContext context, T message)` | ## Explicit-type overloads diff --git a/nservicebus/serialization/index_security_core_[10,).partial.md b/nservicebus/serialization/index_security_core_[10,).partial.md index 0823a567858..02d8404cb84 100644 --- a/nservicebus/serialization/index_security_core_[10,).partial.md +++ b/nservicebus/serialization/index_security_core_[10,).partial.md @@ -9,6 +9,6 @@ snippet: disable-dynamic-type-loading ### Message type inference -When an incoming message does not provide message type information via the `NServiceBus.EnclosedMessageTypes` header, the serializer can attempt to determine the message type based on the message's content (e.g., using Json.NET's `TypeNameHandling` setting). The exact capabilities and behavior depends heavily on the specific serializer being used but might introduce unintended security vulnerabilities. The endpoint can be configured to fail message processing immediately when the `NServiceBus.EnclosedMessageTypes` header does not contain a valid message type without passing the message content to the serializer: +When an incoming message does not provide message type information via the `NServiceBus.EnclosedMessageTypes` header, the serializer can attempt to determine the message type based on the message's content (e.g., using Json.NET's `TypeNameHandling` setting). The exact capabilities and behavior depend heavily on the specific serializer. Message type inference might introduce unintended security vulnerabilities. The endpoint can be configured to fail message processing immediately when the `NServiceBus.EnclosedMessageTypes` header does not contain a valid message type without passing the message content to the serializer: snippet: disable-message-type-inference From 3228e6fc7b4eb3ad45e603e7c85625e577437fa9 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Fri, 11 Sep 2026 21:06:13 +0200 Subject: [PATCH 09/11] Apply batched suggestions from code review Co-authored-by: David Boike --- ...registration-content_core_[10,).partial.md | 2 +- ...tics_adding-sections_core_[10,).partial.md | 2 +- .../trimming-safe-messaging-overloads.md | 20 +++++++++---------- nservicebus/pipeline/message-mutators.md | 2 +- .../index_security_core_[10,).partial.md | 2 +- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/nservicebus/handlers-and-sagas-registration_registration-content_core_[10,).partial.md b/nservicebus/handlers-and-sagas-registration_registration-content_core_[10,).partial.md index b5b77746030..c346d4e803a 100644 --- a/nservicebus/handlers-and-sagas-registration_registration-content_core_[10,).partial.md +++ b/nservicebus/handlers-and-sagas-registration_registration-content_core_[10,).partial.md @@ -103,7 +103,7 @@ snippet: DisableAssemblyScanning When assembly scanning is disabled, message handlers, sagas, features, and installers must be explicitly registered. Messages received without a registered handler or saga [will fail and be moved to the error queue](/nservicebus/handlers/?version=core_10#no-handler-for-a-message). -In trimmed or NativeAOT deployments, message types that are only sent, published, or replied to, and that no registered handler or saga handles, must also be registered explicitly with `AddMessageType()`, available starting in NServiceBus version 10.3. See [registering message types](/nservicebus/messaging/trimming-safe-messaging-overloads.md#registering-message-types). +In trimmed or Native AOT deployments, message types that are only sent, published, or replied to, and that no registered handler or saga handles, must also be registered explicitly with `AddMessageType()`, available starting in NServiceBus version 10.3. See [registering message types](/nservicebus/messaging/trimming-safe-messaging-overloads.md#registering-message-types). ### Fine-grained scanning configuration diff --git a/nservicebus/hosting/startup-diagnostics_adding-sections_core_[10,).partial.md b/nservicebus/hosting/startup-diagnostics_adding-sections_core_[10,).partial.md index 3451e72f95f..a91bb83b1e6 100644 --- a/nservicebus/hosting/startup-diagnostics_adding-sections_core_[10,).partial.md +++ b/nservicebus/hosting/startup-diagnostics_adding-sections_core_[10,).partial.md @@ -6,7 +6,7 @@ Starting in version 10.3, custom sections can be registered with a strongly-type snippet: CustomDiagnosticsSectionTypes -Registering sections with type information makes startup diagnostics serialization AOT-safe and trimming-safe. This is required when reflection-based serialization is disabled, such as in NativeAOT applications. In that case, a section registered with the object-based overload cannot be serialized. NServiceBus logs an error identifying the section, and the diagnostics document is not written. When reflection-based serialization is disabled, every section in the document must be registered with type information. A single legacy section prevents the complete document from being written. +Registering sections with type information makes startup diagnostics serialization AOT-safe and trimming-safe. This is required when reflection-based serialization is disabled, such as in Native AOT applications. In that case, a section registered with the object-based overload cannot be serialized. NServiceBus logs an error identifying the section, and the diagnostics document is not written. When reflection-based serialization is disabled, every section in the document must be registered with type information. A single legacy section prevents the complete document from being written. The object-based overload remains available and continues to work when reflection-based serialization is enabled. diff --git a/nservicebus/messaging/trimming-safe-messaging-overloads.md b/nservicebus/messaging/trimming-safe-messaging-overloads.md index 9155627c5e9..33b5d50c195 100644 --- a/nservicebus/messaging/trimming-safe-messaging-overloads.md +++ b/nservicebus/messaging/trimming-safe-messaging-overloads.md @@ -11,17 +11,17 @@ related: - nservicebus/pipeline/message-mutators --- -Starting in NServiceBus version 10.3.0, the messaging APIs provide strongly-typed overloads that are safe to use with [trimming](https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/) and [NativeAOT](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/). A migration analyzer included in the NServiceBus package guides existing applications to these overloads. +Starting in NServiceBus version 10.3.0, the messaging APIs provide strongly-typed overloads that are safe to use with [trimming](https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/) and [Native AOT](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/). A migration analyzer included in the NServiceBus package guides existing applications to these overloads. ## What does trimming-safe mean -Trimming removes unreferenced code and metadata from an application at publish time, and NativeAOT compiles the application ahead of time. In both scenarios, the runtime type information that reflection-based code depends on may no longer be available. +Trimming removes unreferenced code and metadata from an application at publish time, and Native AOT compiles the application to platform-native code ahead of time. In both scenarios, the runtime type information that reflection-based code depends on may no longer be available. Code is trimming-safe when the types it needs are known at compile time and preserved in the published application. Code that discovers types at runtime, such as `message.GetType()`, is not trimming-safe because the metadata for the type may have been removed. ## Why messaging needs strongly-typed overloads -The object-based overloads, such as `Send(message, options)` and `Publish(message, options)`, determine the message type at runtime by calling `message.GetType()`. When trimming or NativeAOT is enabled, the runtime type information required to route the message may no longer be available, so these overloads cannot be analyzed statically and are annotated with `RequiresUnreferencedCode`. +The object-based overloads, such as `Send(message, options)` and `Publish(message, options)`, determine the message type at runtime by calling `message.GetType()`. When trimming or Native AOT is enabled, the runtime type information required to route the message may no longer be available, so these overloads cannot be analyzed statically and are annotated with `RequiresUnreferencedCode`. Strongly-typed overloads carry the message type either in the generic type argument, as in `Send(message, options)`, or as an explicit `Type` parameter, as in `Send(message, messageType, options)`. Because the message type is supplied by the caller instead of being discovered from the message instance at runtime, the trimmer can analyze these overloads. When passing the type explicitly, use a value the trimmer can see through, such as `typeof(MyMessage)`. @@ -162,16 +162,16 @@ Users who migrate early may add explicit generic type arguments, such as `Send() @@ -181,21 +181,21 @@ configuration.UseSerialization() }); ``` -The [XML serializer](/nservicebus/serialization/xml.md) is not supported with trimming or NativeAOT because it relies on runtime type information and dynamic code generation. +The [XML serializer](/nservicebus/serialization/xml.md) is not supported with trimming or Native AOT because it relies on runtime type information and dynamic code generation. ## Registering message types -With assembly scanning disabled and trimming or NativeAOT enabled, NServiceBus resolves message metadata only from types registered up front. The source-generated [handler and saga registration](/nservicebus/handlers-and-sagas-registration.md) registers the message types handled by the handlers and sagas it adds. Message types that an endpoint only sends, publishes, or replies to, and that no local handler or saga handles, are not covered by that registration and must be registered explicitly: +With assembly scanning disabled and trimming or Native AOT enabled, NServiceBus resolves message metadata only from types registered explicitly. The source-generated [handler and saga registration](/nservicebus/handlers-and-sagas-registration.md) registers the message types handled by the handlers and sagas it adds. Message types that an endpoint only sends, publishes, or replies to, and that no local handler or saga handles, are not covered by that registration and must be registered separately: snippet: RegisterMessageTypeManually -`AddMessageType()` registers the message type together with its hierarchy of base types and implemented interfaces. The type must already be identified as a message by the endpoint's conventions; the method does not classify arbitrary types as messages. In ordinary applications the hierarchy is inferred at runtime, while under trimming or NativeAOT the call is replaced by a source-generated, reflection-free registration. The call must name a concrete message type: the source generator cannot replace a call that forwards a type parameter through a generic helper method. +`AddMessageType()` registers the message type together with its hierarchy of base types and implemented interfaces. The type must already be identified as a message by the endpoint's conventions; the method does not classify arbitrary types as messages. In ordinary applications the hierarchy is inferred at runtime, while under trimming or Native AOT the call is replaced by a source-generated, reflection-free registration. The call must name a concrete message type: the source generator cannot replace a call that forwards a type parameter through a generic helper method. When a required message type is not registered, message processing fails with an exception that names the missing type and the registration to add. Message types that are known when the endpoint starts fail at startup; message types that appear only later fail on first use. ## Related trimming guidance -Strongly-typed messaging is one part of running an NServiceBus endpoint with trimming or NativeAOT: +Strongly-typed messaging is one part of running an NServiceBus endpoint with trimming or Native AOT: - [Startup diagnostics](/nservicebus/hosting/startup-diagnostics.md#adding-startup-diagnostics-sections) — register diagnostics sections with type information so the diagnostics document can be serialized without reflection. - [Registering handlers and sagas](/nservicebus/handlers-and-sagas-registration.md) — source-generated registration is trimming and AOT-friendly; discovery by [assembly scanning](/nservicebus/hosting/assembly-scanning.md) relies on runtime type information. diff --git a/nservicebus/pipeline/message-mutators.md b/nservicebus/pipeline/message-mutators.md index a5ca80615ac..00fde1ce9cc 100644 --- a/nservicebus/pipeline/message-mutators.md +++ b/nservicebus/pipeline/message-mutators.md @@ -35,7 +35,7 @@ snippet: IMutateOutgoingMessages ### Replacing the message instance -Starting in NServiceBus version 10.3, logical message mutators can replace the message instance by calling a strongly-typed method. The typed method keeps the logical message type known at compile time, which is required for [trimming and NativeAOT](/nservicebus/messaging/trimming-safe-messaging-overloads.md#logical-message-mutators). +Starting in NServiceBus version 10.3, logical message mutators can replace the message instance by calling a strongly-typed method. The typed method keeps the logical message type known at compile time, which is required for [trimming and Native AOT](/nservicebus/messaging/trimming-safe-messaging-overloads.md#logical-message-mutators). | Mutator context | Replace the message instance | | -- | -- | diff --git a/nservicebus/serialization/index_security_core_[10,).partial.md b/nservicebus/serialization/index_security_core_[10,).partial.md index 02d8404cb84..72a81e6b0a0 100644 --- a/nservicebus/serialization/index_security_core_[10,).partial.md +++ b/nservicebus/serialization/index_security_core_[10,).partial.md @@ -5,7 +5,7 @@ Incoming messages might refer to a message type that has not yet been loaded by snippet: disable-dynamic-type-loading > [!NOTE] -> When disabling dynamic type loading, all expected message types must be known when the endpoint starts. Message types are typically detected by [assembly scanning](/nservicebus/hosting/assembly-scanning.md). When assembly scanning is disabled, message types can instead be registered explicitly: [source-generated or manual handler and saga registration](/nservicebus/handlers-and-sagas-registration.md) also registers the message types of the handlers and sagas it adds, and message types that are only sent, published, or replied to can be registered with `AddMessageType()` (starting in NServiceBus version 10.3; see [registering message types](/nservicebus/messaging/trimming-safe-messaging-overloads.md#registering-message-types)). In trimmed or NativeAOT deployments, assembly scanning cannot reliably discover message types, so registration must be explicit. +> When disabling dynamic type loading, all expected message types must be known when the endpoint starts. Message types are typically detected by [assembly scanning](/nservicebus/hosting/assembly-scanning.md). When assembly scanning is disabled, message types can instead be registered explicitly: [source-generated or manual handler and saga registration](/nservicebus/handlers-and-sagas-registration.md) also registers the message types of the handlers and sagas it adds, and message types that are only sent, published, or replied to can be registered with `AddMessageType()` (starting in NServiceBus version 10.3; see [registering message types](/nservicebus/messaging/trimming-safe-messaging-overloads.md#registering-message-types)). In trimmed or Native AOT deployments, assembly scanning cannot reliably discover message types, so registration must be explicit. ### Message type inference From 0cd3bbdc9318c9105c1aa2157d4114b9c56ec67c Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Fri, 11 Sep 2026 21:14:07 +0200 Subject: [PATCH 10/11] Address remaining review feedback on trimming docs - Use Assembly.GetTypes() as the trimming-unsafe example and move the GetType()/routing discussion to the messaging section - Explain why runtime type information matters: the message type flows into the EnclosedMessageTypes header and drives hierarchy resolution for handler invocation and subscriber matching - Document combining JsonSerializerContext sources with JsonTypeInfoResolver.Combine and TypeInfoResolverChain --- .../trimming-safe-messaging-overloads.md | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/nservicebus/messaging/trimming-safe-messaging-overloads.md b/nservicebus/messaging/trimming-safe-messaging-overloads.md index 33b5d50c195..a3100201fb1 100644 --- a/nservicebus/messaging/trimming-safe-messaging-overloads.md +++ b/nservicebus/messaging/trimming-safe-messaging-overloads.md @@ -17,11 +17,13 @@ Starting in NServiceBus version 10.3.0, the messaging APIs provide strongly-type Trimming removes unreferenced code and metadata from an application at publish time, and Native AOT compiles the application to platform-native code ahead of time. In both scenarios, the runtime type information that reflection-based code depends on may no longer be available. -Code is trimming-safe when the types it needs are known at compile time and preserved in the published application. Code that discovers types at runtime, such as `message.GetType()`, is not trimming-safe because the metadata for the type may have been removed. +Code is trimming-safe when the types it needs are known at compile time and preserved in the published application. Code that discovers types at runtime, such as `Assembly.GetTypes()`, is not trimming-safe because the metadata for those types may have been removed. ## Why messaging needs strongly-typed overloads -The object-based overloads, such as `Send(message, options)` and `Publish(message, options)`, determine the message type at runtime by calling `message.GetType()`. When trimming or Native AOT is enabled, the runtime type information required to route the message may no longer be available, so these overloads cannot be analyzed statically and are annotated with `RequiresUnreferencedCode`. +The object-based overloads, such as `Send(message, options)` and `Publish(message, options)`, determine the message type at runtime by calling `message.GetType()`. The runtime type's name is written to the [`NServiceBus.EnclosedMessageTypes` header](/nservicebus/messaging/headers.md#serialization-headers-nservicebus-enclosedmessagetypes), which the receiving endpoint uses to map the incoming message back to the same type before deserializing it. NServiceBus also resolves the message type's base types and interfaces to invoke handlers that accept them and to match subscribers when the message is published. + +Resolving a message type and its hierarchy from the message instance relies on reflection. Under trimming or Native AOT, the metadata for types that the application does not reference directly may be removed. Because the trimmer cannot predict what `message.GetType()` will return, it cannot preserve the required metadata. The object-based overloads therefore cannot be analyzed statically and are annotated with `RequiresUnreferencedCode`. Strongly-typed overloads carry the message type either in the generic type argument, as in `Send(message, options)`, or as an explicit `Type` parameter, as in `Send(message, messageType, options)`. Because the message type is supplied by the caller instead of being discovered from the message instance at runtime, the trimmer can analyze these overloads. When passing the type explicitly, use a value the trimmer can see through, such as `typeof(MyMessage)`. @@ -181,6 +183,20 @@ configuration.UseSerialization() }); ``` +Message types can be spread across multiple `JsonSerializerContext` sources, for example a context generated for the endpoint's own messages and a context supplied by a shared contracts package. [JsonTypeInfoResolver.Combine](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.metadata.jsontypeinforesolver.combine) merges them into a single resolver that queries the contexts in order and uses the first one that has metadata for a given type: + +```csharp +configuration.UseSerialization() + .Options(new JsonSerializerOptions + { + TypeInfoResolver = JsonTypeInfoResolver.Combine( + MyMessagesJsonContext.Default, + SharedContractsJsonContext.Default) + }); +``` + +The [JsonSerializerOptions.TypeInfoResolverChain](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.jsonserializeroptions.typeinforesolverchain) list is an alternative for adding resolvers to options that already have one. + The [XML serializer](/nservicebus/serialization/xml.md) is not supported with trimming or Native AOT because it relies on runtime type information and dynamic code generation. ## Registering message types From 90f872c4d88c93c9caa3ddd8b1d141fc1d70c8fd Mon Sep 17 00:00:00 2001 From: David Boike Date: Fri, 11 Sep 2026 16:07:15 -0500 Subject: [PATCH 11/11] Don't mark Core 10 as prerelease, that makes it "not current" - let the integrity test fail instead --- Snippets/Core/Core_10/prerelease.txt | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 Snippets/Core/Core_10/prerelease.txt diff --git a/Snippets/Core/Core_10/prerelease.txt b/Snippets/Core/Core_10/prerelease.txt deleted file mode 100644 index e69de29bb2d..00000000000