dotnet / dotnet/msbuild

Reduce Native AOT / trim blockers in ClickOnce tasks

Open
#14,025 1 comment 0 reactions 0 assignees View on GitHub
Area: ClickOnce backlog triaged
Dominant language
C#
Stars
5.5k
Forks
1.5k
Avg merge
1d 8h
Merged PRs (30d)
141

Description

## Summary

Enabling the trim / Native AOT analyzers (`IsAotCompatible`) on `Microsoft.Build.Tasks` (in #14024) surfaced that the entire ClickOnce subsystem (`src/Tasks/ManifestUtil`, ~35 files / ~10.4k LOC, plus the bootstrapper builder) is incompatible with trimming and Native AOT. That PR annotates the surface honestly with `[RequiresUnreferencedCode]` / `[RequiresDynamicCode]` (and a few `[UnconditionalSuppressMessage]` at structural walls) so the analyzers pass, but the underlying blockers remain.

This issue tracks the root causes and what it would take to actually make ClickOnce manifest generation, signing, and bootstrapper tasks trim/AOT-safe. It is **not** blocking the analyzer rollout — the annotations from #14024 already let `Microsoft.Build.Tasks` build clean.

## Background and Motivation

The incremental AOT-annotation rollout up the MSBuild dependency graph (Framework + StringTools in #14012, Utilities + Tasks in #14024) requires every assembly with `IsAotCompatible` enabled to reach zero IL warnings. For Tasks, the ClickOnce subsystem was the largest source of warnings, and the only honest way to clear them today is to mark the code as trim/AOT-incompatible and let that requirement propagate to the consuming tasks.

These tasks transitively carry the trim/AOT requirement after #14024:

| Task | Blocker(s) |
| --- | --- |
| `GenerateApplicationManifest` | XmlSerializer, XslCompiledTransform |
| `GenerateDeploymentManifest` | XmlSerializer, XslCompiledTransform |
| `GenerateTrustInfo` | XslCompiledTransform |
| `SignFile` | SignedXml, XmlSerializer, XslCompiledTransform |
| `GenerateBootstrapper` | XslCompiledTransform |
| `ResolveNativeReference` | XmlSerializer, XslCompiledTransform |
| `ResolveManifestFiles` | indirect, via manifest read |

All of these are Windows-only (ClickOnce publishing is a Windows feature) and are most commonly invoked by full-framework MSBuild under Visual Studio, where trim/AOT does not apply. That context matters for prioritization.

There are three distinct, independent blockers. Each is a fundamental reflection- or codegen-based BCL facility, not an MSBuild coding slip.

### 1. `XmlSerializer` — reflection-based XML (de)serialization

The manifest object model (`ApplicationManifest`, `DeployManifest`, `AssemblyManifest`, `AssemblyReference`, `FileReference`, `FileAssociation`, `CompatibleFramework`, `AssemblyIdentity`, `BaseReference`, `Manifest`; 10 types carry `[XmlRoot]` / `[XmlElement]` / `[XmlAttribute]`) is (de)serialized with `System.Xml.Serialization.XmlSerializer`:

- `ManifestReader.Deserialize(Stream)` resolves the root type dynamically via `Type.GetType(tn)` (from the XML root element name) then `new XmlSerializer(t).Deserialize(...)`.
- `ManifestWriter.Serialize(Manifest)` calls `new XmlSerializer(manifest.GetType()).Serialize(...)`.

`XmlSerializer` emits a serialization assembly via `Reflection.Emit` at runtime (IL3050) and reflects over the serialized type's members, which the trimmer cannot statically preserve (IL2026).

### 2. `XslCompiledTransform` — runtime IL generation

Manifest reading/writing pipes XML through XSLT stylesheets shipped as embedded resources. `XmlUtil.XslTransform(resource, ...)` constructs an `XslCompiledTransform` and loads a stylesheet:

| Stylesheet | Lines | Used by |
| --- | --- | --- |
| `read2.xsl` | 529 | `ManifestReader.ReadManifest` |
| `write2.xsl` | 386 | `ManifestWriter.WriteManifest` (TFV <= v4.0) |
| `write3.xsl` | 386 | `ManifestWriter.WriteManifest` (TFV > v4.0) |
| `merge.xsl` | 114 | `ManifestWriter` (input-stream merge) |
| `trustinfo2.xsl` | 24 | `TrustInfo.Write` |

`BootstrapperBuilder` (`GenerateBootstrapper`) uses a separate `XslCompiledTransform` for its config transform. `XslCompiledTransform` compiles XSLT to IL through `Reflection.Emit`, so it is inherently `RequiresDynamicCode` (IL3050). It has no trim warning — it is an AOT-only blocker.

### 3. `SignedXml` + `CryptoConfig.AddAlgorithm` — name/type-based crypto resolution

`ManifestSignedXml2` (derives from `System.Security.Cryptography.Xml.SignedXml`) in `mansign2.cs` registers algorithm implementations by type and resolves them by URI at signing/verification time:

```csharp
CryptoConfig.AddAlgorithm(typeof(RSAPKCS1SHA256SignatureDescription), Sha256SignatureMethodUri);
CryptoConfig.AddAlgorithm(typeof(SHA256Managed) /* or SHA256Cng */, Sha256DigestMethod);
```

`SignedXml.ComputeSignature` / `CheckSignature` resolve transforms and digest algorithms via `CryptoConfig.CreateFromName` (reflection — IL2026) and, for `XmlDsigXsltTransform`, fall back to `XslCompiledTransform` (IL3050). The trim/AOT attributes on `SignedXml` live in the BCL (`System.Security.Cryptography.Xml`); MSBuild cannot annotate its way around them, only preserve the concrete algorithm types.

## Proposed Feature

Suggested phasing — the recommended end state for most users is the status quo, with targeted work only if trim (not full AOT) compatibility is requested:

1. **Keep the honest annotations (done in #14024).** They are correct, surface the constraint at PR time, and cost nothing. For most users this is the right permanent state: ClickOnce publish/sign runs under full-framework MSBuild on Windows, where trim/AOT never applies.

2. **If/when trim (not AOT) compatibility is requested:** pursue option 1A (XmlSerializer pre-generation) + option 3A (`DynamicDependency` for crypto algorithms). Together these clear the IL2026 (trim) warnings on the manifest and signing paths without touching the XSLT. Native AOT would remain blocked (documented).

3. **Native AOT for ClickOnce** would additionally require eliminating `XslCompiledTransform` (option 2A/2B) everywhere, including inside `SignedXml`'s `XmlDsigXsltTransform`. This is very high effort, high risk, and — given the Windows-only, VS-driven usage — likely not worth it unless there is concrete demand. Recommend a small feasibility spike on 2B before any commitment, and otherwise document ClickOnce as AOT-incompatible.

### Acceptance criteria (if pursued)

- [ ] Decide the target: trim-only vs full AOT for ClickOnce tasks.
- [ ] (trim) `Microsoft.XmlSerializer.Generator` wired in; `Type.GetType` root resolution replaced with a static switch; manifest read/write paths drop IL2026.
- [ ] (trim) `[DynamicDependency]` added for the SignedXml algorithm types; signing path drops IL2026.
- [ ] (AOT, optional) XSLT transforms eliminated or pre-compiled; manifest and bootstrapper paths drop IL3050.
- [ ] Existing ClickOnce tests still pass (`SecurityUtil_Tests`, `ManifestTaskEnvironmentTests`, `AssemblyFoldersFromConfig_Tests`, the `GenerateApplicationManifest` / `GenerateDeploymentManifest` suites on net472), with round-trip parity on real `.manifest` / `.application` files.
- [ ] Any remaining, genuinely-unavoidable suppressions are documented with a justification.

## Alternative Designs

### Root cause 1: `XmlSerializer`

| Option | What | Difficulty | Notes |
| --- | --- | --- | --- |
| 1A | Pre-generate the serializers with `Microsoft.XmlSerializer.Generator` (sgen) at build time and replace the dynamic `Type.GetType` root resolution with a static type switch | Medium | Clears IL2026 + IL3050 for the manifest read/write path. Requires wiring the generator into the build, and the generated assembly still has caveats. Best XmlSerializer-preserving path. |
| 1B | Hand-write `XmlReader` / `XmlWriter` (de)serialization for the manifest model (as was done for `AssemblyFolderCollection` in #14024) | High | Fully removes the dependency, but the model is large (10+ attributed types, nested collections) and the wire format is a compatibility contract — high regression risk. |
| 1C | Migrate to a source-generated serializer (e.g. System.Text.Json) | Not viable | ClickOnce manifests are XML by specification; the on-disk format cannot change. |
| 1D | Keep `[RequiresUnreferencedCode]` / `[RequiresDynamicCode]` (status quo) | Trivial | Honest, already done. ClickOnce stays trim/AOT-incompatible. |

### Root cause 2: `XslCompiledTransform`

| Option | What | Difficulty | Notes |
| --- | --- | --- | --- |
| 2A | Rewrite the 5 stylesheets as imperative C# XML transforms | Very High | Removes the AOT blocker entirely, but `read2.xsl` alone is 529 lines of transformation logic on a compatibility-critical wire format. High effort, high risk, hard to prove parity. |
| 2B | Pre-compile XSLT to a type at build time (historical `xsltc`) and load the compiled command type | High / uncertain | `xsltc` is .NET Framework-era tooling with unclear modern support; the compiled type may still require dynamic code. Needs a feasibility spike before committing. |
| 2C | Replace XSLT with a lighter, AOT-safe transform (e.g. LINQ-to-XML projection) | Very High | Same scope/risk as 2A; only worth it if the transforms turn out to be mostly structural. |
| 2D | Keep `[RequiresDynamicCode]` (status quo) | Trivial | XSLT is the hardest blocker; this is the realistic answer unless ClickOnce-under-AOT becomes a hard requirement. |

### Root cause 3: `SignedXml` / `CryptoConfig`

| Option | What | Difficulty | Notes |
| --- | --- | --- | --- |
| 3A | Add `[DynamicDependency]` for the concrete algorithm types (`RSAPKCS1SHA256SignatureDescription`, SHA256 impl) so trimming preserves them, clearing IL2026 | Medium | Makes the signing path trim-safe (not AOT-safe). The `XmlDsigXsltTransform` -> `XslCompiledTransform` path still emits IL3050, so this only helps if trimming (not AOT) is the goal. |
| 3B | Make `SignedXml` itself AOT-safe | Blocked (BCL) | The reflection/codegen is inside `System.Security.Cryptography.Xml`; not fixable in MSBuild. |
| 3C | Keep `[RequiresUnreferencedCode]` / `[RequiresDynamicCode]` (status quo) | Trivial | Honest. |

### References

- Introduced by #14024 (Enable AOT/trim analyzers for Utilities and Tasks); follows #14012 (Framework and StringTools).
- Subsystem: `src/Tasks/ManifestUtil/*` (manifest model + XSLT), `mansign2.cs` (signing), `SecurityUtil.cs` (`SignFile`), `BootstrapperUtil/BootstrapperBuilder.cs`.
- Precedent for option 1B: `AssemblyFolderCollection.Load` was migrated from `DataContractSerializer` to a hand-written `XmlDocument` parser in #14024 (small, fixed schema — feasible there, much larger here).

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.