Repository navigation
[Core] Preserve attributed handler constructors during trimming - #38630
Conversation
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>
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.sh | bash -s -- 38630Or
iex "& { $(irm https://raw.githubusercontent.com/dotnet/maui/main/eng/scripts/get-maui-pr.ps1) } 38630" |
|
Azure Pipelines: Successfully started running 1 pipeline(s). There may be pipelines that require an authorized user to comment /azp run to run. |
This comment has been minimized.
This comment has been minimized.
There was a problem hiding this comment.
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
THandlerwithPublicParameterlessConstructor. - Preserves direct
new THandler()activation during trimming.
| File | Description |
|---|---|
src/Core/src/Handlers/Element/ElementHandlerAttribute.cs |
Adds the constructor-preservation annotation. |
MauiBot
left a comment
There was a problem hiding this comment.
AI Review Summary
@simonrozsival — new AI review results are available based on commit
97dc8d2.
🗂️ 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. MauiHandlersFactorydiscoversElementHandlerAttributethrough reflection and callsCreateHandler(), which constructs the generic handler withnew THandler().- The PR annotates
THandlerwithDynamicallyAccessedMemberTypes.PublicParameterlessConstructorso 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 | 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 remainsnew 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
MauiHandlersFactoryare 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 thePublicConstructorsreturn 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 |
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) |
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) |
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 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() |
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) |
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()andCreateHandler() => 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.
<!-- 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>
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>withDynamicallyAccessedMemberTypes.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 constructedElementHandlerAttribute<CheckBoxHandler>then fails with aTypeLoadExceptionbecause the genericnew()constraint can no longer be satisfied.The DAM annotation is the narrow reflection contract for this activation path.
ElementHandlerAttribute<THandler>creates the handler throughnew 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
IsTrimmableproperty and contains no Maps, Foldable, Graphics, or test changes.Validation
net11.0-android37.0successfully.CheckBoxHandlerretained its constructor and the app cold-launched without theTypeLoadException. No new device run was performed for this focused replacement PR.