Skip to content

feat: NativeAOT & trimming compatibility for KumikoUI.Core and KumikoUI.SkiaSharp#1

Open
michaelstonis wants to merge 3 commits intomainfrom
copilot/absolute-python
Open

feat: NativeAOT & trimming compatibility for KumikoUI.Core and KumikoUI.SkiaSharp#1
michaelstonis wants to merge 3 commits intomainfrom
copilot/absolute-python

Conversation

@michaelstonis
Copy link
Copy Markdown
Contributor

Summary

This PR makes KumikoUI.Core and KumikoUI.SkiaSharp compatible 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 blocker

DataGridSource.BuildAccessor() previously used Expression.Lambda<T>().Compile() to build property accessors. Expression.Compile() is annotated [RequiresDynamicCode] and blocks NativeAOT entirely.

Fix: Replaced with a PropertyInfo-based closure — the PropertyInfo chain 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 paths

All 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, SetPropertyValue
  • DrawnComboBox: SetItemsFromSource, GetPropertyValue

Internal plumbing that calls these as an implementation detail is annotated with [UnconditionalSuppressMessage(IL2026)].

3. <IsAotCompatible>true</IsAotCompatible> on both libraries

Enables 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.

DataGridColumn

// AOT-safe getter — used by GetCellValue, sort, filter, group, summaries, auto-fit
public Func<object, object?>? ValueAccessor { get; set; }

// AOT-safe setter — used by SetCellValue (edit write-back)
public Action<object, object?>? ValueSetter { get; set; }

DrawnComboBox

public Func<object, string>?   DisplaySelector { get; set; }
public Func<object, object?>?  ValueSelector   { get; set; }

Usage (no reflection, no IL2026 warnings):

new DataGridColumn
{
    Header        = "Name",
    ValueAccessor = item => ((Employee)item).Name,
    ValueSetter   = (item, v) => ((Employee)item).Name = (string?)v,
}

All internal data paths route through a new GetValueFromItem private helper that checks ValueAccessor first. Internal plumbing in DataGridRenderer, EditSession, GridLayoutEngine, and CellEditorFactory is suppressed with [UnconditionalSuppressMessage(IL2026)].


5. README updated

  • New 🔧 NativeAOT & Trimming section with:
    • IsAotCompatible declaration
    • Explanation of the default reflection path and its [RequiresUnreferencedCode] status
    • AOT-safe delegate usage examples
    • Trimmer root descriptor guidance for apps staying on the reflection path
  • Fixed stale "Nested property paths" description (previously referenced Expression.Property chains)

Testing

  • All 320 existing tests pass
  • Zero IL2026 / IL3050 warnings after changes
  • All 106 pre-existing CS1591 (XML doc) warnings unchanged — no new warnings introduced

Backward Compatibility

✅ Fully backward compatible. PropertyName, DisplayMemberPath, and ValueMemberPath continue to work exactly as before. Delegates are purely additive opt-in.

michaelstonis and others added 3 commits April 2, 2026 08:57
- 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>
Copilot AI review requested due to automatic review settings April 14, 2026 14:00
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 cached PropertyInfo-chain closures in DataGridSource.
  • 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.

Comment thread README.md
```

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.")]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants