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.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.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..ffc172e5b85 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] @@ -1154,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 27e1fb13785..d9260e37460 100644 --- a/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationFixerTests.cs +++ b/src/NServiceBus.Core.Analyzer.Tests.Roslyn5/MessagingMigrationFixerTests.cs @@ -639,6 +639,158 @@ 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() + { + 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..e58f09dcfb3 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, @@ -130,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) @@ -224,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) || @@ -601,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); } @@ -608,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; } @@ -660,7 +757,10 @@ sealed class KnownTypes INamedTypeSymbol messageProcessingContextExtensions, INamedTypeSymbol saga, INamedTypeSymbol outgoingLogicalMessageContext, - INamedTypeSymbol messageCreator) + INamedTypeSymbol incomingLogicalMessageContext, + INamedTypeSymbol messageCreator, + INamedTypeSymbol mutateIncomingMessageContext, + INamedTypeSymbol mutateOutgoingMessageContext) { IMessageSession = messageSession; IPipelineContext = pipelineContext; @@ -670,13 +770,17 @@ sealed class KnownTypes MessageProcessingContextExtensions = messageProcessingContextExtensions; Saga = saga; IOutgoingLogicalMessageContext = outgoingLogicalMessageContext; + IIncomingLogicalMessageContext = incomingLogicalMessageContext; IMessageCreator = messageCreator; + MutateIncomingMessageContext = mutateIncomingMessageContext; + MutateOutgoingMessageContext = mutateOutgoingMessageContext; ContractInterfaces = [ messageSession, pipelineContext, messageProcessingContext, - outgoingLogicalMessageContext + outgoingLogicalMessageContext, + incomingLogicalMessageContext ]; } @@ -688,7 +792,10 @@ 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; } public ImmutableArray ContractInterfaces { get; } readonly ConcurrentDictionary implementsContractCache = new(SymbolEqualityComparer.Default); @@ -757,12 +864,17 @@ 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"); 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 || + incomingLogicalMessageContext is null || + mutateIncomingMessageContext is null || mutateOutgoingMessageContext is null) { knownTypes = null!; return false; @@ -777,7 +889,10 @@ messageProcessingContextExtensions is null || saga is null || messageProcessingContextExtensions, saga, outgoingLogicalMessageContext, - messageCreator); + incomingLogicalMessageContext, + messageCreator, + mutateIncomingMessageContext, + mutateOutgoingMessageContext); return true; } } diff --git a/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt index b292515cc56..3b421c88eb9 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/APIApprovals.ApproveNServiceBus.approved.txt @@ -1881,7 +1881,15 @@ 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)] 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 { @@ -1895,9 +1903,16 @@ 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; } 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 +2060,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..c835831f11f 100644 --- a/src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt +++ b/src/NServiceBus.Core.Tests/ApprovalFiles/TrimmabilityWarnings.ApproveTrimmabilityWarnings.approved.txt @@ -2,9 +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/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.Tests/MessageMutators/MutateInstanceMessage/MutateIncomingMessageBehaviorTests.cs b/src/NServiceBus.Core.Tests/MessageMutators/MutateInstanceMessage/MutateIncomingMessageBehaviorTests.cs index 0fa1ce103f1..f4ca7e73f90 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; } } @@ -162,7 +216,19 @@ 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; + } + } + + class MutatorWhichDeclaresAMessageType : IMutateIncomingMessages + { + public Task MutateIncoming(MutateIncomingMessageContext context) + { + context.UpdateMessageInstance(new MyMessage()); return Task.CompletedTask; } @@ -178,4 +244,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..1161d2f18b1 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; } } @@ -166,9 +220,27 @@ 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; } } + + 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..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 @@ -17,69 +17,62 @@ 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 { 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.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..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,11 +47,28 @@ await mutator.MutateIncoming(mutatorContext) if (mutatorContext.MessageInstanceChanged) { - 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 07f6b1ac709..3a69dfffb95 100644 --- a/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateIncomingMessageContext.cs +++ b/src/NServiceBus.Core/MessageMutators/MutateInstanceMessage/MutateIncomingMessageContext.cs @@ -4,7 +4,9 @@ namespace NServiceBus.MessageMutator; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Threading; +using Particular.Obsoletes; /// /// Provides ways to mutate the outgoing message instance. @@ -29,6 +31,11 @@ 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)] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] set { ArgumentNullException.ThrowIfNull(value); @@ -37,6 +44,28 @@ 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; + MessageInstanceChanged = true; + ReplacementMessageType = messageType; + } + /// /// The current incoming headers. /// @@ -50,4 +79,7 @@ public object Message object message; internal bool MessageInstanceChanged; + + [DynamicallyAccessedMembers(DynamicMemberTypeAccess.Message)] + internal Type? ReplacementMessageType; } \ 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..7e6551878ea 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,11 +53,28 @@ await mutator.MutateOutgoing(mutatorContext) if (mutatorContext.MessageInstanceChanged) { - 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.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 + { + if (mutatorContext.ReplacementMessageType != null) + { + context.UpdateMessage(mutatorContext.OutgoingMessage, mutatorContext.ReplacementMessageType); + } + else + { + // Requires code path to use MutateOutgoingMessageContext.OutgoingMessage 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 8eba75e3684..1804f38e185 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,11 @@ 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)] + [RequiresUnreferencedCode(MessageOperations.RuntimeTypeRoutingTrimmingMessage)] set { ArgumentNullException.ThrowIfNull(value); @@ -40,6 +46,31 @@ 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; + MessageInstanceChanged = true; + ReplacementMessageType = messageType; + } + /// /// The current outgoing headers. /// @@ -73,5 +104,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.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. 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