Logical mutators trimming and AOT improvements - #7925
Merged
Conversation
danielmarbach
commented
Sep 3, 2026
| public object OutgoingMessage | ||
| { | ||
| get => outgoingMessage; | ||
| set |
Contributor
Author
There was a problem hiding this comment.
Should we go ahead and obsolete the setter with a warning (see PR description)?
danielmarbach
commented
Sep 3, 2026
| public object Message | ||
| { | ||
| get => message; | ||
| set |
Contributor
Author
There was a problem hiding this comment.
Should we go ahead and obsolete the setter with a warning (see PR description)?
Member
There was a problem hiding this comment.
I would vote yes. The methods are much more intention revealing anyway.
danielmarbach
force-pushed
the
logical_mutators
branch
from
September 3, 2026 09:41
b4a9c4b to
816183f
Compare
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>(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
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<MyMessage>(new MyMessage()) - context.OutgoingMessage = new MyEvent() -> context.UpdateMessage<MyEvent>(new MyEvent())
danielmarbach
force-pushed
the
logical_mutators
branch
from
September 3, 2026 09:44
816183f to
ccfc991
Compare
Contributor
Author
|
I think this is the last "edge" |
DavidBoike
reviewed
Sep 3, 2026
| public object Message | ||
| { | ||
| get => message; | ||
| set |
Member
There was a problem hiding this comment.
I would vote yes. The methods are much more intention revealing anyway.
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.
…ther RequiresUnreferencedCode is used (#7928)
DavidBoike
approved these changes
Sep 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Short version: logical mutators can now replace the incoming or outgoing message with a declared logical type instead of relying on the runtime type, and the migration analyzer from #7889 steers existing setter-based mutators toward those APIs.
Today a mutator that replaces the message instance assigns the plain context property (
mutatorContext.Message = ...ormutatorContext.OutgoingMessage = ...). When the replacement switches types, the logical message type comes fromnewInstance.GetType()downstream, which the trimmer and NativeAOT cannot analyze. The object-onlyUpdateMessageInstance(object)on the incoming pipeline context has the same problem.This PR adds typed replacement APIs on both mutator contexts and the incoming pipeline context, threads the declared type through the mutator behaviors, and extends the migration analyzer so the legacy pattern gets the same
NSB0039/NSB0040diagnostics the Send/Publish/Reply/UpdateMessage family already has. With #7918 merged, the declared type resolves through the registered metadata cache, so the new path is compatible with strict registered-only mode.This continues the migration family tracked in #7906 and completes the logical-mutator half of the proposal deferred from #7889. The three commits are separate review seams: the runtime and API change, the analyzer extension for property assignment, and analyzer symmetry for
UpdateMessageInstanceinvocations.Compatibility boundary
[OverloadResolutionPriority(-1)]. Existing code behaves exactly as before.IIncomingLogicalMessageContext.UpdateMessageInstance(object)remains available and is marked[PreObsolete]and[RequiresUnreferencedCode], mirroringIOutgoingLogicalMessageContext.UpdateMessage(object)from Add trimming-safe messaging overloads and migration analyzer #7889. Default interface method fallbacks delegate to the object member for binary compatibility; third-party implementations keep working unchanged.DynamicMemberTypeAccess.Messageon the generic type parameter and the explicitTypeparameter, the same contract the other typed existing-message overloads use.MessageTypeValidatorrejects replacement instances that are not assignable to the declared type, both in the pipeline contexts and the mutator contexts.AddMessageType<T>(). A strict cache miss fails with the existing actionable exception instead of silently falling back to hierarchy reflection.What changed
Typed replacement on the incoming pipeline context
IIncomingLogicalMessageContextgains two members: the genericUpdateMessageInstance<T>(T)and the explicitUpdateMessageInstance(object, Type). The concreteIncomingLogicalMessageContextresolves replacement metadata throughLogicalMessageFactory.Create(messageType, instance)without reflecting over the instance, and keeps the existing object overload untouched.The replacement also carries the conservative same-instance semantics from
UpdateMessage: re-supplying the same instance with the same logical type is a metadata no-op, while a different declared type rebuilds metadata even when the instance has not changed.TestableIncomingLogicalMessageContextmirrors the typed members as virtual members, following the outgoing fake from #7889.Declared-type replacement on the mutator contexts
MutateIncomingMessageContext.UpdateMessageInstanceandMutateOutgoingMessageContext.UpdateMessageaccept either a typed instance or an instance plus an explicit annotatedType. The declared type is stored on an internal DAM-annotated carrier property. When a mutator supplied a declared type, the behaviors call the typed pipeline overloads; otherwise they fall back to the legacy object path. The required IL2026 for that fallback is recorded in the trimmability-approval file, as the outgoing behavior already did.The plain property setters are deprecated with warning as part of this PR: the set accessors carry the Particular obsoletion pair (
[ObsoleteMetadata]+[Obsolete], treated as an error from version 11, removed in version 12). Accessor-level obsoletion warns only on assignments and leaves the getter untouched, which matters because in-place mutation of the current message remains supported. The analyzer diagnostics and fixers still apply on top:NSB0039rewrites safe assignments to the typed calls andNSB0040flags the risky ones without a fix, so users get risk-graded guidance plus the compile-time warning signal. Setter removal joins the object-overload removal batch in the next major; the scope is now recorded in #7906.Migration analyzer coverage
MessagingMigrationAnalyzernow recognizes assignment to the mutator context setters through simple-assignment analysis, matched by symbol rather than by property name. Safe assignments (direct creation, sealed types, non-nullable value types) reportNSB0039and the fixer rewrites them to the typed calls; unprovable assignments reportNSB0040without a fix, since replacing the logical type is a routing decision. Assignments use the conservativeUpdateMessageclassification because the setter path preserves the previous logical type when the same instance is re-assigned. Object-typed assignments andNSB0041do not apply here.The object-only
UpdateMessageInstance(object)invocation is now diagnosed likeUpdateMessage(object)was, including calls against the testing fake. The existing invocation fixer covers it unchanged.Follow-ups
UpdateMessageInstanceand the mutator context setters with the rest of the family in the next major, as described in Remove legacy object-only messaging overloads in NServiceBus 11 #7906.Please challenge the compatibility assumptions, especially the chosen escalation for the newly deprecated setters (warn in 10.3, error from 11, removed in 12) and the interaction with the analyzer diagnostics on the same assignment.