Skip to content

Logical mutators trimming and AOT improvements - #7925

Merged
danielmarbach merged 8 commits into
masterfrom
logical_mutators
Sep 4, 2026
Merged

Logical mutators trimming and AOT improvements#7925
danielmarbach merged 8 commits into
masterfrom
logical_mutators

Conversation

@danielmarbach

@danielmarbach danielmarbach commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

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 = ... or mutatorContext.OutgoingMessage = ...). When the replacement switches types, the logical message type comes from newInstance.GetType() downstream, which the trimmer and NativeAOT cannot analyze. The object-only UpdateMessageInstance(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/NSB0040 diagnostics 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 UpdateMessageInstance invocations.

Compatibility boundary

  • All new members are additive. Ordinary calls keep binding to the object members because the generic overloads carry [OverloadResolutionPriority(-1)]. Existing code behaves exactly as before.
  • IIncomingLogicalMessageContext.UpdateMessageInstance(object) remains available and is marked [PreObsolete] and [RequiresUnreferencedCode], mirroring IOutgoingLogicalMessageContext.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.
  • New interface members carry the DAM annotation DynamicMemberTypeAccess.Message on the generic type parameter and the explicit Type parameter, the same contract the other typed existing-message overloads use.
  • MessageTypeValidator rejects replacement instances that are not assignable to the declared type, both in the pipeline contexts and the mutator contexts.
  • Under strict registered-only metadata mode, the declared type must be registered up front through handlers, sagas, or AddMessageType<T>(). A strict cache miss fails with the existing actionable exception instead of silently falling back to hierarchy reflection.
  • The declared type flows from the mutator context to the pipeline context through an internal property that carries the same DAM annotation, so the trimmer preserves the member requirements across that handoff.

What changed

Typed replacement on the incoming pipeline context

IIncomingLogicalMessageContext gains two members: the generic UpdateMessageInstance<T>(T) and the explicit UpdateMessageInstance(object, Type). The concrete IncomingLogicalMessageContext resolves replacement metadata through LogicalMessageFactory.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. TestableIncomingLogicalMessageContext mirrors the typed members as virtual members, following the outgoing fake from #7889.

Declared-type replacement on the mutator contexts

MutateIncomingMessageContext.UpdateMessageInstance and MutateOutgoingMessageContext.UpdateMessage accept either a typed instance or an instance plus an explicit annotated Type. 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: NSB0039 rewrites safe assignments to the typed calls and NSB0040 flags 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

MessagingMigrationAnalyzer now 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) report NSB0039 and the fixer rewrites them to the typed calls; unprovable assignments report NSB0040 without a fix, since replacing the logical type is a routing decision. Assignments use the conservative UpdateMessage classification because the setter path preserves the previous logical type when the same instance is re-assigned. Object-typed assignments and NSB0041 do not apply here.

The object-only UpdateMessageInstance(object) invocation is now diagnosed like UpdateMessage(object) was, including calls against the testing fake. The existing invocation fixer covers it unchanged.

Follow-ups

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.

public object OutgoingMessage
{
get => outgoingMessage;
set

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we go ahead and obsolete the setter with a warning (see PR description)?

public object Message
{
get => message;
set

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we go ahead and obsolete the setter with a warning (see PR description)?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would vote yes. The methods are much more intention revealing anyway.

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

Copy link
Copy Markdown
Contributor Author

I think this is the last "edge"

public object Message
{
get => message;
set

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would vote yes. The methods are much more intention revealing anyway.

danielmarbach and others added 5 commits September 3, 2026 17:28
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.
@danielmarbach
danielmarbach merged commit 7bb5a57 into master Sep 4, 2026
4 checks passed
@danielmarbach
danielmarbach deleted the logical_mutators branch September 4, 2026 09:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants