Skip to content

[Blazor] Ship Native AOT metadata generator package - #68326

Open
javiercn wants to merge 3 commits into
javiercn-aspire-dashboard-aot-strict-prooffrom
javiercn-ship-aot-metadata-generator
Open

[Blazor] Ship Native AOT metadata generator package#68326
javiercn wants to merge 3 commits into
javiercn-aspire-dashboard-aot-strict-prooffrom
javiercn-ship-aot-metadata-generator

Conversation

@javiercn

@javiercn javiercn commented Aug 10, 2026

Copy link
Copy Markdown
Member

Overview

Completes the delivery layer of #68332 by making the Blazor Native AOT metadata generator consumable outside this repository as an explicitly referenced analyzer package. This PR is pinned from javiercn-aspire-dashboard-aot-strict-proof at 43c0eb40916741f531d856f8ed7a0f067548ae3f to javiercn-ship-aot-metadata-generator at 68e935a405993543c831176855314ed9b55e038e (three commits, seven files); it builds on the verified strict reflection-disabled proof in #68302 and its shared MSTest/Native AOT harness, but does not automatically add the generator to Microsoft.AspNetCore.App.Ref. The cross-cutting constraint is that the package remains preview-only and analyzer-only while existing Blazor behavior stays reflection-enabled by default.

Design

The consumer contract is an explicit, non-transitive package reference:

<!-- src/Components/test/testassets/BlazorAotFeatures/BlazorServerAotSample/BlazorServerAotSample.csproj -->
<!-- Applications opt in deliberately. PrivateAssets prevents this preview analyzer from
     becoming a dependency of projects or packages that reference the application. -->
<PackageReference Include="Microsoft.AspNetCore.Components.Endpoints.Generators"
                  Version="$(MicrosoftAspNetCoreComponentsEndpointsGeneratorsVersion)"
                  PrivateAssets="all" />

The package has no runtime contract: it suppresses normal build output and package dependencies, then places only the Roslyn generator under the conventional analyzer path. Preview-only version properties are local to this project, so an outer RC/RTM repository version cannot accidentally stabilize the package.

<!-- src/Components/Endpoints/gen/Microsoft.AspNetCore.Components.Endpoints.Generators.csproj -->
<Project Sdk="Microsoft.NET.Sdk"
         TreatAsLocalProperty="PreReleaseVersionLabel;PreReleaseVersionIteration">
  <PropertyGroup>
    <IsPackable>true</IsPackable>
    <IsShippingPackage>true</IsShippingPackage>
    <IncludeBuildOutput>false</IncludeBuildOutput>
    <SuppressDependenciesWhenPacking>true</SuppressDependenciesWhenPacking>

    <!-- This experimental package intentionally never stabilizes in .NET 11. -->
    <SuppressFinalPackageVersion>true</SuppressFinalPackageVersion>
    <PreReleaseVersionLabel>$(PreviewOnlyPackagePreReleaseVersionLabel)</PreReleaseVersionLabel>
    <PreReleaseVersionIteration>$(PreviewOnlyPackagePreReleaseVersionIteration)</PreReleaseVersionIteration>
  </PropertyGroup>

  <ItemGroup>
    <!-- No lib/, ref/, runtime, build, or buildTransitive asset is packed. -->
    <None Include="$(TargetPath)"
          Pack="true"
          PackagePath="analyzers/dotnet/cs"
          Visible="false" />
  </ItemGroup>
</Project>

This replaces both rejected delivery shapes: consumers no longer need a repository-local analyzer ProjectReference, and the generator is not injected automatically through the ASP.NET Core targeting pack. Explicit acquisition keeps the experimental surface scoped to applications such as Aspire that deliberately adopt it.

Implementation

The strict feature app retains the verified parent's source-tree framework references and shared Native AOT test-harness analyzer. This PR changes only the metadata generator acquisition from a direct analyzer project reference to the private package; it does not reintroduce the removed xUnit runner, custom native harness, targeting-pack override, or duplicated E2E orchestration.

The E2E project remains an MSTest/Microsoft.Testing.Platform executable and delegates the nested publish to the parent's shared E2ENativeAot machinery. A small prerequisite target packs the generator, obtains its exact computed package version, restores the app through the local shipping feed, and appends that version to the app's existing publish properties. The shared target still owns E2ECompileTestHarness=true, the runtime identifier, publish output, and manifest generation.

<!-- src/Components/Testing/testassets/BlazorAotFeatures.E2E.Tests/BlazorAotFeatures.E2E.Tests.csproj -->
<ProjectReference Include="../../../test/testassets/BlazorAotFeatures/BlazorServerAotSample/BlazorServerAotSample.csproj">
  <E2EApp>true</E2EApp>
  <E2EAppMode>publish</E2EAppMode>
  <AdditionalProperties>UseIisNativeAssets=false</AdditionalProperties>
  <AdditionalProperties Condition="'$(BlazorAotStrictMode)' == 'true'">UseIisNativeAssets=false;BlazorAotStrictMode=true</AdditionalProperties>
  <E2ENativeAot>true</E2ENativeAot>
  <E2ERuntimeIdentifier>$(RuntimeIdentifier)</E2ERuntimeIdentifier>
</ProjectReference>

<Target Name="PrepareRazorComponentsMetadataGeneratorPackage" BeforeTargets="PrepareE2EApps">
  <!-- Pack the analyzer without leaking the consuming app's runtime/AOT properties into netstandard2.0. -->
  <MSBuild Projects="$(_RazorComponentsMetadataGeneratorProject)"
           Targets="Pack"
           RemoveProperties="RuntimeIdentifier;SelfContained;PublishAot"
           Properties="Configuration=$(Configuration)" />
  <MSBuild Projects="$(_RazorComponentsMetadataGeneratorProject)"
           Targets="_GetPackageVersionInfo"
           RemoveProperties="RuntimeIdentifier;SelfContained;PublishAot"
           Properties="Configuration=$(Configuration)">
    <Output TaskParameter="TargetOutputs" ItemName="_RazorComponentsMetadataGeneratorPackageInfo" />
  </MSBuild>

  <!-- Preserve the parent's publish properties and add only the exact package version built above. -->
  <ItemGroup>
    <ProjectReference Update="../../../test/testassets/BlazorAotFeatures/BlazorServerAotSample/BlazorServerAotSample.csproj">
      <AdditionalProperties>%(ProjectReference.AdditionalProperties);MicrosoftAspNetCoreComponentsEndpointsGeneratorsVersion=$(_RazorComponentsMetadataGeneratorPackageVersion)</AdditionalProperties>
    </ProjectReference>
  </ItemGroup>
</Target>

Package validation follows the same shared MSTest/MTP test path as the rest of the generator suite. It covers three distinct equivalence classes once each: physical package shape, external analyzer execution, and dependency isolation. The external-consumer test references product binaries already built by the Components leg, restores only the analyzer from the local package directory, proves real metadata source was emitted, then packs the consumer and verifies PrivateAssets="all" prevents the generator from flowing into its nuspec.

// src/Components/Endpoints/test/Microsoft.AspNetCore.Components.Endpoints.Generators.Tests/
// RazorComponentsMetadataGeneratorPackageTests.cs
[TestClass]
public sealed class RazorComponentsMetadataGeneratorPackageTests
{
    [TestMethod]
    public void PackageContainsOnlyPrereleaseAnalyzerAsset()
    {
        Assert.Contains(AnalyzerPath, entries);
        Assert.DoesNotContain(path => path.StartsWith("lib/", StringComparison.OrdinalIgnoreCase), entries);
        Assert.DoesNotContain(path => path.StartsWith("ref/", StringComparison.OrdinalIgnoreCase), entries);
        Assert.DoesNotContain(path => path.StartsWith("runtimes/", StringComparison.OrdinalIgnoreCase), entries);
        CollectionAssert.AreEqual(
            new[] { AnalyzerPath },
            entries.Where(path => path.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)).ToArray());
        Assert.Contains('-', version); // for example, 11.0.0-dev is allowed; 11.0.0 is not
        Assert.IsNull(metadata.Element(ns + "dependencies"));
    }

    [TestMethod]
    public void ExternalConsumerRestoresPackageAndRunsGeneratorWithoutFlowingDependency()
    {
        // The generated temporary project contains this private package reference and built-product hints.
        RunDotNet(
            testDirectory,
            $"build ExternalConsumer.csproj --nologo -v:minimal " +
            $"-p:RestoreAdditionalProjectSources=\"{package.Directory}\"");

        Assert.Contains(
            path => path.EndsWith("ExternalConsumer.TestMetadata.Metadata.g.cs", StringComparison.Ordinal),
            generatedFiles);
        Assert.DoesNotContain(
            element => string.Equals(element.Attribute("id")?.Value, PackageId, StringComparison.OrdinalIgnoreCase),
            consumerNuspec.Descendants());
    }
}

Generated code also owns two diagnostics surfaced by strict external-shaped builds. BL0005 is expected when generated accessors assign component parameters, and CS8620 can arise from nullable substitutions on inherited generic component bases. The emitter suppresses those diagnostics only inside auto-generated source; a nullable generic-base regression test protects the latter without weakening diagnostics in user code.

// src/Components/Endpoints/gen/Emitters/RazorComponentsMetadataGenerator.Emitter.cs
writer.WriteLine("// <auto-generated/>");
writer.WriteLine("#nullable enable");
writer.WriteLine("#pragma warning disable CS8620 // Nullable annotations on inherited generic component bases");
writer.WriteLine("#pragma warning disable BL0005 // Generated component parameter accessors");

Outcome

Equivalence class Result
Restack audit Exactly three package commits on 43c0eb4091; both CI-fix commits retain identical stable patch IDs, while the package commit changes only for the required MSTest/shared-harness migration
Generator regression suite Shared MSTest Test target passed; direct run confirmed 119 passed, 0 failed, 0 skipped
Focused package validation 2 passed: analyzer-only/prerelease package shape plus external generation/non-transitivity
Analyzer package contents Standard NuGet metadata plus exactly analyzers/dotnet/cs/Microsoft.AspNetCore.Components.Endpoints.Generators.dll; no lib, ref, runtime asset, or dependency group
Preview-only contract A simulated final/RTM outer version still emitted a prerelease package; restacking did not change the package project policy
Strict Blazor proof Shared E2ENativeAot build restored Microsoft.AspNetCore.Components.Endpoints.Generators/11.0.0-dev, generated native code, and published with 0 warnings and 0 errors
Browser execution Not repeated: the package layer changes compile-time acquisition only, while the shared parent's browser scenarios and runtime behavior are unchanged
Early Dashboard overlay (c18c367980) Package acquisition was confirmed, but 0/5 telemetry tests executed: the old Aspire 173a2109e scaffold still produced 89 trim/AOT analysis errors and an ILCompiler unsafe-accessor constraint crash. This does not claim validation of the later be77aa36 Dashboard graph or its forms/auth/storage/QuickGrid/dynamic-root scenarios.

@javiercn
javiercn requested a review from a team as a code owner August 10, 2026 18:11
@javiercn
javiercn force-pushed the javiercn-aspire-dashboard-aot-strict-proof branch from 7274404 to 43c0eb4 Compare August 11, 2026 11:49
Package the metadata source generator as an explicitly referenced preview-only analyzer and validate package acquisition from external-shaped applications.

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

Copilot-Session: 5fafb32e-4791-429a-9155-ef4c2ad36f24
Add the locally built analyzer package as an additional restore source so CI can still acquire the ASP.NET Core targeting pack from configured feeds.

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

Copilot-Session: 5fafb32e-4791-429a-9155-ef4c2ad36f24
Keep the external analyzer-package acquisition coverage independent of Microsoft.AspNetCore.App.Ref package production by referencing the product assemblies already built for the Components test leg.

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

Copilot-Session: 5fafb32e-4791-429a-9155-ef4c2ad36f24
@javiercn
javiercn force-pushed the javiercn-ship-aot-metadata-generator branch from 9d79751 to 68e935a Compare August 11, 2026 12:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant