Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .agents/skills/igniteui-blazor-lite-trimming/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
---
name: igniteui-blazor-lite-trimming
description: Keep IgniteUI.Blazor.Lite trim-compatible when changing library code. Use when touching reflection, JsonSerializer calls, DynamicallyAccessedMembers annotations, or suppressions in src/, when the build fails with IL2xxx errors, or when asked about trimming, trim warnings, or the PublishSmoke app.
description: Keep IgniteUI.Blazor.Lite trim- and AOT-compatible when changing library code. Use when touching reflection, expression trees, MakeGenericType, JsonSerializer calls, DynamicallyAccessedMembers annotations, or suppressions in src/, when the build fails with IL2xxx/IL3xxx errors, or when asked about trimming, AOT, or the PublishSmoke/AotSmoke apps.
---
# IgniteUI.Blazor.Lite — Trimming
# IgniteUI.Blazor.Lite — Trimming & AOT

The library ships `IsTrimmable` and must stay trim-clean: every trim-analysis diagnostic (IL2xxx) builds as an **error**, enforced by `dotnet_analyzer_diagnostic.category-Trimming.severity = error` in the repo `.editorconfig`. The source of truth for policy and consumer guidance is [docs/TRIMMING.md](../../../docs/TRIMMING.md) — read its "Maintaining trim compatibility (contributors)" section before working around any IL2xxx error.
The library ships `IsTrimmable` + the AOT/single-file analyzers (`IsAotCompatible` is deferred until Blazor itself supports AOT — dotnet/aspnetcore#51598) and must stay clean: every trim/AOT diagnostic (IL2xxx/IL3xxx) builds as an **error**, enforced by the `.editorconfig` category rules. The source of truth for policy and consumer guidance is [docs/TRIMMING.md](../../../docs/TRIMMING.md) — read its two "Maintaining ... (contributors)" sections before working around any IL error.

## Rules

Expand All @@ -13,9 +13,11 @@ The library ships `IsTrimmable` and must stay trim-clean: every trim-analysis di
3. **Suppress narrowly when a fix is impossible.** `[UnconditionalSuppressMessage("Trimming", "ILxxxx", Justification = "...")]` on the smallest member, with a justification stating why the pattern is safe at runtime; extract a small private helper if needed so the justification matches exactly what the member does. Keep the helper's `Type` parameter unannotated — annotating it only moves the warning to the caller.
4. **Never use `#pragma warning disable` for ILxxxx.** It silences only the build analyzer and leaves no metadata for publish-time trim tooling (ILLink, NativeAOT's ILCompiler).
5. **Don't add reflection over user-supplied or runtime-discovered types** outside the documented data-source boundary (`JSDataSourceSchema` and the `ExtractSchema` entry points).
6. **AOT (IL3xxx): keep delegate types statically known.** Expression trees via `Expression.Lambda<TDelegate>` or `Lambda(Type, ...)` with `typeof` literals (extend the closed delegate-type map in `JsonDataSourceSchema` for new shapes) — never `GetFuncType` or non-generic `Lambda(body, params)`; `Compile()` itself is AOT-safe (interprets under ILC). `MakeGenericType`/`MakeGenericMethod` only over statically visible or class-constrained closed sets. Suppressions: `[UnconditionalSuppressMessage("AOT", "IL3050:RequiresDynamicCode", ...)]`, same narrowness/justification rules as trimming.

## Verification

- `dotnet build src/IgniteUI.Blazor.Lite.csproj` must be free of IL diagnostics (they fail the build).
- For changes touching reflection, serialization, or annotations, run the automated browser checks over the trimmed publish: `dotnet test tests/IgniteUI.Blazor.Lite.IntegrationTests --filter Category=TrimmedPublish --settings .runsettings` (`TrimmedPublishSmokeTest`; publishes the smoke app net10.0 on demand). It is category-scoped — not part of the per-component integration sweep — and covers net10.0 only; the manual checklist in `tests/IgniteUI.Blazor.Lite.PublishSmoke/README.md` covers the other TFMs. Trimming only happens at publish (`dotnet run` proves nothing).
- Verify claims about linker behavior empirically in the smoke app — a control-vs-fix publish pair, not reasoning from documentation alone.
- For AOT changes: `tests/IgniteUI.Blazor.Lite.AotSmoke` runs the RequiresDynamicCode-adjacent paths under real ILC (CI does this on linux-x64; locally `dotnet run --project tests/IgniteUI.Blazor.Lite.AotSmoke -c Release -p:SimulateNoDynamicCode=true` covers the interpreter paths without the native toolchain — see its README).
6 changes: 3 additions & 3 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -344,11 +344,11 @@ dotnet_diagnostic.IDE0005.severity = suggestion

#### Trimming ####

# REPO: every trim-analysis diagnostic (IL2xxx) is an error; category bulk-config covers future
# codes too. Inert in projects without the trim analyzer. Suppression policy: docs/TRIMMING.md.
# For future AOT work: dotnet_analyzer_diagnostic.category-AOT.severity = error
# REPO: every trim/AOT-analysis diagnostic (IL2xxx/IL3xxx) is an error; category bulk-config covers
# future codes too. Inert in projects without the analyzers. Suppression policy: docs/TRIMMING.md.
dotnet_analyzer_diagnostic.category-Trimming.severity = error
dotnet_analyzer_diagnostic.category-SingleFile.severity = error
dotnet_analyzer_diagnostic.category-AOT.severity = error

#### Generated code — keep LAST so it overrides all sections above ####

Expand Down
10 changes: 10 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,16 @@ jobs:
dotnet publish tests/IgniteUI.Blazor.Lite.PublishSmoke --configuration Release --framework net9.0
dotnet publish tests/IgniteUI.Blazor.Lite.PublishSmoke --configuration Release --framework net10.0

# NativeAOT (ILC) gate: publishes the console smoke app rooting the whole library
# (fails on ILC warnings) and runs its checks — behavior the build analyzer cannot see.
Comment thread
MayaKirova marked this conversation as resolved.
# Success is exit code 100 (aspnetcore trimming-test convention: no accidental passes).
- name: Publish and run NativeAOT smoke app
run: |
dotnet publish tests/IgniteUI.Blazor.Lite.AotSmoke --configuration Release -r linux-x64
status=0
tests/IgniteUI.Blazor.Lite.AotSmoke/bin/Release/net10.0/linux-x64/publish/IgniteUI.Blazor.Lite.AotSmoke || status=$?
test "$status" -eq 100

- name: Build VS Templates
run: dotnet pack templates/IgniteUI.Blazor.Templates/

Expand Down
62 changes: 62 additions & 0 deletions .github/workflows/wasm-aot-smoke.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
name: Wasm AOT Smoke

# Manual: Blazor WASM AOT compilation (Mono AOT) of the smoke app + the browser checks against it.
# Slow (multi-minute AOT compile) and emits no IL3xxx (that is the NativeAOT smoke's job in ci.yml) —
# this validates the actual wasm-AOT consumer scenario end-to-end.
on:
workflow_dispatch:

permissions:
contents: read

jobs:
wasm-aot:
name: Wasm AOT publish & browser smoke
runs-on: ubuntu-latest

steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Setup .NET
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: |
8.0.x
9.0.x
10.0.x

- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 22
cache: npm

- name: Install npm dependencies
run: npm ci

- name: Build TypeScript/Webpack assets
run: npm run build

- name: Copy component themes to wwwroot
run: npm run copythemes

- name: Install wasm-tools workload
run: dotnet workload install wasm-tools

- name: Publish smoke app with AOT compilation
run: dotnet publish tests/IgniteUI.Blazor.Lite.PublishSmoke --configuration Release --framework net10.0 -p:RunAOTCompilation=true

- name: Build integration tests
run: dotnet build tests/IgniteUI.Blazor.Lite.IntegrationTests --configuration Release

- name: Install Playwright
shell: pwsh
run: ./tests/IgniteUI.Blazor.Lite.IntegrationTests/bin/Release/net10.0/playwright.ps1 install

# The TrimmedPublish fixture serves whatever publish output exists — here, the AOT-compiled one.
- name: Run browser checks against the AOT publish
run: >
dotnet test ./tests/IgniteUI.Blazor.Lite.IntegrationTests/IgniteUI.Blazor.Lite.IntegrationTests.csproj
--settings ./.runsettings --no-build --configuration Release
--filter Category=TrimmedPublish
1 change: 1 addition & 0 deletions IgniteUI.Blazor.Lite.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<Project Path="stories/IgniteUI.Blazor.Stories.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/IgniteUI.Blazor.Lite.AotSmoke/IgniteUI.Blazor.Lite.AotSmoke.csproj" />
<Project Path="tests/IgniteUI.Blazor.Lite.IntegrationTests/IgniteUI.Blazor.Lite.IntegrationTests.csproj" />
<Project Path="tests/IgniteUI.Blazor.Lite.PublishSmoke/IgniteUI.Blazor.Lite.PublishSmoke.csproj" />
<Project Path="tests/IgniteUI.Blazor.Lite.TestBed.Client/IgniteUI.Blazor.Lite.TestBed.Client.csproj" />
Expand Down
18 changes: 17 additions & 1 deletion docs/TRIMMING.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ public class MyDataItem { ... }

or with a [trimmer root descriptor](https://learn.microsoft.com/dotnet/core/deploying/trimming/trimming-options#root-descriptors) listing the types.

> **Prefer the `DynamicDependency` form.** It is honored by every trim/AOT pipeline. Annotating the type itself works under Blazor WebAssembly's ILLink (verified in the trimmed browser smoke) but was found **insufficient under NativeAOT's ILC** (verified 2026-09-01 via the AotSmoke gate) — the class-level annotation only takes effect where a `Type` value flows through annotated locations, and the data-source entry points deliberately take unannotated `Type`s.

Preservation must cover **every complex type reachable from the item type**, not just the root: if `MyDataItem` has an `Address` property whose members the component renders, `Address` needs the same treatment — the schema builder reflects over nested object types as it encounters them.

## Module preloading: trim-safe by design
Expand All @@ -39,7 +41,21 @@ The only caveat is **third-party module types**: a custom class with a `Register

## Native AOT

Not supported yet. The data-source layer compiles expression-tree getters (`RequiresDynamicCode`), which is a planned follow-up; trimming and wasm AOT compilation of the interpreter-hosted kind are unaffected.
The library's code is AOT-clean: it builds warning-free under the [AOT analyzer](https://learn.microsoft.com/dotnet/core/deploying/native-aot/) (`EnableAotAnalyzer`, errors via `.editorconfig`), verified under real ILC by the AotSmoke gate. The public `IsAotCompatible` flag is deliberately **not** set yet — Blazor itself is not AOT-compatible (`Microsoft.AspNetCore.Components` ships `IsTrimmable` only; [dotnet/aspnetcore#51598](https://github.com/dotnet/aspnetcore/issues/51598) tracks it), so the claim would outrun the platform; flip it when that lands. What this means per deployment model:

- **Blazor WebAssembly** — unaffected either way: both the default interpreter and `RunAOTCompilation=true` publishes use Mono AOT with the interpreter retained, so no NativeAOT semantics apply.
- **NativeAOT (ILC)** — the library's expression-tree getters run in `System.Linq.Expressions`' interpreted form (a documented NativeAOT limitation: slower, not broken), and all generic instantiations the library creates at runtime are statically visible or reference-type-shared. Note ILC deployment of *Blazor apps* is not a supported platform scenario today (ASP.NET Core NativeAOT excludes Blazor; MAUI BlazorWebView under `PublishAot` is undocumented upstream and unverified) — the analyzer-clean code positions the library for those consumers as they materialize.
- The trimming guidance above (preserving data item types) applies under AOT with one sharpening: use the `DynamicDependency` form — see the note in that section.

## Maintaining AOT compatibility (contributors)

AOT diagnostics (IL3xxx) build as errors like trim ones (`dotnet_analyzer_diagnostic.category-AOT.severity = error`). Follow [intrinsic RequiresDynamicCode APIs](https://learn.microsoft.com/dotnet/core/deploying/native-aot/intrinsic-requiresdynamiccode-apis) and these repo rules:

1. **Avoid dynamic code outright** where a static shape exists.
2. **Expression trees: the delegate type must be statically known** — use `Expression.Lambda<TDelegate>(...)` or `Expression.Lambda(Type delegateType, ...)` with a `typeof` literal (see the closed delegate-type map in `JsonDataSourceSchema`). Never `Expression.GetFuncType` or the non-generic `Lambda(body, params)` overloads — those are the `RequiresDynamicCode` sites; `Compile()` itself is AOT-safe (interprets).
3. **`MakeGenericType`/`MakeGenericMethod` only over closed sets** that are statically visible or class-constrained (the net9+ analyzer proves the `where T : class` pattern; net8 needs a narrow suppression).
4. **Suppression policy**: `[UnconditionalSuppressMessage("AOT", "IL3050:RequiresDynamicCode", Justification = ...)]` on the smallest member — the ID is what ILC matches; the justification states the runtime invariant. Same no-`#pragma` rule as trimming.
5. **Verify under real ILC**: `tests/IgniteUI.Blazor.Lite.AotSmoke` (see its README) — CI publishes and runs it; locally `dotnet run -p:SimulateNoDynamicCode=true` covers the interpreter paths without the native toolchain.

## Maintaining trim compatibility (contributors)

Expand Down
8 changes: 7 additions & 1 deletion src/IgniteUI.Blazor.Lite.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,13 @@
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<RepositoryUrl>https://github.com/IgniteUI/igniteui-blazor</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<!-- Trim-analysis diagnostics build as errors (.editorconfig, category-Trimming); suppression policy: docs/TRIMMING.md. -->
<!-- Trim/single-file/AOT diagnostics build as errors (.editorconfig category rules); suppression policy: docs/TRIMMING.md.
The code is AOT-clean, but the public IsAotCompatible claim waits for the platform: Blazor itself is not
AOT-compatible yet (Microsoft.AspNetCore.Components ships IsTrimmable only; dotnet/aspnetcore#51598) —
replace these three with <IsAotCompatible>true</IsAotCompatible> when that lands. -->
<IsTrimmable>true</IsTrimmable>
<EnableAotAnalyzer>true</EnableAotAnalyzer>
<EnableSingleFileAnalyzer>true</EnableSingleFileAnalyzer>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
Expand All @@ -63,6 +68,7 @@

<ItemGroup Condition="'$(ExposeInternalsToTests)' != 'false'">
<InternalsVisibleTo Include="IgniteUI.Blazor.Tests" />
<InternalsVisibleTo Include="IgniteUI.Blazor.Lite.AotSmoke" />
<InternalsVisibleTo Include="IgniteUI.Blazor.Lite.PublishSmoke" />
</ItemGroup>

Expand Down
4 changes: 3 additions & 1 deletion src/componentsBase/BaseRendererControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -770,6 +770,7 @@ internal void AdjustDynamicContent(string? containerId, string? contentType, str
}

private Dictionary<Type, Func<DynamicContentInfo?>> _dynamicContentBuilders = new Dictionary<Type, Func<DynamicContentInfo?>>();
[UnconditionalSuppressMessage("AOT", "IL3050:RequiresDynamicCode", Justification = "DynamicContentInfo<T> is class-constrained and templateContentType only ever holds reference types (typeof literals via the internal UpdateTemplate relay), so the instantiation uses AOT shared generics. The net9+ analyzer proves this via the class constraint; this suppression covers the net8 analyzer only.")]
private DynamicContentInfo? BuildDynamicContentInfo(string? contentType, string? templateId)
{
var templateContentType = TemplateContentType(templateId);
Expand All @@ -784,7 +785,7 @@ internal void AdjustDynamicContent(string? containerId, string? contentType, str
System.Linq.Expressions.NewExpression newExp = System.Linq.Expressions.Expression.New(createType);
System.Linq.Expressions.UnaryExpression conversion = System.Linq.Expressions.Expression.Convert(newExp, nonGen);

var getNew = (Func<DynamicContentInfo>)System.Linq.Expressions.Expression.Lambda(conversion).Compile();
var getNew = System.Linq.Expressions.Expression.Lambda<Func<DynamicContentInfo>>(conversion).Compile();
_dynamicContentBuilders[templateContentType] = getNew;
}
else
Expand Down Expand Up @@ -3456,6 +3457,7 @@ private void SetPropertyValue(object item, System.Reflection.PropertyInfo proper
break;
}
}
[UnconditionalSuppressMessage("AOT", "IL3050:RequiresDynamicCode", Justification = "Creates the property's own array PropertyType, which is present in metadata whenever the property exists.")]
private void SetPropertyValue(object item, System.Reflection.PropertyInfo property, object value)
{
System.Type? type = Nullable.GetUnderlyingType(property.PropertyType);
Expand Down
1 change: 1 addition & 0 deletions src/componentsBase/DynamicContentHolder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ internal class DynamicComponentChangingEventArgs

internal class DynamicContentInfo<T>
: DynamicContentInfo
where T : class // reference types only — keeps the MakeGenericType instantiation AOT-shareable
{
public DynamicContentInfo()
{
Expand Down
2 changes: 1 addition & 1 deletion src/componentsBase/IgbTemplateContent.razor
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
@namespace IgniteUI.Blazor.Controls
@typeparam T
@typeparam T where T : class
Comment thread
MayaKirova marked this conversation as resolved.

<div>
@if (Template != null && _hasPopulatedContext)
Expand Down
Loading
Loading