feat: NativeAOT & trimming compatibility for KumikoUI.Core and KumikoUI.SkiaSharp#1
Open
michaelstonis wants to merge 3 commits intomainfrom
Open
feat: NativeAOT & trimming compatibility for KumikoUI.Core and KumikoUI.SkiaSharp#1michaelstonis wants to merge 3 commits intomainfrom
michaelstonis wants to merge 3 commits intomainfrom
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. |
Comment on lines
+195
to
200
| /// 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) |
Comment on lines
82
to
+96
| /// <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; } | ||
|
|
Comment on lines
+777
to
+778
| [UnconditionalSuppressMessage("Trimming", "IL2026", | ||
| Justification = "DataGridSource uses reflection by design. Callers must configure trimming to preserve data item types.")] |
Comment on lines
1624
to
1631
| { | ||
| 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; | ||
| } |
Comment on lines
+1635
to
+1640
| return item => | ||
| { | ||
| object? current = item; | ||
| foreach (var prop in props) | ||
| { | ||
| if (current == null) return null; |
Comment on lines
+320
to
+321
| [UnconditionalSuppressMessage("Trimming", "IL2026", | ||
| Justification = "GridLayoutEngine is part of the library and operates on columns configured by the consuming app. Reflection use is intentional.")] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.