Skip to content

Avoid reflective default key comparer creation in compiled models (NativeAOT)#38427

Closed
Copilot wants to merge 4 commits into
mainfrom
copilot/compiled-models-aot-compatibility-fix
Closed

Avoid reflective default key comparer creation in compiled models (NativeAOT)#38427
Copilot wants to merge 4 commits into
mainfrom
copilot/compiled-models-aot-compatibility-fix

Conversation

Copilot AI commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Compiled models crash under NativeAOT when constructing key value factories for key types whose generic instantiation isn't statically present (e.g. Guid). Resolving the key comparer flowed through CoreTypeMapping.KeyComparer, which uses the reflective ValueComparer.CreateDefault(Type, bool)MethodInfo.MakeGenericMethod() — a [RequiresDynamicCode] API that throws at runtime under AOT (int happened to work only because CreateDefault<int> is statically referenced elsewhere).

Changes

  • Move the fix into compiled-model generation so NativeAOT compiled models force key-relevant properties onto a cloned type mapping whose key comparer is emitted via the generic ValueComparer.CreateDefault<T>(favorStructuralComparisons: true) path instead of the reflective lazy fallback.
  • Generate the default key comparer in the type mapping, not on the property:
    • CreateDefaultTypeMapping now always returns the default mapping instance.
    • Type-mapping code generation now emits a clone whenever a NativeAOT key/foreign-key/unique-index property still relies on the default key comparer for its CLR type.
    • Redundant property-level SetKeyComparer(...) emission is removed in that case, so GetKeyValueComparer() falls back to property.TypeMapping.KeyComparer.
  • Keep shared runtime paths unchanged:
    • SimplePrincipalKeyValueFactory<TKey> and IProperty.CreateKeyEqualityComparer<TProperty>() go back to the normal GetKeyValueComparer() codepath for all property implementations.
    • The temporary RuntimeProperty.GetKeyValueComparer(Func<ValueComparer>) overload and CoreTypeMapping.FindKeyComparer() accessor are removed.
id.TypeMapping = InMemoryTypeMapping.Default.Clone(
    comparer: ...,
    keyComparer: ValueComparer.CreateDefault<Guid>(favorStructuralComparisons: true),
    providerValueComparer: ...);

Notes

  • The fix is now scoped to compiled models, which is where the NativeAOT failure occurs.
  • Custom/model-configured comparers are still honored; the new emission only applies to the default key-comparer case.
  • A separate, pre-existing MakeGenericType path in ToNullableComparer (nullable-FK dependent factories) remains out of scope and unaffected behaviorally.
  • Adapted the coverage into compiled-model tests and refreshed the affected scaffolding baselines instead of keeping the focused RuntimePropertyTest.

…on (AOT)

Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
@AndriySvyryd AndriySvyryd requested a review from Copilot June 13, 2026 01:17
Copilot AI changed the title [WIP] Fix compiled models to generate AOT compatible code Avoid reflective default key comparer creation in compiled models (NativeAOT) Jun 13, 2026
Copilot AI requested a review from AndriySvyryd June 13, 2026 01:18

Copilot AI left a comment

Copy link
Copy Markdown

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 addresses a NativeAOT incompatibility in compiled models by avoiding reflection-based default ValueComparer creation for key comparers. It does so by flowing the generic type argument (TKey / TProperty) into key-comparer construction so that ValueComparer.CreateDefault<T>(...) can be used instead of the [RequiresDynamicCode] ValueComparer.CreateDefault(Type, ...).

Changes:

  • Add CoreTypeMapping.FindKeyComparer() to access an already-configured/created key comparer without triggering reflective default creation.
  • Add RuntimeProperty.GetKeyValueComparer(Func<ValueComparer>) to allow supplying an AOT-safe generic default factory while still honoring any comparer configured in the model/type mapping.
  • Update key comparer creation paths (IProperty.CreateKeyEqualityComparer<TProperty>(), SimplePrincipalKeyValueFactory<TKey>) and add focused unit tests (RuntimePropertyTest).

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
test/EFCore.Tests/Metadata/RuntimePropertyTest.cs Adds tests validating the new factory-based comparer path doesn’t force creation of CoreTypeMapping.KeyComparer and still honors model-provided comparers.
src/EFCore/Storage/CoreTypeMapping.cs Introduces FindKeyComparer() to read an existing key comparer without creating a default one.
src/EFCore/Metadata/RuntimeProperty.cs Adds a GetKeyValueComparer(Func<ValueComparer>) overload to support a generic default factory and avoid reflection-based comparer creation.
src/EFCore/Metadata/IProperty.cs Updates CreateKeyEqualityComparer<TProperty>() to use the new factory-based path for RuntimeProperty.
src/EFCore/ChangeTracking/Internal/SimplePrincipalKeyValueFactory.cs Updates key factory construction to use the new factory-based key comparer path for RuntimeProperty.

Comment thread src/EFCore/Storage/CoreTypeMapping.cs Outdated
Comment thread test/EFCore.Tests/Metadata/RuntimePropertyTest.cs Outdated
Comment thread src/EFCore/ChangeTracking/Internal/SimplePrincipalKeyValueFactory.cs Outdated
Comment thread src/EFCore/Metadata/IProperty.cs Outdated
Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
&& (property.IsKey()
|| property.IsForeignKey()
|| property.IsUniqueIndex())
&& keyValueComparer == typeMappingKeyComparer

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot If the keyValueComparer is the same as the typeMappingKeyComparer then why would we need to generate code to call SetKeyComparer? We already generate code for the comparer inside the type mapping, so GetKeyValueComparer would just fallback to property.TypeMapping.KeyComparer

So, CreateDefaultTypeMapping should be changed to never return null.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 6a5387f. The NativeAOT default key-comparer case now flows through type-mapping generation, CreateDefaultTypeMapping always returns the default mapping instance, and the redundant property-level SetKeyComparer(...) emission was removed.

Co-authored-by: AndriySvyryd <6539701+AndriySvyryd@users.noreply.github.com>
@AndriySvyryd

Copy link
Copy Markdown
Member

Superseded by #38440

@AndriySvyryd AndriySvyryd deleted the copilot/compiled-models-aot-compatibility-fix branch June 17, 2026 06:58
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.

3 participants