From 81bd13f327b107fa47813dc0a700b4afcc8bde76 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Thu, 3 Sep 2026 10:35:15 +0200 Subject: [PATCH 1/8] Make logical message mutators trimming-aware with declared message types Add typed replacement APIs so incoming and outgoing instance mutators can declare the logical message type instead of relying on runtime type discovery: - IIncomingLogicalMessageContext gains UpdateMessageInstance(T) and UpdateMessageInstance(object, Type) following the typed UpdateMessage precedent on IOutgoingLogicalMessageContext, with PreObsolete and RequiresUnreferencedCode on the runtime-type-routing object overload - IncomingLogicalMessageContext resolves replacement metadata through LogicalMessageFactory without reflection over the instance, preserving strict registered-only metadata mode - MutateIncomingMessageContext.UpdateMessageInstance and MutateOutgoingMessageContext.UpdateMessage accept typed or explicitly typed replacements and carry the declared type via a DAM-annotated internal property - Mutator behaviors pass the declared type into the pipeline contexts, keeping the legacy object path untouched - TestableIncomingLogicalMessageContext mirrors the typed members --- ...IApprovals.ApproveNServiceBus.approved.txt | 13 +++ ...s.ApproveTrimmabilityWarnings.approved.txt | 3 + .../MutateIncomingMessageBehaviorTests.cs | 70 +++++++++++ .../MutateOutgoingMessageBehaviorTests.cs | 70 +++++++++++ ...oming_mutator_replaces_message_instance.cs | 45 +++----- .../IncomingLogicalMessageContextTests.cs | 109 ++++++++++++++++++ .../TypedMessageInstanceOverloadsTests.cs | 60 ++++++++++ .../MutateIncomingMessageBehavior.cs | 9 +- .../MutateIncomingMessageContext.cs | 31 +++++ .../MutateOutgoingMessageBehavior.cs | 9 +- .../MutateOutgoingMessageContext.cs | 30 +++++ .../IIncomingLogicalMessageContext.cs | 38 +++++- .../Incoming/IncomingLogicalMessageContext.cs | 44 +++++++ .../TestableIncomingLogicalMessageContext.cs | 28 +++++ 14 files changed, 529 insertions(+), 30 deletions(-) create mode 100644 src/NServiceBus.Core.Tests/Pipeline/Incoming/IncomingLogicalMessageContextTests.cs diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt index b292515cc56..d822e792cc6 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt @@ -1882,6 +1882,8 @@ namespace NServiceBus.MessageMutator public System.Threading.CancellationToken CancellationToken { get; } public System.Collections.Generic.Dictionary Headers { get; } public object Message { get; set; } + public void UpdateMessageInstance(object newMessage, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type messageType) { } + public void UpdateMessageInstance<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(T newMessage) { } } public class MutateIncomingTransportMessageContext : NServiceBus.ICancellableContext { @@ -1898,6 +1900,8 @@ namespace NServiceBus.MessageMutator public object OutgoingMessage { get; set; } public bool TryGetIncomingHeaders([System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out System.Collections.Generic.IReadOnlyDictionary? incomingHeaders) { } public bool TryGetIncomingMessage([System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out object? incomingMessage) { } + public void UpdateMessage(object newMessage, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type messageType) { } + public void UpdateMessage<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(T newMessage) { } } public class MutateOutgoingTransportMessageContext : NServiceBus.ICancellableContext { @@ -2045,7 +2049,16 @@ namespace NServiceBus.Pipeline System.Collections.Generic.Dictionary Headers { get; } NServiceBus.Pipeline.LogicalMessage Message { get; } bool MessageHandled { get; set; } + [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("When trimming is enabled, routing a message using its runtime type cannot be stat" + + "ically analyzed by the trimmer. Use the generic overload or, when the message ty" + + "pe is not known at compile time, the overload accepting an explicit Type.")] void UpdateMessageInstance(object newInstance); + [System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessage("Trimming", "IL2026", Justification="The default interface implementation preserves compatibility with third-party imp" + + "lementations. Built-in implementations override this method and preserve the dec" + + "lared message type.")] + void UpdateMessageInstance(object newInstance, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type messageType); + [System.Runtime.CompilerServices.OverloadResolutionPriority(-1)] + void UpdateMessageInstance<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(T newInstance); } public interface IIncomingPhysicalMessageContext : NServiceBus.Extensibility.IExtendable, NServiceBus.ICancellableContext, NServiceBus.IMessageProcessingContext, NServiceBus.IPipelineContext, NServiceBus.Pipeline.IBehaviorContext, NServiceBus.Pipeline.IIncomingContext { diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt index 4f8f1f1378e..59ad7798ebe 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt @@ -2,6 +2,9 @@ The following trimming warnings are present in NServiceBus.Core. Changes that make this list longer should not be approved. ----- +src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateIncomingMessageBehavior.cs + IL2026: Using member 'NServiceBus.Pipeline.IIncomingLogicalMessageContext.UpdateMessageInstance(Object)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. When trimming is enabled, routing a message using its runtime type cannot be statically analyzed by the trimmer. Use the generic overload or, when the message type is not known at compile time, the overload accepting an explicit Type. + src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageBehavior.cs IL2026: Using member 'NServiceBus.Pipeline.IOutgoingLogicalMessageContext.UpdateMessage(Object)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. When trimming is enabled, routing a message using its runtime type cannot be statically analyzed by the trimmer. Use the generic overload or, when the message type is not known at compile time, the overload accepting an explicit Type. diff --git a/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/MutateIncomingMessageBehaviorTests.cs b/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/MutateIncomingMessageBehaviorTests.cs index 0fa1ce103f1..7a85c80fbc4 100644 --- a/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/MutateIncomingMessageBehaviorTests.cs +++ b/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/MutateIncomingMessageBehaviorTests.cs @@ -1,5 +1,6 @@ namespace NServiceBus.Core.Tests.MessageMutators.MutateInstanceMessage; +using System; using System.Threading.Tasks; using MessageMutator; using Microsoft.Extensions.DependencyInjection; @@ -126,15 +127,68 @@ public async Task When_mutator_modifies_the_body_should_update_the_body() Assert.That(context.UpdateMessageCalled, Is.True); } + [Test] + public async Task When_mutator_declares_a_message_type_should_use_the_explicit_type_overload() + { + var behavior = new MutateIncomingMessageBehavior([]); + + var context = new InterceptUpdateMessageIncomingLogicalMessageContext(); + + context.Services.AddTransient(sp => new MutatorWhichDeclaresAMessageType()); + + await behavior.Invoke(context, ctx => Task.CompletedTask); + + using (Assert.EnterMultipleScope()) + { + Assert.That(context.UpdateMessageObjCalled, Is.False); + Assert.That(context.UpdateMessageWithTypeCalled, Is.True); + Assert.That(context.DeclaredMessageType, Is.EqualTo(typeof(IMyMessage))); + Assert.That(context.Message.MessageType, Is.EqualTo(typeof(IMyMessage))); + } + } + + [Test] + public async Task When_mutator_uses_the_object_setter_should_use_the_object_overload() + { + var behavior = new MutateIncomingMessageBehavior([]); + + var context = new InterceptUpdateMessageIncomingLogicalMessageContext(); + + context.Services.AddTransient(sp => new MutatorWhichMutatesTheBody()); + + await behavior.Invoke(context, ctx => Task.CompletedTask); + + using (Assert.EnterMultipleScope()) + { + Assert.That(context.UpdateMessageObjCalled, Is.True); + Assert.That(context.UpdateMessageWithTypeCalled, Is.False); + } + } + class InterceptUpdateMessageIncomingLogicalMessageContext : TestableIncomingLogicalMessageContext { public bool UpdateMessageCalled { get; private set; } + public bool UpdateMessageObjCalled { get; private set; } + + public bool UpdateMessageWithTypeCalled { get; private set; } + + public Type DeclaredMessageType { get; private set; } + public override void UpdateMessageInstance(object newInstance) { base.UpdateMessageInstance(newInstance); UpdateMessageCalled = true; + UpdateMessageObjCalled = true; + } + + public override void UpdateMessageInstance(object newInstance, Type messageType) + { + base.UpdateMessageInstance(newInstance, messageType); + + UpdateMessageWithTypeCalled = true; + DeclaredMessageType = messageType; } } @@ -168,6 +222,16 @@ public Task MutateIncoming(MutateIncomingMessageContext context) } } + class MutatorWhichDeclaresAMessageType : IMutateIncomingMessages + { + public Task MutateIncoming(MutateIncomingMessageContext context) + { + context.UpdateMessageInstance(new MyMessage()); + + return Task.CompletedTask; + } + } + class MutateIncomingMessagesReturnsNull : IMutateIncomingMessages { public Task MutateIncoming(MutateIncomingMessageContext context) @@ -178,4 +242,10 @@ public Task MutateIncoming(MutateIncomingMessageContext context) class TestMessage : IMessage { } + + interface IMyMessage : IMessage + { } + + class MyMessage : IMyMessage + { } } diff --git a/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageBehaviorTests.cs b/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageBehaviorTests.cs index b16ed6426c1..98265830aad 100644 --- a/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageBehaviorTests.cs +++ b/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageBehaviorTests.cs @@ -1,5 +1,6 @@ namespace NServiceBus.Core.Tests.MessageMutators.MutateInstanceMessage; +using System; using System.Threading.Tasks; using MessageMutator; using Microsoft.Extensions.DependencyInjection; @@ -122,15 +123,68 @@ public async Task When_mutator_modifies_the_body_should_update_the_body() Assert.That(context.UpdateMessageCalled, Is.True); } + [Test] + public async Task When_mutator_declares_a_message_type_should_use_the_explicit_type_overload() + { + var behavior = new MutateOutgoingMessageBehavior([]); + + var context = new InterceptUpdateMessageOutgoingLogicalMessageContext(); + + context.Services.AddTransient(sp => new MutatorWhichDeclaresAMessageType()); + + await behavior.Invoke(context, ctx => Task.CompletedTask); + + using (Assert.EnterMultipleScope()) + { + Assert.That(context.UpdateMessageObjCalled, Is.False); + Assert.That(context.UpdateMessageWithTypeCalled, Is.True); + Assert.That(context.DeclaredMessageType, Is.EqualTo(typeof(IMyMessage))); + Assert.That(context.Message.MessageType, Is.EqualTo(typeof(IMyMessage))); + } + } + + [Test] + public async Task When_mutator_uses_the_object_setter_should_use_the_object_overload() + { + var behavior = new MutateOutgoingMessageBehavior([]); + + var context = new InterceptUpdateMessageOutgoingLogicalMessageContext(); + + context.Services.AddTransient(sp => new MutatorWhichMutatesTheBody()); + + await behavior.Invoke(context, ctx => Task.CompletedTask); + + using (Assert.EnterMultipleScope()) + { + Assert.That(context.UpdateMessageObjCalled, Is.True); + Assert.That(context.UpdateMessageWithTypeCalled, Is.False); + } + } + class InterceptUpdateMessageOutgoingLogicalMessageContext : TestableOutgoingLogicalMessageContext { public bool UpdateMessageCalled { get; private set; } + public bool UpdateMessageObjCalled { get; private set; } + + public bool UpdateMessageWithTypeCalled { get; private set; } + + public Type DeclaredMessageType { get; private set; } + public override void UpdateMessage(object newInstance) { base.UpdateMessage(newInstance); UpdateMessageCalled = true; + UpdateMessageObjCalled = true; + } + + public override void UpdateMessage(object newInstance, Type messageType) + { + base.UpdateMessage(newInstance, messageType); + + UpdateMessageWithTypeCalled = true; + DeclaredMessageType = messageType; } } @@ -171,4 +225,20 @@ public Task MutateOutgoing(MutateOutgoingMessageContext context) return Task.CompletedTask; } } + + class MutatorWhichDeclaresAMessageType : IMutateOutgoingMessages + { + public Task MutateOutgoing(MutateOutgoingMessageContext context) + { + context.UpdateMessage(new MyMessage()); + + return Task.CompletedTask; + } + } + + interface IMyMessage : IMessage + { } + + class MyMessage : IMyMessage + { } } \ No newline at end of file diff --git a/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/When_incoming_mutator_replaces_message_instance.cs b/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/When_incoming_mutator_replaces_message_instance.cs index c1dc73df095..8a7dc427fb8 100644 --- a/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/When_incoming_mutator_replaces_message_instance.cs +++ b/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/When_incoming_mutator_replaces_message_instance.cs @@ -17,62 +17,53 @@ public class When_incoming_mutator_replaces_message_instance [Test] public async Task Should_recompute_metadata_for_the_replacement_instance_type() { - var registry = new MessageMetadataRegistry(); - registry.Initialize(new Conventions().IsMessageType, true); - registry.RegisterMessageTypes([typeof(OriginalMessage), typeof(ReplacementMessage)]); - - var context = CreateContext(registry, new ReplaceWithReplacementMessageMutator()); + var context = CreateContext(new ReplaceWithReplacementMessageMutator()); + var behavior = new MutateIncomingMessageBehavior([]); - await context.Behavior.Invoke(context.Context, ctx => Task.CompletedTask); + await behavior.Invoke(context, ctx => Task.CompletedTask); using (Assert.EnterMultipleScope()) { - Assert.That(context.Context.Message.Instance, Is.TypeOf()); - Assert.That(context.Context.Message.MessageType, Is.EqualTo(typeof(ReplacementMessage))); - Assert.That(context.Context.Message.Metadata.MessageType, Is.EqualTo(typeof(ReplacementMessage))); + Assert.That(context.Message.Instance, Is.TypeOf()); + Assert.That(context.Message.MessageType, Is.EqualTo(typeof(ReplacementMessage))); + Assert.That(context.Message.Metadata.MessageType, Is.EqualTo(typeof(ReplacementMessage))); } } [Test] public async Task Should_keep_original_metadata_when_instance_is_not_replaced() { - var registry = new MessageMetadataRegistry(); - registry.Initialize(new Conventions().IsMessageType, true); - registry.RegisterMessageTypes([typeof(OriginalMessage), typeof(ReplacementMessage)]); - - var context = CreateContext(registry, new DoNothingMutator()); + var context = CreateContext(new DoNothingMutator()); + var behavior = new MutateIncomingMessageBehavior([]); - await context.Behavior.Invoke(context.Context, ctx => Task.CompletedTask); + await behavior.Invoke(context, ctx => Task.CompletedTask); using (Assert.EnterMultipleScope()) { - Assert.That(context.Context.Message.Instance, Is.TypeOf()); - Assert.That(context.Context.Message.MessageType, Is.EqualTo(typeof(OriginalMessage))); + Assert.That(context.Message.Instance, Is.TypeOf()); + Assert.That(context.Message.MessageType, Is.EqualTo(typeof(OriginalMessage))); } } - static ContextFixture CreateContext(MessageMetadataRegistry registry, IMutateIncomingMessages mutator) + static IncomingLogicalMessageContext CreateContext(IMutateIncomingMessages mutator) { + var registry = new MessageMetadataRegistry(); + registry.Initialize(new Conventions().IsMessageType, true); + registry.RegisterMessageTypes([typeof(OriginalMessage), typeof(ReplacementMessage)]); + var services = new ServiceCollection(); services.AddSingleton(registry); services.AddSingleton(); services.AddSingleton(new TrimmingSafeMessageMapper()); + services.AddSingleton(mutator); IServiceProvider provider = services.BuildServiceProvider(); var parentContext = new TestableIncomingPhysicalMessageContext(); parentContext.Extensions.Set(provider); var logicalMessage = new LogicalMessage(registry.GetMessageMetadata(typeof(OriginalMessage)), new OriginalMessage()); - var context = new IncomingLogicalMessageContext(logicalMessage, parentContext); - var behavior = new MutateIncomingMessageBehavior([mutator]); - return new ContextFixture(context, behavior); - } - - class ContextFixture(IncomingLogicalMessageContext context, MutateIncomingMessageBehavior behavior) - { - public IncomingLogicalMessageContext Context { get; } = context; - public MutateIncomingMessageBehavior Behavior { get; } = behavior; + return new IncomingLogicalMessageContext(logicalMessage, parentContext); } class ReplaceWithReplacementMessageMutator : IMutateIncomingMessages diff --git a/src/NServiceBus.Core.Tests/Pipeline/Incoming/IncomingLogicalMessageContextTests.cs b/src/NServiceBus.Core.Tests/Pipeline/Incoming/IncomingLogicalMessageContextTests.cs new file mode 100644 index 00000000000..59e968fd5f1 --- /dev/null +++ b/src/NServiceBus.Core.Tests/Pipeline/Incoming/IncomingLogicalMessageContextTests.cs @@ -0,0 +1,109 @@ +namespace NServiceBus.Core.Tests.Pipeline.Incoming; + +using System; +using MessageInterfaces; +using MessageInterfaces.MessageMapper.Reflection; +using Microsoft.Extensions.DependencyInjection; +using NServiceBus.Pipeline; +using NUnit.Framework; +using Testing; +using Unicast.Messages; + +[TestFixture] +public class IncomingLogicalMessageContextTests +{ + [Test] + public void Updating_the_message_to_a_new_type_should_update_the_MessageType() + { + var context = CreateContext(typeof(MyDifferentMessage)); + + var differentMessage = new MyDifferentMessage(); + context.UpdateMessageInstance(differentMessage); + + using (Assert.EnterMultipleScope()) + { + Assert.That(context.Message.MessageType, Is.EqualTo(typeof(MyDifferentMessage))); + Assert.That(context.Message.Instance, Is.SameAs(differentMessage)); + } + } + + [Test] + public void Updating_the_existing_instance_with_a_different_explicit_type_should_use_that_type() + { + var message = new MySubMessage(); + var context = CreateContext(typeof(MySubMessage), message); + + context.UpdateMessageInstance(message); + + using (Assert.EnterMultipleScope()) + { + Assert.That(context.Message.MessageType, Is.EqualTo(typeof(MyDifferentMessage))); + Assert.That(context.Message.Instance, Is.SameAs(message)); + } + } + + [Test] + public void Updating_the_existing_instance_with_the_same_type_should_preserve_the_metadata() + { + var message = new MyDifferentMessage(); + var context = CreateContext(typeof(MyDifferentMessage), message); + + var metadataBefore = context.Message.Metadata; + + context.UpdateMessageInstance(message); + + Assert.That(context.Message.Metadata, Is.SameAs(metadataBefore)); + } + + [Test] + public void Updating_with_an_explicit_type_that_is_not_assignable_should_throw() + { + var context = CreateContext(typeof(MyDifferentMessage)); + + Assert.Throws(() => context.UpdateMessageInstance(new MyDifferentMessage(), typeof(string))); + } + + [Test] + public void Updating_with_a_null_instance_should_throw() + { + var context = CreateContext(typeof(MyDifferentMessage)); + + Assert.Throws(() => context.UpdateMessageInstance(null!, typeof(MyDifferentMessage))); + } + + [Test] + public void Updating_with_a_null_message_type_should_throw() + { + var context = CreateContext(typeof(MyDifferentMessage)); + + Assert.Throws(() => context.UpdateMessageInstance(new MyDifferentMessage(), null!)); + } + + static IncomingLogicalMessageContext CreateContext(Type messageType, object instance = null) + { + var registry = new MessageMetadataRegistry(); + registry.Initialize(new Conventions().IsMessageType, true); + registry.RegisterMessageTypes([typeof(MyDifferentMessage), typeof(MySubMessage)]); + var services = new ServiceCollection(); + services.AddSingleton(registry); + services.AddSingleton(); + services.AddSingleton(new TrimmingSafeMessageMapper()); + IServiceProvider provider = services.BuildServiceProvider(); + + var parentContext = new TestableIncomingPhysicalMessageContext(); + parentContext.Extensions.Set(provider); + + instance ??= new MyDifferentMessage(); + + var logicalMessage = new LogicalMessage(registry.GetMessageMetadata(messageType), instance); + var context = new IncomingLogicalMessageContext(logicalMessage, parentContext); + + return context; + } + + class MyDifferentMessage : IMessage + { } + + class MySubMessage : MyDifferentMessage + { } +} diff --git a/src/NServiceBus.Core.Tests/TypedMessageInstanceOverloadsTests.cs b/src/NServiceBus.Core.Tests/TypedMessageInstanceOverloadsTests.cs index b72c5522885..9683dbd95ea 100644 --- a/src/NServiceBus.Core.Tests/TypedMessageInstanceOverloadsTests.cs +++ b/src/NServiceBus.Core.Tests/TypedMessageInstanceOverloadsTests.cs @@ -332,6 +332,66 @@ public void Testable_outgoing_context_explicit_type_preserves_declared_type_and_ Assert.That(context.Message.Instance, Is.SameAs(message)); } + [Test] + public void Testable_incoming_context_ordinary_call_uses_runtime_type() + { + var context = new TestableIncomingLogicalMessageContext(); + var message = (IMyMessage)new MyMessage(); + + context.UpdateMessageInstance(message); + + Assert.That(context.Message.MessageType, Is.EqualTo(typeof(MyMessage))); + } + + [Test] + public void Testable_incoming_context_explicit_generic_call_uses_specified_type() + { + var context = new TestableIncomingLogicalMessageContext(); + var message = new MyMessage(); + + context.UpdateMessageInstance(message); + + Assert.That(context.Message.MessageType, Is.EqualTo(typeof(IMyMessage))); + } + + [Test] + public void Testable_incoming_context_explicit_type_validates_declared_type() + { + var context = new TestableIncomingLogicalMessageContext(); + object message = new MyMessage(); + + Assert.Throws(() => context.UpdateMessageInstance(message, typeof(MyOtherMessage))); + } + + [Test] + public void Testable_incoming_context_explicit_type_rejects_null_instance() + { + var context = new TestableIncomingLogicalMessageContext(); + + Assert.Throws(() => context.UpdateMessageInstance(null!, typeof(IMyMessage))); + } + + [Test] + public void Testable_incoming_context_explicit_type_rejects_null_message_type() + { + var context = new TestableIncomingLogicalMessageContext(); + var message = new MyMessage(); + + Assert.Throws(() => context.UpdateMessageInstance(message, null!)); + } + + [Test] + public void Testable_incoming_context_explicit_type_preserves_declared_type_and_instance() + { + var context = new TestableIncomingLogicalMessageContext(); + object message = new MyMessage(); + + context.UpdateMessageInstance(message, typeof(IMyMessage)); + + Assert.That(context.Message.MessageType, Is.EqualTo(typeof(IMyMessage))); + Assert.That(context.Message.Instance, Is.SameAs(message)); + } + [Test] public async Task Default_interface_fallback_Send_uses_object_overload() { diff --git a/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateIncomingMessageBehavior.cs b/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateIncomingMessageBehavior.cs index 6f1e1423048..9eb24e88fd2 100644 --- a/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateIncomingMessageBehavior.cs +++ b/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateIncomingMessageBehavior.cs @@ -46,7 +46,14 @@ await mutator.MutateIncoming(mutatorContext) if (mutatorContext.MessageInstanceChanged) { - context.UpdateMessageInstance(mutatorContext.Message); + if (mutatorContext.ReplacementMessageType != null) + { + context.UpdateMessageInstance(mutatorContext.Message, mutatorContext.ReplacementMessageType); + } + else + { + context.UpdateMessageInstance(mutatorContext.Message); + } } await next(context).ConfigureAwait(false); diff --git a/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateIncomingMessageContext.cs b/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateIncomingMessageContext.cs index 07f6b1ac709..acf748485a5 100644 --- a/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateIncomingMessageContext.cs +++ b/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateIncomingMessageContext.cs @@ -4,6 +4,7 @@ namespace NServiceBus.MessageMutator; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Threading; /// @@ -37,6 +38,30 @@ public object Message } } + /// + /// Replaces the current incoming message with the provided typed message instance. + /// + /// The type used to update the message. It determines the logical message type and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The replacement message instance. + public void UpdateMessageInstance<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T newMessage) + { + UpdateMessageInstance(newMessage!, typeof(T)); + } + + /// + /// Replaces the current incoming message with the provided message instance and message type. The declared type determines the logical message type. + /// + /// The replacement message instance. Must be assignable to . + /// The declared logical message type. It can differ from the runtime type of as long as the instance is assignable to it. + /// or is . + /// is not assignable to . + public void UpdateMessageInstance(object newMessage, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType) + { + MessageTypeValidator.Validate(newMessage, messageType); + Message = newMessage; + ReplacementMessageType = messageType; + } + /// /// The current incoming headers. /// @@ -50,4 +75,10 @@ public object Message object message; internal bool MessageInstanceChanged; + + /// + /// The declared logical message type of when a mutator supplied an explicit type, otherwise . + /// + [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] + internal Type? ReplacementMessageType { get; set; } } \ No newline at end of file diff --git a/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageBehavior.cs b/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageBehavior.cs index 8fedadcebe2..cf45ef52c18 100644 --- a/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageBehavior.cs +++ b/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageBehavior.cs @@ -52,7 +52,14 @@ await mutator.MutateOutgoing(mutatorContext) if (mutatorContext.MessageInstanceChanged) { - context.UpdateMessage(mutatorContext.OutgoingMessage); + if (mutatorContext.ReplacementMessageType != null) + { + context.UpdateMessage(mutatorContext.OutgoingMessage, mutatorContext.ReplacementMessageType); + } + else + { + context.UpdateMessage(mutatorContext.OutgoingMessage); + } } await next(context).ConfigureAwait(false); diff --git a/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageContext.cs b/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageContext.cs index 8eba75e3684..131bc3c0912 100644 --- a/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageContext.cs +++ b/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageContext.cs @@ -40,6 +40,30 @@ public object OutgoingMessage } } + /// + /// Replaces the current outgoing message with the provided typed message instance. + /// + /// The type used to update the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The replacement message instance. + public void UpdateMessage<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T newMessage) + { + UpdateMessage(newMessage!, typeof(T)); + } + + /// + /// Replaces the current outgoing message with the provided message instance and message type. The declared type controls how the message is routed and the message type header recorded on the message. + /// + /// The replacement message instance. Must be assignable to . + /// The declared logical message type. It can differ from the runtime type of as long as the instance is assignable to it. + /// or is . + /// is not assignable to . + public void UpdateMessage(object newMessage, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType) + { + MessageTypeValidator.Validate(newMessage, messageType); + OutgoingMessage = newMessage; + ReplacementMessageType = messageType; + } + /// /// The current outgoing headers. /// @@ -73,5 +97,11 @@ public bool TryGetIncomingHeaders([NotNullWhen(true)] out IReadOnlyDictionary + /// The declared logical message type of when a mutator supplied an explicit type, otherwise . + /// + [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] + internal Type? ReplacementMessageType { get; set; } + object outgoingMessage; } \ No newline at end of file diff --git a/src/NServiceBus.Core/Pipeline/Incoming/IIncomingLogicalMessageContext.cs b/src/NServiceBus.Core/Pipeline/Incoming/IIncomingLogicalMessageContext.cs index 4af169ac758..f554a585482 100644 --- a/src/NServiceBus.Core/Pipeline/Incoming/IIncomingLogicalMessageContext.cs +++ b/src/NServiceBus.Core/Pipeline/Incoming/IIncomingLogicalMessageContext.cs @@ -2,7 +2,11 @@ namespace NServiceBus.Pipeline; +using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using Particular.Obsoletes; /// /// A context of behavior execution in logical message processing stage. @@ -25,8 +29,40 @@ public interface IIncomingLogicalMessageContext : IIncomingContext bool MessageHandled { get; set; } /// - /// Updates the message instance contained in . + /// Updates the message instance contained in . /// /// The new instance. + [PreObsolete("https://github.com/Particular/NServiceBus/issues/7906", + ReplacementTypeOrMember = "UpdateMessageInstance(T)", + Note = "The object-only overload uses message.GetType() at runtime which is not trimming safe. Use the generic overload instead.")] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] void UpdateMessageInstance(object newInstance); + + /// + /// Updates the message instance contained in while preserving the specified message type. + /// + /// The type used to update the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The replacement message instance. + [OverloadResolutionPriority(-1)] + void UpdateMessageInstance<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T newInstance) + { + UpdateMessageInstance(newInstance!, typeof(T)); + } + + /// + /// Updates the message instance contained in with the specified message type. The declared type controls how the message is routed and the message type header recorded on the message. + /// + /// The replacement message instance. Must be assignable to . + /// The declared logical message type. It can differ from the runtime type of as long as the instance is assignable to it. + /// or is . + /// is not assignable to . + /// + /// Third-party implementations that inherit this default implementation fall back to the object overload and route by the runtime type of . Override this method to preserve a declared that differs from the runtime type. + /// + [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = MessageOperations.DefaultInterfaceTrimmingSuppressionJustification)] + void UpdateMessageInstance(object newInstance, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType) + { + MessageTypeValidator.Validate(newInstance, messageType); + UpdateMessageInstance(newInstance); + } } \ No newline at end of file diff --git a/src/NServiceBus.Core/Pipeline/Incoming/IncomingLogicalMessageContext.cs b/src/NServiceBus.Core/Pipeline/Incoming/IncomingLogicalMessageContext.cs index 9ed9aee336f..147493cd987 100644 --- a/src/NServiceBus.Core/Pipeline/Incoming/IncomingLogicalMessageContext.cs +++ b/src/NServiceBus.Core/Pipeline/Incoming/IncomingLogicalMessageContext.cs @@ -4,7 +4,10 @@ namespace NServiceBus; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using Microsoft.Extensions.DependencyInjection; +using Particular.Obsoletes; using Pipeline; class IncomingLogicalMessageContext : IncomingContext, IIncomingLogicalMessageContext @@ -28,6 +31,10 @@ public IncomingLogicalMessageContext(LogicalMessage logicalMessage, string messa public bool MessageHandled { get; set; } + [PreObsolete("https://github.com/Particular/NServiceBus/issues/7906", + ReplacementTypeOrMember = "UpdateMessageInstance(T)", + Note = "The object-only overload uses message.GetType() at runtime which is not trimming safe. Use the generic overload instead.")] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] public void UpdateMessageInstance(object newInstance) { ArgumentNullException.ThrowIfNull(newInstance); @@ -45,4 +52,41 @@ public void UpdateMessageInstance(object newInstance) Message.Metadata = newLogicalMessage.Metadata; } + + /// + /// Updates the message instance contained in while preserving the specified message type. + /// + /// The type used to update the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The replacement message instance. + [OverloadResolutionPriority(-1)] + public void UpdateMessageInstance<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T newInstance) + => UpdateMessageInstance(newInstance!, typeof(T)); + + /// + /// Updates the message instance contained in with the specified message type. The declared type controls how the message is routed and the message type header recorded on the message. + /// + /// The replacement message instance. Must be assignable to . + /// The declared logical message type. It can differ from the runtime type of as long as the instance is assignable to it. + /// or is . + /// is not assignable to . + public void UpdateMessageInstance(object newInstance, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType) + { + ArgumentNullException.ThrowIfNull(newInstance); + ArgumentNullException.ThrowIfNull(messageType); + MessageTypeValidator.Validate(newInstance, messageType); + + var sameInstance = ReferenceEquals(Message.Instance, newInstance); + + Message.Instance = newInstance; + + if (sameInstance && Message.Metadata.MessageType == messageType) + { + return; + } + + var factory = Builder.GetRequiredService(); + var newLogicalMessage = factory.Create(messageType, newInstance); + + Message.Metadata = newLogicalMessage.Metadata; + } } \ No newline at end of file diff --git a/src/NServiceBus.Testing.Fakes/TestableIncomingLogicalMessageContext.cs b/src/NServiceBus.Testing.Fakes/TestableIncomingLogicalMessageContext.cs index 04716b85aeb..cd9fe1f780b 100644 --- a/src/NServiceBus.Testing.Fakes/TestableIncomingLogicalMessageContext.cs +++ b/src/NServiceBus.Testing.Fakes/TestableIncomingLogicalMessageContext.cs @@ -1,6 +1,9 @@ namespace NServiceBus.Testing; +using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; using Pipeline; using Unicast.Messages; @@ -35,8 +38,33 @@ public TestableIncomingLogicalMessageContext(IMessageCreator messageCreator = nu /// Updates the message instance contained in . /// /// The new instance. + [RequiresUnreferencedCode(DynamicMemberTypeAccess.RuntimeTypeRoutingTrimmingMessage)] public virtual void UpdateMessageInstance(object newInstance) { Message = new LogicalMessage(new MessageMetadata(newInstance.GetType()), newInstance); } + + /// + /// Updates the message instance contained in while preserving the specified message type. + /// + /// The type used to update the message. It determines the logical message type and can differ from the runtime type of the message instance as long as the instance is assignable to T. + /// The replacement message instance. + [OverloadResolutionPriority(-1)] + public virtual void UpdateMessageInstance<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T newInstance) + { + UpdateMessageInstance(newInstance!, typeof(T)); + } + + /// + /// Updates the message instance contained in with the specified message type. The declared type determines the logical message type. + /// + /// The replacement message instance. Must be assignable to . + /// The declared logical message type. It can differ from the runtime type of as long as the instance is assignable to it. + /// or is . + /// is not assignable to . + public virtual void UpdateMessageInstance(object newInstance, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType) + { + MessageTypeValidator.Validate(newInstance, messageType); + Message = new LogicalMessage(new MessageMetadata(messageType), newInstance); + } } \ No newline at end of file From df7af62342d866beaa0e922dd65b5e111ef1d8ef Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Thu, 3 Sep 2026 10:41:14 +0200 Subject: [PATCH 2/8] Extend message migration analyzer to mutator context setters The legacy mutator pattern assigns an object to the Message or OutgoingMessage property of a mutator context, which routes by the runtime type. Analyze SimpleAssignment operations on those two setters and reuse the existing NSB0039/NSB0040 classification (direct creation and value types are provably safe; everything else is a runtime-type routing warning), gated by the same trimming/AOT build properties. The code fixer rewrites a safe assignment to the typed replacement API: - context.Message = new MyMessage() -> context.UpdateMessageInstance(new MyMessage()) - context.OutgoingMessage = new MyEvent() -> context.UpdateMessage(new MyEvent()) --- .../MessagingMigrationFixer.cs | 85 ++++++++ .../MessagingMigrationAnalyzerTests.cs | 192 ++++++++++++++++++ .../MessagingMigrationFixerTests.cs | 114 +++++++++++ .../MessagingMigrationAnalyzer.cs | 109 +++++++++- 4 files changed, 497 insertions(+), 3 deletions(-) diff --git a/src/NServiceBus.Core.Analyzer.Fixes/MessagingMigrationFixer.cs b/src/NServiceBus.Core.Analyzer.Fixes/MessagingMigrationFixer.cs index e3808dca7de..f7edb0f1931 100644 --- a/src/NServiceBus.Core.Analyzer.Fixes/MessagingMigrationFixer.cs +++ b/src/NServiceBus.Core.Analyzer.Fixes/MessagingMigrationFixer.cs @@ -68,6 +68,24 @@ public override async Task RegisterCodeFixesAsync(CodeFixContext context) continue; } + if (node is AssignmentExpressionSyntax assignment && + TryGetMutatorReplacementMethod(semanticModel, assignment, out var replacementMethodName)) + { + context.RegisterCodeFix( + CodeAction.Create( + "Use the strongly typed message overload", + cancellationToken => ReplaceAssignmentWithTypedCall( + context.Document, + root, + assignment, + replacementMethodName, + messageType!, + cancellationToken), + EquivalenceKey), + diagnostic); + continue; + } + if (node.FirstAncestorOrSelf() is not { } invocation || !CanAddTypeArgument(invocation.Expression)) { @@ -209,6 +227,73 @@ static Task AddTypeArgumentToMethodReference( return Task.FromResult(document.WithSyntaxRoot(root.ReplaceNode(methodReference, updatedMethodReference))); } + static bool TryGetMutatorReplacementMethod( + SemanticModel? semanticModel, + AssignmentExpressionSyntax assignment, + out string methodName) + { + methodName = null!; + if (semanticModel is null || assignment.Left is not MemberAccessExpressionSyntax memberAccess) + { + return false; + } + + var propertySymbol = semanticModel.GetSymbolInfo(memberAccess).Symbol; + if (propertySymbol is not IPropertySymbol + { + ContainingType: { } containingType + }) + { + return false; + } + + var containingTypeName = containingType.ToDisplayString(); + if (propertySymbol.Name == "Message" && + containingTypeName == "NServiceBus.MessageMutator.MutateIncomingMessageContext") + { + methodName = "UpdateMessageInstance"; + return true; + } + + if (propertySymbol.Name == "OutgoingMessage" && + containingTypeName == "NServiceBus.MessageMutator.MutateOutgoingMessageContext") + { + methodName = "UpdateMessage"; + return true; + } + + return false; + } + + static Task ReplaceAssignmentWithTypedCall( + Document document, + SyntaxNode root, + AssignmentExpressionSyntax assignment, + string methodName, + string messageType, + CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + var memberAccess = (MemberAccessExpressionSyntax)assignment.Left; + var typeArgument = SyntaxFactory.ParseTypeName(messageType) + .WithAdditionalAnnotations(Simplifier.Annotation); + var typeArguments = SyntaxFactory.TypeArgumentList( + SyntaxFactory.SingletonSeparatedList(typeArgument)); + + var invocation = SyntaxFactory.InvocationExpression( + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + memberAccess.Expression, + SyntaxFactory.GenericName(SyntaxFactory.Identifier(methodName), typeArguments) + .WithTriviaFrom(memberAccess.Name)), + SyntaxFactory.ArgumentList( + SyntaxFactory.SingletonSeparatedList(SyntaxFactory.Argument(assignment.Right)))) + .WithAdditionalAnnotations(Formatter.Annotation); + + return Task.FromResult(document.WithSyntaxRoot(root.ReplaceNode(assignment, invocation))); + } + static ExpressionSyntax AddTypeArgumentToExpression(ExpressionSyntax expression, string messageType) { var typeArgument = SyntaxFactory.ParseTypeName(messageType) diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationAnalyzerTests.cs b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationAnalyzerTests.cs index 560cea107f4..48328676823 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationAnalyzerTests.cs +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationAnalyzerTests.cs @@ -1010,6 +1010,198 @@ async Task Bar(IMessageSession session) return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); } + // ===== Mutator context setters ===== + + [Test] + public Task NSB0039_MutatorIncomingContext_DirectObjectCreation() + { + var source = + """ + using NServiceBus; + using NServiceBus.MessageMutator; + + class Foo + { + void Bar(MutateIncomingMessageContext context) + { + [|context.Message = new MyMessage()|]; + } + } + + class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_MutatorOutgoingContext_DirectObjectCreation() + { + var source = + """ + using NServiceBus; + using NServiceBus.MessageMutator; + + class Foo + { + void Bar(MutateOutgoingMessageContext context) + { + [|context.OutgoingMessage = new MyEvent()|]; + } + } + + class MyEvent : IEvent { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0039_MutatorIncomingContext_ValueType() + { + var source = + """ + using NServiceBus; + using NServiceBus.MessageMutator; + + class Foo + { + void Bar(MutateIncomingMessageContext context, MyValue message) + { + [|context.Message = message|]; + } + } + + struct MyValue : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0040_MutatorIncomingContext_VarObjectCreation() + { + var source = + """ + using NServiceBus; + using NServiceBus.MessageMutator; + + class Foo + { + void Bar(MutateIncomingMessageContext context) + { + var message = new MyMessage(); + [|context.Message = message|]; + } + } + + class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_MutatorOutgoingContext_VarObjectCreation() + { + var source = + """ + using NServiceBus; + using NServiceBus.MessageMutator; + + class Foo + { + void Bar(MutateOutgoingMessageContext context) + { + var message = new MyEvent(); + [|context.OutgoingMessage = message|]; + } + } + + class MyEvent : IEvent { } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_MutatorIncomingContext_SealedVariable() + { + var source = + """ + using NServiceBus; + using NServiceBus.MessageMutator; + + class Foo + { + void Bar(MutateIncomingMessageContext context, MyMessage message) + { + [|context.Message = message|]; + } + } + + sealed class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_MutatorOutgoingContext_CreatedByMessageCreator() + { + var source = + """ + using NServiceBus; + using NServiceBus.MessageMutator; + + class Foo + { + void Bar(MutateOutgoingMessageContext context, IMessageCreator creator) + { + [|context.OutgoingMessage = creator.CreateInstance()|]; + } + } + + class MyEvent : IEvent { } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NoDiagnostic_MutatorContext_ObjectType() + { + var source = + """ + using NServiceBus; + using NServiceBus.MessageMutator; + + class Foo + { + void Bar(MutateIncomingMessageContext context) + { + context.Message = new object(); + } + } + """; + return Assert(source); + } + + [Test] + public Task NoDiagnostic_MutatorContext_UnrelatedMessageProperty() + { + var source = + """ + using NServiceBus; + + class Foo + { + public object Message { get; set; } + + void Bar(Foo foo) + { + foo.Message = new MyMessage(); + } + } + + class MyMessage : IMessage { } + """; + return Assert(source); + } + // ===== NSB0041: Generic T == object ===== [Test] diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationFixerTests.cs b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationFixerTests.cs index 27e1fb13785..704981fb5d5 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationFixerTests.cs +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationFixerTests.cs @@ -639,6 +639,120 @@ class MyMessage : IMessage { } return Assert(original, expected); } + [Test] + public Task MutatorIncomingContext_Message() + { + var original = + """ + using NServiceBus; + using NServiceBus.MessageMutator; + + class Foo + { + void Bar(MutateIncomingMessageContext context) + { + context.Message = new MyMessage(); + } + } + + class MyMessage : IMessage { } + """; + + var expected = + """ + using NServiceBus; + using NServiceBus.MessageMutator; + + class Foo + { + void Bar(MutateIncomingMessageContext context) + { + context.UpdateMessageInstance(new MyMessage()); + } + } + + class MyMessage : IMessage { } + """; + + return Assert(original, expected); + } + + [Test] + public Task MutatorOutgoingContext_OutgoingMessage() + { + var original = + """ + using NServiceBus; + using NServiceBus.MessageMutator; + + class Foo + { + void Bar(MutateOutgoingMessageContext context) + { + context.OutgoingMessage = new MyEvent(); + } + } + + class MyEvent : IEvent { } + """; + + var expected = + """ + using NServiceBus; + using NServiceBus.MessageMutator; + + class Foo + { + void Bar(MutateOutgoingMessageContext context) + { + context.UpdateMessage(new MyEvent()); + } + } + + class MyEvent : IEvent { } + """; + + return Assert(original, expected); + } + + [Test] + public Task MutatorIncomingContext_MessageValueType() + { + var original = + """ + using NServiceBus; + using NServiceBus.MessageMutator; + + class Foo + { + void Bar(MutateIncomingMessageContext context, MyValue message) + { + context.Message = message; + } + } + + struct MyValue : IMessage { } + """; + + var expected = + """ + using NServiceBus; + using NServiceBus.MessageMutator; + + class Foo + { + void Bar(MutateIncomingMessageContext context, MyValue message) + { + context.UpdateMessageInstance(message); + } + } + + struct MyValue : IMessage { } + """; + + return Assert(original, expected); + } + [Test] public Task MethodGroup_SessionSend() { diff --git a/src/NServiceBus.Core.Analyzer/MessagingMigrationAnalyzer.cs b/src/NServiceBus.Core.Analyzer/MessagingMigrationAnalyzer.cs index 6d7a0a82886..0309621e406 100644 --- a/src/NServiceBus.Core.Analyzer/MessagingMigrationAnalyzer.cs +++ b/src/NServiceBus.Core.Analyzer/MessagingMigrationAnalyzer.cs @@ -73,9 +73,101 @@ public override void Initialize(AnalysisContext context) startContext.RegisterOperationAction( operationContext => AnalyzeDelegateCreation(operationContext, knownTypes, severityConfiguration), OperationKind.DelegateCreation); + startContext.RegisterOperationAction( + operationContext => AnalyzeSimpleAssignment(operationContext, knownTypes, severityConfiguration), + OperationKind.SimpleAssignment); }); } + static void AnalyzeSimpleAssignment( + OperationAnalysisContext context, + KnownTypes knownTypes, + MigrationDiagnosticConfiguration severityConfiguration) + { + var assignment = (ISimpleAssignmentOperation)context.Operation; + if (assignment.Target is not IPropertyReferenceOperation + { + Instance: not null, + Property: { } property + }) + { + return; + } + + var replacementMethodName = ResolveMutatorReplacementMethod(property, knownTypes); + if (replacementMethodName is null) + { + return; + } + + var messageValue = UnwrapImplicitConversions(assignment.Value); + var messageType = messageValue.Type; + if (messageType is null || messageType.TypeKind == TypeKind.Dynamic || + messageValue.ConstantValue is { HasValue: true, Value: null }) + { + return; + } + + if (!messageType.CanBeReferencedByName) + { + return; + } + + // An object-typed assignment would be fixed to the generic overload with T = System.Object, + // which immediately violates NSB0041. Never offer a fixable NSB0039 for the object type. + if (messageType.SpecialType == SpecialType.System_Object) + { + return; + } + + // Mutator contexts preserve the previous logical type when the same instance is assigned + // again, mirroring UpdateMessage. Only direct creation and value types are provably safe. + if (IsRoutingEquivalent(messageValue, messageType, knownTypes.IMessageCreator, isUpdateMessage: true)) + { + if (!severityConfiguration.IsEnabled(context, assignment.Syntax.SyntaxTree, UseGenericTypeRule)) + { + return; + } + + context.ReportDiagnostic(Diagnostic.Create( + UseGenericTypeRule, + assignment.Syntax.GetLocation(), + ImmutableDictionary.Empty.Add( + MessageTypeProperty, + messageType.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)), + messageType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat))); + } + else + { + if (!severityConfiguration.IsEnabled(context, assignment.Syntax.SyntaxTree, RuntimeTypeMayDifferRule)) + { + return; + } + + context.ReportDiagnostic(Diagnostic.Create( + RuntimeTypeMayDifferRule, + assignment.Syntax.GetLocation(), + messageType.ToDisplayString(SymbolDisplayFormat.MinimallyQualifiedFormat))); + } + } + + static string? ResolveMutatorReplacementMethod(IPropertySymbol property, KnownTypes knownTypes) + { + if (property.Name == "Message" && + SymbolEqualityComparer.Default.Equals(property.ContainingType, knownTypes.MutateIncomingMessageContext)) + { + return "UpdateMessageInstance"; + } + + if (property.Name == "OutgoingMessage" && + SymbolEqualityComparer.Default.Equals(property.ContainingType, knownTypes.MutateOutgoingMessageContext)) + { + return "UpdateMessage"; + } + + return null; + } + static void AnalyzeDelegateCreation( OperationAnalysisContext context, KnownTypes knownTypes, @@ -660,7 +752,9 @@ sealed class KnownTypes INamedTypeSymbol messageProcessingContextExtensions, INamedTypeSymbol saga, INamedTypeSymbol outgoingLogicalMessageContext, - INamedTypeSymbol messageCreator) + INamedTypeSymbol messageCreator, + INamedTypeSymbol mutateIncomingMessageContext, + INamedTypeSymbol mutateOutgoingMessageContext) { IMessageSession = messageSession; IPipelineContext = pipelineContext; @@ -671,6 +765,8 @@ sealed class KnownTypes Saga = saga; IOutgoingLogicalMessageContext = outgoingLogicalMessageContext; IMessageCreator = messageCreator; + MutateIncomingMessageContext = mutateIncomingMessageContext; + MutateOutgoingMessageContext = mutateOutgoingMessageContext; ContractInterfaces = [ messageSession, @@ -689,6 +785,8 @@ sealed class KnownTypes public INamedTypeSymbol Saga { get; } public INamedTypeSymbol IOutgoingLogicalMessageContext { get; } public INamedTypeSymbol IMessageCreator { get; } + public INamedTypeSymbol MutateIncomingMessageContext { get; } + public INamedTypeSymbol MutateOutgoingMessageContext { get; } public ImmutableArray ContractInterfaces { get; } readonly ConcurrentDictionary implementsContractCache = new(SymbolEqualityComparer.Default); @@ -758,11 +856,14 @@ public static bool TryCreate(Compilation compilation, out KnownTypes knownTypes) var saga = compilation.GetTypeByMetadataName("NServiceBus.Saga"); var outgoingLogicalMessageContext = compilation.GetTypeByMetadataName("NServiceBus.Pipeline.IOutgoingLogicalMessageContext"); var messageCreator = compilation.GetTypeByMetadataName("NServiceBus.IMessageCreator"); + var mutateIncomingMessageContext = compilation.GetTypeByMetadataName("NServiceBus.MessageMutator.MutateIncomingMessageContext"); + var mutateOutgoingMessageContext = compilation.GetTypeByMetadataName("NServiceBus.MessageMutator.MutateOutgoingMessageContext"); if (messageSession is null || pipelineContext is null || messageProcessingContext is null || messageSessionExtensions is null || pipelineContextExtensions is null || messageProcessingContextExtensions is null || saga is null || - outgoingLogicalMessageContext is null || messageCreator is null) + outgoingLogicalMessageContext is null || messageCreator is null || + mutateIncomingMessageContext is null || mutateOutgoingMessageContext is null) { knownTypes = null!; return false; @@ -777,7 +878,9 @@ messageProcessingContextExtensions is null || saga is null || messageProcessingContextExtensions, saga, outgoingLogicalMessageContext, - messageCreator); + messageCreator, + mutateIncomingMessageContext, + mutateOutgoingMessageContext); return true; } } From ccfc9915d7617b80d36cb45e2e13d482466a4637 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Thu, 3 Sep 2026 10:51:51 +0200 Subject: [PATCH 3/8] Add incoming UpdateMessageInstance coverage to the migration analyzer --- .../MessagingMigrationAnalyzerTests.cs | 152 ++++++++++++++++++ .../MessagingMigrationFixerTests.cs | 38 +++++ .../MessagingMigrationAnalyzer.cs | 20 ++- 3 files changed, 206 insertions(+), 4 deletions(-) diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationAnalyzerTests.cs b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationAnalyzerTests.cs index 48328676823..ffc172e5b85 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationAnalyzerTests.cs +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationAnalyzerTests.cs @@ -1346,6 +1346,158 @@ void Bar(IOutgoingLogicalMessageContext context, object message) return Assert(source, DiagnosticIds.GenericMessageTypeIsObject); } + // ===== UpdateMessageInstance on IIncomingLogicalMessageContext ===== + + [Test] + public Task NSB0039_UpdateMessageInstanceDirectObjectCreation() + { + var source = + """ + using NServiceBus; + using NServiceBus.Pipeline; + using System.Threading.Tasks; + + class Foo + { + void Bar(IIncomingLogicalMessageContext context) + { + [|context.UpdateMessageInstance(new MyMessage())|]; + } + } + + class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.UseGenericMessageType); + } + + [Test] + public Task NSB0040_UpdateMessageInstanceVarObjectCreation() + { + var source = + """ + using NServiceBus; + using NServiceBus.Pipeline; + + class MyMessage : IMessage { } + + class Foo + { + void Bar(IIncomingLogicalMessageContext context) + { + var message = new MyMessage(); + [|context.UpdateMessageInstance(message)|]; + } + } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_UpdateMessageInstanceCreatedByMessageCreator() + { + var source = + """ + using NServiceBus; + using NServiceBus.Pipeline; + + class Foo + { + void Bar(IIncomingLogicalMessageContext context, IMessageCreator creator) + { + [|context.UpdateMessageInstance(creator.CreateInstance())|]; + } + } + + public interface IMyMessage { } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0040_UpdateMessageInstanceSealedVariable() + { + var source = + """ + using NServiceBus; + using NServiceBus.Pipeline; + + class Foo + { + void Bar(IIncomingLogicalMessageContext context, MyMessage message) + { + [|context.UpdateMessageInstance(message)|]; + } + } + + public sealed class MyMessage : IMessage { } + """; + return Assert(source, DiagnosticIds.RuntimeTypeMayDiffer); + } + + [Test] + public Task NSB0041_GenericTIsObject_UpdateMessageInstance() + { + var source = + """ + using NServiceBus.Pipeline; + + class Foo + { + void Bar(IIncomingLogicalMessageContext context, object message) + { + [|context.UpdateMessageInstance(message)|]; + } + } + """; + return Assert(source, DiagnosticIds.GenericMessageTypeIsObject); + } + + [Test] + public Task NoDiagnostic_UpdateMessageInstance_UnrelatedMethod() + { + var source = + """ + using NServiceBus; + + class Helper + { + public void UpdateMessageInstance(object message) { } + } + + class Foo + { + void Bar(Helper helper, MyMessage message) + { + helper.UpdateMessageInstance(message); + } + } + + class MyMessage : IMessage { } + """; + return Assert(source); + } + + [Test] + public Task NSB0039_TestableIncomingLogicalMessageContext_UpdateMessageInstance() + { + var source = + """ + using NServiceBus; + using NServiceBus.Testing; + + class Foo + { + void Bar(TestableIncomingLogicalMessageContext context) + { + [|context.UpdateMessageInstance(new MyMessage())|]; + } + } + + class MyMessage : IMessage { } + """; + return FakeMigrationTest(source).AssertDiagnostics(DiagnosticIds.UseGenericMessageType); + } + // ===== Method groups and delegates ===== [Test] diff --git a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationFixerTests.cs b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationFixerTests.cs index 704981fb5d5..d9260e37460 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationFixerTests.cs +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationFixerTests.cs @@ -639,6 +639,44 @@ class MyMessage : IMessage { } return Assert(original, expected); } + [Test] + public Task UpdateMessageInstance() + { + var original = + """ + using NServiceBus; + using NServiceBus.Pipeline; + + class Foo + { + void Bar(IIncomingLogicalMessageContext context) + { + context.UpdateMessageInstance(new MyMessage()); + } + } + + class MyMessage : IMessage { } + """; + + var expected = + """ + using NServiceBus; + using NServiceBus.Pipeline; + + class Foo + { + void Bar(IIncomingLogicalMessageContext context) + { + context.UpdateMessageInstance(new MyMessage()); + } + } + + class MyMessage : IMessage { } + """; + + return Assert(original, expected); + } + [Test] public Task MutatorIncomingContext_Message() { diff --git a/src/NServiceBus.Core.Analyzer/MessagingMigrationAnalyzer.cs b/src/NServiceBus.Core.Analyzer/MessagingMigrationAnalyzer.cs index 0309621e406..e58f09dcfb3 100644 --- a/src/NServiceBus.Core.Analyzer/MessagingMigrationAnalyzer.cs +++ b/src/NServiceBus.Core.Analyzer/MessagingMigrationAnalyzer.cs @@ -222,7 +222,7 @@ static void AnalyzeDelegateCreation( // UpdateMessage reference types remain ambiguous because same-instance replacement can // preserve the previous logical type. Value-type method groups cannot bind here (CS0123). - var isRoutingEquivalent = declaration.Name == "UpdateMessage" + var isRoutingEquivalent = declaration.Name is "UpdateMessage" or "UpdateMessageInstance" ? messageType.IsValueType : IsRoutingEquivalentMessageType(messageType); if (isRoutingEquivalent) @@ -316,7 +316,7 @@ argument.Parameter is not null && return; } - var isUpdateMessage = declaration.Name == "UpdateMessage"; + var isUpdateMessage = declaration.Name is "UpdateMessage" or "UpdateMessageInstance"; var isStableVarObjectCreation = !isUpdateMessage && IsStableVarObjectCreation(messageValue, messageArgument, invocation, invocation.SemanticModel!); if (IsRoutingEquivalent(messageValue, messageType, knownTypes.IMessageCreator, isUpdateMessage) || @@ -693,6 +693,11 @@ static bool IsTargetMethod(IMethodSymbol method, KnownTypes knownTypes, out IMet return method.Name == "UpdateMessage"; } + if (SymbolEqualityComparer.Default.Equals(containingType, knownTypes.IIncomingLogicalMessageContext)) + { + return method.Name == "UpdateMessageInstance"; + } + return ImplementsKnownContractMember(method, knownTypes, out contractMember); } @@ -700,7 +705,7 @@ static bool IsTargetMethod(IMethodSymbol method, KnownTypes knownTypes, out IMet static bool ImplementsKnownContractMember(IMethodSymbol method, KnownTypes knownTypes, out IMethodSymbol? contractMember) { contractMember = null; - if (method.Name is not ("Send" or "Publish" or "Reply" or "UpdateMessage")) + if (method.Name is not ("Send" or "Publish" or "Reply" or "UpdateMessage" or "UpdateMessageInstance")) { return false; } @@ -752,6 +757,7 @@ sealed class KnownTypes INamedTypeSymbol messageProcessingContextExtensions, INamedTypeSymbol saga, INamedTypeSymbol outgoingLogicalMessageContext, + INamedTypeSymbol incomingLogicalMessageContext, INamedTypeSymbol messageCreator, INamedTypeSymbol mutateIncomingMessageContext, INamedTypeSymbol mutateOutgoingMessageContext) @@ -764,6 +770,7 @@ sealed class KnownTypes MessageProcessingContextExtensions = messageProcessingContextExtensions; Saga = saga; IOutgoingLogicalMessageContext = outgoingLogicalMessageContext; + IIncomingLogicalMessageContext = incomingLogicalMessageContext; IMessageCreator = messageCreator; MutateIncomingMessageContext = mutateIncomingMessageContext; MutateOutgoingMessageContext = mutateOutgoingMessageContext; @@ -772,7 +779,8 @@ sealed class KnownTypes messageSession, pipelineContext, messageProcessingContext, - outgoingLogicalMessageContext + outgoingLogicalMessageContext, + incomingLogicalMessageContext ]; } @@ -784,6 +792,7 @@ sealed class KnownTypes public INamedTypeSymbol MessageProcessingContextExtensions { get; } public INamedTypeSymbol Saga { get; } public INamedTypeSymbol IOutgoingLogicalMessageContext { get; } + public INamedTypeSymbol IIncomingLogicalMessageContext { get; } public INamedTypeSymbol IMessageCreator { get; } public INamedTypeSymbol MutateIncomingMessageContext { get; } public INamedTypeSymbol MutateOutgoingMessageContext { get; } @@ -855,6 +864,7 @@ public static bool TryCreate(Compilation compilation, out KnownTypes knownTypes) var messageProcessingContextExtensions = compilation.GetTypeByMetadataName("NServiceBus.MessageProcessingContextExtensions"); var saga = compilation.GetTypeByMetadataName("NServiceBus.Saga"); var outgoingLogicalMessageContext = compilation.GetTypeByMetadataName("NServiceBus.Pipeline.IOutgoingLogicalMessageContext"); + var incomingLogicalMessageContext = compilation.GetTypeByMetadataName("NServiceBus.Pipeline.IIncomingLogicalMessageContext"); var messageCreator = compilation.GetTypeByMetadataName("NServiceBus.IMessageCreator"); var mutateIncomingMessageContext = compilation.GetTypeByMetadataName("NServiceBus.MessageMutator.MutateIncomingMessageContext"); var mutateOutgoingMessageContext = compilation.GetTypeByMetadataName("NServiceBus.MessageMutator.MutateOutgoingMessageContext"); @@ -863,6 +873,7 @@ public static bool TryCreate(Compilation compilation, out KnownTypes knownTypes) messageSessionExtensions is null || pipelineContextExtensions is null || messageProcessingContextExtensions is null || saga is null || outgoingLogicalMessageContext is null || messageCreator is null || + incomingLogicalMessageContext is null || mutateIncomingMessageContext is null || mutateOutgoingMessageContext is null) { knownTypes = null!; @@ -878,6 +889,7 @@ outgoingLogicalMessageContext is null || messageCreator is null || messageProcessingContextExtensions, saga, outgoingLogicalMessageContext, + incomingLogicalMessageContext, messageCreator, mutateIncomingMessageContext, mutateOutgoingMessageContext); From 1d7b442ec127f5994847cdaaa86d797f0d198d71 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Thu, 3 Sep 2026 17:15:49 +0200 Subject: [PATCH 4/8] Simplify --- .../MutateIncomingMessageContext.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateIncomingMessageContext.cs b/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateIncomingMessageContext.cs index acf748485a5..7b936f841b6 100644 --- a/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateIncomingMessageContext.cs +++ b/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateIncomingMessageContext.cs @@ -43,10 +43,7 @@ public object Message /// /// The type used to update the message. It determines the logical message type and can differ from the runtime type of the message instance as long as the instance is assignable to T. /// The replacement message instance. - public void UpdateMessageInstance<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T newMessage) - { - UpdateMessageInstance(newMessage!, typeof(T)); - } + public void UpdateMessageInstance<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T newMessage) => UpdateMessageInstance(newMessage!, typeof(T)); /// /// Replaces the current incoming message with the provided message instance and message type. The declared type determines the logical message type. @@ -76,9 +73,6 @@ public void UpdateMessageInstance(object newMessage, [DynamicallyAccessedMembers internal bool MessageInstanceChanged; - /// - /// The declared logical message type of when a mutator supplied an explicit type, otherwise . - /// [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] - internal Type? ReplacementMessageType { get; set; } + internal Type? ReplacementMessageType; } \ No newline at end of file From 0c135cac9b86e7220a08fe8b06f5ceb95596036b Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Thu, 3 Sep 2026 17:38:34 +0200 Subject: [PATCH 5/8] Deprecate mutator context setters in favor of typed replacement APIs The Message and OutgoingMessage setters on the logical mutator contexts route by the runtime type, which the trimmer cannot analyze. The typed UpdateMessage/UpdateMessageInstance APIs added in this branch are the replacement, so the setters now warn on assignment using the Particular.Obsoletes authoring pair (error from 11, removed in 12). The setter-level attribute leaves the getter silent, verified on the current compiler. The typed methods assign the backing field directly to avoid self-triggered warnings, and deliberate legacy-path test coverage is pragma-suppressed until the setters are removed with the object-overload batch tracked in #7906. --- .../When_incoming_mutator_changes_message_type.cs | 2 ++ .../Mutators/When_outgoing_mutator_replaces_instance.cs | 2 ++ .../APIApprovals.ApproveNServiceBus.approved.txt | 5 +++++ .../MutateIncomingMessageBehaviorTests.cs | 2 ++ .../MutateOutgoingMessageBehaviorTests.cs | 2 ++ .../When_incoming_mutator_replaces_message_instance.cs | 2 ++ .../MutateInstanceMessage/MutateIncomingMessageContext.cs | 8 +++++++- .../MutateInstanceMessage/MutateOutgoingMessageContext.cs | 8 +++++++- 8 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/NServiceBus.AcceptanceTests/Core/Mutators/When_incoming_mutator_changes_message_type.cs b/src/NServiceBus.AcceptanceTests/Core/Mutators/When_incoming_mutator_changes_message_type.cs index 0fcec518f63..ba140f0f21d 100644 --- a/src/NServiceBus.AcceptanceTests/Core/Mutators/When_incoming_mutator_changes_message_type.cs +++ b/src/NServiceBus.AcceptanceTests/Core/Mutators/When_incoming_mutator_changes_message_type.cs @@ -45,7 +45,9 @@ public class MessageMutator : IMutateIncomingMessages public Task MutateIncoming(MutateIncomingMessageContext context) { var original = (OriginalMessage)context.Message; +#pragma warning disable CS0618 // Deliberate coverage of the legacy runtime-type-routing setter until its removal context.Message = new NewMessage { SomeId = original.SomeId }; +#pragma warning restore CS0618 return Task.CompletedTask; } } diff --git a/src/NServiceBus.AcceptanceTests/Core/Mutators/When_outgoing_mutator_replaces_instance.cs b/src/NServiceBus.AcceptanceTests/Core/Mutators/When_outgoing_mutator_replaces_instance.cs index 53c8c2cdcfb..5f91a6c299d 100644 --- a/src/NServiceBus.AcceptanceTests/Core/Mutators/When_outgoing_mutator_replaces_instance.cs +++ b/src/NServiceBus.AcceptanceTests/Core/Mutators/When_outgoing_mutator_replaces_instance.cs @@ -38,7 +38,9 @@ public Task MutateOutgoing(MutateOutgoingMessageContext context) { if (context.OutgoingMessage is V1Message) { +#pragma warning disable CS0618 // Deliberate coverage of the legacy runtime-type-routing setter until its removal context.OutgoingMessage = new V2Message(); +#pragma warning restore CS0618 } return Task.CompletedTask; } diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt index d822e792cc6..16096408eb4 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt @@ -1881,6 +1881,9 @@ namespace NServiceBus.MessageMutator public MutateIncomingMessageContext(object message, System.Collections.Generic.Dictionary headers, System.Threading.CancellationToken cancellationToken = default) { } public System.Threading.CancellationToken CancellationToken { get; } public System.Collections.Generic.Dictionary Headers { get; } + [set: System.Obsolete("Use \'UpdateMessageInstance(T)\' or \'UpdateMessageInstance(object, Type)\' instea" + + "d. Will be treated as an error from version 11.0.0. Will be removed in version 1" + + "2.0.0.", false)] public object Message { get; set; } public void UpdateMessageInstance(object newMessage, [System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] System.Type messageType) { } public void UpdateMessageInstance<[System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.None | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicParameterlessConstructor | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.NonPublicConstructors | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties | System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.Interfaces)] T>(T newMessage) { } @@ -1897,6 +1900,8 @@ namespace NServiceBus.MessageMutator public MutateOutgoingMessageContext(object outgoingMessage, System.Collections.Generic.Dictionary outgoingHeaders, object? incomingMessage, System.Collections.Generic.IReadOnlyDictionary? incomingHeaders, System.Threading.CancellationToken cancellationToken = default) { } public System.Threading.CancellationToken CancellationToken { get; } public System.Collections.Generic.Dictionary OutgoingHeaders { get; } + [set: System.Obsolete("Use \'UpdateMessage(T)\' or \'UpdateMessage(object, Type)\' instead. Will be treat" + + "ed as an error from version 11.0.0. Will be removed in version 12.0.0.", false)] public object OutgoingMessage { get; set; } public bool TryGetIncomingHeaders([System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out System.Collections.Generic.IReadOnlyDictionary? incomingHeaders) { } public bool TryGetIncomingMessage([System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out object? incomingMessage) { } diff --git a/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/MutateIncomingMessageBehaviorTests.cs b/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/MutateIncomingMessageBehaviorTests.cs index 7a85c80fbc4..f4ca7e73f90 100644 --- a/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/MutateIncomingMessageBehaviorTests.cs +++ b/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/MutateIncomingMessageBehaviorTests.cs @@ -216,7 +216,9 @@ class MutatorWhichMutatesTheBody : IMutateIncomingMessages { public Task MutateIncoming(MutateIncomingMessageContext context) { +#pragma warning disable CS0618 // Deliberate coverage of the legacy runtime-type-routing setter until its removal context.Message = new object(); +#pragma warning restore CS0618 return Task.CompletedTask; } diff --git a/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageBehaviorTests.cs b/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageBehaviorTests.cs index 98265830aad..1161d2f18b1 100644 --- a/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageBehaviorTests.cs +++ b/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageBehaviorTests.cs @@ -220,7 +220,9 @@ class MutatorWhichMutatesTheBody : IMutateOutgoingMessages { public Task MutateOutgoing(MutateOutgoingMessageContext context) { +#pragma warning disable CS0618 // Deliberate coverage of the legacy runtime-type-routing setter until its removal context.OutgoingMessage = new object(); +#pragma warning restore CS0618 return Task.CompletedTask; } diff --git a/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/When_incoming_mutator_replaces_message_instance.cs b/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/When_incoming_mutator_replaces_message_instance.cs index 8a7dc427fb8..47317c70202 100644 --- a/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/When_incoming_mutator_replaces_message_instance.cs +++ b/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/When_incoming_mutator_replaces_message_instance.cs @@ -70,7 +70,9 @@ class ReplaceWithReplacementMessageMutator : IMutateIncomingMessages { public Task MutateIncoming(MutateIncomingMessageContext context) { +#pragma warning disable CS0618 // Deliberate coverage of the legacy runtime-type-routing setter until its removal context.Message = new ReplacementMessage(); +#pragma warning restore CS0618 return Task.CompletedTask; } } diff --git a/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateIncomingMessageContext.cs b/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateIncomingMessageContext.cs index 7b936f841b6..165a0f887a7 100644 --- a/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateIncomingMessageContext.cs +++ b/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateIncomingMessageContext.cs @@ -6,6 +6,7 @@ namespace NServiceBus.MessageMutator; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Threading; +using Particular.Obsoletes; /// /// Provides ways to mutate the outgoing message instance. @@ -30,6 +31,10 @@ public MutateIncomingMessageContext(object message, Dictionary h public object Message { get => message; + [ObsoleteMetadata(ReplacementTypeOrMember = "UpdateMessageInstance(T)", + TreatAsErrorFromVersion = "11", + RemoveInVersion = "12")] + [Obsolete("Use 'UpdateMessageInstance(T)' or 'UpdateMessageInstance(object, Type)' instead. Will be treated as an error from version 11.0.0. Will be removed in version 12.0.0.", false)] set { ArgumentNullException.ThrowIfNull(value); @@ -55,7 +60,8 @@ public object Message public void UpdateMessageInstance(object newMessage, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType) { MessageTypeValidator.Validate(newMessage, messageType); - Message = newMessage; + message = newMessage; + MessageInstanceChanged = true; ReplacementMessageType = messageType; } diff --git a/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageContext.cs b/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageContext.cs index 131bc3c0912..ce9256835c6 100644 --- a/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageContext.cs +++ b/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageContext.cs @@ -6,6 +6,7 @@ namespace NServiceBus.MessageMutator; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Threading; +using Particular.Obsoletes; /// /// Provides ways to mutate the outgoing message instance. @@ -32,6 +33,10 @@ public MutateOutgoingMessageContext(object outgoingMessage, Dictionary outgoingMessage; + [ObsoleteMetadata(ReplacementTypeOrMember = "UpdateMessage(T)", + TreatAsErrorFromVersion = "11", + RemoveInVersion = "12")] + [Obsolete("Use 'UpdateMessage(T)' or 'UpdateMessage(object, Type)' instead. Will be treated as an error from version 11.0.0. Will be removed in version 12.0.0.", false)] set { ArgumentNullException.ThrowIfNull(value); @@ -60,7 +65,8 @@ public object OutgoingMessage public void UpdateMessage(object newMessage, [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] Type messageType) { MessageTypeValidator.Validate(newMessage, messageType); - OutgoingMessage = newMessage; + outgoingMessage = newMessage; + MessageInstanceChanged = true; ReplacementMessageType = messageType; } From 165451afd0901a8dd5e7e0f72b31c4f26c1e86a0 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Thu, 3 Sep 2026 17:45:38 +0200 Subject: [PATCH 6/8] Simplify interface method --- .../Pipeline/Outgoing/IOutgoingLogicalMessageContext.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/NServiceBus.Core/Pipeline/Outgoing/IOutgoingLogicalMessageContext.cs b/src/NServiceBus.Core/Pipeline/Outgoing/IOutgoingLogicalMessageContext.cs index 04edfafd28f..b39e249a71e 100644 --- a/src/NServiceBus.Core/Pipeline/Outgoing/IOutgoingLogicalMessageContext.cs +++ b/src/NServiceBus.Core/Pipeline/Outgoing/IOutgoingLogicalMessageContext.cs @@ -39,10 +39,7 @@ public interface IOutgoingLogicalMessageContext : IOutgoingContext /// The type used to update the message. It determines how the message is routed and the message type header recorded on the message, and can differ from the runtime type of the message instance as long as the instance is assignable to T. /// The replacement message instance. [OverloadResolutionPriority(-1)] - void UpdateMessage<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T newInstance) - { - UpdateMessage(newInstance!, typeof(T)); - } + void UpdateMessage<[DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] T>(T newInstance) => UpdateMessage(newInstance!, typeof(T)); /// /// Updates the message instance with the specified message type. The declared type controls how the message is routed and the message type header recorded on the message. From a31329dca4577f1dcae82cd5ac418450da926b5c Mon Sep 17 00:00:00 2001 From: David Boike Date: Thu, 3 Sep 2026 14:40:42 -0500 Subject: [PATCH 7/8] Suppress mutator warnings because code path only reachable if using other RequiresUnreferencedCode is used (#7928) --- ...IApprovals.ApproveNServiceBus.approved.txt | 6 +++++ ...s.ApproveTrimmabilityWarnings.approved.txt | 6 ----- .../MutateIncomingMessageBehavior.cs | 27 +++++++++++++------ .../MutateIncomingMessageContext.cs | 1 + .../MutateOutgoingMessageBehavior.cs | 27 +++++++++++++------ .../MutateOutgoingMessageContext.cs | 1 + 6 files changed, 46 insertions(+), 22 deletions(-) diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt index 16096408eb4..3b421c88eb9 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt @@ -1881,6 +1881,9 @@ namespace NServiceBus.MessageMutator public MutateIncomingMessageContext(object message, System.Collections.Generic.Dictionary headers, System.Threading.CancellationToken cancellationToken = default) { } public System.Threading.CancellationToken CancellationToken { get; } public System.Collections.Generic.Dictionary Headers { get; } + [set: System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("When trimming is enabled, routing a message using its runtime type cannot be stat" + + "ically analyzed by the trimmer. Use the generic overload or, when the message ty" + + "pe is not known at compile time, the overload accepting an explicit Type.")] [set: System.Obsolete("Use \'UpdateMessageInstance(T)\' or \'UpdateMessageInstance(object, Type)\' instea" + "d. Will be treated as an error from version 11.0.0. Will be removed in version 1" + "2.0.0.", false)] @@ -1900,6 +1903,9 @@ namespace NServiceBus.MessageMutator public MutateOutgoingMessageContext(object outgoingMessage, System.Collections.Generic.Dictionary outgoingHeaders, object? incomingMessage, System.Collections.Generic.IReadOnlyDictionary? incomingHeaders, System.Threading.CancellationToken cancellationToken = default) { } public System.Threading.CancellationToken CancellationToken { get; } public System.Collections.Generic.Dictionary OutgoingHeaders { get; } + [set: System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("When trimming is enabled, routing a message using its runtime type cannot be stat" + + "ically analyzed by the trimmer. Use the generic overload or, when the message ty" + + "pe is not known at compile time, the overload accepting an explicit Type.")] [set: System.Obsolete("Use \'UpdateMessage(T)\' or \'UpdateMessage(object, Type)\' instead. Will be treat" + "ed as an error from version 11.0.0. Will be removed in version 12.0.0.", false)] public object OutgoingMessage { get; set; } diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt index 59ad7798ebe..c835831f11f 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt @@ -2,12 +2,6 @@ The following trimming warnings are present in NServiceBus.Core. Changes that make this list longer should not be approved. ----- -src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateIncomingMessageBehavior.cs - IL2026: Using member 'NServiceBus.Pipeline.IIncomingLogicalMessageContext.UpdateMessageInstance(Object)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. When trimming is enabled, routing a message using its runtime type cannot be statically analyzed by the trimmer. Use the generic overload or, when the message type is not known at compile time, the overload accepting an explicit Type. - -src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageBehavior.cs - IL2026: Using member 'NServiceBus.Pipeline.IOutgoingLogicalMessageContext.UpdateMessage(Object)' which has 'RequiresUnreferencedCodeAttribute' can break functionality when trimming application code. When trimming is enabled, routing a message using its runtime type cannot be statically analyzed by the trimmer. Use the generic overload or, when the message type is not known at compile time, the overload accepting an explicit Type. - src/NServiceBus.Core/Routing/AssemblyRouteSource.cs IL2046: Member 'NServiceBus.AssemblyRouteSource.GenerateRoutes(Conventions)' with 'RequiresUnreferencedCodeAttribute' implements interface member 'NServiceBus.IRouteSource.GenerateRoutes(Conventions)' without 'RequiresUnreferencedCodeAttribute'. 'RequiresUnreferencedCodeAttribute' annotations must match across all interface implementations or overrides. diff --git a/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateIncomingMessageBehavior.cs b/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateIncomingMessageBehavior.cs index 9eb24e88fd2..ef9de36e969 100644 --- a/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateIncomingMessageBehavior.cs +++ b/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateIncomingMessageBehavior.cs @@ -4,6 +4,7 @@ namespace NServiceBus; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using MessageMutator; using Microsoft.Extensions.DependencyInjection; @@ -46,18 +47,28 @@ await mutator.MutateIncoming(mutatorContext) if (mutatorContext.MessageInstanceChanged) { - if (mutatorContext.ReplacementMessageType != null) - { - context.UpdateMessageInstance(mutatorContext.Message, mutatorContext.ReplacementMessageType); - } - else - { - context.UpdateMessageInstance(mutatorContext.Message); - } + UpdateMessageInstance(context, mutatorContext); } await next(context).ConfigureAwait(false); } + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", + Justification = "Path without compiler-known type can only be visited if MutateIncomingMessageContext.Message setter is used.")] +#pragma warning disable PS0015 // Multiple cancellable contexts are fine here + static void UpdateMessageInstance(IIncomingLogicalMessageContext context, MutateIncomingMessageContext mutatorContext) +#pragma warning restore PS0015 + { + if (mutatorContext.ReplacementMessageType != null) + { + context.UpdateMessageInstance(mutatorContext.Message, mutatorContext.ReplacementMessageType); + } + else + { + // Requires code path to use MutateIncomingMessageContext.Message which is marked as RequiresUnreferencedCode + context.UpdateMessageInstance(mutatorContext.Message); + } + } + volatile bool hasIncomingMessageMutators = true; } \ No newline at end of file diff --git a/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateIncomingMessageContext.cs b/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateIncomingMessageContext.cs index 165a0f887a7..3a69dfffb95 100644 --- a/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateIncomingMessageContext.cs +++ b/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateIncomingMessageContext.cs @@ -35,6 +35,7 @@ public object Message TreatAsErrorFromVersion = "11", RemoveInVersion = "12")] [Obsolete("Use 'UpdateMessageInstance(T)' or 'UpdateMessageInstance(object, Type)' instead. Will be treated as an error from version 11.0.0. Will be removed in version 12.0.0.", false)] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] set { ArgumentNullException.ThrowIfNull(value); diff --git a/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageBehavior.cs b/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageBehavior.cs index cf45ef52c18..df8494f653c 100644 --- a/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageBehavior.cs +++ b/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageBehavior.cs @@ -4,6 +4,7 @@ namespace NServiceBus; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using MessageMutator; using Microsoft.Extensions.DependencyInjection; @@ -52,18 +53,28 @@ await mutator.MutateOutgoing(mutatorContext) if (mutatorContext.MessageInstanceChanged) { - if (mutatorContext.ReplacementMessageType != null) - { - context.UpdateMessage(mutatorContext.OutgoingMessage, mutatorContext.ReplacementMessageType); - } - else - { - context.UpdateMessage(mutatorContext.OutgoingMessage); - } + UpdateMessage(context, mutatorContext); } await next(context).ConfigureAwait(false); } + [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", + Justification = "Path without compiler-known type can only be visited if MutateOutgoingMessageContext.Message setter is used.")] +#pragma warning disable PS0015 // Multiple cancellable contexts are fine here + static void UpdateMessage(IOutgoingLogicalMessageContext context, MutateOutgoingMessageContext mutatorContext) +#pragma warning restore PS0015 + { + if (mutatorContext.ReplacementMessageType != null) + { + context.UpdateMessage(mutatorContext.OutgoingMessage, mutatorContext.ReplacementMessageType); + } + else + { + // Requires code path to use MutateOutgoingMessageContext.Message which is marked as RequiresUnreferencedCode + context.UpdateMessage(mutatorContext.OutgoingMessage); + } + } + volatile bool hasOutgoingMessageMutators = true; } \ No newline at end of file diff --git a/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageContext.cs b/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageContext.cs index ce9256835c6..1804f38e185 100644 --- a/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageContext.cs +++ b/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageContext.cs @@ -37,6 +37,7 @@ public object OutgoingMessage TreatAsErrorFromVersion = "11", RemoveInVersion = "12")] [Obsolete("Use 'UpdateMessage(T)' or 'UpdateMessage(object, Type)' instead. Will be treated as an error from version 11.0.0. Will be removed in version 12.0.0.", false)] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] set { ArgumentNullException.ThrowIfNull(value); From 75a4fa1b629b6dfd3a991686343647ecadffa821 Mon Sep 17 00:00:00 2001 From: Daniel Marbach Date: Thu, 3 Sep 2026 21:42:42 +0200 Subject: [PATCH 8/8] Fix property name in outgoing mutator suppression justification --- ..._01a0662e-95f5-7570-adde-5ca565b482e5.html | 4332 +++++++++++++++++ .../MutateOutgoingMessageBehavior.cs | 4 +- 2 files changed, 4334 insertions(+), 2 deletions(-) create mode 100644 pi-session-2026-09-03T07-32-04-213Z_01a0662e-95f5-7570-adde-5ca565b482e5.html diff --git a/pi-session-2026-09-03T07-32-04-213Z_01a0662e-95f5-7570-adde-5ca565b482e5.html b/pi-session-2026-09-03T07-32-04-213Z_01a0662e-95f5-7570-adde-5ca565b482e5.html new file mode 100644 index 00000000000..3851f4dcb13 --- /dev/null +++ b/pi-session-2026-09-03T07-32-04-213Z_01a0662e-95f5-7570-adde-5ca565b482e5.html @@ -0,0 +1,4332 @@ + + + + + + Session Export + + + + + +
+ + +
+
+
+
+
+ +
+
+ + + + + + + + + + + + + diff --git a/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageBehavior.cs b/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageBehavior.cs index df8494f653c..7e6551878ea 100644 --- a/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageBehavior.cs +++ b/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateOutgoingMessageBehavior.cs @@ -60,7 +60,7 @@ await mutator.MutateOutgoing(mutatorContext) } [UnconditionalSuppressMessage("ReflectionAnalysis", "IL2026", - Justification = "Path without compiler-known type can only be visited if MutateOutgoingMessageContext.Message setter is used.")] + Justification = "Path without compiler-known type can only be visited if MutateOutgoingMessageContext.OutgoingMessage setter is used.")] #pragma warning disable PS0015 // Multiple cancellable contexts are fine here static void UpdateMessage(IOutgoingLogicalMessageContext context, MutateOutgoingMessageContext mutatorContext) #pragma warning restore PS0015 @@ -71,7 +71,7 @@ static void UpdateMessage(IOutgoingLogicalMessageContext context, MutateOutgoing } else { - // Requires code path to use MutateOutgoingMessageContext.Message which is marked as RequiresUnreferencedCode + // Requires code path to use MutateOutgoingMessageContext.OutgoingMessage which is marked as RequiresUnreferencedCode context.UpdateMessage(mutatorContext.OutgoingMessage); } }