Repository navigation
feat: NativeAOT & trimming compatibility for KumikoUI.Core and KumikoUI.SkiaSharp - #1
michaelstonis wants to merge 3 commits into
Conversation
- Replace Expression.Lambda<T>().Compile() in DataGridSource.BuildAccessor with a PropertyInfo-based closure, eliminating the [RequiresDynamicCode] blocker for NativeAOT. The PropertyInfo chain is still resolved once and cached, so repeated access per-column remains fast. - Add [RequiresUnreferencedCode] to all public and private methods that use reflection-based property access by name (GetCellValue, SetCellValue, GetCellDisplayText, GetOrCreateAccessor, BuildAccessor, SetPropertyValue, GetCellValueDirect, DrawnComboBox.GetPropertyValue, DrawnComboBox.SetItemsFromSource). This ensures consuming apps receive a trimmer/AOT warning and know to preserve their data item types. - Add [UnconditionalSuppressMessage(IL2026)] to internal plumbing methods (PassesAllFilters, PassesOtherFilters, RebuildView, BuildGroupedFlatView, CollectColumnValues) that call the annotated helpers as an implementation detail — reflection here is intentional and controlled. - Remove unused 'using System.Linq.Expressions' from DataGridSource.cs. - Set <IsAotCompatible>true</IsAotCompatible> on KumikoUI.Core and KumikoUI.SkiaSharp project files to enable the AOT/trim analyzers and declare library compatibility. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Eliminates all remaining IL2026 warnings from library-internal code by
providing reflection-free delegate alternatives and suppressing intentional
reflection in internal plumbing.
DataGridColumn — two new optional properties:
- ValueAccessor (Func<object, object?>) — AOT-safe property reader
- ValueSetter (Action<object, object?>) — AOT-safe property writer
When set, all internal data paths (GetCellValue, SetCellValue, sort,
grouping, filter, summaries) use the delegate directly and skip the
reflection-based accessor cache entirely.
DrawnComboBox — two new optional properties:
- DisplaySelector (Func<object, string>) — AOT-safe display text
- ValueSelector (Func<object, object?>) — AOT-safe value extraction
SetItemsFromSource() prefers these over DisplayMemberPath/ValueMemberPath
when set. Callers that only use the delegates incur no reflection at all.
Internal plumbing annotated with [UnconditionalSuppressMessage(IL2026)]:
- DataGridSource.GetUniqueColumnValues
- DataGridRenderer.DrawRows, DrawFrozenRows, DrawRowDragOverlay
- EditSession.BeginEdit, CommitEdit, ToggleBooleanCell
- GridLayoutEngine.CalculateAutoFitWidth
- CellEditorFactory.CreateComboBoxEditor
The reflection path (PropertyName / DisplayMemberPath) continues to work
unchanged — existing code requires no modifications. Reflection is only
used when no delegate is provided.
Usage example (fully AOT-safe, no IL2026 warnings):
new DataGridColumn
{
Header = "Name",
ValueAccessor = item => ((Person)item).FirstName,
ValueSetter = (item, v) => ((Person)item).FirstName = (string?)v,
}
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- New '�� NativeAOT & Trimming' section covering:
- IsAotCompatible declaration on Core and SkiaSharp
- Default reflection-based path and its [RequiresUnreferencedCode] status
- AOT-safe delegate path (ValueAccessor/ValueSetter on DataGridColumn,
DisplaySelector/ValueSelector on DrawnComboBox) with code examples
- Trimming guidance for apps that continue using PropertyName-based binding
- Updated 'Nested property paths' description to reflect that accessors now
use a PropertyInfo closure cache rather than Expression.Compile()
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
This PR updates KumikoUI.Core and KumikoUI.SkiaSharp to be more compatible with NativeAOT and trimmed apps by removing Expression.Compile() usage, adding trimming annotations/suppressions, and introducing delegate-based binding APIs to avoid reflection.
Changes:
- Replaced
Expression.Compile()-based property accessors with cachedPropertyInfo-chain closures inDataGridSource. - Added AOT-safe delegate binding options (
ValueAccessor/ValueSetter,DisplaySelector/ValueSelector) with reflection fallbacks. - Enabled AOT analyzers via
<IsAotCompatible>true</IsAotCompatible>and documented trimming/AOT guidance in the README.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| src/KumikoUI.SkiaSharp/KumikoUI.SkiaSharp.csproj | Enables AOT analyzer support via IsAotCompatible. |
| src/KumikoUI.Core/KumikoUI.Core.csproj | Enables AOT analyzer support via IsAotCompatible. |
| src/KumikoUI.Core/Models/DataGridSource.cs | Replaces compiled expressions with reflection-based cached accessors; routes through delegate-first value access; adds trimming annotations/suppressions. |
| src/KumikoUI.Core/Models/DataGridColumn.cs | Adds delegate-based value getter/setter APIs for AOT-safe binding. |
| src/KumikoUI.Core/Components/DrawnComboBox.cs | Adds delegate-based display/value selectors and trimming annotations for reflection path. |
| src/KumikoUI.Core/Layout/GridLayoutEngine.cs | Suppresses trimming warnings for auto-fit calculation path. |
| src/KumikoUI.Core/Editing/EditSession.cs | Suppresses trimming warnings for edit lifecycle methods that call into reflection-enabled paths. |
| src/KumikoUI.Core/Editing/CellEditorFactory.cs | Suppresses trimming warnings for combo-box editor creation path. |
| src/KumikoUI.Core/DataGridRenderer.cs | Suppresses trimming warnings for row rendering paths that call into reflection-enabled accessors. |
| README.md | Documents the new reflection vs delegate binding behavior and trimming/AOT guidance. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ``` | ||
|
|
||
| Accessors are compiled once via `Expression.Property` chains and cached. | ||
| The `PropertyInfo` chain is resolved once via reflection and captured in a closure that is cached per `"{TypeName}.{propertyPath}"` key — subsequent accesses on the same column are allocation-free. |
| /// Populate items from a data source. | ||
| /// Prefers <see cref="DisplaySelector"/>/<see cref="ValueSelector"/> (AOT-safe) when set; | ||
| /// otherwise falls back to <see cref="DisplayMemberPath"/>/<see cref="ValueMemberPath"/> via reflection. | ||
| /// </summary> | ||
| [RequiresUnreferencedCode("Accesses properties on data item types by name via DisplayMemberPath/ValueMemberPath. Ensure the public properties of your data types are preserved when trimming, or use DisplaySelector/ValueSelector instead.")] | ||
| public void SetItemsFromSource(System.Collections.IEnumerable source) |
| /// <summary>Property path on the data item (supports nested: "Address.City").</summary> | ||
| public string PropertyName { get; set; } = string.Empty; | ||
|
|
||
| /// <summary> | ||
| /// Optional AOT-safe delegate for reading a cell value from a data item. | ||
| /// When set, this is used instead of the reflection-based <see cref="PropertyName"/> accessor. | ||
| /// Prefer this over <see cref="PropertyName"/> in NativeAOT or trimmed applications. | ||
| /// <example> | ||
| /// <code> | ||
| /// new DataGridColumn { ValueAccessor = item => ((Person)item).FirstName } | ||
| /// </code> | ||
| /// </example> | ||
| /// </summary> | ||
| public Func<object, object?>? ValueAccessor { get; set; } | ||
|
|
| [UnconditionalSuppressMessage("Trimming", "IL2026", | ||
| Justification = "DataGridSource uses reflection by design. Callers must configure trimming to preserve data item types.")] |
| { | ||
| var prop = body.Type.GetProperty(part, | ||
| var prop = currentType.GetProperty(parts[i], | ||
| BindingFlags.Public | BindingFlags.Instance | BindingFlags.IgnoreCase); | ||
| if (prop == null) | ||
| return _ => null; // Graceful fallback | ||
| body = Expression.Property(body, prop); | ||
| return _ => null; // Graceful fallback for unknown property | ||
| props[i] = prop; | ||
| currentType = prop.PropertyType; | ||
| } |
| return item => | ||
| { | ||
| object? current = item; | ||
| foreach (var prop in props) | ||
| { | ||
| if (current == null) return null; |
| [UnconditionalSuppressMessage("Trimming", "IL2026", | ||
| Justification = "GridLayoutEngine is part of the library and operates on columns configured by the consuming app. Reflection use is intentional.")] |
|
Superseded by #137 (issue #17), which makes property access Native AOT and trimming safe without falling back to slower reflection: a |
Summary
This PR makes
KumikoUI.CoreandKumikoUI.SkiaSharpcompatible with NativeAOT and trimmed .NET applications, and adds an AOT-safe delegate API as a zero-reflection alternative to string property paths.Changes
1. Remove
Expression.Compile()— critical AOT blockerDataGridSource.BuildAccessor()previously usedExpression.Lambda<T>().Compile()to build property accessors.Expression.Compile()is annotated[RequiresDynamicCode]and blocks NativeAOT entirely.Fix: Replaced with a
PropertyInfo-based closure — thePropertyInfochain for a dotted property path is resolved once and captured in a closure, then cached per"{TypeName}.{propertyPath}"key. Behaviour is identical; no dynamic IL generation.2.
[RequiresUnreferencedCode]annotations on all reflection-based pathsAll public and internal methods that use reflection-based property access by name are annotated so consuming apps receive correct trimmer warnings:
DataGridSource:GetCellValue,SetCellValue,GetCellDisplayText,GetOrCreateAccessor,BuildAccessor,GetCellValueDirect,SetPropertyValueDrawnComboBox:SetItemsFromSource,GetPropertyValueInternal plumbing that calls these as an implementation detail is annotated with
[UnconditionalSuppressMessage(IL2026)].3.
<IsAotCompatible>true</IsAotCompatible>on both librariesEnables the AOT/trim Roslyn analyzers at build time so future regressions produce build warnings immediately.
4. AOT-safe delegate API
Adds optional delegate properties as a zero-reflection alternative to
PropertyName/DisplayMemberPath. The library checks delegates first and only falls back to reflection when absent — fully backward compatible.DataGridColumnDrawnComboBoxUsage (no reflection, no IL2026 warnings):
All internal data paths route through a new
GetValueFromItemprivate helper that checksValueAccessorfirst. Internal plumbing inDataGridRenderer,EditSession,GridLayoutEngine, andCellEditorFactoryis suppressed with[UnconditionalSuppressMessage(IL2026)].5. README updated
IsAotCompatibledeclaration[RequiresUnreferencedCode]statusExpression.Propertychains)Testing
IL2026/IL3050warnings after changesCS1591(XML doc) warnings unchanged — no new warnings introducedBackward Compatibility
✅ Fully backward compatible.
PropertyName,DisplayMemberPath, andValueMemberPathcontinue to work exactly as before. Delegates are purely additive opt-in.