diff --git a/TRIMMING_AUDIT.md b/TRIMMING_AUDIT.md deleted file mode 100644 index cf184cf98..000000000 --- a/TRIMMING_AUDIT.md +++ /dev/null @@ -1,398 +0,0 @@ -I have all the verified findings. Writing the report directly as my output. - -# Radzen.Blazor Trimming Audit - -## 1. Executive Summary - -This audit examined the shippable `Radzen.Blazor` library for trimming and AOT hazards that survive the clean trim-analyzer build because they live in the analyzer's blind spots: JS-interop serialization, reflection over the consumer's model type `T`, dynamic LINQ, and `System.Text.Json` of user types. - -**Confirmed real hazards: 53.** By severity: - -- **High: 14** - mostly the data-component reflection-over-`T` family (DataGrid/DropDown/Chart/Scheduler bind by string `Property`/`ValueProperty` and crash when the consumer's model lives in a trimmable assembly), plus three concrete JS-interop DTO gaps (`RadzenUpload`, `RadzenGoogleMap`) and two `System.Text.Json`-of-consumer-`T` deserialization gaps (`ReadAsync`, `ODataServiceResult`). -- **Medium: 32** - the bulk of the dynamic-LINQ / `RequiresUnreferencedCode`-laundered-through-suppression surface, the public `DynamicExtensions` API that suppresses instead of propagating, the `[RequiresDynamicCode]` annotation gap across the whole library (AOT-only), and several public generics missing `[DynamicallyAccessedMembers]`. -- **Low: 7** - edge cases (user-defined struct OData properties, custom `IList`/`ICollection` collection assignment, nullable-enum filter anonymous object, opt-in `RadzenComponentActivator` override path). - -**Overall risk posture.** The library is *trim-clean by suppression, not by construction*. Roughly 188 `[UnconditionalSuppressMessage]` entries silence `IL2xxx` warnings with the human promise "Data item types are preserved by the application." That promise holds for the common Blazor WASM default (the consumer's app assembly is not member-trimmed), which is exactly why the existing tiny `TrimTest` passes. It breaks the moment a consumer's model lives in a separately referenced `true` library, or the app opts into `TrimMode=full`, or full AOT. None of those configurations are exercised by any test, so every one of these hazards is latent. - -**The single most important gap:** the data-component generic parameters (`TItem` on `RadzenDataGrid`/`RadzenDataList`/`RadzenPivotDataGrid`, `T` on `PagedDataBoundComponent`/`DataBoundFormComponent`/`DropDownBase`, `TItem` on `CartesianSeries`/`RadzenScheduler`/`RadzenDataFilter`) carry **no `[DynamicallyAccessedMembers]`**. The suppressions delegate model-member preservation to the consumer but provide no mechanism to enforce it. Annotating these generic parameters with `[DynamicallyAccessedMembers(PublicProperties | PublicFields)]` is the one change that closes the largest share of the High findings, and it must start at the base classes (`PagedDataBoundComponent`, `DataBoundFormComponent`) or the analyzer will block downstream annotations with `IL2091`. - -A secondary systemic gap: **zero `[RequiresDynamicCode]` annotations exist anywhere in the library**, and the AOT analyzer is disabled. Native AOT consumers get no warning before hitting `PlatformNotSupportedException` from `Reflection.Emit` and value-type `MakeGenericType`/`MakeGenericMethod`. This is AOT-only (standard Blazor WASM `RunAOTCompilation` keeps an interpreter fallback), hence Medium, but it is a real undocumented cliff. - ---- - -## 2. Findings by Severity - -### High - -#### H1. Data-component base generics lack `[DynamicallyAccessedMembers]`; all dynamic LINQ over `T` inherits the gap - -- **File:** `Radzen.Blazor/PagedDataBoundComponent.cs:22`, `Radzen.Blazor/DataBoundFormComponent.cs:27` -- **What breaks:** A consumer publishes WASM with `PublishTrimmed=true` and binds a `RadzenDataGrid`/`RadzenDataList`/`RadzenDropDown`/`RadzenDropDownDataGrid`/`RadzenPivotDataGrid` to a model in a **separate** `IsTrimmable` assembly (EF entities, a Shared/Models project). The trimmer strips the model's property getters (only setters survive, rooted by object initializers). The first sort/filter/group/`TextProperty` search throws `ArgumentException "Expression must be readable"` / `"'X' is not a member of type 'Y'"` from the dynamic-LINQ / `Expression.PropertyOrField` path. Empirically reproduced end-to-end. -- **Root cause:** `PagedDataBoundComponent` and `DataBoundFormComponent` declare `T` with no DAM and suppress `IL2087`/`IL2091` with `Justification = TrimMessages.DataTypePreserved`. The "preserved by the application" promise is false when the model assembly is itself trimmed; nothing flows a keep-properties requirement to the trimmer. -- **Remediation (start at the base, propagate down):** - ```csharp - // PagedDataBoundComponent.cs:22 - public class PagedDataBoundComponent< - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicFields)] T> - : RadzenComponent - - // DataBoundFormComponent.cs:27 - public class DataBoundFormComponent< - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicFields)] T> - : FormComponent - ``` - Then add the same annotation to every derived public generic so the requirement is not lost: `RadzenDataGrid` (`RadzenDataGrid.razor.cs:99`), `RadzenDataGridColumn` (`RadzenDataGridColumn.razor.cs:51`), `RadzenDataList` (`RadzenDataList.razor.cs:39`), `RadzenPivotDataGrid` (`RadzenPivotDataGrid.razor.cs:54`), `DropDownBase` (`DropDownBase.cs:18`), `RadzenDropDown` (`RadzenDropDown.razor.cs:35`), `RadzenDropDownDataGrid` (`RadzenDropDownDataGrid.razor.cs:53`), `RadzenDataFilter`/`RadzenDataFilterProperty`. Use `PublicProperties | PublicFields` (matches what `PropertyAccess`/dynamic LINQ read). After this lands, delete the now-redundant `IL2087`/`IL2090`/`IL2091` suppressions (keep the `IL2026` suppressions - the `PropertyAccess`/`QueryableExtension` helpers remain `[RequiresUnreferencedCode]`). **LinkerConfig cannot fix this** - the model is a consumer type unknown to the library; DAM flow is the only mechanism. - -#### H2. `DropDownBase`: `ValueProperty`/`TextProperty` reflection over the consumer item type, guarded only by suppressions - -- **File:** `Radzen.Blazor/DropDownBase.cs:353` (and getters at `:498`/`:504`, single-select at `:1382`/`:1400`) -- **What breaks:** ``. Under trimming `Person.Id`/`Person.FullName` getters are stripped (`TValue=int` says nothing about `Person`; `Data` is `IEnumerable` so the element type is invisible). `OnParametersSet` -> `GetGetter` -> `PropertyAccess.Getter("Id", typeof(Person))` throws `ArgumentException` on render; or `GetProperty(elementType,"Id")` returns null and single-select returns the whole `Person` then fails to cast to `int` -> `InvalidCastException`. Shared by `RadzenDropDownDataGrid`, `RadzenListBox`, `RadzenCheckBoxList`, `RadzenRadioButtonList`, `RadzenSelectBar`, `RadzenAutoComplete`, `RadzenPickList`. -- **Root cause:** `elementType = PropertyAccess.GetElementType(Data.GetType())` is computed at runtime; the DAM chain is severed at the library/consumer boundary and papered over by `IL2070`/`IL2075` suppressions (`DropDownBase.cs:306-307, 475-476, 521-524, 1361-1364`). -- **Remediation:** Apply the H1 DAM annotation to `DropDownBase` and forward it from the concrete dropdowns (covers the `TValue` path). Additionally make `GetElementType` flow the requirement on its return so the IL2070 suppression at `DropDownBase.cs:353/1400` becomes unnecessary: - ```csharp - // PropertyAccess.cs:181 - [return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] - internal static Type GetElementType( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.Interfaces | DynamicallyAccessedMemberTypes.PublicProperties)] Type type) - ``` - Document that consumers using `ValueProperty`/`TextProperty` under trimming must root their model (or rely on the DAM flow above). - -#### H3. `QueryableExtension` filter/sort/group engine reflects over consumer `T`; RUC absorbed by class-level suppression - -- **File:** `Radzen.Blazor/QueryableExtension.cs:525` (`GetNestedPropertyExpression`), reached from `RadzenDataGrid.razor.cs:258, 2116-2117, 2154` -- **What breaks:** Cell render (`PropertyAccess.Getter(Property)` at `RadzenDataGridColumn.razor.cs:244/250`) and filter/sort/group (`Where(columns)`/`OrderBy(orderBy)`/`GroupByMany`) all route to `Expression.PropertyOrField` / `Type.GetProperty(name)`. With a member-trimmed model, `GetProperty("Name")` returns null and `Expression.PropertyOrField` throws, crashing render/sort/filter/group. Also: the `In`/`NotIn`/`Contains` filter branches resolve `Enumerable.Contains/Any/Intersect/Except` by reflection, and **`System.Linq.Enumerable` is not preserved in LinkerConfig**. -- **Root cause:** Every `QueryableExtension` method is `[RequiresUnreferencedCode]`, but `RadzenDataGrid`'s class-level `[UnconditionalSuppressMessage(IL2026)]` (`RadzenDataGrid.razor.cs:88`) swallows the obligation; no DAM on `TItem`. -- **Remediation:** - 1. Apply the H1 DAM annotation (preserves the model's accessors). - 2. Add to `LinkerConfig.xml` (the `In`/collection filter path resolves `Enumerable` by name; it is not preserved like `Queryable` at line 4): - ```xml - - - - ``` - 3. Add `[DynamicallyAccessedMembers(PublicProperties | PublicFields)]` to the `T` of the public `QueryableExtension.Where`/`OrderBy`/`GroupByMany` overloads so direct callers also get the flow. - -#### H4. `PropertyAccess` helpers require DAM on `Type`, but all callers pass unannotated `typeof(TItem)` - -- **File:** `Radzen.Blazor/PropertyAccess.cs:431` (`GetPropertyType`), `:474` (`GetProperty`) -- **What breaks:** These two methods are the only `PublicProperties`-annotated sinks. Callers (`RadzenDataGridColumn.razor.cs:213/230`, `DropDownBase.cs:353/1400`, `RadzenDataFilterProperty.cs:63/355`, `CartesianSeries.cs:106/132/159`) pass `typeof(TItem)`/`Data.GetType()` with no matching source annotation -> `IL2087`, suppressed at class level. At runtime `GetProperty` returns null and the column type/filter degrades or `MakeGenericType(null)` throws. -- **Root cause:** The DAM contract is satisfied at the sink but never flowed from the source generic. -- **Remediation:** Same as H1 - annotating the component generic parameters makes these calls satisfiable and lets the `IL2087`/`IL2090` suppressions be removed. Keep the `IL2026` suppressions (`GetPropertyType`/`GetProperty`/`Getter` stay `[RequiresUnreferencedCode]`). Add `PublicFields` where dictionary/anonymous/field binding occurs (`PropertyAccess.cs:76` `GetField`). - -#### H5. `CartesianSeries` / chart series: `CategoryProperty`/`ValueProperty` reflection - safe for consumer `T`, but **crashes for the library `ChartDataPoint`** (Spreadsheet charts) - -- **File:** `Radzen.Blazor/CartesianSeries.cs:50, 68`; reached via `Documents/Spreadsheet/ChartOverlay.cs:285-288` -- **What breaks:** The general consumer-chart case does **not** crash (the consumer model lives in the un-trimmed app assembly). But `RadzenSpreadsheet`'s chart feature renders `RadzenColumnSeries` with `CategoryProperty="Category"`/`ValueProperty="Value"` over `Radzen.Documents.Spreadsheet.ChartDataPoint` - a library type in the `IsTrimmable` `Radzen.Blazor` assembly. Its setters survive (rooted by an object initializer in `ChartDataResolver.cs:68`), but its getters (`get_Category`/`get_Value`), read only via expression-tree reflection, are stripped. Inserting/rendering a spreadsheet chart on a trimmed build throws `ArgumentException "The property 'Value' has no 'get' accessor"`. -- **Root cause:** Library type whose getters are reachable only through `PropertyAccess.Getter("Value")`; member-trimmed because the assembly is `IsTrimmable`. -- **Remediation:** Add to `LinkerConfig.xml` (note the `Radzen.Documents.Spreadsheet` namespace): - ```xml - - ``` - Do **not** blanket-annotate `CartesianSeries` - the consumer path is already safe and over-annotation is noise. - -#### H6. `RadzenUpload.OnChange` / `RadzenFileInput.OnChange`: `PreviewFileInfo`/`FileInfo` JSInvokable params not preserved - -- **File:** `Radzen.Blazor/RadzenUpload.razor.cs:440` (and `RadzenFileInput.razor.cs:188`) -- **What breaks:** Selecting a file on a trimmed page: JS calls `invokeMethodAsync('RadzenUpload.OnChange', [{Name,Size,Url}])`; Blazor's `DotNetDispatcher` deserializes into `IEnumerable` via reflection STJ. Decompilation of the trimmed assembly confirms the trimmer stripped the public parameterless ctors of `PreviewFileInfo` **and** `FileInfo`, the `PreviewFileInfo.Url` setter, `FileInfo.ContentType` setter, and `FileInfo.LastModified`. STJ throws inside `OnChange`; the JS `try/catch{}` swallows it, so selecting a file silently does nothing - no row, no preview, `Change` never fires. -- **Root cause:** `preserve="methods"` on `RadzenUpload` keeps the JSInvokable dispatch target but not the parameter DTO members. Neither DTO is in LinkerConfig. -- **Remediation:** Add to `LinkerConfig.xml` (note: namespace `Radzen`, not `Radzen.Blazor`): - ```xml - - - ``` - -#### H7. `RadzenUpload.OnProgress`: `FileInfo` JSInvokable param not preserved - -- **File:** `Radzen.Blazor/RadzenUpload.razor.cs:465` -- **What breaks:** During upload, `invokeMethodAsync('RadzenUpload.OnProgress', ..., [{Name,Size}], ...)` deserializes into `IEnumerable`. The trimmed-away parameterless `FileInfo()` ctor (never statically `new`'d - only the `IBrowserFile` ctor is used) makes STJ throw on the first progress event; the JS `catch` swallows it, so `Progress` never fires. -- **Root cause / Remediation:** Same DTO gap as H6; the `Radzen.FileInfo` LinkerConfig entry above fixes both. - -#### H8. `RadzenGoogleMap.OnMapClick`: `GoogleMapClickEventArgs` JSInvokable param not preserved - -- **File:** `Radzen.Blazor/RadzenGoogleMap.razor.cs:183` -- **What breaks:** Clicking the map sends `{Position:{Lat,Lng}}` to `OnMapClick(GoogleMapClickEventArgs args)`. `GoogleMapClickEventArgs.Position` is never assigned in C# (only via JSON), so its setter and parameterless ctor are trimmed. Either deserialization throws or `args.Position` is null on every `MapClick`. -- **Root cause:** LinkerConfig preserves `GoogleMapPosition` and `GoogleMapMarkerData` (lines 17-18) but missed the wrapper `GoogleMapClickEventArgs`. -- **Remediation:** Add to `LinkerConfig.xml` (namespace `Radzen`): - ```xml - - ``` - -#### H9. `HttpResponseMessageExtensions.ReadAsync`: reflection-STJ of the consumer model with no trim seam - -- **File:** `Radzen.Blazor/HttpResponseMessageExtensions.cs:27` (deserialize at `:36`) -- **What breaks:** Scaffolded OData services call `await response.ReadAsync>()`. Under trimming `Category`'s setters/ctor are stripped; reflection-based `DeserializeAsync` returns rows with all-default properties (silent blank grids) or throws `NotSupportedException`/`ConstructorContainsNullParameterNames` for records/ctor-bound DTOs. The canonical Radzen-generated data-access path. -- **Root cause:** `T` has no DAM, the method is not `[RequiresUnreferencedCode]`, and the class-level `[UnconditionalSuppressMessage(IL2026)]` (`:14`) swallows the only warning. The sibling write path `ODataJsonSerializer.Serialize` already does the right thing (DAM on `TValue`). -- **Remediation:** Annotate `T` and remove the class-level suppression: - ```csharp - public static async Task ReadAsync< - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicConstructors)] T>( - this HttpResponseMessage response) - ``` - DAM flows through `ODataServiceResult` -> `IEnumerable`. `PublicConstructors` (not just parameterless) guards records/positional types. - -#### H10. `ODataServiceResult` preserved as wrapper only; the element type `T` is not (false comfort) - -- **File:** `Radzen.Blazor/OData.cs:57`, `LinkerConfig.xml:15` -- **What breaks:** `` keeps `Count`/`Value` on the open generic but **not** the members of the closed element type. `ReadAsync>()` still needs `Category`'s setters/ctor, which are unprotected. -- **Root cause:** `preserve="all"` on an open generic does not recurse into the type argument; the element type lives in the consumer assembly. -- **Remediation:** Fixed by H9's DAM annotation on `ReadAsync` (the requirement flows into the element type). Add a comment in `LinkerConfig.xml` noting that line 15 preserves only the wrapper and that element types are preserved via the `ReadAsync` DAM flow. - -#### H11. `DynamicExtensions` public dynamic-LINQ API suppresses instead of propagating `[RequiresUnreferencedCode]` - -- **File:** `Radzen.Blazor/DynamicExtensions.cs:11-13` -- **What breaks:** A consumer with `using System.Linq.Dynamic.Core;` calling `query.Where("Foo == @0", val)` / `.OrderBy("Bar")` / `.Select("new { X, Y }")` gets **zero** analyzer warning, then crashes at runtime: (a) the `Select("new {...}")` projection reaches `DynamicTypeFactory.CreateType` -> `AssemblyBuilder.DefineDynamicAssembly` (AOT crash), and (b) any cast/qualified type name (`"(MyApp.Foo)x"`, enum members) reaches `ResolveTypeFromAssemblies` -> `AppDomain.GetAssemblies().GetTypes()`, which under trimming returns an incomplete set -> `"Could not resolve type"`. -- **Root cause:** The class-level `[UnconditionalSuppressMessage(IL2026)]`+`[IL2072]` silences the obligation that downstream `[RequiresUnreferencedCode]` `ExpressionParser`/`QueryableExtension` would raise, and the public `Where`/`OrderBy`/`Select` methods are not themselves `[RequiresUnreferencedCode]`, so nothing reaches the consumer. -- **Remediation:** Remove the class-level suppressions and mark each public method `[RequiresUnreferencedCode(TrimMessages.DynamicLinqReflection)]` (`Where` `:26`, `OrderBy` `:68`, `Select` `:88`, `Select` `:114`). For the projection path also add `[RequiresDynamicCode(...)]` (see M-AOT findings). Add localized `[UnconditionalSuppressMessage]` only at the internal Radzen call sites where `T` is a preserved model. - -#### H12. DataGrid runtime sorting never exercised by TrimTest - latent crash in separate trimmable model assembly - -- **File:** `Radzen.Blazor/RadzenDataGrid.razor.cs:2079, 2173` (`view.OrderBy(orderBy)`) -- **What breaks:** Empirically verified: with the model in an `IsTrimmable` library, `get_Name` etc. are removed; cell display via `PropertyAccess.GetValue/Getter` throws `ArgumentException "Property Get method was not found."` on first render, and clicking a sort header throws `NullReferenceException` from the compiled delegate. With the model in the rooted app assembly (as the current TrimTest has it) it does not reproduce - masking the bug. -- **Root cause / Remediation:** Same DAM gap as H1. Plus a test gap: add a `true` model class library to the TrimTest solution and a runtime sort/click step (see section 3). - -#### H13. DataGrid runtime filtering with operators never exercised - `Where(filters)` over consumer `T` plus unpreserved `Enumerable` - -- **File:** `Radzen.Blazor/RadzenDataGrid.razor.cs:2154` -- **What breaks:** Typing in a filter box (`Contains`, `GreaterThan`, `In` for enums/collections) invokes `Where(columns)` -> `GetExpression` reflecting on `T` by name and resolving `Enumerable.Contains/Any/Intersect/Except` by reflection. Stripped model member -> `ArgumentException`; stripped `Enumerable` overload (not in LinkerConfig) -> `InvalidOperationException`. The grid renders until the first filter, then crashes. -- **Remediation:** H1 DAM annotation + the `System.Linq.Enumerable` LinkerConfig entry from H3. Add a TrimTest step that applies one filter per operator family (string `Contains`, numeric `GreaterThan`, bool equals, `DateTime` range, enum `In`) at runtime. - -#### H14. Nested property paths (`Customer.Name`) never exercised - per-segment reflection over nested consumer types - -- **File:** `Radzen.Blazor/PropertyAccess.cs:109` (per-segment `Expression.PropertyOrField` / `GetProperty`) -- **What breaks:** Empirically reproduced under `TrimMode=full`/`link` with the model in a separate assembly: a column `Property="Customer.Name"` (or `Scheduler StartProperty`/`Chart CategoryProperty` on a nested type) resolves `Customer` on `TItem`, then `Name` on the `Customer` type. `[DAM(PublicProperties)]` on `TItem` is **not** transitive into the nested property type, so `get_Name` on `Customer` is stripped. The reflection path throws `ArgumentException "Property Get method was not found"`; the expression path throws `"Expression must be readable"`. -- **Root cause:** DAM does not recurse into nested property types; `LinkerConfig` cannot name the consumer's `Customer`. -- **Remediation (layered, no single fix):** - 1. Flow DAM through `PropertyInfo.PropertyType` so the first nested level is rooted: - ```csharp - // PropertyAccess.cs GetPropertyTypeIncludeInterface - [return: DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] - private static Type? GetPropertyTypeIncludeInterface( - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] Type? type, string? property) - ``` - 2. In `PropertyAccess.GetValue` (`:233`), when `GetProperty` returns a `PropertyInfo` with a null `GetMethod`, throw a clear diagnostic ("Property 'X.Y' accessor was trimmed; preserve nested model type via DynamicDependency or LinkerConfig") instead of the opaque BCL exception. - 3. Document that deeper nested chains require the consumer to root the intermediate type (`[DynamicDependency]` / a `TrimmerRootDescriptor` in their app). - 4. Add a TrimTest nested-path scenario (section 3). - ---- - -### Medium - -The Medium findings divide into four families. Each entry lists file:line, the runtime symptom, and the exact fix. - -#### Reflection-over-`T` family (same root cause as H1-H4, narrower trigger or graceful degrade) - -- **M1. `RadzenDataGridColumn.SetColumnDefaults`** - `Radzen.Blazor/RadzenDataGridColumn.razor.cs:213/230/269`. `PropertyAccess.GetPropertyType(typeof(TItem), ...)` and `typeof(TItem).GetProperty(Property)` with unannotated `TItem`; `MakeGenericType(pt!)` NREs if `pt` is null. **Fix:** H1 DAM on `RadzenDataGridColumn`; also guard `RadzenDataGridColumn.razor.cs:215` with `typeof(IEnumerable<>).MakeGenericType(pt ?? typeof(object))`. -- **M2. `RadzenDataGridColumn.GetValue` nested paths** - `RadzenDataGridColumn.razor.cs:781`. Dotted `Property` routes to `PropertyAccess.GetValue` -> silent wrong/blank cells (not a crash). **Fix:** H1 + H14. -- **M3. `RadzenScheduler`** - `RadzenScheduler.razor.cs:647`. `PropertyAccess.Getter(StartProperty)` on a model hydrated only via JSON/interop. **Fix:** add `[DynamicallyAccessedMembers(PublicProperties | PublicFields)]` to `TItem` on `RadzenScheduler.razor.cs:55`. -- **M4. `RadzenTree` hierarchical binding** - `RadzenTree.razor.cs:318`. `PropertyAccess.GetValue/Getter` on `TextProperty`/`ChildrenProperty`/`CheckableProperty`; `Data` is `IEnumerable` (no generic anchor) - structurally weaker than the grid. Crashes only under `TrimMode=full` / trimmable node-type library. **Fix:** consider a generic `RadzenTree` with DAM; minimum, document the contract and add a TrimTest tree scenario. -- **M5. `RadzenDropDownDataGrid`** - `RadzenDropDownDataGrid.razor.cs:696`. Search-as-you-type builds multi-column `Where` over the item type via reflection. **Fix:** H1 DAM forwarding + TrimTest coverage with a filtered typed list. -- **M6. `RadzenPivotDataGrid` dynamic aggregation** - `RadzenPivotDataGrid.razor.cs:1310-1384`. `GetPropertyType(typeof(TItem), property)` then `items.Select(property).Sum(propertyType)`; on stripped property, Sum falls to `items.Count()` (wrong total) or `catch{return null}` (blank). **Fix:** H1; and prefer returning null over `items.Count()` for a misconfigured numeric property (`RadzenPivotDataGrid.razor.cs:1363-1365`). -- **M7. `RadzenDataGrid` class-level blanket suppression of 11 IL codes** - `RadzenDataGrid.razor.cs:88`. Hides grouping/sorting reflection over `TItem` and any future reflection in the 4000-line partial class. **Fix:** H1 DAM; move the 11 suppressions off the class onto the specific dynamic-LINQ members (`LoadGroups`, `GroupedPagedView`, sort helpers) so future reflection is not auto-silenced. -- **M8. `RadzenPivotDataGrid` class-level blanket suppression** - `RadzenPivotDataGrid.razor.cs:43`. Same pattern as M7; `catch{return null}` turns crashes into silent wrong totals. **Fix:** H1 + narrow suppression scope + the null-vs-Count fix in M6. -- **M9. `RadzenDataGridColumn.GetValue` reflection (engine)** - covered by H3/H14. -- **M10. `RadzenDataGrid.View` override launders RUC through non-RUC base (IL2046)** - `RadzenDataGrid.razor.cs:2126`. `OrderBy(string)`/`Cast(firstItem.GetType())` are RUC; the non-RUC base `View` forces an `IL2046` suppression. **Fix:** H1 DAM makes the suppressions guard safe code; keep the `IL2046` suppression (the base `View` cannot be RUC). -- **M11. `PagedDataBoundComponent` base virtuals not RUC though every override is unsafe** - `PagedDataBoundComponent.cs:298/314/332`. Structural; do **not** add RUC to the base (it would taint trim-safe `RadzenDataList` and not propagate past the re-suppressing override). **Fix:** H1 DAM + the runtime TrimTest sort/filter step. -- **M12. `RadzenDataAnnotationValidator.Validate`** - `RadzenDataAnnotationValidator.cs:76`. `PropertyAccess.Getter` + `Validator.TryValidateProperty` over the model; common attributes are safe (bound property is rooted; attribute instances are kept by default). Real residual: `[Compare("Other")]`, custom `ValidationAttribute` reflecting siblings, and resource-based messages (`ErrorMessageResourceType`). **Fix:** add `[DynamicallyAccessedMembers(All)]` to `TItem` on `RadzenTemplateForm` (`RadzenTemplateForm.cs:33`); preserve standard `ValidationAttribute` subtypes in LinkerConfig if resource messages are used; add a TrimTest validator scenario covering `[Compare]` + a resource-message attribute. -- **M13. `RadzenComponentActivator.CreateInstance` / `Override`** - `RadzenComponentActivator.cs:42/56`. DAM on the incoming `componentType` does not flow to the registered override type; `Activator.CreateInstance`/`MakeGenericType` on the override throws `MissingMethodException` under trimming. **Fix:** - ```csharp - public void Override() - => Override(typeof(TOriginal), typeof(TOverride)); - - public void Override(Type original, - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] Type @override) - ``` - -#### Dynamic-LINQ / suppression family - -- **M14. `DynamicExtensions` class-level suppression masks RUC + assembly-scan type resolution** - `DynamicExtensions.cs:11`. Custom filter expressions / enum casts named in filter strings resolve via `ResolveTypeFromAssemblies` (`ExpressionParser.cs:936`); trimmed type -> `"Could not resolve type"`. **Fix:** same as H11 (propagate RUC). Document that types named in `CustomFilterExpression` strings must be rooted by the consumer; harden the `ResolveTypeFromAssemblies` null path with a trimming-aware message. -- **M15. `ExpressionParser.ResolveTypeFromAssemblies`** - `ExpressionParser.cs:936`. `AppDomain.GetAssemblies().GetTypes()` to resolve a type by `FullName` from a filter string; the library itself emits enum `FullName` casts (`ExpressionSerializer.cs:221`). **Fix:** change `ExpressionSerializer.FormatValue` to emit the underlying numeric value with a primitive cast (e.g. `(int)2`) instead of the enum `FullName`, removing the assembly-scan dependency for enums; add a TrimTest enum-filter round-trip. -- **M16. `ExpressionParser` `MakeGenericMethod`/`MakeGenericType` + reflection over `T`** - `ExpressionParser.cs:360, 668, 785, 842, 913`. Nested `"Customer.Name"` resolves members on the un-DAM'd `Customer` type; `MakeGeneric*` is AOT-unsafe. **Fix:** H1 DAM (roots root-model members; nested types remain a documented consumer responsibility) + `[RequiresDynamicCode]` (M-AOT). -- **M17. `QueryableExtension` `MakeGenericMethod`/`MakeGenericType`/`AsQueryable`** - `QueryableExtension.cs:296, 314, 335, 400, 421, 443, 941, ...`. Trimming is mitigated (`System.Linq.Queryable` is `preserve="all"` at LinkerConfig line 4); the gap is AOT-only (value-type closed generics). **Fix:** `[RequiresDynamicCode]` on each (M-AOT). Do **not** add an `Enumerable` entry for this finding (`ToList` survives via static use) - but note H3 needs `Enumerable` for the `In`-filter reflection. -- **M18. `GroupByMany` builds `Expression>` per property** - `QueryableExtension.cs:143`. `GroupResult` is preserved (LinkerConfig line 80); the consumer's grouped property is not. **Fix:** H1 DAM; add `[DynamicallyAccessedMembers(PublicProperties | PublicFields)]` to the `T` of public `GroupByMany`; add a TrimTest grouping + pivot scenario. -- **M19. `QueryableExtension.Where(columns)` / `Where(RadzenDataFilter)` invoked from suppressed DataGrid paths; nested-type preservation gap** - `QueryableExtension.cs:1429`. Nested filter paths (`Customer.Address.City`) resolve members on intermediate types not covered by root-model DAM. **Fix:** H1 + H14; add a nested-path filter scenario and a `FilterOperator.Custom` scenario (the latter exercises the `ExpressionParser` string path at `:1471`). -- **M20. `ODataJsonSerializer.Serialize` nested complex/collection types not preserved** - `OData.cs:126`. `[DAM(PublicProperties)]` on `TValue` is shallow; nested navigation/element types are not preserved. In practice the `ComplexPropertiesConverter` *drops* class-typed complex properties from the OData body, so no observable loss for the common shape. Real residual: **user-defined struct** properties (Low, see L1). **Fix:** keep DAM; document the shallow-preservation contract; the struct case is the actionable part (L1). -- **M21. `GetExpression`/`GetNestedPropertyExpression` filter expression trees over `T`** - `QueryableExtension.cs:525`. Covered by H3/H13; string methods (`Contains`/`StartsWith`/`ToLower`) are on `System.String` and safe; the consumer-model member access is the gap. **Fix:** H1. - -#### AOT (`IL3050` / `[RequiresDynamicCode]`) family - AOT-only, library-wide - -The library has **zero `[RequiresDynamicCode]`** annotations and the AOT analyzer is off; enabling it surfaces ~214 `IL3050` warnings. Standard Blazor WASM `RunAOTCompilation` keeps an interpreter fallback (no crash), so these are Native-AOT-only - hence Medium. None is fixable via LinkerConfig (`IL3050` is a codegen, not a metadata, concern). - -- **M22. `DynamicTypeFactory.CreateType` uses `Reflection.Emit`** - `DynamicTypeFactory.cs:18` (`AssemblyBuilder.DefineDynamicAssembly`). Hard Native-AOT failure (`PlatformNotSupportedException`), reached from `ExpressionParser.cs:560` for `new {...}` projections (Pivot aggregates, dynamic `Select` with aliases). **Fix:** - ```csharp - // TrimMessages - internal const string DynamicCodeGeneration = - "Uses Reflection.Emit to generate types at runtime, which is not supported under AOT."; - // DynamicTypeFactory.cs:9 - [RequiresUnreferencedCode(TrimMessages.DynamicTypeGeneration)] - [RequiresDynamicCode(TrimMessages.DynamicCodeGeneration)] - public static Type CreateType(...) - ``` - Propagate to `ExpressionParser` (`:17`) and the public `DynamicExtensions.Select` overloads (`:88`/`:114`). Best durable fix: replace the anonymous-type emit with a `ValueTuple`/`Dictionary` projection so the path is AOT-survivable. -- **M23. `PropertyAccess.Getter` `Expression.Compile()`** - `PropertyAccess.cs:35, 115, 265`. `[RequiresDynamicCode]` is missing; under the AOT interpreter the simple member-access lambdas Radzen builds generally survive, so no mainstream crash. **Fix:** add `[RequiresDynamicCode]` alongside the existing `[RequiresUnreferencedCode]` so the requirement is honest; add parallel `IL3050` suppressions (with justification "interpreter handles simple property-access lambdas") at the component call sites that already suppress `IL2026`. -- **M24. `RadzenPivotDataGrid` / `RadzenDataGrid` build `RadzenNumeric` via `MakeGenericType`/`MakeGenericMethod` at render time** - `RadzenPivotDataGrid.razor.cs:2175/2199/2200`, `RadzenDataGrid.razor.cs:981/1005/1006`. Value-type `RadzenNumeric`/`` instantiations have no AOT-generated code; render-path so `[RequiresDynamicCode]` cannot be applied. **Fix:** guard with `if (RuntimeFeature.IsDynamicCodeSupported)` + a non-generic fallback, or force static instantiation of the common nullable-numeric closed generics in a preserved never-called method. -- **M25. `QueryableExtension` / `ExpressionParser` `MakeGeneric*`/`AsQueryable` (20 + 6 IL3050)** - same family as M17/M16. **Fix:** `[RequiresDynamicCode]` on the public entry points; enable `true` to make these visible (M28). -- **M26. `DropDownBase.GetItemType` `MakeGenericType(Nullable<>)` at render time** - `DropDownBase.cs:315` (and `:534, 1655, 1663, 1713`). Value-type `Nullable<>` instantiation, AOT-unsafe; on the normal render path. **Fix:** guard with `RuntimeFeature.IsDynamicCodeSupported` or force-instantiate common nullable value types; note the csproj advertises `IsAotCompatible=true` for net8.0 but the analyzer never actually runs - either honor it (M28) or drop the claim. -- **M27. `AIChatService` / OData JSON of consumer types under AOT** - reflection-STJ is `[RequiresDynamicCode]`. **Fix:** `[RequiresDynamicCode]` on `ReadAsync`/`Serialize`/`AIChatService` send path; long term, a source-generated `JsonSerializerContext`. -- **M28. AOT analyzer disabled; `IsAotCompatible=true` is inert** - `Radzen.Blazor.csproj:7-9`. The csproj's `IsAotCompatible` assignment does not reach the SDK's `EnableAotAnalyzer` derivation, so 214 `IL3050` are invisible. **Fix:** set `true` explicitly, triage the warnings via the annotations above, and **decide the contract**: either back `IsAotCompatible=true` with the analyzer or remove it and document "trim-compatible, not Native-AOT-compatible (Blazor WASM `RunAOTCompilation` works via interpreter fallback)." - -#### JS-interop / `RequiresUnreferencedCode` family - -- **M29. `RadzenHtmlEditor.GetSelectionAttributes`** - `RadzenHtmlEditor.razor.cs:511` (and the class-level `IL2091` suppression at `:56`). Public generic forwards consumer `T` to `JSRuntime.InvokeAsync`; the class suppression hides the `IL2091` from the consumer. Internal callers (`ImageAttributes`/`LinkAttributes`) are safe (LinkerConfig lines 36-37), but an external `T` is unprotected. **Fix:** - ```csharp - public ValueTask GetSelectionAttributes< - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties | DynamicallyAccessedMemberTypes.PublicConstructors)] T>( - string selector, string[] attributes) - ``` - This satisfies `InvokeAsync` at `:518` (so the misapplied class-level `IL2091` is no longer needed for this call) and flows the requirement to the consumer's `T`. -- **M30. `ODataJsonSerializer` + `ComplexPropertiesConverter` nested-type STJ** - `OData.cs:126/153`. Covered by M20/L1; the converter's `T` is unannotated (`IL2091` suppressed). **Fix:** - ```csharp - public class ComplexPropertiesConverter< - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)] T> : JsonConverter - ``` - then remove the now-unnecessary `IL2091` suppressions at `OData.cs:125/152`. -- **M31. `DialogService.OpenSide(Type)` / `OpenSideAsync(Type)` lack DAM + RUC** (asymmetry vs `Open(Type)`) - `DialogService.cs:298/245`. The generic `OpenSide` overloads are correctly `[DAM(All)]`; the dynamic-`Type` side overloads have neither, yet the same `Type` reaches `OpenComponent`. **Fix:** - ```csharp - [RequiresUnreferencedCode(TrimMessages.GenericMethodReflection)] - public void OpenSide(string title, - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type componentType, - Dictionary? parameters = null, SideDialogOptions? options = null) - // same for OpenSideAsync(Type) at :245 - ``` -- **M32. `RadzenComponentActivator` `MakeGenericType` + `Activator.CreateInstance` on override type** - duplicate root cause of M13; the override-as-`Type` (`RadzenComponentActivator.cs:42`) is the same gap viewed from the activator side. **Fix:** the `Override` parameter annotations in M13. - ---- - -### Low - -- **L1. `ODataJsonSerializer.Serialize` - user-defined struct properties** - `OData.cs:97-98` (`IsComplex` requires `type.IsClass`). A user struct property is not classified complex, not stripped by the converter, and serializes as `{}` under trimming (silent loss in the POST body). BCL structs (Guid, DateTime, decimal) are safe. **Fix:** broaden `IsComplex` so non-`System` user value types are also excluded from the OData body, e.g. treat `baseType.IsValueType && !baseType.IsPrimitive && namespace not starting with "System"` as complex. Add a TrimTest OData scenario with a user-struct property. -- **L2. `DropDownBase` multi-select `Activator.CreateInstance()` on a custom `IList` `TValue`** - `DropDownBase.cs:1640`. `List`/`ObservableCollection` are safe (BCL ctors rooted); a custom `class SelectedProducts : List{}` whose ctor is reflection-only throws `MissingMethodException`. **Fix:** - ```csharp - public class DropDownBase< - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] T> - : DataBoundFormComponent - ``` - (compatible with H1's `PublicProperties` - OR the two member types together), then remove the `IL2091` suppression at `:1629`. Or wrap line 1640 in try/catch falling back to a `List<>`. -- **L3. `DropDownBase.ReferenceGenericCollectionAssignment` reflective `GetMethod(Add/Remove/Clear)`** - `DropDownBase.cs:1717-1719`. Dead on all shipped paths (`PreserveCollectionOnSelection` is never set true). If a consumer subclasses and opts in with a custom `ICollection<>`, null `GetMethod` -> `NullReferenceException` from the `!.Invoke`. **Fix:** set `canHandle = (clearMethod != null && addMethod != null && removeMethod != null)` so a trimmed method degrades to the default `List<>` rebuild instead of NRE. -- **L4. `RadzenComponentActivator` override (DAM not flowed)** - covered by M13/M32; Low when viewed as the opt-in advanced feature. -- **L5. Nullable-enum filter row builds an anonymous `new { Value, Text }` bound to a DropDown via `TextProperty`/`ValueProperty`** - `RadzenDataGrid.razor:491`. The anonymous type's `get_Value`/`get_Text` are stripped (only ctor + backing fields are statically referenced), so the filter DropDown throws `ArgumentException "'Value' is not a member of type '<>f__AnonymousType...'"` when filtering a nullable-enum column. Cannot be fixed by LinkerConfig (anonymous type has an unspeakable name). **Fix:** replace the anonymous object with the preserved named type `DropDownItem` (already `preserve` at LinkerConfig line 13): - ```razor - new object[]{ new Radzen.DropDownItem { - Value = Convert.ChangeType(-1, Enum.GetUnderlyingType(Nullable.GetUnderlyingType(column.FilterPropertyType) ?? column.FilterPropertyType)), - Text = EnumNullFilterText } } - ``` - Add a TrimTest nullable-enum filter column. -- **L6. `ExpressionParser.ResolveTypeFromAssemblies` enum-cast resolution (PivotDataGrid drill-down)** - `ExpressionParser.cs:936`. PivotDataGrid drill-down feeds an enum `FullName` cast into `items.Where(string)`; trimmed enum -> `"Could not resolve type"`. Standard enum column filtering uses the typed `FilterDescriptor` path and is unaffected. **Fix:** the `ExpressionSerializer` numeric-cast change from M15 closes this; or document/root the enum. -- **L7. `ODataJsonSerializer.Serialize` data-loss nuance** - `OData.cs:140`. No crash (named reference types under-serialize gracefully; the converter drops complex props by design). Already mitigated for the crash dimension. **Fix:** document that nested complex/collection types referenced by `TValue` must be independently rooted; the actionable struct sub-case is L1. - ---- - -## 3. Hardening the Process - -The trim analyzer is clean and the existing `TrimTest` publishes clean, yet 53 real hazards remain. The gate is meaningless because (a) the app's model lives in the rooted app assembly, (b) the test only checks publish-time IL warnings and never runs the rendered components, and (c) it exercises ~4 components out of the library. - -### (a) Scenarios to add to `Radzen.Blazor.TrimTest` - -**Critical structural change:** move the data-item model into a **separate referenced class library** compiled with `true` (`Radzen.Blazor.TrimTest.Models`). This is the *only* configuration that reproduces the reflection-over-`T` family (H1-H14). Keep the model's getters unreferenced by app code (set only via object initializers / JSON). - -Add to `App.razor` (binding by string `Property`/`ValueProperty`/`CategoryProperty`, and driving runtime interaction): - -```razor -@* DataGrid: sort + filter (every operator family) + group + nested path *@ - - - - - @* enum *@ - @* nested *@ - - - - -@* DropDown / ListBox / DropDownDataGrid by ValueProperty/TextProperty *@ - - - -@* Chart by property name *@ - - -@* PivotDataGrid: dynamic aggregation (Sum/Average over a value type) *@ - - - - - -@* Scheduler, Tree, Upload, FileInput, GoogleMap (roots the JSInvokable DTO graph) *@ - - - - - - - -``` - -And in `OnAfterRenderAsync(firstRender)`, **actually run** the reflective paths (publish-only IL checks cannot catch the runtime throw): -```csharp -grid.ColumnsCollection[0].SetFilterValue("A"); // string Contains -// set numeric GreaterThan, bool equals, DateTime range, enum In on other columns -grid.Groups.Add(new GroupDescriptor { Property = "Status" }); -await grid.OrderBy("UnusedSortOnly"); -await grid.Reload(); -``` - -Add direct exercises of the JS-interop / JSON paths that markup cannot trigger at publish time: -```csharp -// OData / ReadAsync round-trip against a stubbed HttpResponseMessage (no network) -var result = await stubbedResponse.ReadAsync>(); -// record case to catch ctor stripping: -var rec = await stubbedResponse.ReadAsync(); // record OrderRecord(int Id, string Name); -// OData serialize with nested complex + collection + user struct + DateTime? -var json = Radzen.ODataJsonSerializer.Serialize(new Parent { Id = 1, Nav = new(){Name="x"}, Items = new(){ new(){Name="y"} }, Loc = new Geo{...} }); -// dynamic-LINQ projection that emits an anonymous type (AOT path) -var proj = orders.AsQueryable().Select("CustomerName as Name, Total"); -``` - -### (b) Automated tests - -Extend `LinkerConfigTests` (or add `InteropPreservationTests`) so the suppression promises are machine-checked, not hoped: - -1. **JSInvokable parameter/return preservation.** Reflect over the `Radzen.Blazor` assembly, find every `[JSInvokable]` method, and for each parameter type and `InvokeAsync`/`InvokeVoidAsync` argument type that is a Radzen-owned DTO, assert it (or a `` for it) is present in `LinkerConfig.xml`. This would have caught H6/H7/H8 (`FileInfo`, `PreviewFileInfo`, `GoogleMapClickEventArgs`). -2. **`InvokeAsync` return-type preservation.** Scan for `InvokeAsync` call sites where `T` is a Radzen DTO and assert preservation (would catch the `Rect`/HTML-editor DTO family before it regresses). -3. **No anonymous type reaches an interop boundary.** A Roslyn-analyzer-style test (or a compiled IL scan) that fails if a `new { ... }` expression flows into `JSRuntime.InvokeAsync`/`InvokeVoidAsync` or `JsonSerializer.Serialize`. This guards the exact crash class that started this effort and would catch L5. -4. **DAM-consistency test.** Assert that the public data-component generic parameters (`RadzenDataGrid`, `DropDownBase`, `PagedDataBoundComponent`, etc.) carry `[DynamicallyAccessedMembers]` - fails if a future refactor drops the H1 annotation. -5. **Runtime smoke tests (bUnit)** against the trimmable-model library: render the grid/dropdown/chart, click a sort header, apply a filter, group - assert no exception and non-empty cells. - -### (c) CI gate - -Add a CI leg that runs `dotnet publish -c Release` on `Radzen.Blazor.TrimTest` with `PublishTrimmed=true` and `TreatWarningsAsErrors=true`, then **launches the published app and drives the runtime scenarios above** (a Playwright/Selenium pass clicking sort headers, applying filters, opening dialogs, selecting an upload file). The current `nuget.yml` only builds/tests/packs - it never publishes trimmed. Add a second leg with `true` (requires the `wasm-tools` workload) and a third with `true` on the library to gate the `IL3050` family (M22-M28). - -### (d) Trimming-safety checklist for future PRs - -- Does this code pass an object to `JSRuntime.InvokeAsync`/`InvokeVoidAsync`, or call `JsonSerializer.Serialize`/`Deserialize`? If yes: it must be a **named** type (never `new { }`), and that type must be in `LinkerConfig.xml` with `preserve="all"` (or the generic carries `[DynamicallyAccessedMembers]`). -- Does this code add a `[JSInvokable]` method? Its declaring type needs `preserve="methods"` and **every parameter/return DTO** needs `preserve="all"`. -- Does this code reflect over a consumer generic `T` (`typeof(T).GetProperty`, `PropertyAccess.*`, dynamic LINQ, `Expression.PropertyOrField`)? The generic parameter must carry `[DynamicallyAccessedMembers(PublicProperties | PublicFields)]` - do **not** add a new `[UnconditionalSuppressMessage]` to silence it. -- Does this code use `Reflection.Emit`, `MakeGenericType`/`MakeGenericMethod` over a runtime type, or `Expression.Compile`? Add `[RequiresDynamicCode]` next to `[RequiresUnreferencedCode]`. -- Adding a `[UnconditionalSuppressMessage]`? Prefer flowing the requirement (DAM/RUC) so the consumer is warned. Scope the suppression to the specific member, never the whole class. The justification must be *enforceable*, not a hope. -- New component that binds by string `Property`/`ValueProperty`/`CategoryProperty`? Add a TrimTest scenario over the **trimmable model library** and a runtime interaction step. - ---- - -## 4. What Was Checked and Cleared - -The following were investigated and found **not** to be hazards (builds confidence that the real findings are not noise): - -- **Framework event-arg DTOs to JS** (`MouseEventArgs` -> `Radzen.resizeSplitter`, `RadzenSplitterPane`): named framework type, rooted by Blazor's own event-dispatch infrastructure, analyzer-visible. Safe. -- **`RadzenSlider` generic `TValue` to JS**: all supported `TValue` are numeric primitives / `IEnumerable` with intrinsic STJ converters; non-`IConvertible` types are unsupported regardless of trimming. Safe. -- **All 40 `[JSInvokable]` declaring types** (including 8 declared in `.razor` files): every one matches a `preserve="methods"` entry in `LinkerConfig.xml`; no inherited-method gap. Verified complete. -- **AIChat `ChatCompletionRequest`/`ChatCompletionMessage` (`List Messages`)**: only ever populated with the named, `preserve="all"` `ChatCompletionMessage`; no anonymous types; no consumer-injectable element type. Safe (maintenance footgun noted). -- **`RadzenDataGrid` markup instantiation**: empirically verified that the Razor compiler's `OpenComponent<[DAM(All)] TComponent>` flows `All` to `Order`, preserving even an unbound property - so the *markup, app-assembly-model* case is safe. (The H1 hazard is the *separate trimmable-assembly model* case, which this does not cover.) -- **Enum reflection** (`EnumExtensions.GetDisplayDescription`, `EnumAsKeyValuePair`, `ConvertType.ChangeType` enum branch, `Enum.GetValues`): ILLink does not strip enum value fields (atomic with the type) nor custom-attribute instances by default (no `RemoveAttributeInstances` config in the repo), and the attribute types are statically rooted. Degradation would be cosmetic (raw name), never a crash. Safe under trimming (AOT `Enum.GetValues(Type)` is a separate, out-of-scope `IL3050`). -- **`DataRow`/`DataTable`/`IDictionary` binding** (`Type.GetType("System."+name)`, `GetProperty("Item")`): the `(Type)it["x"]` string format makes `typeString` empty -> always falls back to `typeof(object)`; `DataRow.Item` indexers are rooted by `System.Data.Common`'s own internal callers; the path is `[RequiresUnreferencedCode]`. Safe. -- **`DialogService.Open`/`OpenAsync`/`OpenSide`**: `[DAM(All)]` on the generic parameter roots all of `T`'s members at the call site; the downstream `Type`-laundering through `OpenComponent` does not un-root them. Safe (the dynamic-`Type` *side* overloads are the M31 gap). -- **`ComplexPropertiesConverter` (write path)**: `Serialize` is `[DAM(PublicProperties)]`; the converter is only ever parameterized by that concrete `TValue`, so STJ's requirement is satisfied; nested complex props are intentionally dropped from the body. Safe except the struct edge (L1). -- **DataGrid grouping / dynamic data binding (`TItem==object`, DataTable/ExpandoObject) / Excel-CSV export / Gauge-RangeNavigator-Sankey-Heatmap-Treemap-SpiderChart interop returns / HtmlEditor typed returns / Spreadsheet `VirtualRegion` + xlsx**: each verified either rooted by the non-trimmed consumer app assembly, preserved in `LinkerConfig`, primitive-typed, or reflection-free XML. Export byte-generation is server-side, outside the package. Safe. - -Net: the dismissed set confirms the audit distinguishes "rooted by framework/consumer assembly or already in LinkerConfig" from "genuinely unprotected library-side gap." The 53 real hazards are concentrated in exactly the analyzer's documented blind spots - reflection over the consumer's model type, JS-interop DTO deserialization, dynamic LINQ, and `System.Text.Json` of user types - and the single highest-leverage fix is the H1 DAM annotation on the data-component base generics, followed by the four LinkerConfig DTO additions (H5-H8) and the `ReadAsync` DAM annotation (H9). \ No newline at end of file diff --git a/e2e/DEMOS_DEEP_FINDINGS.md b/e2e/DEMOS_DEEP_FINDINGS.md deleted file mode 100644 index 284fa9a44..000000000 --- a/e2e/DEMOS_DEEP_FINDINGS.md +++ /dev/null @@ -1,108 +0,0 @@ -# Demos deep-interaction trim crawl - findings - -Upgrades the shallow demos crawl (`demos-crawl.mjs`, load + render assertion) to -**deep interaction** coverage of the trim-risky components only - the ones heavy on -reflection, dynamic LINQ, anonymous-object materialization, `[JSInvokable]` DTOs, -`MakeGenericType`, and JS interop. Every other route is intentionally ignored. - -The crawler (`demos-deep-crawl.mjs`) drives each component through a real user -interaction, then checks for a trim break: the Blazor error UI (`#blazor-error-ui`) -becoming visible, an unhandled page error, or a `console.error` raised **during** the -interaction. A clean interaction = PASS. - -## Scope (same as the shallow crawl - read this) - -This exercises **Radzen's own** reflection / interop / render code, not consumer -models. The demos' models (Northwind etc.) live in the **app assembly**, which under -the default partial trim mode is **not** member-trimmed, so this catches Radzen-OWN -trim regressions (stripped `[JSInvokable]` DTOs, render-path reflection, dynamic-LINQ -expression building, `MakeGenericType` aggregates) - not the consumer-model hazard -(H1) covered by `Radzen.Blazor.TrimRuntimeTest` / `Radzen.Blazor.TrimTest`. - -## Result - -**14/14 component interactions PASS, 0 FAIL, 0 selector-unresolved** - verified against -the **actual WASM-client-trimmed publish** (not just the Debug serve). No risky -component broke under deep interaction. - -Proof trimming was in effect for the verification run: the IL linker ran ("Optimizing -assemblies for size"), `Radzen.Blazor.wasm` was emitted at ~4.0 MB (the trimmed size), -and the publish produced **0 IL2xxx/IL3xxx** warnings. The interaction signals were -byte-for-byte identical between the trimmed and the non-trimmed Debug serve (same row -counts, same 60 enum options, same 96/185 filtered items, same 314 pivot aggregate -cells, Bold still mutated the markup, the chart tooltip still rendered, dialogs still -opened) - so every reflection / dynamic-LINQ / anonymous-object / `[JSInvokable]` / -`MakeGenericType` / interop path the interactions touch survives trimming. - -CI runs the identical script against the same trimmed publish on every build - see -ci.yml `demos-deep-interaction-crawl` (non-blocking). - -## Components driven, the hazard each exercises, and the interaction signal - -| # | Route | Component / hazard | Interaction | Signal observed | -|---|---|---|---|---| -| 1 | `/datagrid-filter-api` | DataGrid filtering - dynamic LINQ `Where` + `string.Contains` | type "an" in the inline Simple filter, Enter | row count changed on filter | -| 2 | `/datagrid-sort-api` | DataGrid sorting - dynamic LINQ `OrderBy` over T | click a `.rz-sortable-column` header | first cell re-ordered (Steven -> Nancy) | -| 3 | `/datagrid-grouping-api` | DataGrid grouping - `GroupResult` / dynamic grouping | expand then collapse a group row toggler | toggled twice, no error | -| 4 | `/datagrid-enum-filter` | DataGrid enum / nullable-enum filter - anonymous `{Text,Value}` objects | open the `.rz-grid-filter-icon` popup, open the enum value dropdown, pick a value, Apply | 60 enum options materialized + filter applied | -| 5 | `/dropdown-datagrid` | DropDownDataGrid - `TextProperty` reflection | open, type "a" in the embedded grid's `.rz-lookup-search-input` | embedded grid filtered to 15 rows | -| 6 | `/dropdown-filtering` | filterable DropDown - `TextProperty` reflection | open, type "a" in `.rz-dropdown-filter` | 96 filtered items | -| 7 | `/autocomplete` | AutoComplete - `TextProperty` reflection | type "a" | 185 filtered suggestions | -| 8 | `/pivot-data-grid` | PivotDataGrid - `MakeGenericType` aggregates over T | assert aggregate cells; click a sortable pivot header | 314 aggregate cells materialized | -| 9 | `/upload` | Upload - `[JSInvokable]` PreviewFileInfo/FileInfo DTO | `setInputFiles` a temp .txt | 1 file row rendered (preview DTO round-trip) | -| 10 | `/html-editor` | HtmlEditor - interop DTO `RadzenHtmlEditorCommandState` | type, select-all (DOM range), click Bold | Bold command mutated the markup | -| 11 | `/spreadsheet` | Spreadsheet - `[JSInvokable]` CellEventArgs (the original production crash) | click a cell, type a value, Enter, select a 2x2 range | cell edit + range selection, no interop error | -| 12 | `/column-chart` | Chart - JS interop (tooltip / measure-text) | hover the column bars | tooltip element (`.rz-chart-tooltip`) rendered | -| 13 | `/dialog` | Dialog - `DialogService` dynamic open | click the demo open button, capture + close the modal | 2 dialog containers, opened and closed | -| 14 | `/scheduler` | Scheduler - `StartProperty`/`EndProperty` reflection + JS interop | click an appointment | edit dialog opened (2 dialog containers) | - -## Selectors - -Driven by Radzen's **stable component CSS classes**, consistent across the library, so -the suite survives demo content/markup changes rather than binding to per-demo markup: - -- DataGrid: `.rz-cell-filter input.rz-textbox` (Simple text filter), `th.rz-sortable-column`, - `.rz-grid-filter-icon` + `.rz-overlaypanel .rz-grid-filter` (Advanced filter popup), - `.rz-row-toggler` (group expand). -- DropDown / AutoComplete: `.rz-dropdown`, `.rz-dropdown-trigger`, `.rz-dropdown-filter`, - `.rz-lookup-search-input`, `.rz-autocomplete-input`, `.rz-dropdown-item`. -- Pivot: `.rz-pivot-table`, `.rz-pivot-drill-down-header .rz-tree-toggler`. -- Upload: `input[type=file]`, `.rz-fileupload-row`. -- HtmlEditor: `.rz-html-editor-content`, `.rz-html-editor-toolbar button[title*="Bold"]`. -- Spreadsheet: `.rz-spreadsheet`, `[data-row][data-column]`. -- Chart: `.rz-chart svg`, `.rz-chart-tooltip`. -- Dialog: `.rz-dialog-wrapper`, `.rz-dialog`, `.rz-dialog-titlebar-close`. -- Scheduler: `.rz-scheduler`, `.rz-event`, `.rz-slot`. - -## How to reproduce - -```sh -# Dev loop - drive a NON-trimmed Debug serve and iterate fast (reuse one server): -ASPNETCORE_ENVIRONMENT=Development \ - dotnet run --project RadzenBlazorDemos.Host --no-build --urls http://localhost:8088 -# (launchSettings overrides ASPNETCORE_URLS; pass --urls so Chromium can reach the port) - -cd e2e && npm install && npx playwright install chromium -DEMOS_URL=http://localhost:8088 node demos-deep-crawl.mjs -``` - -The trimmed publish + serve is exactly the shallow crawl's (see `DEMOS_TRIM_FINDINGS.md`); -CI reuses those flags for the deep job. Exit 0 if all PASS, else 1. - -## Knobs (env vars) - -- `DEMOS_URL` (default `http://localhost:8088`) -- `DEMOS_PAGE_TIMEOUT_MS` (nav timeout, default 45000) -- `DEMOS_SETTLE_MS` (post-render / post-interaction settle, default 2500) -- `DEMOS_STEP_TIMEOUT_MS` (per-selector resolve budget before selector-unresolved, default 8000) -- `DEMOS_REPORT` (path to write a JSON result report) - -## Known gaps (honest) - -- **GoogleMap (`/googlemap`)**: not crawled - needs an external Google Maps API key/network. -- The CI deep-interaction job is **non-blocking** (`continue-on-error: true`) initially: - DOM interactions can be flaky on a cold CI runner. Promote to blocking once it has - proven stable across a few runs. -- Pivot drill-down: the default `/pivot-data-grid` layout is flat (no collapsible groups), - so the drill-down toggle is exercised only opportunistically; the `MakeGenericType` - aggregate hazard is still fully exercised because the aggregate cells render on load. diff --git a/e2e/DEMOS_TRIM_FINDINGS.md b/e2e/DEMOS_TRIM_FINDINGS.md deleted file mode 100644 index d49de60f9..000000000 --- a/e2e/DEMOS_TRIM_FINDINGS.md +++ /dev/null @@ -1,138 +0,0 @@ -# Demos trim-safety crawl - findings - -Discovery MVP: publish the Radzen Blazor demos with **member-trimming of `Radzen.Blazor`** -enabled, serve them, and crawl high-risk component pages in a real browser to find -components that break at runtime under trimming. - -## Result - -**No Radzen component broke under member-trimming.** 24/24 crawled routes rendered an -interactive Radzen component with no Blazor error UI and no runtime console/page error -attributable to Radzen. Driving a column-header sort on a DataGrid (`/datagrid-sort-api`) -re-ordered the rows correctly, so the reflection-over-`T` sort/filter expression path - the -path the static `TrimmingContractTests` / `trim-smoke` gate flags - works here. - -### Why it passes (scope, read this) - -This crawl exercises **Radzen's own** reflection / JS-interop / render code, not consumer -models. The demos' models (Northwind etc.) live in the **app assembly**, which under the -default **partial** trim mode is **not** member-trimmed - so Radzen's reflection over those -models still finds the getters/setters. That is the intended scope: it catches Radzen-OWN -trim bugs (stripped `[JSInvokable]` DTOs, render-path reflection, interop), and there are -none. It does **not** reproduce the consumer-model hazard (H1) where a trimmable consumer -assembly loses members Radzen reads by `string Property` - that is what -`Radzen.Blazor.TrimRuntimeTest` / `Radzen.Blazor.TrimTest` cover. - -## What was trimmed (proof trimming actually happened) - -- Published the ASP.NET Core host `RadzenBlazorDemos.Host` against the **local** - `Radzen.Blazor` source (`true>` on the `trim` branch), with member-trimming - scoped to the **WASM client only** (`-p:TrimWasmClient=true`). The server is left - untrimmed on purpose - trimming it breaks MVC application-part discovery - (`Type.GetType` over a trimmed `ProvideApplicationPartFactoryAttribute` factory). -- The IL linker ran ("Optimizing assemblies for size"). `Radzen.Blazor.wasm` shrank from - 4,201,984 bytes (untrimmed source DLL) to 4,009,753 bytes in the published `_framework` - (~190 KB / ~4.5% of members removed). 0 IL2xxx/IL3xxx trim warnings (the library is - annotated/audited on this branch). -- The reduction is modest because the demos use almost the whole public surface of Radzen, - so app-level member-trimming only removes what is genuinely unreachable. - -## How to reproduce - -```sh -# 1. Publish the host; trim only the WASM client, reference local Radzen source. -dotnet publish RadzenBlazorDemos.Host/RadzenBlazorDemos.Host.csproj -c Release \ - -p:RadzenLocalProject=true -p:TrimWasmClient=true \ - -p:GenerateLlmsTxt=false -p:CopyDemoPagesToWwwroot=false \ - -o - -# 2. The demo-source-viewer dir is carved out; create the empty dir the host expects. -mkdir -p /wwwroot/demos /wwwroot/images - -# 3. Serve (use a port Chromium does not block - NOT 5060). -cd && ASPNETCORE_URLS=http://localhost:8088 ASPNETCORE_ENVIRONMENT=Production \ - dotnet RadzenBlazorDemos.Host.dll - -# 4. Crawl. -cd e2e && npm install && npx playwright install chromium -DEMOS_URL=http://localhost:8088 node demos-crawl.mjs -``` - -## Per-route results - -`data` = the example's data source. `inline` = in-memory list literals (failures here would be -purely Radzen-own). `db` = EF Core **InMemory** provider + Northwind models in the app -assembly (still 100% client-side, no server DB; seeded in-process via `NorthwindContext.AddData`). - -| Route | Component | Data | Result | Notes | -|---|---|---|---|---| -| `/` | Home | inline | PASS | | -| `/datagrid` | DataGrid | db | PASS | | -| `/datagrid-dynamic` | DataGrid (dynamic data) | inline | PASS | 10 data rows | -| `/datagrid-enum-filter` | DataGrid (enum filter) | inline | PASS | | -| `/datagrid-filter-template` | DataGrid (filter template) | inline | PASS | | -| `/datagrid-realtime` | DataGrid (realtime) | inline | PASS | | -| `/datagrid-filter-api` | DataGrid (filtering) | db | PASS | rows populated | -| `/datagrid-sort-api` | DataGrid (sorting) | db | PASS | header-click sort re-ordered rows | -| `/datagrid-grouping-api` | DataGrid (grouping) | db | PASS | | -| `/datagrid-multiple-selection` | DataGrid (selection) | db | PASS | | -| `/datagrid-column-template` | DataGrid (column template) | db | PASS | | -| `/pivot-data-grid` | PivotDataGrid | db | PASS | aggregates rendered | -| `/dropdown` | DropDown | db | PASS | | -| `/dropdown-datagrid` | DropDownDataGrid | db | PASS | | -| `/listbox` | ListBox | db | PASS | | -| `/tree` | Tree | db | PASS | | -| `/tree-checkboxes` | Tree (checkboxes) | db | PASS | | -| `/charts` | Chart (gallery) | inline | PASS | SVG rendered | -| `/column-chart` | Chart (column) | inline | PASS | | -| `/pie-chart` | Chart (pie) | inline | PASS | | -| `/scheduler` | Scheduler | inline | PASS | | -| `/radial-gauge` | RadialGauge | inline | PASS | | -| `/html-editor` | HtmlEditor | inline | PASS | | -| `/upload` | Upload | inline | PASS | | - -## Ignored (non-trim) noise - -The detector filters two error classes that are NOT trim breakage: - -- **CSP `img-src` violations** for `/images/*.svg|png`: the demos ship a strict - `Content-Security-Policy: img-src data: https:`, which blocks local images served over - `http` once the demo-source-viewer / image assets are carved out of the lean publish. -- **`Uncaught ReferenceError: define is not defined`**: the lazy-loaded Monaco editor - (Edit Source / Playground) AMD loader, which is deliberately not exercised. Pages render - fully without it. - -## Carve-outs and coverage gaps (honest) - -- **Roslyn live-code editor / Playground** and **`/docs/api`** (lazy `Radzen.Blazor.Api.dll`, - `Microsoft.CodeAnalysis.*`): not crawled (trim/AOT-hostile, irrelevant to component trim-safety). -- **GoogleMap (`/googlemap`)**: not crawled - needs an external Google Maps API key/network. -- **Server-side prerender vs WASM hydration**: the demos are a Blazor Web App - (`InteractiveWebAssembly`, `blazor.web.js`). Each page is prerendered by the **untrimmed** - server and then hydrated by the **trimmed** WASM client. Trim breakage would surface during - or after hydration as a page/console error or a stuck `#blazor-error-ui` - none did. -- Interaction depth is shallow (load + one sort/select on a subset). Deeper edit/filter-popup - flows were not automated. - -## Build-infra blocker found (pre-existing, unrelated to trimming) - -A clean `dotnet build/publish -c Release` of the demos against the **local project** -(`RadzenLocalProject=true`) crashes in the SDK 10.0.301 task -`GenerateStaticWebAssetsDevelopmentManifest` with `InvalidOperationException: Sequence -contains more than one element`. Root cause: `Radzen.Blazor.Api` referenced the -`Radzen.Blazor` **NuGet package** in Release while the demos referenced the **project**, so two -`_content/Radzen.Blazor` static-asset sets (package + project, each fingerprinted) collided and -tripped the SDK dev-manifest tree builder. Reproduces with the package ref too once both -sources are present. Fixed for the crawl by routing the whole graph through the local project -(see csproj changes). This is a demos/SDK build quirk, **not** a `Radzen.Blazor` runtime trim -issue. - -## Reusable scaffolding committed - -- `e2e/demos-crawl.mjs` - the Playwright crawler (curated routes, noise filter, positive - render assertion, JSON report via `DEMOS_REPORT`). -- `.github/workflows` - a `workflow_dispatch` CI job (manual) that publishes the host - trimmed and runs the crawl. -- Publish flags added to `RadzenBlazorDemos.csproj` / `Radzen.Blazor.Api.csproj`: - `RadzenLocalProject`, `TrimWasmClient`, and skip switches for `GenerateLlmsTxt` / - `CopyDemoPagesToWwwroot`.