Skip to content

chore: Preserve generated *ViewModel constructors, properties - #2966

Merged
jonpryor merged 1 commit into
mainfrom
dev/jonpryor/jonp-preserve-Model-ctors-and-properties
Dec 9, 2025
Merged

chore: Preserve generated *ViewModel constructors, properties#2966
jonpryor merged 1 commit into
mainfrom
dev/jonpryor/jonp-preserve-Model-ctors-and-properties

Conversation

@jonpryor

@jonpryor jonpryor commented Dec 1, 2025

Copy link
Copy Markdown
Contributor

Context: unoplatform/uno@559b1f7

While running the uno.chefs app on macOS under NativeAOT:

dotnet publish -c Release -r osx-x64 -f net10.0-desktop -p:TargetFrameworkOverride=net10.0-desktop -bl \
  Chefs/Chefs.csproj \
  -p:SelfContained=true -p:PublishAot=true -p:IsAotCompatible=true -p:UseSkiaRendering=true \
  -p:IlcGenerateMapFile=true -p:IlcGenerateMstatFile=true -p:IlcGenerateDgmlFile=true -p:IlcGenerateMetadataLog=true \
  -p:EmitCompilerGeneratedFiles=true -p:CompilerGeneratedFilesOutputPath=`pwd`/_gen
Chefs/bin/Release/net10.0-desktop/osx-x64/publish/Chefs

Console output would contain the following warnings:

fail: Uno.UI.Dispatching.NativeDispatcher[0]
      NativeDispatcher unhandled exception
      System.InvalidOperationException: A suitable constructor for type 'Chefs.Presentation.ShellViewModel' could not be located. Ensure the type is concrete and services are registered for all parameters of a public constructor.

With a local patch in Uno.dll, we see that the above
InvalidOperationException is from
Microsoft.Extensions.DependencyInjection:

fail: Uno.UI.Dispatching.NativeDispatcher[0]
      NativeDispatcher unhandled exception
      System.InvalidOperationException: A suitable constructor for type 'Chefs.Presentation.ShellViewModel' could not be located. Ensure the type is concrete and services are registered for all parameters of a public constructor.
         at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateConstructorCallSite(ResultCache, ServiceIdentifier, Type, CallSiteChain) + 0x3e9
         at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateExact(ServiceDescriptor, ServiceIdentifier, CallSiteChain, Int32) + 0x181
         at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.TryCreateExact(ServiceIdentifier, CallSiteChain) + 0x14f
         at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateCallSite(ServiceIdentifier serviceIdentifier, CallSiteChain callSiteChain) + 0xc9
         at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.GetCallSite(ServiceIdentifier, CallSiteChain) + 0x92
         at Microsoft.Extensions.DependencyInjection.ServiceProvider.CreateServiceAccessor(ServiceIdentifier serviceIdentifier) + 0x68
         at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey, Func`2) + 0xd2
         at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(ServiceIdentifier, ServiceProviderEngineScope) + 0x39
         at Uno.Extensions.Navigation.Navigators.ControlNavigator.<>c__DisplayClass8_0.<<CreateViewModel>b__0>d.MoveNext() + 0x80

The problem is that trimming removed the ShellViewModel constructor,
along with other members.

Question: why were these members removed?

Answer 1: because the members weren't used, via static analysis.
This is the case here, as there is no new ShellViewModel() within
uno.chefs or its generated code.

Answer 2: Even when "Answer 1" isn't true, the linker not removing
a member doesn't mean that the member can be accessed through
Reflection! Reflection requires a different set of data.

We thus need to convince the trimmer to not only preserve the members,
but also to allow the members to be used through Reflection.

From unoplatform/uno@559b1f78:

The two predominant ways to do this are via the custom attributes:

  • DynamicallyAccessedMembersAttribute
  • DynamicDependencyAttribute

At this point we know we have e.g. ShellModel (user-written code),
and ShellViewModel (from src/Uno.Extensions.Reactive.Generator).

How are these types associated with each other at runtime?

They're associated with each other at runtime via a dictionary
generated by BindableViewModelMappingGenerator:

// generated code
namespace Chefs;
partial class ReactiveViewModelMappings {
  public static readonly IDictionary<Type, Type> ViewModelMappings = new Dictionary<Type, Type> {
    { typeof(ShellModel), typeof(ShellViewModel) },
    // …
  };
}

Use of IViewRegistry.FindViewByModel<T>() allows finding the latter
from the former, e.g. views.FindViewByModel<ShellModel>() returns a
MappedViewMap with MappedViewMap.MappedViewModel being
ShellViewModel:

Console.WriteLine(views.FindByViewModel<ShellModel>());
// MappedViewMap { View = , ViewSelector = , ViewModel = Chefs.Presentation.ShellModel, Data = , ResultData = , ViewAttributes = , MappedViewModel = Chefs.Presentation.ShellViewModel }

To preserve the ShellViewModel members, where do we put
[DynamicallyAccessedMembers] or [DynamicDependency]?

There are two plausible places:

  1. On ModelAttribute and IModel<T>
  2. Within BindableViewModelMappingGenerator-generated code.

(1) ModelAttribute and IModel<T> are viable locations because
ViewModelGenTool_* emits partial types for e.g. ShellModel:

// generated code
[Model(typeof(ShellViewModel))]
partial record ShellModel : IModel<ShellViewModel> {
}

While this works, it also means that using [Model]/IModel<T>
without MVUX would result in app size increases.

Which leaves (2): update BindableViewModelMappingGenerator so that
a codepath uses [DynamicallyAccessedMembers].

Update BindableViewModelMappingGenerator to instead emit code
similar to:

namespace Chefs;
static partial class ReactiveViewModelMappings
{
  private const DynamicallyAccessedMemberTypes Types =
      DynamicallyAccessedMemberTypes.PublicConstructors
    | DynamicallyAccessedMemberTypes.PublicProperties
    ;

  private static void AddMapping(
    Dictionary<Type, Type> dict,
    [DynamicallyAccessedMembers(Types)] Type key,
    [DynamicallyAccessedMembers(Types)] Type value)
    => dict.Add(key, value);

  public static readonly IDictionary<Type, Type> ViewModelMappings;

  static ReactiveViewModelMappings()
  {
    var mappings = new Dictionary<Type, Type>();
    AddMapping(mappings, typeof(Chefs.Presentation.ShellModel), typeof(global::Chefs.Presentation.ShellViewModel));
    // …
    ViewModelMappings = mappings;
  }
}

The introduction of ReactiveViewModelMappings.AddMapping() allows
a place to provide [DynamicallyAccessedMembers], which in turn
allows the trimmer to preserve constructors and properties on
ShellViewModel, which fixes the InvalidOperationException.

Note that properties must also be preserved, as if only constructors
are preserved we will see this failure message:

fail: Uno.UI.DataBinding.BindingPropertyHelper[0]
      The [Pages] property getter does not exist on type [Chefs.Presentation.WelcomeViewModel]

@jonpryor
jonpryor force-pushed the dev/jonpryor/jonp-preserve-Model-ctors-and-properties branch 4 times, most recently from e43d9e2 to 0040fcd Compare December 1, 2025 21:11
@jonpryor
jonpryor requested a review from dr1rrb December 1, 2025 21:12
jonpryor added a commit that referenced this pull request Dec 2, 2025
Context: #2966
Context: unoplatform/uno.chefs#1709

While running the uno.chefs app on macOS under NativeAOT:

	dotnet publish -c Release -r osx-x64 -f net10.0-desktop -p:TargetFrameworkOverride=net10.0-desktop -bl \
	  Chefs/Chefs.csproj \
	  -p:SelfContained=true -p:PublishAot=true -p:IsAotCompatible=true -p:UseSkiaRendering=true \
	  -p:IlcGenerateMapFile=true -p:IlcGenerateMstatFile=true -p:IlcGenerateDgmlFile=true \
	  -p:EmitCompilerGeneratedFiles=true -p:CompilerGeneratedFilesOutputPath=`pwd`/_gen
	Chefs/bin/Release/net10.0-desktop/osx-x64/publish/Chefs

(along with a change from unoplatform/uno.chefs#1709 to `Program.cs`
to call `App.InitializeLogging()`…)

Console output would contain the following error:

	fail: Uno.UI.DataBinding.BindingPropertyHelper[0]
	      The [CurrentIndex] property getter does not exist on type [Chefs.Business.Models.BindableIntIteratorViewModel]
	fail: Uno.UI.DataBinding.BindingPropertyHelper[0]
	      The property setter for [CurrentIndex] does not exist on [Chefs.Business.Models.BindableIntIteratorViewModel]
	fail: Uno.UI.DataBinding.BindingPropertyHelper[0]
	      The [Value] property getter does not exist on type [Chefs.Business.Models.BindableIntIteratorViewModel]

PR #2966 had a similar set of messages, and the general fix is to use
`[DynamicallyAccessedMembers]` so that NativeAOT preserves property
metadata.

The problem *here* is that there is no good place to put
`[DynamicallyAccessedMembers]` that will preserve the ViewModel types
generated by Uno.Extensions.Reactive.Generator:

	namespace Chefs.Business.Models {
	  [GeneratedCodeAttribute("ViewModelGenerator_2", "2")]
	  [Bindable(typeof(IntIterator))]
	  public partial class BindableIntIteratorViewModel : global::Uno.Extensions.Reactive.Bindings.Bindable<Chefs.Business.Models.IntIterator> {
	    public IntIterator Value {get => …; set => …;}
	    public int CurrentIndex {get => …; set => …;}
	  }
	}
	namespace Chefs.Presentation {
	  [GeneratedCodeAttribute("ViewModelGenTool_3", "3")]
	  [Bindable(typeof(global::Chefs.Presentation.WelcomeModel))]
	  public partial class WelcomeViewModel {
	    protected WelcomeViewModel(global::Chefs.Presentation.WelcomeModel model) {
	      // …
	      Pages ??= new Chefs.Business.Models.BindableIntIteratorViewModel(…);
	      // …
	    }
	    public Chefs.Business.Models.BindableIntIteratorViewModel Pages { get; private set; }
	  }
	}

Nothing tells NativeAOT to preserve the `BindableIntIteratorViewModel`
properties, so they're "removed" from Reflection metadata.

Update `BindableFromFeedProperty.cs` to use `[DynamicDependency]`
on the `WelcomeViewModel.Pages` property accessor:

	namespace Chefs.Presentation {
	  public partial class WelcomeViewModel {
	    public Chefs.Business.Models.BindableIntIteratorViewModel Pages
	    {
	      [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties, typeof(Chefs.Business.Models.BindableIntIteratorViewModel))]
	      get;
	      private set;
	    }
	  }
	}

This change informs NativeAOT that if the `WelcomeViewModel.Pages`
property getter is preserved, then the public properties on
`BindableIntIteratorViewModel` should also be made accessible via
Reflection.  This fixes the original message:

	fail: Uno.UI.DataBinding.BindingPropertyHelper[0]
	      The [CurrentIndex] property getter does not exist on type [Chefs.Business.Models.BindableIntIteratorViewModel]

…only to replace it with this *different* set of failure messages:

	fail: Uno.UI.DataBinding.BindingPropertyHelper[0]
	      The [CanMovePrevious] property getter does not exist on type [Chefs.Business.Models.IntIterator]
	fail: Uno.UI.DataBinding.BindingPropertyHelper[0]
	      The [CanMovePrevious] property getter does not exist on type [Chefs.Business.Models.IntIterator]
	fail: Uno.UI.DataBinding.BindingPropertyHelper[0]
	      The [CanMoveNext] property getter does not exist on type [Chefs.Business.Models.IntIterator]
	fail: Uno.UI.DataBinding.BindingPropertyHelper[0]
	      The [CanMoveNext] property getter does not exist on type [Chefs.Business.Models.IntIterator]

Address this new set of messages by updating `Bindable<T>` to

	partial class Bindable<
	  [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors | DynamicallyAccessedMemberTypes.PublicProperties)]
	  T
	> : IBindable
	{
	}

As `BindableIntIteratorViewModel` inherits from `Bindable<IntIterator>`
this causes the public properties on `IntIterator` to be preserved,
fixing the previous set of failure messages.
Comment thread src/Uno.Extensions.Reactive/Presentation/Bindings/ModelAttribute.cs Outdated
jonpryor added a commit to unoplatform/uno that referenced this pull request Dec 2, 2025
Context: unoplatform/uno.chefs#1712
Context: #21920
Context: unoplatform/uno.extensions#2966
Context: unoplatform/uno.extensions#2969
Context: https://discord.com/channels/732297728826277939/732297837953679412/1444909702512119899

While running the uno.chefs app on macOS under NativeAOT:

	dotnet publish -c Release -r osx-x64 -f net10.0-desktop -p:TargetFrameworkOverride=net10.0-desktop -bl \
	  Chefs/Chefs.csproj \
	  -p:SelfContained=true -p:PublishAot=true -p:IsAotCompatible=true -p:UseSkiaRendering=true \
	  -p:IlcGenerateMapFile=true -p:IlcGenerateMstatFile=true -p:IlcGenerateDgmlFile=true \
	  -p:EmitCompilerGeneratedFiles=true -p:CompilerGeneratedFilesOutputPath=`pwd`/_gen
	Chefs/bin/Release/net10.0-desktop/osx-x64/publish/Chefs

(along with a change from unoplatform/uno.chefs#1712)

Console output would contain the following errors:

	fail: Uno.UI.Dispatching.NativeDispatcher[0]
	      The [TabNavigation] property getter does not exist on type [Microsoft.UI.Xaml.Controls.FlipView]
	fail: Uno.UI.Dispatching.NativeDispatcher[0]
	      The [Y] property getter does not exist on type [Windows.Foundation.Point]
	fail: Uno.UI.Dispatching.NativeDispatcher[0]
	      The [X] property getter does not exist on type [Windows.Foundation.Point]

At this point, a description of the relation between NativeAOT and
Reflection would be useful.

Parts of System.Reflection can be used.  Which parts *can* be used,
and how to use them, is oddly *not* well specified in the
[Native AOT deployment][0] documentation.

What doesn't work is outlined in the [limitations][1] section:

> * No dynamic loading, for example, `Assembly.LoadFile`.
> * No run-time code generation, for example, `System.Reflection.Emit`.

What *always* appears to work is:

  * `Type.GetType()` returns a `Type` instance.

What ***can*** work is:

  * `Type.GetType(string)`, *so long as* an assembly-qualified name
    is used as a *string constant*

        // bad
        string name = GetSomeTypeName();
        Type type = Type.GetType(name);

        // good
        Type type = Type.GetType("My.Example.TypeName, ExampleAssembly");

  * `Type.GetMethods()`, but see fine print below.
  * `Type.GetProperties()`, but see fine print below.
  * `MethodInfo.Invoke()`
  * …

**The Fine Print**: in order for many Reflection-based APIs such as
`Type.GetMethods()` to work, the program must contain
"reflection metadata."

We can now describe, briefly, what NativeAOT does:

 1. Accept IL/assemblies
 2. Internally trims the IL, optimizes them, etc.
 3. Generates a native binary containing two separate bits of data:

    * Native machine code for execution
    * "reflection metadata"

There is no facility to separately dump reflection metadata.

Programs can control what is contained within reflection metadata
by using string constants with Reflection APIs, for example:

	typeof(SomeKnownType).GetMethod("ConstantName")

will ensure that reflection metadata contains
`SomeKnownType.ConstantName()`, and `ConstantName()` will be returned
from `typeof(SomeKnownType).GetMethods()` and can be invoked with
`MethodInfo.Invoke()`.

If you *don't* use `typeof(SomeKnownType)` or string constants with
`Type.GetType(string)` or `type.GetMethod("constant")`, then you need
to "suggest" that some pieces of information be added to
reflection metadata.

The two predominant ways to do this are via the custom attributes:

  * [`DynamicallyAccessedMembersAttribute`][2]
  * [`DynamicDependencyAttribute`][3]

From the Uno perspective, enter `BindingPropertyHelper`: *everything*
is based on Reflection!

Fortunately NativeAOT "supports" Reflection!

*Unfortunately*, we now need to convince NativeAOT to store the
reflection metadata Uno requires in order to work!

Various other PRs have dealt with some of this, such as #21920.

Returning to the original failure messages, e.g.

	fail: Uno.UI.Dispatching.NativeDispatcher[0]
	      The [TabNavigation] property getter does not exist on type [Microsoft.UI.Xaml.Controls.FlipView]

During the app build, `src/SourceGenerators/Uno.UI.SourceGenerators`
generates a a `BindableMetadata.g.cs` file which will mention
`[Bindable]` types, e.g. for `Microsoft.UI.Xaml.Controls.FlipView`:

	/// <summary>
	/// Builder for Microsoft.UI.Xaml.Controls.FlipView
	/// </summary>
	[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute]
	static class MetadataBuilder_239
	{
	  internal static global::Uno.UI.DataBinding.IBindableType Build(global::Uno.UI.DataBinding.BindableType parent)
	  {
	    var bindableType = parent ?? new global::Uno.UI.DataBinding.BindableType(151, typeof(global::Microsoft.UI.Xaml.Controls.FlipView));
	    // …
	  }
	  private static object CreateInstance() => new global::Microsoft.UI.Xaml.Controls.FlipView();
	}

`BindableMetadata.g.cs` was needed in the ye olde Xamarin world order
to tell *its* linker to e.g. "preserve the `FlipView` default ctor!"

*Now*, we can build upon this infrastructure and update `BindableType`
to request that Reflection information regarding properties, fields,
and constructors be retained.  We can do this by updating the
`BindableType` constructor:

	partial class BindableType {
	  internal const DynamicallyAccessedMemberTypes TypeRequirements =
	    DynamicallyAccessedMemberTypes.PublicProperties | …;
	  public BindableType(int estimatedPropertySize, [DynamicallyAccessedMembers(TypeRequirements)] Type sourceType ) => …
	}

*Because* we have `BindableMetadata.g.cs` *and* because it provides
`typeof(FlipView)`, this directs NativeAOT to make property and other
reflection metadata available to the app, *fixing* the error messages.

Expand this addition of `[DynamicallyAccessedMembers]` to also include
`BindableProperty` and related types.

[0]: https://learn.microsoft.comdotnet/core/deploying/native-aot
[1]: https://learn.microsoft.comdotnet/core/deploying/native-aot/#limitations-of-native-aot-deployment
[2]: https://learn.microsoft.comdotnet/api/system.diagnostics.codeanalysis.dynamicallyaccessedmembersattribute?view=net-10.0
[3]: https://learn.microsoft.comdotnet/api/system.diagnostics.codeanalysis.dynamicdependencyattribute?view=net-10.0
jonpryor added a commit that referenced this pull request Dec 3, 2025
Context: #2966

While running the uno.chefs app on macOS under NativeAOT:

	dotnet publish -c Release -r osx-x64 -f net10.0-desktop -p:TargetFrameworkOverride=net10.0-desktop -bl \
	  Chefs/Chefs.csproj \
	  -p:SelfContained=true -p:PublishAot=true -p:IsAotCompatible=true -p:UseSkiaRendering=true \
	  -p:IlcGenerateMapFile=true -p:IlcGenerateMstatFile=true -p:IlcGenerateDgmlFile=true \
	  -p:EmitCompilerGeneratedFiles=true -p:CompilerGeneratedFilesOutputPath=`pwd`/_gen
	Chefs/bin/Release/net10.0-desktop/osx-x64/publish/Chefs

Console output would contain the following error:

	fail: Uno.UI.DataBinding.BindingPropertyHelper[0]
	      The [CurrentIndex] property getter does not exist on type [Chefs.Business.Models.BindableIntIteratorViewModel]
	fail: Uno.UI.DataBinding.BindingPropertyHelper[0]
	      The property setter for [CurrentIndex] does not exist on [Chefs.Business.Models.BindableIntIteratorViewModel]
	fail: Uno.UI.DataBinding.BindingPropertyHelper[0]
	      The [Value] property getter does not exist on type [Chefs.Business.Models.BindableIntIteratorViewModel]

PR #2966 had a similar set of messages, and the general fix is to use
`[DynamicallyAccessedMembers]` so that NativeAOT preserves property
metadata.

The problem *here* is that there is no good place to put
`[DynamicallyAccessedMembers]` that will preserve the ViewModel types
generated by Uno.Extensions.Reactive.Generator:

	namespace Chefs.Business.Models {
	  [GeneratedCodeAttribute("ViewModelGenerator_2", "2")]
	  [Bindable(typeof(IntIterator))]
	  public partial class BindableIntIteratorViewModel : global::Uno.Extensions.Reactive.Bindings.Bindable<Chefs.Business.Models.IntIterator> {
	    public IntIterator Value {get => …; set => …;}
	    public int CurrentIndex {get => …; set => …;}
	  }
	}
	namespace Chefs.Presentation {
	  [GeneratedCodeAttribute("ViewModelGenTool_3", "3")]
	  [Bindable(typeof(global::Chefs.Presentation.WelcomeModel))]
	  public partial class WelcomeViewModel {
	    protected WelcomeViewModel(global::Chefs.Presentation.WelcomeModel model) {
	      // …
	      Pages ??= new Chefs.Business.Models.BindableIntIteratorViewModel(…);
	      // …
	    }
	    public Chefs.Business.Models.BindableIntIteratorViewModel Pages { get; private set; }
	  }
	}

Nothing tells NativeAOT to preserve the `BindableIntIteratorViewModel`
properties, so they're "removed" from Reflection metadata.

Update `BindableFromFeedProperty.cs` to use `[DynamicDependency]`
on the `WelcomeViewModel.Pages` property accessor:

	namespace Chefs.Presentation {
	  public partial class WelcomeViewModel {
	    public Chefs.Business.Models.BindableIntIteratorViewModel Pages
	    {
	      [DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties, typeof(Chefs.Business.Models.BindableIntIteratorViewModel))]
	      get;
	      private set;
	    }
	  }
	}

This change informs NativeAOT that if the `WelcomeViewModel.Pages`
property getter is preserved, then the public properties on
`BindableIntIteratorViewModel` should also be made accessible via
Reflection.  This fixes the original message:

	fail: Uno.UI.DataBinding.BindingPropertyHelper[0]
	      The [CurrentIndex] property getter does not exist on type [Chefs.Business.Models.BindableIntIteratorViewModel]

…only to replace it with this *different* set of failure messages:

	fail: Uno.UI.DataBinding.BindingPropertyHelper[0]
	      The [CanMovePrevious] property getter does not exist on type [Chefs.Business.Models.IntIterator]
	fail: Uno.UI.DataBinding.BindingPropertyHelper[0]
	      The [CanMovePrevious] property getter does not exist on type [Chefs.Business.Models.IntIterator]
	fail: Uno.UI.DataBinding.BindingPropertyHelper[0]
	      The [CanMoveNext] property getter does not exist on type [Chefs.Business.Models.IntIterator]
	fail: Uno.UI.DataBinding.BindingPropertyHelper[0]
	      The [CanMoveNext] property getter does not exist on type [Chefs.Business.Models.IntIterator]

Address this new set of messages by updating `Bindable<T>` to

	partial class Bindable<
	  [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicProperties)]
	  T
	> : IBindable
	{
	}

As `BindableIntIteratorViewModel` inherits from `Bindable<IntIterator>`
this causes the public properties on `IntIterator` to be preserved,
fixing the previous set of failure messages.

Adding `[DynamicallyAccessedMembers]` to `Bindable<T>` breaks
`BindableEnumerable<…>`:

	BindableEnumerable.cs(22,23): error IL2091:
	  'T' generic argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicProperties' in 'Uno.Extensions.Reactive.Bindings.Bindable<T>'.
	  The generic parameter 'TCollection' of 'Uno.Extensions.Reactive.Bindings.BindableEnumerable<TCollection, TItem, TBindableItem>' does not have matching annotations.
	  The source value must declare at least the same requirements as those declared on the target location it is assigned to.

Annotate the `TCollection` type parameter accordingly to fix the error.
@jonpryor
jonpryor requested a review from dr1rrb December 3, 2025 12:55
@jonpryor
jonpryor force-pushed the dev/jonpryor/jonp-preserve-Model-ctors-and-properties branch from 0040fcd to c0d699a Compare December 3, 2025 13:11
jonpryor added a commit to unoplatform/uno that referenced this pull request Dec 3, 2025
Context: unoplatform/uno.chefs#1712
Context: #21920
Context: unoplatform/uno.extensions#2966
Context: unoplatform/uno.extensions#2969
Context: https://discord.com/channels/732297728826277939/732297837953679412/1444909702512119899

While running the uno.chefs app on macOS under NativeAOT:

	dotnet publish -c Release -r osx-x64 -f net10.0-desktop -p:TargetFrameworkOverride=net10.0-desktop -bl \
	  Chefs/Chefs.csproj \
	  -p:SelfContained=true -p:PublishAot=true -p:IsAotCompatible=true -p:UseSkiaRendering=true \
	  -p:IlcGenerateMapFile=true -p:IlcGenerateMstatFile=true -p:IlcGenerateDgmlFile=true \
	  -p:EmitCompilerGeneratedFiles=true -p:CompilerGeneratedFilesOutputPath=`pwd`/_gen
	Chefs/bin/Release/net10.0-desktop/osx-x64/publish/Chefs

(along with a change from unoplatform/uno.chefs#1712)

Console output would contain the following errors:

	fail: Uno.UI.Dispatching.NativeDispatcher[0]
	      The [TabNavigation] property getter does not exist on type [Microsoft.UI.Xaml.Controls.FlipView]
	fail: Uno.UI.Dispatching.NativeDispatcher[0]
	      The [Y] property getter does not exist on type [Windows.Foundation.Point]
	fail: Uno.UI.Dispatching.NativeDispatcher[0]
	      The [X] property getter does not exist on type [Windows.Foundation.Point]

At this point, a description of the relation between NativeAOT and
Reflection would be useful.

Parts of System.Reflection can be used.  Which parts *can* be used,
and how to use them, is oddly *not* well specified in the
[Native AOT deployment][0] documentation.

What doesn't work is outlined in the [limitations][1] section:

> * No dynamic loading, for example, `Assembly.LoadFile`.
> * No run-time code generation, for example, `System.Reflection.Emit`.

What *always* appears to work is:

  * `Type.GetType()` returns a `Type` instance.

What ***can*** work is:

  * `Type.GetType(string)`, *so long as* an assembly-qualified name
    is used as a *string constant*

        // bad
        string name = GetSomeTypeName();
        Type type = Type.GetType(name);

        // good
        Type type = Type.GetType("My.Example.TypeName, ExampleAssembly");

  * `Type.GetMethods()`, but see fine print below.
  * `Type.GetProperties()`, but see fine print below.
  * `MethodInfo.Invoke()`
  * …

**The Fine Print**: in order for many Reflection-based APIs such as
`Type.GetMethods()` to work, the program must contain
"reflection metadata."

We can now describe, briefly, what NativeAOT does:

 1. Accept IL/assemblies
 2. Internally trims the IL, optimizes them, etc.
 3. Generates a native binary containing two separate bits of data:

    * Native machine code for execution
    * "reflection metadata"

There is no facility to separately dump reflection metadata.

Programs can control what is contained within reflection metadata
by using string constants with Reflection APIs, for example:

	typeof(SomeKnownType).GetMethod("ConstantName")

will ensure that reflection metadata contains
`SomeKnownType.ConstantName()`, and `ConstantName()` will be returned
from `typeof(SomeKnownType).GetMethods()` and can be invoked with
`MethodInfo.Invoke()`.

If you *don't* use `typeof(SomeKnownType)` or string constants with
`Type.GetType(string)` or `type.GetMethod("constant")`, then you need
to "suggest" that some pieces of information be added to
reflection metadata.

The two predominant ways to do this are via the custom attributes:

  * [`DynamicallyAccessedMembersAttribute`][2]
  * [`DynamicDependencyAttribute`][3]

From the Uno perspective, enter `BindingPropertyHelper`: *everything*
is based on Reflection!

Fortunately NativeAOT "supports" Reflection!

*Unfortunately*, we now need to convince NativeAOT to store the
reflection metadata Uno requires in order to work!

Various other PRs have dealt with some of this, such as #21920.

Returning to the original failure messages, e.g.

	fail: Uno.UI.Dispatching.NativeDispatcher[0]
	      The [TabNavigation] property getter does not exist on type [Microsoft.UI.Xaml.Controls.FlipView]

During the app build, `src/SourceGenerators/Uno.UI.SourceGenerators`
generates a a `BindableMetadata.g.cs` file which will mention
`[Bindable]` types, e.g. for `Microsoft.UI.Xaml.Controls.FlipView`:

	/// <summary>
	/// Builder for Microsoft.UI.Xaml.Controls.FlipView
	/// </summary>
	[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute]
	static class MetadataBuilder_239
	{
	  internal static global::Uno.UI.DataBinding.IBindableType Build(global::Uno.UI.DataBinding.BindableType parent)
	  {
	    var bindableType = parent ?? new global::Uno.UI.DataBinding.BindableType(151, typeof(global::Microsoft.UI.Xaml.Controls.FlipView));
	    // …
	  }
	  private static object CreateInstance() => new global::Microsoft.UI.Xaml.Controls.FlipView();
	}

`BindableMetadata.g.cs` was needed in the ye olde Xamarin world order
to tell *its* linker to e.g. "preserve the `FlipView` default ctor!"

*Now*, we can build upon this infrastructure and update `BindableType`
to request that Reflection information regarding properties, fields,
and constructors be retained.  We can do this by updating the
`BindableType` constructor:

	partial class BindableType {
	  internal const DynamicallyAccessedMemberTypes TypeRequirements =
	    DynamicallyAccessedMemberTypes.PublicProperties | …;
	  public BindableType(int estimatedPropertySize, [DynamicallyAccessedMembers(TypeRequirements)] Type sourceType ) => …
	}

*Because* we have `BindableMetadata.g.cs` *and* because it provides
`typeof(FlipView)`, this directs NativeAOT to make property and other
reflection metadata available to the app, *fixing* the error messages.

Expand this addition of `[DynamicallyAccessedMembers]` to also include
`BindableProperty` and related types.

[0]: https://learn.microsoft.comdotnet/core/deploying/native-aot
[1]: https://learn.microsoft.comdotnet/core/deploying/native-aot/#limitations-of-native-aot-deployment
[2]: https://learn.microsoft.comdotnet/api/system.diagnostics.codeanalysis.dynamicallyaccessedmembersattribute?view=net-10.0
[3]: https://learn.microsoft.comdotnet/api/system.diagnostics.codeanalysis.dynamicdependencyattribute?view=net-10.0
@jonpryor
jonpryor enabled auto-merge December 3, 2025 15:19
jonpryor added a commit to unoplatform/uno that referenced this pull request Dec 3, 2025
Context: unoplatform/uno.chefs#1712
Context: #21920
Context: unoplatform/uno.extensions#2966
Context: unoplatform/uno.extensions#2969
Context: https://discord.com/channels/732297728826277939/732297837953679412/1444909702512119899

While running the uno.chefs app on macOS under NativeAOT:

	dotnet publish -c Release -r osx-x64 -f net10.0-desktop -p:TargetFrameworkOverride=net10.0-desktop -bl \
	  Chefs/Chefs.csproj \
	  -p:SelfContained=true -p:PublishAot=true -p:IsAotCompatible=true -p:UseSkiaRendering=true \
	  -p:IlcGenerateMapFile=true -p:IlcGenerateMstatFile=true -p:IlcGenerateDgmlFile=true \
	  -p:EmitCompilerGeneratedFiles=true -p:CompilerGeneratedFilesOutputPath=`pwd`/_gen
	Chefs/bin/Release/net10.0-desktop/osx-x64/publish/Chefs

(along with a change from unoplatform/uno.chefs#1712)

Console output would contain the following errors:

	fail: Uno.UI.Dispatching.NativeDispatcher[0]
	      The [TabNavigation] property getter does not exist on type [Microsoft.UI.Xaml.Controls.FlipView]
	fail: Uno.UI.Dispatching.NativeDispatcher[0]
	      The [Y] property getter does not exist on type [Windows.Foundation.Point]
	fail: Uno.UI.Dispatching.NativeDispatcher[0]
	      The [X] property getter does not exist on type [Windows.Foundation.Point]

At this point, a description of the relation between NativeAOT and
Reflection would be useful.

Parts of System.Reflection can be used.  Which parts *can* be used,
and how to use them, is oddly *not* well specified in the
[Native AOT deployment][0] documentation.

What doesn't work is outlined in the [limitations][1] section:

> * No dynamic loading, for example, `Assembly.LoadFile`.
> * No run-time code generation, for example, `System.Reflection.Emit`.

What *always* appears to work is:

  * `Type.GetType()` returns a `Type` instance.

What ***can*** work is:

  * `Type.GetType(string)`, *so long as* an assembly-qualified name
    is used as a *string constant*

        // bad
        string name = GetSomeTypeName();
        Type type = Type.GetType(name);

        // good
        Type type = Type.GetType("My.Example.TypeName, ExampleAssembly");

  * `Type.GetMethods()`, but see fine print below.
  * `Type.GetProperties()`, but see fine print below.
  * `MethodInfo.Invoke()`
  * …

**The Fine Print**: in order for many Reflection-based APIs such as
`Type.GetMethods()` to work, the program must contain
"reflection metadata."

We can now describe, briefly, what NativeAOT does:

 1. Accept IL/assemblies
 2. Internally trims the IL, optimizes them, etc.
 3. Generates a native binary containing two separate bits of data:

    * Native machine code for execution
    * "reflection metadata"

There is no facility to separately dump reflection metadata.

Programs can control what is contained within reflection metadata
by using string constants with Reflection APIs, for example:

	typeof(SomeKnownType).GetMethod("ConstantName")

will ensure that reflection metadata contains
`SomeKnownType.ConstantName()`, and `ConstantName()` will be returned
from `typeof(SomeKnownType).GetMethods()` and can be invoked with
`MethodInfo.Invoke()`.

If you *don't* use `typeof(SomeKnownType)` or string constants with
`Type.GetType(string)` or `type.GetMethod("constant")`, then you need
to "suggest" that some pieces of information be added to
reflection metadata.

The two predominant ways to do this are via the custom attributes:

  * [`DynamicallyAccessedMembersAttribute`][2]
  * [`DynamicDependencyAttribute`][3]

From the Uno perspective, enter `BindingPropertyHelper`: *everything*
is based on Reflection!

Fortunately NativeAOT "supports" Reflection!

*Unfortunately*, we now need to convince NativeAOT to store the
reflection metadata Uno requires in order to work!

Various other PRs have dealt with some of this, such as #21920.

Returning to the original failure messages, e.g.

	fail: Uno.UI.Dispatching.NativeDispatcher[0]
	      The [TabNavigation] property getter does not exist on type [Microsoft.UI.Xaml.Controls.FlipView]

During the app build, `src/SourceGenerators/Uno.UI.SourceGenerators`
generates a a `BindableMetadata.g.cs` file which will mention
`[Bindable]` types, e.g. for `Microsoft.UI.Xaml.Controls.FlipView`:

	/// <summary>
	/// Builder for Microsoft.UI.Xaml.Controls.FlipView
	/// </summary>
	[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute]
	static class MetadataBuilder_239
	{
	  internal static global::Uno.UI.DataBinding.IBindableType Build(global::Uno.UI.DataBinding.BindableType parent)
	  {
	    var bindableType = parent ?? new global::Uno.UI.DataBinding.BindableType(151, typeof(global::Microsoft.UI.Xaml.Controls.FlipView));
	    // …
	  }
	  private static object CreateInstance() => new global::Microsoft.UI.Xaml.Controls.FlipView();
	}

`BindableMetadata.g.cs` was needed in the ye olde Xamarin world order
to tell *its* linker to e.g. "preserve the `FlipView` default ctor!"

*Now*, we can build upon this infrastructure and update `BindableType`
to request that Reflection information regarding properties, fields,
and constructors be retained.  We can do this by updating the
`BindableType` constructor:

	partial class BindableType {
	  internal const DynamicallyAccessedMemberTypes TypeRequirements =
	    DynamicallyAccessedMemberTypes.PublicProperties | …;
	  public BindableType(int estimatedPropertySize, [DynamicallyAccessedMembers(TypeRequirements)] Type sourceType ) => …
	}

*Because* we have `BindableMetadata.g.cs` *and* because it provides
`typeof(FlipView)`, this directs NativeAOT to make property and other
reflection metadata available to the app, *fixing* the error messages.

Expand this addition of `[DynamicallyAccessedMembers]` to also include
`BindableProperty` and related types.

[0]: https://learn.microsoft.comdotnet/core/deploying/native-aot
[1]: https://learn.microsoft.comdotnet/core/deploying/native-aot/#limitations-of-native-aot-deployment
[2]: https://learn.microsoft.comdotnet/api/system.diagnostics.codeanalysis.dynamicallyaccessedmembersattribute?view=net-10.0
[3]: https://learn.microsoft.comdotnet/api/system.diagnostics.codeanalysis.dynamicdependencyattribute?view=net-10.0
jonpryor added a commit to unoplatform/uno that referenced this pull request Dec 3, 2025
Context: unoplatform/uno.chefs#1712
Context: #21920
Context: unoplatform/uno.extensions#2966
Context: unoplatform/uno.extensions#2969
Context: https://discord.com/channels/732297728826277939/732297837953679412/1444909702512119899

While running the uno.chefs app on macOS under NativeAOT:

	dotnet publish -c Release -r osx-x64 -f net10.0-desktop -p:TargetFrameworkOverride=net10.0-desktop -bl \
	  Chefs/Chefs.csproj \
	  -p:SelfContained=true -p:PublishAot=true -p:IsAotCompatible=true -p:UseSkiaRendering=true \
	  -p:IlcGenerateMapFile=true -p:IlcGenerateMstatFile=true -p:IlcGenerateDgmlFile=true \
	  -p:EmitCompilerGeneratedFiles=true -p:CompilerGeneratedFilesOutputPath=`pwd`/_gen
	Chefs/bin/Release/net10.0-desktop/osx-x64/publish/Chefs

Console output would contain the following errors:

	fail: Uno.UI.Dispatching.NativeDispatcher[0]
	      The [TabNavigation] property getter does not exist on type [Microsoft.UI.Xaml.Controls.FlipView]
	fail: Uno.UI.Dispatching.NativeDispatcher[0]
	      The [Y] property getter does not exist on type [Windows.Foundation.Point]
	fail: Uno.UI.Dispatching.NativeDispatcher[0]
	      The [X] property getter does not exist on type [Windows.Foundation.Point]

At this point, a description of the relation between NativeAOT and
Reflection would be useful.

*Parts* of System.Reflection can be used.  Which parts *can* be used,
and how to use them, is oddly *not* well specified in the
[Native AOT deployment][0] documentation.

What doesn't work is outlined in the [limitations][1] section:

> * No dynamic loading, for example, `Assembly.LoadFile`.
> * No run-time code generation, for example, `System.Reflection.Emit`.

What *always* appears to work is:

  * `Type.GetType()` returns a `Type` instance.

What ***can*** work is:

  * `Type.GetType(string)`, *so long as* an assembly-qualified name
    is used as a *string constant*

        // bad
        string name = GetSomeTypeName();
        Type type = Type.GetType(name);

        // good
        Type type = Type.GetType("My.Example.TypeName, ExampleAssembly");

  * `Type.GetMethod(string)`, but see fine print below.
  * `Type.GetProperty(string)`, but see fine print below.
  * `MethodInfo.Invoke()`
  * …

**The Fine Print**: in order for many Reflection-based APIs such as
`Type.GetMethod(string)` to work, the program must contain
"reflection metadata."

We can now describe, briefly, what NativeAOT does:

 1. Accept IL/assemblies as input
 2. Internally trim the IL, optimizes them, etc.
 3. Generates a native binary containing two separate bits of data:

    * Native machine code for execution
    * "reflection metadata"

You can get an inkling of what methods survived trimming and become
native code by using `dotnet publish -p:IlcGenerateMapFile=true …`,
which will produce a `.mstat` file, a PE file which references every
type and member which is in the native binary, ***after*** inlining.
(Meaning if a member is inlined, it *won't* be present!)
Use e.g. `monodis --memberref App.mstat` to list post-trimmed members.

You can get an inkling of the "reflection metadata" that is present
by using `dotnet publish -p:IlcGenerateMetadataLog=true …`, which
produces a `.metadata.csv` file.  It's not particularly scrutible,
but can be used to verify observed behavior.  Consider one of the above
failure messages:

	The [TabNavigation] property getter does not exist on type [Microsoft.UI.Xaml.Controls.FlipView]

An obvious question to ask: is the `TabNavigation` property in the
reflection metadata?  Search for `"TabNavigation"` (with quotes!),
and it's *not* in there.  Compare to when this commit is in use, and:

	66087019, Property, "TabNavigation", "3410be9a 6810bea8 5410beae 5410beb2"
	3410be9a, ConstantStringValue, "TabNavigation", ""

The 3rd column is the property name `TabNavigation`, while the 2nd
column is the type; for properties, we want `Property`.  The 4th
column is "Children", one of which is:

	5410beae, MethodSemantics, "Getter : [HasThis]  Microsoft.UI.Xaml.Input.KeyboardNavigationMode get_TabNavigation()", "50086c65"

which at least verifies that `TabNavigation` is in play.

Programs can control what is contained within reflection metadata
by using string constants with Reflection APIs, for example:

	typeof(SomeKnownType).GetMethod("ConstantName")

will ensure that reflection metadata contains
`SomeKnownType.ConstantName()`, and `ConstantName()` will be returned
from `typeof(SomeKnownType).GetMethods()` and can be invoked with
`MethodInfo.Invoke()`.

If you *don't* use `typeof(SomeKnownType)` or string constants with
`Type.GetType(string)` or `type.GetMethod("constant")`, then you need
to "suggest" that some pieces of information be added to
reflection metadata.

The two predominant ways to do this are via the custom attributes:

  * [`DynamicallyAccessedMembersAttribute`][2]
  * [`DynamicDependencyAttribute`][3]

From the Uno perspective, enter `BindingPropertyHelper`: *everything*
is based on Reflection!

Fortunately NativeAOT "supports" Reflection!  (See above.)

*Unfortunately*, we now need to convince NativeAOT to store the
reflection metadata Uno requires in order to work!

Various other PRs have dealt with some of this, such as #21920.

Returning to the original failure messages, e.g.

	fail: Uno.UI.Dispatching.NativeDispatcher[0]
	      The [TabNavigation] property getter does not exist on type [Microsoft.UI.Xaml.Controls.FlipView]

During the app build, `src/SourceGenerators/Uno.UI.SourceGenerators`
generates a `BindableMetadata.g.cs` file which will mention
`[Bindable]` types, e.g. for `Microsoft.UI.Xaml.Controls.FlipView`:

	/// <summary>
	/// Builder for Microsoft.UI.Xaml.Controls.FlipView
	/// </summary>
	[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute]
	static class MetadataBuilder_239
	{
	  internal static global::Uno.UI.DataBinding.IBindableType Build(global::Uno.UI.DataBinding.BindableType parent)
	  {
	    var bindableType = parent ?? new global::Uno.UI.DataBinding.BindableType(151, typeof(global::Microsoft.UI.Xaml.Controls.FlipView));
	    // …
	  }
	  private static object CreateInstance() => new global::Microsoft.UI.Xaml.Controls.FlipView();
	}

`BindableMetadata.g.cs` was needed in the ye olde Xamarin world order
to tell *its* linker to e.g. "preserve the `FlipView` default ctor!"

*Now*, we can build upon this infrastructure and update `BindableType`
to request that Reflection information regarding properties, fields,
and constructors be retained.  We can do this by updating the
`BindableType` constructor:

	partial class BindableType {
	  internal const DynamicallyAccessedMemberTypes TypeRequirements =
	    DynamicallyAccessedMemberTypes.PublicProperties | …;
	  public BindableType(int estimatedPropertySize, [DynamicallyAccessedMembers(TypeRequirements)] Type sourceType) => …
	}

*Because* we have `BindableMetadata.g.cs` *and* because it provides
`typeof(FlipView)`, this directs NativeAOT to make property and other
reflection metadata available to the app, *fixing* the error messages.

Expand this addition of `[DynamicallyAccessedMembers]` to also include
`BindableProperty` and related types.

Note: the IL2111 warning "Method 'System.Type.TypeInitializer.get' …"
is *caused by* `typeof(Type)` (?!), which is particularly odd given
that we don't use `Type.TypeInitializer`!
Thanks to Alexander Köplinger for [looking into this][4].

[0]: https://learn.microsoft.comdotnet/core/deploying/native-aot
[1]: https://learn.microsoft.comdotnet/core/deploying/native-aot/#limitations-of-native-aot-deployment
[2]: https://learn.microsoft.comdotnet/api/system.diagnostics.codeanalysis.dynamicallyaccessedmembersattribute?view=net-10.0
[3]: https://learn.microsoft.comdotnet/api/system.diagnostics.codeanalysis.dynamicdependencyattribute?view=net-10.0
[4]: https://discord.com/channels/732297728826277939/732297837953679412/1445773677152174150
jonpryor added a commit to unoplatform/uno that referenced this pull request Dec 3, 2025
Context: unoplatform/uno.chefs#1712
Context: #21920
Context: unoplatform/uno.extensions#2966
Context: unoplatform/uno.extensions#2969
Context: https://discord.com/channels/732297728826277939/732297837953679412/1444909702512119899

While running the uno.chefs app on macOS under NativeAOT:

	dotnet publish -c Release -r osx-x64 -f net10.0-desktop -p:TargetFrameworkOverride=net10.0-desktop -bl \
	  Chefs/Chefs.csproj \
	  -p:SelfContained=true -p:PublishAot=true -p:IsAotCompatible=true -p:UseSkiaRendering=true \
	  -p:IlcGenerateMapFile=true -p:IlcGenerateMstatFile=true -p:IlcGenerateDgmlFile=true \
	  -p:EmitCompilerGeneratedFiles=true -p:CompilerGeneratedFilesOutputPath=`pwd`/_gen
	Chefs/bin/Release/net10.0-desktop/osx-x64/publish/Chefs

Console output would contain the following errors:

	fail: Uno.UI.Dispatching.NativeDispatcher[0]
	      The [TabNavigation] property getter does not exist on type [Microsoft.UI.Xaml.Controls.FlipView]
	fail: Uno.UI.Dispatching.NativeDispatcher[0]
	      The [Y] property getter does not exist on type [Windows.Foundation.Point]
	fail: Uno.UI.Dispatching.NativeDispatcher[0]
	      The [X] property getter does not exist on type [Windows.Foundation.Point]

At this point, a description of the relation between NativeAOT and
Reflection would be useful.

*Parts* of System.Reflection can be used.  Which parts *can* be used,
and how to use them, is oddly *not* well specified in the
[Native AOT deployment][0] documentation.

What doesn't work is outlined in the [limitations][1] section:

> * No dynamic loading, for example, `Assembly.LoadFile`.
> * No run-time code generation, for example, `System.Reflection.Emit`.

What *always* appears to work is:

  * `Type.GetType()` returns a `Type` instance.

What ***can*** work is:

  * `Type.GetType(string)`, *so long as* an assembly-qualified name
    is used as a *string constant*

        // bad
        string name = GetSomeTypeName();
        Type type = Type.GetType(name);

        // good
        Type type = Type.GetType("My.Example.TypeName, ExampleAssembly");

  * `Type.GetMethod(string)`, but see fine print below.
  * `Type.GetProperty(string)`, but see fine print below.
  * `MethodInfo.Invoke()`
  * …

**The Fine Print**: in order for many Reflection-based APIs such as
`Type.GetMethod(string)` to work, the program must contain
"reflection metadata."

We can now describe, briefly, what NativeAOT does:

 1. Accept IL/assemblies as input
 2. Internally trim the IL, optimizes them, etc.
 3. Generates a native binary containing two separate bits of data:

    * Native machine code for execution
    * "reflection metadata"

You can get an inkling of what methods survived trimming and become
native code by using `dotnet publish -p:IlcGenerateMapFile=true …`,
which will produce a `.mstat` file, a PE file which references every
type and member which is in the native binary, ***after*** inlining.
(Meaning if a member is inlined, it *won't* be present!)
Use e.g. `monodis --memberref App.mstat` to list post-trimmed members.

You can get an inkling of the "reflection metadata" that is present
by using `dotnet publish -p:IlcGenerateMetadataLog=true …`, which
produces a `.metadata.csv` file.  It's not particularly scrutible,
but can be used to verify observed behavior.  Consider one of the above
failure messages:

	The [TabNavigation] property getter does not exist on type [Microsoft.UI.Xaml.Controls.FlipView]

An obvious question to ask: is the `TabNavigation` property in the
reflection metadata?  Search for `"TabNavigation"` (with quotes!),
and it's *not* in there.  Compare to when this commit is in use, and:

	66087019, Property, "TabNavigation", "3410be9a 6810bea8 5410beae 5410beb2"
	3410be9a, ConstantStringValue, "TabNavigation", ""

The 3rd column is the property name `TabNavigation`, while the 2nd
column is the type; for properties, we want `Property`.  The 4th
column is "Children", one of which is:

	5410beae, MethodSemantics, "Getter : [HasThis]  Microsoft.UI.Xaml.Input.KeyboardNavigationMode get_TabNavigation()", "50086c65"

which at least verifies that `TabNavigation` is in play.

Programs can control what is contained within reflection metadata
by using string constants with Reflection APIs, for example:

	typeof(SomeKnownType).GetMethod("ConstantName")

will ensure that reflection metadata contains
`SomeKnownType.ConstantName()`, and `ConstantName()` will be returned
from `typeof(SomeKnownType).GetMethods()` and can be invoked with
`MethodInfo.Invoke()`.

If you *don't* use `typeof(SomeKnownType)` or string constants with
`Type.GetType(string)` or `type.GetMethod("constant")`, then you need
to "suggest" that some pieces of information be added to
reflection metadata.

The two predominant ways to do this are via the custom attributes:

  * [`DynamicallyAccessedMembersAttribute`][2]
  * [`DynamicDependencyAttribute`][3]

From the Uno perspective, enter `BindingPropertyHelper`: *everything*
is based on Reflection!

Fortunately NativeAOT "supports" Reflection!  (See above.)

*Unfortunately*, we now need to convince NativeAOT to store the
reflection metadata Uno requires in order to work!

Various other PRs have dealt with some of this, such as #21920.

Returning to the original failure messages, e.g.

	fail: Uno.UI.Dispatching.NativeDispatcher[0]
	      The [TabNavigation] property getter does not exist on type [Microsoft.UI.Xaml.Controls.FlipView]

During the app build, `src/SourceGenerators/Uno.UI.SourceGenerators`
generates a `BindableMetadata.g.cs` file which will mention
`[Bindable]` types, e.g. for `Microsoft.UI.Xaml.Controls.FlipView`:

	/// <summary>
	/// Builder for Microsoft.UI.Xaml.Controls.FlipView
	/// </summary>
	[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute]
	static class MetadataBuilder_239
	{
	  internal static global::Uno.UI.DataBinding.IBindableType Build(global::Uno.UI.DataBinding.BindableType parent)
	  {
	    var bindableType = parent ?? new global::Uno.UI.DataBinding.BindableType(151, typeof(global::Microsoft.UI.Xaml.Controls.FlipView));
	    // …
	  }
	  private static object CreateInstance() => new global::Microsoft.UI.Xaml.Controls.FlipView();
	}

`BindableMetadata.g.cs` was needed in the ye olde Xamarin world order
to tell *its* linker to e.g. "preserve the `FlipView` default ctor!"

*Now*, we can build upon this infrastructure and update `BindableType`
to request that Reflection information regarding properties, fields,
and constructors be retained.  We can do this by updating the
`BindableType` constructor:

	partial class BindableType {
	  internal const DynamicallyAccessedMemberTypes TypeRequirements =
	    DynamicallyAccessedMemberTypes.PublicProperties | …;
	  public BindableType(int estimatedPropertySize, [DynamicallyAccessedMembers(TypeRequirements)] Type sourceType) => …
	}

*Because* we have `BindableMetadata.g.cs` *and* because it provides
`typeof(FlipView)`, this directs NativeAOT to make property and other
reflection metadata available to the app, *fixing* the error messages.

Expand this addition of `[DynamicallyAccessedMembers]` to also include
`BindableProperty` and related types.

Note: the IL2111 warning "Method 'System.Type.TypeInitializer.get' …"
is *caused by* `typeof(Type)` (?!), which is particularly odd given
that we don't use `Type.TypeInitializer`!
Thanks to Alexander Köplinger for [looking into this][4].

[0]: https://learn.microsoft.comdotnet/core/deploying/native-aot
[1]: https://learn.microsoft.comdotnet/core/deploying/native-aot/#limitations-of-native-aot-deployment
[2]: https://learn.microsoft.comdotnet/api/system.diagnostics.codeanalysis.dynamicallyaccessedmembersattribute?view=net-10.0
[3]: https://learn.microsoft.comdotnet/api/system.diagnostics.codeanalysis.dynamicdependencyattribute?view=net-10.0
[4]: https://discord.com/channels/732297728826277939/732297837953679412/1445773677152174150
jonpryor added a commit to unoplatform/uno that referenced this pull request Dec 4, 2025
Context: unoplatform/uno.chefs#1712
Context: #21920
Context: unoplatform/uno.extensions#2966
Context: unoplatform/uno.extensions#2969
Context: https://discord.com/channels/732297728826277939/732297837953679412/1444909702512119899

While running the uno.chefs app on macOS under NativeAOT:

	dotnet publish -c Release -r osx-x64 -f net10.0-desktop -p:TargetFrameworkOverride=net10.0-desktop -bl \
	  Chefs/Chefs.csproj \
	  -p:SelfContained=true -p:PublishAot=true -p:IsAotCompatible=true -p:UseSkiaRendering=true \
	  -p:IlcGenerateMapFile=true -p:IlcGenerateMstatFile=true -p:IlcGenerateDgmlFile=true \
	  -p:EmitCompilerGeneratedFiles=true -p:CompilerGeneratedFilesOutputPath=`pwd`/_gen
	Chefs/bin/Release/net10.0-desktop/osx-x64/publish/Chefs

Console output would contain the following errors:

	fail: Uno.UI.Dispatching.NativeDispatcher[0]
	      The [TabNavigation] property getter does not exist on type [Microsoft.UI.Xaml.Controls.FlipView]
	fail: Uno.UI.Dispatching.NativeDispatcher[0]
	      The [Y] property getter does not exist on type [Windows.Foundation.Point]
	fail: Uno.UI.Dispatching.NativeDispatcher[0]
	      The [X] property getter does not exist on type [Windows.Foundation.Point]

At this point, a description of the relation between NativeAOT and
Reflection would be useful.

*Parts* of System.Reflection can be used.  Which parts *can* be used,
and how to use them, is oddly *not* well specified in the
[Native AOT deployment][0] documentation.

What doesn't work is outlined in the [limitations][1] section:

> * No dynamic loading, for example, `Assembly.LoadFile`.
> * No run-time code generation, for example, `System.Reflection.Emit`.

What *always* appears to work is:

  * `Type.GetType()` returns a `Type` instance.

What ***can*** work is:

  * `Type.GetType(string)`, *so long as* an assembly-qualified name
    is used as a *string constant*

        // bad
        string name = GetSomeTypeName();
        Type type = Type.GetType(name);

        // good
        Type type = Type.GetType("My.Example.TypeName, ExampleAssembly");

  * `Type.GetMethod(string)`, but see fine print below.
  * `Type.GetProperty(string)`, but see fine print below.
  * `MethodInfo.Invoke()`
  * …

**The Fine Print**: in order for many Reflection-based APIs such as
`Type.GetMethod(string)` to work, the program must contain
"reflection metadata."

We can now describe, briefly, what NativeAOT does:

 1. Accept IL/assemblies as input
 2. Internally trim the IL, optimizes them, etc.
 3. Generates a native binary containing two separate bits of data:

    * Native machine code for execution
    * "reflection metadata"

You can get an inkling of what methods survived trimming and become
native code by using `dotnet publish -p:IlcGenerateMstatFile=true …`,
which will produce a `.mstat` file, a PE file which references every
type and member which is in the native binary, ***after*** inlining.
(Meaning if a member is inlined, it *won't* be present!)
Use e.g. `monodis --memberref App.mstat` to list post-trimmed members.

You can get an inkling of the "reflection metadata" that is present
by using `dotnet publish -p:IlcGenerateMetadataLog=true …`, which
produces a `.metadata.csv` file.  It's not particularly scrutible,
but can be used to verify observed behavior.  Consider one of the above
failure messages:

	The [TabNavigation] property getter does not exist on type [Microsoft.UI.Xaml.Controls.FlipView]

An obvious question to ask: is the `TabNavigation` property in the
reflection metadata?  Search for `"TabNavigation"` (with quotes!),
and it's *not* in there.  Compare to when this commit is in use, and:

	66087019, Property, "TabNavigation", "3410be9a 6810bea8 5410beae 5410beb2"
	3410be9a, ConstantStringValue, "TabNavigation", ""

The 3rd column is the property name `TabNavigation`, while the 2nd
column is the type; for properties, we want `Property`.  The 4th
column is "Children", one of which is:

	5410beae, MethodSemantics, "Getter : [HasThis]  Microsoft.UI.Xaml.Input.KeyboardNavigationMode get_TabNavigation()", "50086c65"

which at least verifies that `TabNavigation` is in play.

Programs can control what is contained within reflection metadata
by using string constants with Reflection APIs, for example:

	typeof(SomeKnownType).GetMethod("ConstantName")

will ensure that reflection metadata contains
`SomeKnownType.ConstantName()`, and `ConstantName()` will be returned
from `typeof(SomeKnownType).GetMethods()` and can be invoked with
`MethodInfo.Invoke()`.

If you *don't* use `typeof(SomeKnownType)` or string constants with
`Type.GetType(string)` or `type.GetMethod("constant")`, then you need
to "suggest" that some pieces of information be added to
reflection metadata.

The two predominant ways to do this are via the custom attributes:

  * [`DynamicallyAccessedMembersAttribute`][2]
  * [`DynamicDependencyAttribute`][3]

From the Uno perspective, enter `BindingPropertyHelper`: *everything*
is based on Reflection!

Fortunately NativeAOT "supports" Reflection!  (See above.)

*Unfortunately*, we now need to convince NativeAOT to store the
reflection metadata Uno requires in order to work!

Various other PRs have dealt with some of this, such as #21920.

Returning to the original failure messages, e.g.

	fail: Uno.UI.Dispatching.NativeDispatcher[0]
	      The [TabNavigation] property getter does not exist on type [Microsoft.UI.Xaml.Controls.FlipView]

During the app build, `src/SourceGenerators/Uno.UI.SourceGenerators`
generates a `BindableMetadata.g.cs` file which will mention
`[Bindable]` types, e.g. for `Microsoft.UI.Xaml.Controls.FlipView`:

	/// <summary>
	/// Builder for Microsoft.UI.Xaml.Controls.FlipView
	/// </summary>
	[global::System.Runtime.CompilerServices.CompilerGeneratedAttribute]
	static class MetadataBuilder_239
	{
	  internal static global::Uno.UI.DataBinding.IBindableType Build(global::Uno.UI.DataBinding.BindableType parent)
	  {
	    var bindableType = parent ?? new global::Uno.UI.DataBinding.BindableType(151, typeof(global::Microsoft.UI.Xaml.Controls.FlipView));
	    // …
	  }
	  private static object CreateInstance() => new global::Microsoft.UI.Xaml.Controls.FlipView();
	}

`BindableMetadata.g.cs` was needed in the ye olde Xamarin world order
to tell *its* linker to e.g. "preserve the `FlipView` default ctor!"

*Now*, we can build upon this infrastructure and update `BindableType`
to request that Reflection information regarding properties, fields,
and constructors be retained.  We can do this by updating the
`BindableType` constructor:

	partial class BindableType {
	  internal const DynamicallyAccessedMemberTypes TypeRequirements =
	    DynamicallyAccessedMemberTypes.PublicProperties | …;
	  public BindableType(int estimatedPropertySize, [DynamicallyAccessedMembers(TypeRequirements)] Type sourceType) => …
	}

*Because* we have `BindableMetadata.g.cs` *and* because it provides
`typeof(FlipView)`, this directs NativeAOT to make property and other
reflection metadata available to the app, *fixing* the error messages.

Expand this addition of `[DynamicallyAccessedMembers]` to also include
`BindableProperty` and related types.

Note: the IL2111 warning "Method 'System.Type.TypeInitializer.get' …"
is *caused by* `typeof(Type)` (?!), which is particularly odd given
that we don't use `Type.TypeInitializer`!
Thanks to Alexander Köplinger for [looking into this][4].

[0]: https://learn.microsoft.comdotnet/core/deploying/native-aot
[1]: https://learn.microsoft.comdotnet/core/deploying/native-aot/#limitations-of-native-aot-deployment
[2]: https://learn.microsoft.comdotnet/api/system.diagnostics.codeanalysis.dynamicallyaccessedmembersattribute?view=net-10.0
[3]: https://learn.microsoft.comdotnet/api/system.diagnostics.codeanalysis.dynamicdependencyattribute?view=net-10.0
[4]: https://discord.com/channels/732297728826277939/732297837953679412/1445773677152174150
@jonpryor

jonpryor commented Dec 8, 2025

Copy link
Copy Markdown
Contributor Author

Given the investigation, what are possible fixes?

A fix is the original approach in this PR: update ModelAttribute.Bindable/IModel<T> to use [DynamicallyAccessedMembers] to preserve constructors.

An alternate approach may be to update the pattern used "downstream" in uno.chefs, so that instead of a Dictionary<Type, Type> being constructed and passed around, there is instead a set of method invocations:

foo.AddViewModelMapping(typeof(ShellModel), typeof(ShellViewModel));
// …

AddViewModelMapping() could then have Type parameters which use [DynamicallyAccessedMembers], allowing NativeAOT to "see" that things should be preserved.

Unanswered question: what is the real API for this, how are the apps and generators updated to use it, etc.

Possible answer: we could retain the existing "API" of ReactiveViewModelMappings.ViewModelMappings, but turn the field into a property which performs the above AddViewModelMapping() ("somehow") before returning a runtime dictionary?

@jonpryor

jonpryor commented Dec 8, 2025

Copy link
Copy Markdown
Contributor Author

Aside: this is dead code:

// Attempt to create view model using reflection
try
{
var ctr = mapping.ViewModel.GetNavigationConstructor(navigator!, Region.Services!, out var args);
if (ctr is not null)
{
return ctr.Invoke(args);
}
}
catch
{
if (Logger.IsEnabled(LogLevel.Information)) Logger.LogInformationMessage("ViewModel not included in RouteMap, and unable to instance using Activator instead of ServiceProvider");
}
return default;

It's dead code because services.GetService(mapping!.ViewModel); never returns null; it throws!

var created = services.GetService(mapping!.ViewModel);

This is in fact the unhanded exception I'm seeing, because services.GetService(…) throws. (We never hit mapping.ViewModel.GetNavigationConstructor(…)!)

@jonpryor

jonpryor commented Dec 9, 2025

Copy link
Copy Markdown
Contributor Author

Building upon alternate approaches, another alternate approach -- which maintains API and ABI -- is to update BindableViewModelMappingGenerator.cs to emit code which involves [DynamicallyAccessedMembers]:

diff --git a/src/Uno.Extensions.Reactive.Generator/Bindables/BindableViewModelMappingGenerator.cs b/src/Uno.Extensions.Reactive.Generator/Bindables/BindableViewModelMappingGenerator.cs
index c71c55905..da99c8bf1 100644
--- a/src/Uno.Extensions.Reactive.Generator/Bindables/BindableViewModelMappingGenerator.cs
+++ b/src/Uno.Extensions.Reactive.Generator/Bindables/BindableViewModelMappingGenerator.cs
@@ -33,16 +33,30 @@ internal class BindableViewModelMappingGenerator : ICodeGenTool
 				{this.GetCodeGenAttribute()}
 				public static partial class ReactiveViewModelMappings
 				{{
+					private const global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes Types =
+						  global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors
+						| global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties
+						;
+					private static global::System.Collections.Generic.KeyValuePair<global::System.Type, global::System.Type> E(
+						[global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(Types)] global::System.Type key,
+						[global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(Types)] global::System.Type value)
+						=> new global::System.Collections.Generic.KeyValuePair<global::System.Type, global::System.Type>(key, value);
 					/// <summary>
 					/// Gets a mapping from a declared view model type (key) to its bindable counterpart type (value) generated by the feeds platform.
 					/// </summary>
 					/// <remarks>
 					/// This can be used in navigation engine to work only with the declared type while the navigation takes care to use the bindable friendly version.
 					/// </remarks>
-					public static readonly global::System.Collections.Generic.IDictionary<global::System.Type, global::System.Type> ViewModelMappings = new global::System.Collections.Generic.Dictionary<global::System.Type, global::System.Type>
+					public static readonly global::System.Collections.Generic.IDictionary<global::System.Type, global::System.Type> ViewModelMappings;
+					
+					static ReactiveViewModelMappings()
 					{{
-						{_bindableVMs.Select(kvp => $"{{ typeof({kvp.Key}), typeof({kvp.Value}) }},").Align(6)}
-					}};
+						var mappings = new global::System.Collections.Generic.Dictionary<global::System.Type, global::System.Type>();
+						var collection = (global::System.Collections.Generic.ICollection<
+							global::System.Collections.Generic.KeyValuePair<global::System.Type, global::System.Type>>) mappings;
+						{_bindableVMs.Select(kvp => $"collection.Add(E(typeof({kvp.Key}), typeof({kvp.Value})));").Align(6)}
+						ViewModelMappings = mappings;
+					}}
 				}}
 			}}".Align(0);
 

Resulting C# code is:

namespace Chefs
{
	[global::System.CodeDom.Compiler.GeneratedCodeAttribute("BindableViewModelMappingGenerator", "1")]
	public static partial class ReactiveViewModelMappings
	{
		private const global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes Types =
			  global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicConstructors
			| global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMemberTypes.PublicProperties
			;
		private static global::System.Collections.Generic.KeyValuePair<global::System.Type, global::System.Type> E(
			[global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(Types)] global::System.Type key,
			[global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(Types)] global::System.Type value)
			=> new global::System.Collections.Generic.KeyValuePair<global::System.Type, global::System.Type>(key, value);
		/// <summary>
		/// Gets a mapping from a declared view model type (key) to its bindable counterpart type (value) generated by the feeds platform.
		/// </summary>
		/// <remarks>
		/// This can be used in navigation engine to work only with the declared type while the navigation takes care to use the bindable friendly version.
		/// </remarks>
		public static readonly global::System.Collections.Generic.IDictionary<global::System.Type, global::System.Type> ViewModelMappings;

		static ReactiveViewModelMappings()
		{
			var mappings = new global::System.Collections.Generic.Dictionary<global::System.Type, global::System.Type>();
			var collection = (global::System.Collections.Generic.ICollection<
				global::System.Collections.Generic.KeyValuePair<global::System.Type, global::System.Type>>) mappings;
			collection.Add(E(typeof(Chefs.Presentation.CookbookDetailModel), typeof(global::Chefs.Presentation.CookbookDetailViewModel)));
			collection.Add(E(typeof(Chefs.Presentation.CreateUpdateCookbookModel), typeof(global::Chefs.Presentation.CreateUpdateCookbookViewModel)));
			collection.Add(E(typeof(Chefs.Presentation.FavoriteRecipesModel), typeof(global::Chefs.Presentation.FavoriteRecipesViewModel)));
			collection.Add(E(typeof(Chefs.Presentation.FilterModel), typeof(global::Chefs.Presentation.FilterViewModel)));
			collection.Add(E(typeof(Chefs.Presentation.GenericDialogModel), typeof(global::Chefs.Presentation.GenericDialogViewModel)));
			collection.Add(E(typeof(Chefs.Presentation.HomeModel), typeof(global::Chefs.Presentation.HomeViewModel)));
			collection.Add(E(typeof(Chefs.Presentation.LiveCookingModel), typeof(global::Chefs.Presentation.LiveCookingViewModel)));
			collection.Add(E(typeof(Chefs.Presentation.LoginModel), typeof(global::Chefs.Presentation.LoginViewModel)));
			collection.Add(E(typeof(Chefs.Presentation.MainModel), typeof(global::Chefs.Presentation.MainViewModel)));
			collection.Add(E(typeof(Chefs.Presentation.MapModel), typeof(global::Chefs.Presentation.MapViewModel)));
			collection.Add(E(typeof(Chefs.Presentation.NotificationsModel), typeof(global::Chefs.Presentation.NotificationsViewModel)));
			collection.Add(E(typeof(Chefs.Presentation.ProfileModel), typeof(global::Chefs.Presentation.ProfileViewModel)));
			collection.Add(E(typeof(Chefs.Presentation.RecipeDetailsModel), typeof(global::Chefs.Presentation.RecipeDetailsViewModel)));
			collection.Add(E(typeof(Chefs.Presentation.RegistrationModel), typeof(global::Chefs.Presentation.RegistrationViewModel)));
			collection.Add(E(typeof(Chefs.Presentation.SearchModel), typeof(global::Chefs.Presentation.SearchViewModel)));
			collection.Add(E(typeof(Chefs.Presentation.SettingsModel), typeof(global::Chefs.Presentation.SettingsViewModel)));
			collection.Add(E(typeof(Chefs.Presentation.ShellModel), typeof(global::Chefs.Presentation.ShellViewModel)));
			collection.Add(E(typeof(Chefs.Presentation.WelcomeModel), typeof(global::Chefs.Presentation.WelcomeViewModel)));
			ViewModelMappings = mappings;
		}
	}
}

The change is to use ICollection<T>.Add(T) instead of Dictionary<TKey, TValue>.Add(TKey, Value), which allows us to introduce a E(Type, Type) method with the appropriate [DynamicallyAccessedMembers] information that NativeAOT needs.

This allows things to work without the original ModelAttribute.Bindable/IModel<T> changes.

What I'm still uncertain about is whether it's the correct fix.

@jonpryor

jonpryor commented Dec 9, 2025

Copy link
Copy Markdown
Contributor Author

The benefit to using IDictionary<KeyValuePair<Type, Type>>.Add(KeyValuePair<Type, Type>) is that no new API surface area is introduced.

The downside is that it should (technically) be slower, as its going through an interface invocation instead of hitting Dictionary<Type, Type> directly.

We could change what the generator produces to still not add new API while avoiding interface invocations.

Another approach would be to add new API:

namespace Uno.Extensions.Reactive;

public class ModelToViewModelMapping : IDictionary<Type, Type> {
    public void Add([DAM()] Type key, [DAM()] Type value) =>}

which would allow minimal changes to the generator -- it would emit new ModelToViewMapping() instead of new Dictionary<Type, Type> -- while adding new API surface area. I'm not sure that this is a good tradeoff, and will break any code that assumes that ViewModelMappings is a Dictionary<Type, Type>.


We thus have 2-3 approaches to preserving constructors on Uno.Extensions.Reactive.Generator-generated types:

  1. Update ModelAttribute.Bindable and IModel<T>, as was originally done in this PR.
  2. Update Uno.Extensions.Reactive.Generator output so that .ViewModelMappings generation goes through methods which are appropriately annotated with [DynamicallyAccessedMembers]
  3. (2) but the "methods appropriately annotated" are public API.

Given these three approaches, and given that it's Uno.Extensions.Reactive.Generator-generated types which are in play, my personal preference is (1).

@dr1rrb

dr1rrb commented Dec 9, 2025

Copy link
Copy Markdown
Member

Building upon alternate approaches, another alternate approach -- which maintains API and ABI -- is to update BindableViewModelMappingGenerator.cs to emit code which involves [DynamicallyAccessedMembers]:

../..

What I'm still uncertain about is whether it's the correct fix.

Yes definitely the best approach IMO. That dictionary is generated for the navigation to be able to know which VM to create when the user attempts to navigate to a given model type.

About the Add and the perf impact : negligible in that case. But I think a simpler code that would be enough could be:

		static ReactiveViewModelMappings()
		{
			var mappings = new global::System.Collections.Generic.Dictionary<global::System.Type, global::System.Type>();
			Add(mappings, typeof(Chefs.Presentation.CookbookDetailModel), typeof(global::Chefs.Presentation.CookbookDetailViewModel));
			Add(mappings, typeof(Chefs.Presentation.CreateUpdateCookbookModel), typeof(global::Chefs.Presentation.CreateUpdateCookbookViewModel));

			ViewModelMappings = mappings;
		}
		
private static void Add(
	global::System.Collections.Generic.Dictionary<global::System.Type, global::System.Type> mappings,
	[global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(Types)] global::System.Type key,
	[global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(Types)] global::System.Type value)
		=> mappings.Add(key, value);

100% retro compatible.

@dr1rrb

dr1rrb commented Dec 9, 2025

Copy link
Copy Markdown
Member

Building upon alternate approaches, another alternate approach -- which maintains API and ABI -- is to update BindableViewModelMappingGenerator.cs to emit code which involves [DynamicallyAccessedMembers]:
../..
What I'm still uncertain about is whether it's the correct fix.

Yes definitely the best approach IMO. That dictionary is generated for the navigation to be able to know which VM to create when the user attempts to navigate to a given model type.

About the Add and the perf impact : negligible in that case. But I think a simpler code that would be enough could be:

		static ReactiveViewModelMappings()
		{
			var mappings = new global::System.Collections.Generic.Dictionary<global::System.Type, global::System.Type>();
			Add(mappings, typeof(Chefs.Presentation.CookbookDetailModel), typeof(global::Chefs.Presentation.CookbookDetailViewModel));
			Add(mappings, typeof(Chefs.Presentation.CreateUpdateCookbookModel), typeof(global::Chefs.Presentation.CreateUpdateCookbookViewModel));

			ViewModelMappings = mappings;
		}
		
private static void Add(
	global::System.Collections.Generic.Dictionary<global::System.Type, global::System.Type> mappings,
	[global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(Types)] global::System.Type key,
	[global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(Types)] global::System.Type value)
		=> mappings.Add(key, value);

100% retro compatible.

Actually thinking again about it, I think the [DAM] should be set only on the value:

private static void Add(
	global::System.Collections.Generic.Dictionary<global::System.Type, global::System.Type> mappings,
	global::System.Type key,
	[global::System.Diagnostics.CodeAnalysis.DynamicallyAccessedMembers(Types)] global::System.Type value)
		=> mappings.Add(key, value);

We are not supposed to create dynamically instances of the model itself (i.e. the key). They are created without releflection in the generated ctor of the VM ^^

@jonpryor
jonpryor force-pushed the dev/jonpryor/jonp-preserve-Model-ctors-and-properties branch 2 times, most recently from 51509ec to 71cd426 Compare December 9, 2025 18:09
@jonpryor jonpryor changed the title chore: Preserve ModelAttribute.Bindable constructors, properties chore: Preserve generated *ViewModel constructors, properties Dec 9, 2025
Context: unoplatform/uno@559b1f7

While running the uno.chefs app on macOS under NativeAOT:

	dotnet publish -c Release -r osx-x64 -f net10.0-desktop -p:TargetFrameworkOverride=net10.0-desktop -bl \
	  Chefs/Chefs.csproj \
	  -p:SelfContained=true -p:PublishAot=true -p:IsAotCompatible=true -p:UseSkiaRendering=true \
	  -p:IlcGenerateMapFile=true -p:IlcGenerateMstatFile=true -p:IlcGenerateDgmlFile=true -p:IlcGenerateMetadataLog=true \
	  -p:EmitCompilerGeneratedFiles=true -p:CompilerGeneratedFilesOutputPath=`pwd`/_gen
	Chefs/bin/Release/net10.0-desktop/osx-x64/publish/Chefs

Console output would contain the following warnings:

	fail: Uno.UI.Dispatching.NativeDispatcher[0]
	      NativeDispatcher unhandled exception
	      System.InvalidOperationException: A suitable constructor for type 'Chefs.Presentation.ShellViewModel' could not be located. Ensure the type is concrete and services are registered for all parameters of a public constructor.

With a local patch in `Uno.dll`, we see that the above
`InvalidOperationException` is from
Microsoft.Extensions.DependencyInjection:

	fail: Uno.UI.Dispatching.NativeDispatcher[0]
	      NativeDispatcher unhandled exception
	      System.InvalidOperationException: A suitable constructor for type 'Chefs.Presentation.ShellViewModel' could not be located. Ensure the type is concrete and services are registered for all parameters of a public constructor.
	         at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateConstructorCallSite(ResultCache, ServiceIdentifier, Type, CallSiteChain) + 0x3e9
	         at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateExact(ServiceDescriptor, ServiceIdentifier, CallSiteChain, Int32) + 0x181
	         at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.TryCreateExact(ServiceIdentifier, CallSiteChain) + 0x14f
	         at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.CreateCallSite(ServiceIdentifier serviceIdentifier, CallSiteChain callSiteChain) + 0xc9
	         at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.GetCallSite(ServiceIdentifier, CallSiteChain) + 0x92
	         at Microsoft.Extensions.DependencyInjection.ServiceProvider.CreateServiceAccessor(ServiceIdentifier serviceIdentifier) + 0x68
	         at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey, Func`2) + 0xd2
	         at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(ServiceIdentifier, ServiceProviderEngineScope) + 0x39
	         at Uno.Extensions.Navigation.Navigators.ControlNavigator.<>c__DisplayClass8_0.<<CreateViewModel>b__0>d.MoveNext() + 0x80

The problem is that trimming removed the `ShellViewModel` constructor,
along with other members.

Question: why were these members removed?

Answer 1: because the members weren't *used*, via static analysis.
This is the case here, as there is no `new ShellViewModel()` within
`uno.chefs` or its generated code.

Answer 2: Even when "Answer 1" isn't true, the linker *not* removing
a member *doesn't* mean that the member can be accessed through
Reflection!  Reflection requires a different set of data.

We thus need to convince the trimmer to not only preserve the members,
but also to allow the members to be used through Reflection.

From unoplatform/uno@559b1f78:

> The two predominant ways to do this are via the custom attributes:
>
>   * `DynamicallyAccessedMembersAttribute`
>   * `DynamicDependencyAttribute`

At this point we know we have e.g. `ShellModel` (user-written code),
and `ShellViewModel` (from `src/Uno.Extensions.Reactive.Generator`).

How are these types associated with each other at runtime?

They're associated with each other at runtime via a dictionary
generated by `BindableViewModelMappingGenerator`:

	// generated code
	namespace Chefs;
	partial class ReactiveViewModelMappings {
	  public static readonly IDictionary<Type, Type> ViewModelMappings = new Dictionary<Type, Type> {
	    { typeof(ShellModel), typeof(ShellViewModel) },
	    // …
	  };
	}

Use of `IViewRegistry.FindViewByModel<T>()` allows finding the latter
from the former, e.g. `views.FindViewByModel<ShellModel>()` returns a
`MappedViewMap` with `MappedViewMap.MappedViewModel` being
`ShellViewModel`:

	Console.WriteLine(views.FindByViewModel<ShellModel>());
	// MappedViewMap { View = , ViewSelector = , ViewModel = Chefs.Presentation.ShellModel, Data = , ResultData = , ViewAttributes = , MappedViewModel = Chefs.Presentation.ShellViewModel }

To preserve the `ShellViewModel` members, where do we put
`[DynamicallyAccessedMembers]` or `[DynamicDependency]`?

There are two plausible places:

 1. On `ModelAttribute` and `IModel<T>`
 2. Within `BindableViewModelMappingGenerator`-generated code.

(1) `ModelAttribute` and `IModel<T>` are viable locations because
`ViewModelGenTool_*` emits partial types for e.g. `ShellModel`:

	// generated code
	[Model(typeof(ShellViewModel))]
	partial record ShellModel : IModel<ShellViewModel> {
	}

While this works, it also means that using `[Model]`/`IModel<T>`
without MVUX would result in app size increases.

Which leaves (2): update `BindableViewModelMappingGenerator` so that
a codepath uses `[DynamicallyAccessedMembers]`.

Update `BindableViewModelMappingGenerator` to instead emit code
similar to:

	namespace Chefs;
	static partial class ReactiveViewModelMappings
	{
	  private const DynamicallyAccessedMemberTypes Types =
	      DynamicallyAccessedMemberTypes.PublicConstructors
	    | DynamicallyAccessedMemberTypes.PublicProperties
	    ;

	  private static void AddMapping(
	    Dictionary<Type, Type> dict,
	    Type key,
	    [DynamicallyAccessedMembers(Types)] Type value)
	    => dict.Add(key, value);

	  public static readonly IDictionary<Type, Type> ViewModelMappings;

	  static ReactiveViewModelMappings()
	  {
	    var mappings = new Dictionary<Type, Type>();
	    AddMapping(mappings, typeof(Chefs.Presentation.ShellModel), typeof(global::Chefs.Presentation.ShellViewModel));
	    // …
	    ViewModelMappings = mappings;
	  }
	}

The introduction of `ReactiveViewModelMappings.AddMapping()` allows
a place to provide `[DynamicallyAccessedMembers]`, which in turn
allows the trimmer to preserve constructors and properties on
`ShellViewModel`, which fixes the `InvalidOperationException`.

Note that properties must also be preserved, as if only constructors
are preserved we will see this failure message:

	fail: Uno.UI.DataBinding.BindingPropertyHelper[0]
	      The [Pages] property getter does not exist on type [Chefs.Presentation.WelcomeViewModel]
@jonpryor
jonpryor force-pushed the dev/jonpryor/jonp-preserve-Model-ctors-and-properties branch from 71cd426 to 30d79ed Compare December 9, 2025 18:18
@jonpryor
jonpryor merged commit 73b32d1 into main Dec 9, 2025
14 of 15 checks passed
@jonpryor
jonpryor deleted the dev/jonpryor/jonp-preserve-Model-ctors-and-properties branch December 9, 2025 18:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants