Skip to content

[Core] Preserve attributed handler constructors during trimming - #38630

Merged
kubaflo merged 1 commit into
net11.0from
simonrozsival-preserve-attributed-handlers
Sep 18, 2026
Merged

kubaflo merged 1 commit into
net11.0from
simonrozsival-preserve-attributed-handlers

Conversation

@simonrozsival

Copy link
Copy Markdown
Member

Note

Are you waiting for the changes in this PR to be merged?
It would be very helpful if you could test the resulting artifacts from this PR and let us know in a comment if this change resolves your issue. Thank you!

Summary

Annotate the generic handler type parameter on ElementHandlerAttribute<THandler> with DynamicallyAccessedMemberTypes.PublicParameterlessConstructor.

During combined trimming, linker-injected handler attributes can remain while the public parameterless constructor of the referenced handler is removed. This was observed at startup with CheckBoxHandler: loading the constructed ElementHandlerAttribute<CheckBoxHandler> then fails with a TypeLoadException because the generic new() constraint can no longer be satisfied.

The DAM annotation is the narrow reflection contract for this activation path. ElementHandlerAttribute<THandler> creates the handler through new THandler() and exposes its type for reflective attribute discovery, so the referenced handler's public parameterless constructor must be retained.

This fix is independent of whether or where individual assemblies opt into partial trimming. It does not change any IsTrimmable property and contains no Maps, Foldable, Graphics, or test changes.

Validation

  • Built the owning Core project for net11.0-android37.0 successfully.
  • Compiled the owning Android target with warnings treated as errors: 0 warnings, 0 errors.
  • The prior investigation validated the combined-trimming scenario on a physical device: the linked CheckBoxHandler retained its constructor and the app cold-launched without the TypeLoadException. No new device run was performed for this focused replacement PR.

Annotate ElementHandlerAttribute's generic handler parameter so trimming preserves the public parameterless constructor required by handler activation.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings September 17, 2026 11:49
@simonrozsival
simonrozsival deployed to copilot-pat-pool September 17, 2026 11:49 — with GitHub Actions Active
@github-actions

Copy link
Copy Markdown
Contributor

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 38630

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 38630"

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).
There may be pipelines that require an authorized user to comment /azp run to run.

@kubaflo

This comment has been minimized.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot review overview

🔵 Needs a closer look

Fresh device validation was not supplied, so final human review is warranted.

Review effort: Lite
Findings: None

What changed in this PR

Adds a trimming annotation to preserve public parameterless constructors for attributed handlers.

Changes:

  • Annotates THandler with PublicParameterlessConstructor.
  • Preserves direct new THandler() activation during trimming.
File Description
src/​Core/​src/​Handlers/​Element/​ElementHandlerAttribute.cs Adds the constructor-preservation annotation.

@github-actions github-actions Bot added the s/agent-review-in-progress AI review is currently running for this PR label Sep 17, 2026

@MauiBot MauiBot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

AI Review Summary

@simonrozsival — new AI review results are available based on commit 97dc8d2.

Gate No Tests Confidence Low Platform All Platforms


🗂️ Review Sessions — click to expand
🚦 Gate — Test Before & After Fix

Gate Result: ⚠️ SKIPPED

No tests were detected in this PR.

Recommendation: Add tests to verify the fix using the write-tests-agent.


📋 Pre-Flight — Context & Validation

Issue: No linked issue was available in the materialized PR metadata
PR: #38630 - [Core] Preserve attributed handler constructors
Platforms Affected: All platforms; requested validation platform is Android
Files Changed: 1 implementation, 0 test

Key Findings

  • The PR changes only src/Core/src/Handlers/Element/ElementHandlerAttribute.cs.
  • MauiHandlersFactory discovers ElementHandlerAttribute through reflection and calls CreateHandler(), which constructs the generic handler with new THandler().
  • The PR annotates THandler with DynamicallyAccessedMemberTypes.PublicParameterlessConstructor so trimming preserves the constructor required by generic handler activation.
  • Gate result: SKIPPED because no tests were detected in the PR. Gate verification was not rerun.
  • GitHub discussion metadata was unavailable without authentication; the public patch contained one commit and no linked issue or discussion context.

Code Review Summary

Verdict: SKIPPED
Confidence: N/A
Errors: 0 | Warnings: 0 | Suggestions: 0

Key code review findings:

  • Code review was intentionally deferred to STEP 5b as required by the execution contract.

Fix Candidates

# Source Approach Test Result Files Changed Notes
PR PR #38630 Annotate the generic handler parameter so trimming preserves its public parameterless constructor ⚠️ SKIPPED (Gate found no tests) src/Core/src/Handlers/Element/ElementHandlerAttribute.cs Original PR

🔬 Code Review — Deep Analysis

Code Review — PR #38630

Independent Assessment

What this changes: Adds a DynamicallyAccessedMembers(PublicParameterlessConstructor) contract to the THandler generic parameter of the internal ElementHandlerAttribute<THandler>.

Inferred motivation: ElementHandlerAttribute<THandler>.CreateHandler() constructs the handler with new THandler(). A constructed attribute can survive combined trimming while the handler constructor is otherwise removed, leaving an invalid generic instantiation or an unusable activation path. The annotation tells the trimmer to preserve exactly the public parameterless constructor required by the existing new() constraint.

Approach assessment: The generic-parameter contract is the narrowest direct expression of the existing activation requirement. It avoids replacing statically typed construction with reflection and avoids rooting members beyond the constructor actually required.

Reconciliation with PR Narrative

Author claims: Combined trimming can retain linker-injected handler attributes while removing the referenced handler's public parameterless constructor, causing a TypeLoadException; annotating THandler retains that constructor.

Agreement/disagreement: The code and consumer trace agree with the stated root cause and fix. The implementation does not alter IsTrimmable, handler registration, mapper behavior, or platform-specific lifecycle code. The physical-device result described by the author is consistent with the dataflow contract, but no new regression test is included in the submitted diff.

Prior Review Reconciliation

No prior review findings were available in the materialized context. GitHub review, inline-comment, and issue-comment surfaces could not be retrieved because gh is unauthenticated and anonymous API access is rate-limited; no prior ❌ Error finding can therefore be independently reconciled.

Blast Radius Assessment

  • Runs for all instances: The attribute's generic metadata contract applies to every constructed ElementHandlerAttribute<THandler>, but it changes only trimming reachability; normal runtime handler creation remains new THandler().
  • Startup impact: The affected fallback is reachable during handler resolution and app startup, but the change only retains a constructor and adds no executable startup work.
  • Static/shared state: None added or changed.
  • Null/default behavior: Not applicable; no values, state, or platform views are introduced.

CI Status

  • Gate result: ⚠️ SKIPPED — no tests were detected in the PR.
  • Required-check result: Undetermined because GitHub CLI authentication and anonymous API access are unavailable.
  • Classification: Coverage gap, not a demonstrated implementation failure.
  • Action taken: Confidence capped at low; no Gate rerun was performed.

Findings

No validated inline code findings. inline-findings.json contains [].

Failure-Mode Probing

  • Handler type has no usable public parameterless constructor after trimming: the new generic DAM contract roots that constructor, matching the existing new() constraint.
  • Handler is resolved from dependency injection rather than an injected attribute: the DI-first paths in MauiHandlersFactory are unchanged; the annotation has no runtime effect on those instances.
  • Attribute is discovered only through HandlerType: typeof(THandler) remains unchanged, and the existing localized IL2073 suppression around the PublicConstructors return contract remains outside this PR's scope.
  • Handler disconnect/reconnect, null Parent/Handler/PlatformView, and repeated resolution: no lifecycle state, subscriptions, guards, or latches are changed.

External Output Contract

Not applicable.

Trim/AOT Evidence Chain

Link Source evidence Reachability implication
Generic activation contract where THandler : IElementHandler, new() Runtime activation requires an accessible public parameterless constructor.
Activation site ElementHandlerAttribute<THandler>.CreateHandler() => new THandler() The constructor is directly required whenever the reflected attribute fallback creates a handler.
Added trim contract [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] THandler Constructed generic arguments must retain the exact constructor required by activation.
Reflected attribute consumer MauiHandlersFactory.TryGetElementHandlerAttribute() and GetHandler(Type) A retained attribute can reach CreateHandler() even when normal DI registration is absent.
Type-only consumer HandlerType => typeof(THandler) and MauiHandlersFactory.GetHandlerType() Type lookup remains intact; the existing localized suppression governs its broader PublicConstructors return annotation.

Verdict: NEEDS_DISCUSSION

Confidence: low

Summary: The submitted code is technically sound and no actionable reviewer refinement was identified. The verdict is capped at NEEDS_DISCUSSION, rather than LGTM, because the trusted Gate was skipped and current required-check/prior-review evidence is unavailable; this is an evidence gap, not a concrete defect in the fix.


🛠️ Try-Fix — Analysis & Comparison

Fix Candidates

# Source Approach Test Result Files Changed Notes
1 gpt-5.3-codex / try-fix Put DynamicDependency(PublicParameterlessConstructor, typeof(THandler)) on CreateHandler() so the activation site roots the constructor ⚠️ BLOCKED None Baseline script rejected the pre-existing dirty worktree before edits; validation was not run
2 gpt-5.6-sol / try-fix Flow an annotated Type from typeof(THandler) into Activator.CreateInstance(Type) ⚠️ BLOCKED None Baseline script rejected the same pre-existing dirty worktree before edits; validation was not run
PR PR #38630 Annotate generic parameter THandler with DynamicallyAccessedMembers(PublicParameterlessConstructor) ⚠️ Gate skipped 1 file Original PR; no tests detected

Candidate 1 Details

Approach: Root the public parameterless constructor at CreateHandler() with DynamicDependency rather than expressing the requirement on the generic type parameter.

Diff: No candidate diff was applied.

Test Results: Not run. pwsh .github/scripts/EstablishBrokenBaseline.ps1 failed because the worktree contains pre-existing unrelated changes and did not create .github/.baseline-state.json.

Failure Analysis: The try-fix restoration contract prohibits editing tracked files without a baseline state. The candidate was therefore recorded as Blocked, the exact -Restore command was run, and the target file remained unchanged.

Inline Self-Review: 0 findings.

Candidate 2 Details

Approach: Return typeof(THandler) through a helper annotated for PublicParameterlessConstructor, then create the handler with Activator.CreateInstance(Type), avoiding both the PR's generic-parameter annotation and candidate 1's DynamicDependency.

Diff: No candidate diff was applied.

Test Results: Not run. pwsh .github/scripts/EstablishBrokenBaseline.ps1 again failed because the worktree contains pre-existing unrelated changes and did not create .github/.baseline-state.json.

Failure Analysis: The safe restoration boundary was unavailable, so editing and validation were prohibited. The exact -Restore command ran, and the target file was verified unchanged. No empirical conclusion can be drawn about this mechanism.

Inline Self-Review: 0 findings.

Cross-Pollination

Skipped by execution contract.

Exhausted: Yes — the bounded two candidates were attempted; both were blocked before implementation.
Selected Fix: No alternative selected — neither candidate produced a testable diff. PR #38630 remains the only implemented fix, with Gate skipped because it adds no tests.


🏁 Report — Final Recommendation

⚠️ Final Recommendation: REQUEST CHANGES

Winner: pr

The submitted PR is the single winning candidate. It is the only implemented alternative that directly expresses the trimming contract at the generic type boundary used by both new THandler() and typeof(THandler). The expert review found no concrete defect, inline-findings.json is empty, and the mechanical regression cross-reference is CLEAN.

Approval is not recommended yet because the trusted Gate was SKIPPED: the PR adds no regression test, and current required-check evidence is unavailable. This is an evidence gap rather than a request to replace the implementation. A focused trimming regression test that demonstrates constructor retention would address the remaining review concern.

Comparative Ranking

Rank Candidate Implementation Validation / regression evidence Assessment
1 pr Adds DynamicallyAccessedMembers(PublicParameterlessConstructor) to ElementHandlerAttribute<THandler>.THandler Gate ⚠️ SKIPPED; author reports prior Android build and physical-device combined-trimming validation; regression cross-reference CLEAN Winner. Narrow, declarative, statically typed, and aligned with the existing new() constraint.
2 pr-plus-reviewer Identical to pr; expert review produced no actionable patch No additional command run because no candidate code changed; inherits the PR's skipped Gate Correct but provides no improvement over pr, so the raw submitted candidate wins the tie.
3 try-fix-1 Proposed DynamicDependency(PublicParameterlessConstructor, typeof(THandler)) on CreateHandler() ⚠️ BLOCKED before editing; no diff and no validation Lower-ranked because it has no executable candidate or regression evidence and is less direct than declaring the requirement on THandler.
4 try-fix-2 Proposed flowing an annotated Type into Activator.CreateInstance(Type) ⚠️ BLOCKED before editing; no diff and no validation Lowest-ranked because it has no executable evidence and would replace direct generic construction with reflection without a demonstrated benefit.

No candidate passed or failed a regression test: the PR Gate was skipped, and both try-fix attempts were blocked before implementation. Consistent with the ranking rule, blocked and unvalidated proposals rank below the implemented PR; no failed candidate is ranked above a passing candidate.

Expert Review Reconciliation

  • Correctness: The annotation preserves precisely the public parameterless constructor required by where THandler : IElementHandler, new() and CreateHandler() => new THandler().
  • Trim/AOT behavior: The contract is attached to the generic parameter carried by the constructed handler attribute, which is the relevant dataflow boundary when the attribute survives trimming.
  • Runtime behavior: No activation strategy, handler registration, startup ordering, shared state, or platform lifecycle behavior changes.
  • Findings: 0 errors, 0 warnings, 0 suggestions; required inline artifact is [].
  • Uncertainty: Gate and required-check evidence do not establish a current regression-safe build, so the final recommendation cannot be APPROVE.

Required Change

Add focused automated coverage for the combined-trimming scenario, or otherwise provide trusted Gate evidence that exercises constructor retention and handler activation. The submitted implementation itself should be retained.


📱 UI Tests — ViewBaseTests

Detected UI test categories: ViewBaseTests

✅ Deep UI tests — 118 passed, 0 failed, 1 skipped across 1 category on platform-pool agent (replaces in-process counts above).

🧪 UI Test Execution Results (deep, platform pool)

Category Tests Snapshot diffs
ViewBaseTests 118/119 (1 skipped) ✓ —
📎 Download drop-deep-uitests artifact (TRX + snapshot diffs)

🧭 Next Steps — review latest findings

No alternative fix was selected for this run. Review the session findings and CI results before merging.

@MauiBot MauiBot added s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review) and removed s/agent-review-in-progress AI review is currently running for this PR labels Sep 17, 2026
@kubaflo
kubaflo merged commit fe30163 into net11.0 Sep 18, 2026
41 of 45 checks passed
@kubaflo
kubaflo deleted the simonrozsival-preserve-attributed-handlers branch September 18, 2026 11:25
@github-actions github-actions Bot added this to the .NET 11.0-preview7 milestone Sep 18, 2026
kubaflo pushed a commit that referenced this pull request Sep 22, 2026
<!-- Please let the below note in for people that find this PR -->
> [!NOTE]
> Are you waiting for the changes in this PR to be merged?
> It would be very helpful if you could [test the resulting
artifacts](https://github.com/dotnet/maui/wiki/Testing-PR-Builds) from
this PR and let us know in a comment if this change resolves your issue.
Thank you!

### Dependency

This PR is stacked on #38630, which separately preserves handler
constructors referenced through `ElementHandlerAttribute<THandler>` when
trimming is enabled.

### Root Cause

`Microsoft.Maui.RuntimeFeature.EnableMauiAspire` uses the runtime's
`[FeatureSwitchDefinition]` pattern, but normal optimized Android builds
previously left `_EnableMauiAspire` unset. As a result, the linker did
not substitute the disabled feature and the guarded
environment-variable/dev-tunnel implementation—including the
source-generated `LocalhostRegex`—survived publishing.

### Description of Change

- Resolve `_EnableMauiAspire` and add its
`RuntimeHostConfigurationOption` globally from the Core package during
MSBuild evaluation, so both Controls and `UseMauiCore` consumers receive
the switch without target-order dependencies.
- Keep Aspire enabled for non-optimized builds and substitute it away
from optimized builds.
- Preserve validation of explicitly configured `_EnableMauiAspire`
values.
- Leave Aspire-enabled builds unchanged.
- Add focused MSBuild coverage for default disabled/enabled behavior,
explicit optimized-build warnings, and the runtime option's
`Value`/`Trim` metadata.
- Add URL rewrite coverage for dev-tunnel behavior.
- Update the feature-switch documentation for regular optimized builds
and the validation opt-out.

### Release Targeting

This stack targets `net11.0`. If it cannot be accepted after the .NET 11
RC2 snap, it may need to move to .NET 12.

### Android Release Measurements

A fresh RC2 full-package build and stock .NET 11 Android arm64 Release
partial-trim comparison of baseline `2eed04f873` against the complete
stack (`00267ae424` + `3bde0d5ec3`) plus the XAML/Shadow changes
produced:

| Output | Baseline | Patched stack | Delta |
|---|---:|---:|---:|
| Signed APK | 21,428,303 bytes | 21,354,575 bytes | -73,728 bytes |
| linked `Microsoft.Maui.dll` | 828,416 bytes | 820,224 bytes | -8,192
bytes |
| linked `Microsoft.Maui.Controls.dll` | 1,533,952 bytes | 1,513,472
bytes | -20,480 bytes |
| linked `Microsoft.Maui.Controls.Xaml.dll` | 136,704 bytes | 27,648
bytes | -109,056 bytes |

In the non-Aspire output, `ConfigureEnvironmentVariables` is reduced to
a return-only stub and `LocalhostRegex` is absent. An identical
Aspire-enabled publish retains the implementation and regex.

`System.Text.RegularExpressions.dll` remains because the sample still
reaches regex through `Microsoft.Maui.Controls.ShadowTypeConverter`;
`System.Private.Xml.dll` also contains a generated `LanguageRegex`. This
PR removes the MAUI Aspire regex root but does not eliminate the
framework assembly entirely.

### Validation

- `DOTNET_ROLL_FORWARD=Major dotnet test
src/Core/tests/UnitTests/Core.UnitTests.csproj --no-restore --filter
FullyQualifiedName~HostBuilderAppConfigurationTests --logger
'console;verbosity=minimal' -m:1 -p:PublicApiType=Validate` — 7 passed
on `net11.0`.
- `DOTNET_ROLL_FORWARD=Major dotnet test
src/Controls/tests/Xaml.UnitTests/Controls.Xaml.UnitTests.csproj
--no-restore --filter
'FullyQualifiedName~MauiAspireFeatureSwitchMatchesOptimization|FullyQualifiedName~ExplicitMauiAspireFeatureSwitchWarnsInOptimizedBuild'
--logger 'console;verbosity=minimal' -m:1 -p:PublicApiType=Validate` — 5
passed on `net11.0`.
- Fresh RC2 package builds and arm64 partial publishes succeeded for
baseline and the final stack.
- The complete two-PR stack passed the combined preview7 XAML + Shadow +
Aspire contract validation (8/8).
- Linked `CheckBoxHandler` retains its public parameterless constructor,
platform creation, connect, and disconnect methods.
- A clean physical-device cold launch remained alive with PID 7613 and
no `TypeLoadException`.

Reusable validation artifacts are at
`/Users/simon/.copilot/session-state/6e13df7f-1302-47fa-b0fa-6d104a1dc0a8/files/final-stack-validation`.

### Issues Fixed

N/A

---------

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

This branch was successfully deployed

1 active deployment
copilot-pat-pool — 97dc8d28 Deployed Sep 17, 2026 by simonrozsival via conclusion #1947
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

s/agent-changes-requested AI agent recommends changes - found a better alternative or issues s/agent-fix-pr-picked AI could not beat the PR fix - PR is the best among all candidates s/agent-reviewed PR was reviewed by AI agent workflow (full 4-phase review)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants