diff --git a/src/EFCore.Design/Scaffolding/Internal/CSharpRuntimeModelCodeGenerator.cs b/src/EFCore.Design/Scaffolding/Internal/CSharpRuntimeModelCodeGenerator.cs
index 432db1e9007..d06a982a176 100644
--- a/src/EFCore.Design/Scaffolding/Internal/CSharpRuntimeModelCodeGenerator.cs
+++ b/src/EFCore.Design/Scaffolding/Internal/CSharpRuntimeModelCodeGenerator.cs
@@ -2826,6 +2826,23 @@ private void CreateSkipNavigation(
SetNavigationBaseProperties(navigation, memberAccessReplacements, parameters);
+ if (parameters.ForNativeAot)
+ {
+ AddNamespace(navigation.TargetEntityType.ClrType, parameters.Namespaces);
+ AddNamespace(navigation.DeclaringEntityType.ClrType, parameters.Namespaces);
+ mainBuilder
+ .Append(navigationVariable)
+ .AppendLine(".SetManyToManyLoaderFactory(")
+ .IncrementIndent()
+ .Append("static (factory, navigation) => factory.Create<")
+ .Append(_code.Reference(navigation.TargetEntityType.ClrType))
+ .Append(", ")
+ .Append(_code.Reference(navigation.DeclaringEntityType.ClrType))
+ .AppendLine(">(navigation));")
+ .DecrementIndent()
+ .AppendLine();
+ }
+
CreateAnnotations(navigation, _annotationCodeGenerator.Generate, parameters);
mainBuilder
diff --git a/src/EFCore/ChangeTracking/CollectionEntry.cs b/src/EFCore/ChangeTracking/CollectionEntry.cs
index 859ff54a344..ead8a89190d 100644
--- a/src/EFCore/ChangeTracking/CollectionEntry.cs
+++ b/src/EFCore/ChangeTracking/CollectionEntry.cs
@@ -337,7 +337,8 @@ private void EnsureInitialized()
[field: AllowNull, MaybeNull]
private ICollectionLoader TargetLoader
=> field ??= Metadata is IRuntimeSkipNavigation skipNavigation
- ? skipNavigation.GetManyToManyLoader()
+ ? skipNavigation.GetManyToManyLoader(
+ InternalEntry.Context.GetDependencies().ManyToManyLoaderFactory)
: new EntityFinderCollectionLoaderAdapter(
InternalEntry.StateManager.CreateEntityFinder(Metadata.TargetEntityType),
(INavigation)Metadata);
diff --git a/src/EFCore/DbContext.cs b/src/EFCore/DbContext.cs
index e7a86ec66fa..d7f5f7405f3 100644
--- a/src/EFCore/DbContext.cs
+++ b/src/EFCore/DbContext.cs
@@ -211,6 +211,16 @@ IDbSetSource IDbContextDependencies.SetSource
IEntityFinderFactory IDbContextDependencies.EntityFinderFactory
=> DbContextDependencies.EntityFinderFactory;
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ [EntityFrameworkInternal]
+ IManyToManyLoaderFactory IDbContextDependencies.ManyToManyLoaderFactory
+ => DbContextDependencies.ManyToManyLoaderFactory;
+
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
diff --git a/src/EFCore/Infrastructure/EntityFrameworkServicesBuilder.cs b/src/EFCore/Infrastructure/EntityFrameworkServicesBuilder.cs
index 7bb16caffa9..6804603d072 100644
--- a/src/EFCore/Infrastructure/EntityFrameworkServicesBuilder.cs
+++ b/src/EFCore/Infrastructure/EntityFrameworkServicesBuilder.cs
@@ -59,6 +59,7 @@ public static readonly IDictionary CoreServices
{ typeof(IDbSetInitializer), new ServiceCharacteristics(ServiceLifetime.Singleton) },
{ typeof(IDbSetSource), new ServiceCharacteristics(ServiceLifetime.Singleton) },
{ typeof(IEntityFinderSource), new ServiceCharacteristics(ServiceLifetime.Singleton) },
+ { typeof(IManyToManyLoaderFactory), new ServiceCharacteristics(ServiceLifetime.Singleton) },
{ typeof(IStructuralTypeMaterializerSource), new ServiceCharacteristics(ServiceLifetime.Singleton) },
{ typeof(ITypeMappingSource), new ServiceCharacteristics(ServiceLifetime.Singleton) },
{ typeof(IModelCustomizer), new ServiceCharacteristics(ServiceLifetime.Singleton) },
@@ -239,6 +240,7 @@ public virtual EntityFrameworkServicesBuilder TryAddCoreServices()
TryAdd();
TryAdd();
TryAdd();
+ TryAdd();
TryAdd();
TryAdd();
TryAdd();
diff --git a/src/EFCore/Internal/DbContextDependencies.cs b/src/EFCore/Internal/DbContextDependencies.cs
index e3cf9768c6e..c5af0d4aaba 100644
--- a/src/EFCore/Internal/DbContextDependencies.cs
+++ b/src/EFCore/Internal/DbContextDependencies.cs
@@ -35,6 +35,7 @@ public DbContextDependencies(
IChangeDetector changeDetector,
IDbSetSource setSource,
IEntityFinderSource entityFinderSource,
+ IManyToManyLoaderFactory manyToManyLoaderFactory,
IEntityGraphAttacher entityGraphAttacher,
IAsyncQueryProvider queryProvider,
IStateManager stateManager,
@@ -51,6 +52,7 @@ public DbContextDependencies(
UpdateLogger = updateLogger;
InfrastructureLogger = infrastructureLogger;
EntityFinderFactory = new EntityFinderFactory(entityFinderSource, stateManager, setSource, currentContext.Context);
+ ManyToManyLoaderFactory = manyToManyLoaderFactory;
}
///
@@ -69,6 +71,14 @@ public DbContextDependencies(
///
public IEntityFinderFactory EntityFinderFactory { get; }
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ public IManyToManyLoaderFactory ManyToManyLoaderFactory { get; }
+
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
diff --git a/src/EFCore/Internal/IDbContextDependencies.cs b/src/EFCore/Internal/IDbContextDependencies.cs
index aaf0787f834..48a550764a5 100644
--- a/src/EFCore/Internal/IDbContextDependencies.cs
+++ b/src/EFCore/Internal/IDbContextDependencies.cs
@@ -35,6 +35,14 @@ public interface IDbContextDependencies
///
IEntityFinderFactory EntityFinderFactory { get; }
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ IManyToManyLoaderFactory ManyToManyLoaderFactory { get; }
+
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
diff --git a/src/EFCore/Internal/IManyToManyLoaderFactory.cs b/src/EFCore/Internal/IManyToManyLoaderFactory.cs
new file mode 100644
index 00000000000..5586a42cd3a
--- /dev/null
+++ b/src/EFCore/Internal/IManyToManyLoaderFactory.cs
@@ -0,0 +1,37 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.EntityFrameworkCore.Internal;
+
+///
+/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+/// the same compatibility standards as public APIs. It may be changed or removed without notice in
+/// any release. You should only use it directly in your code with extreme caution and knowing that
+/// doing so can result in application failures when updating to a new Entity Framework Core release.
+///
+///
+/// The service lifetime is . This means a single instance
+/// is used by many instances and the loaders it creates may be cached on the
+/// (singleton) model, so implementations and the loaders they return must be thread-safe and must not
+/// capture any scoped service or .
+///
+public interface IManyToManyLoaderFactory
+{
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ ICollectionLoader Create(ISkipNavigation skipNavigation);
+
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ ICollectionLoader Create(ISkipNavigation skipNavigation)
+ where TEntity : class
+ where TSourceEntity : class;
+}
diff --git a/src/EFCore/Internal/ManyToManyLoaderFactory.cs b/src/EFCore/Internal/ManyToManyLoaderFactory.cs
index e5a772b8fb0..5b01f448bdb 100644
--- a/src/EFCore/Internal/ManyToManyLoaderFactory.cs
+++ b/src/EFCore/Internal/ManyToManyLoaderFactory.cs
@@ -11,38 +11,31 @@ namespace Microsoft.EntityFrameworkCore.Internal;
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
-public class ManyToManyLoaderFactory
+public class ManyToManyLoaderFactory : IManyToManyLoaderFactory
{
private static readonly MethodInfo GenericCreate
= typeof(ManyToManyLoaderFactory).GetTypeInfo().GetDeclaredMethod(nameof(CreateManyToMany))!;
- private ManyToManyLoaderFactory()
- {
- }
-
- ///
- /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
- /// the same compatibility standards as public APIs. It may be changed or removed without notice in
- /// any release. You should only use it directly in your code with extreme caution and knowing that
- /// doing so can result in application failures when updating to a new Entity Framework Core release.
- ///
- public static readonly ManyToManyLoaderFactory Instance = new();
-
- ///
- /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
- /// the same compatibility standards as public APIs. It may be changed or removed without notice in
- /// any release. You should only use it directly in your code with extreme caution and knowing that
- /// doing so can result in application failures when updating to a new Entity Framework Core release.
- ///
+ ///
public virtual ICollectionLoader Create(ISkipNavigation skipNavigation)
=> (ICollectionLoader)GenericCreate.MakeGenericMethod(
skipNavigation.TargetEntityType.ClrType,
skipNavigation.DeclaringEntityType.ClrType)
- .Invoke(null, [skipNavigation])!;
+ .Invoke(this, [skipNavigation])!;
+
+ ///
+ public virtual ICollectionLoader Create(ISkipNavigation skipNavigation)
+ where TEntity : class
+ where TSourceEntity : class
+ => new ManyToManyLoader(skipNavigation);
+ // Invoked via reflection by the non-generic Create above (GenericCreate). It deliberately
+ // forwards to the virtual Create<,> so that a provider overriding Create<,> is still honored
+ // on the reflection path. Do not inline this into MakeGenericMethod against Create<,> directly:
+ // Create is overloaded, so GetDeclaredMethod(nameof(Create)) would be ambiguous.
[UsedImplicitly]
- private static ICollectionLoader CreateManyToMany(ISkipNavigation skipNavigation)
+ private ICollectionLoader CreateManyToMany(ISkipNavigation skipNavigation)
where TEntity : class
- where TTargetEntity : class
- => new ManyToManyLoader(skipNavigation);
+ where TSourceEntity : class
+ => Create(skipNavigation);
}
diff --git a/src/EFCore/Metadata/Internal/IRuntimeSkipNavigation.cs b/src/EFCore/Metadata/Internal/IRuntimeSkipNavigation.cs
index b6d6ddecb8f..c6d56c3fb88 100644
--- a/src/EFCore/Metadata/Internal/IRuntimeSkipNavigation.cs
+++ b/src/EFCore/Metadata/Internal/IRuntimeSkipNavigation.cs
@@ -19,5 +19,5 @@ public interface IRuntimeSkipNavigation : ISkipNavigation, IRuntimeNavigationBas
/// any release. You should only use it directly in your code with extreme caution and knowing that
/// doing so can result in application failures when updating to a new Entity Framework Core release.
///
- ICollectionLoader GetManyToManyLoader();
+ ICollectionLoader GetManyToManyLoader(IManyToManyLoaderFactory factory);
}
diff --git a/src/EFCore/Metadata/Internal/SkipNavigation.cs b/src/EFCore/Metadata/Internal/SkipNavigation.cs
index c5fb4cf508d..e8ce3ed26ec 100644
--- a/src/EFCore/Metadata/Internal/SkipNavigation.cs
+++ b/src/EFCore/Metadata/Internal/SkipNavigation.cs
@@ -20,6 +20,7 @@ public class SkipNavigation : PropertyBase, IMutableSkipNavigation, IConventionS
// Warning: Never access these fields directly as access needs to be thread-safe
private bool _collectionAccessorInitialized;
+ private ICollectionLoader? _manyToManyLoader;
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -367,21 +368,6 @@ public virtual IClrCollectionAccessor? CollectionAccessor
return ClrCollectionAccessorFactory.Instance.Create(navigation);
});
- ///
- /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
- /// the same compatibility standards as public APIs. It may be changed or removed without notice in
- /// any release. You should only use it directly in your code with extreme caution and knowing that
- /// doing so can result in application failures when updating to a new Entity Framework Core release.
- ///
- [field: AllowNull, MaybeNull]
- public virtual ICollectionLoader ManyToManyLoader
- => NonCapturingLazyInitializer.EnsureInitialized(
- ref field, this, static navigation =>
- {
- navigation.EnsureReadOnly();
- return ManyToManyLoaderFactory.Instance.Create(navigation);
- });
-
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
@@ -480,12 +466,12 @@ IReadOnlySkipNavigation IReadOnlySkipNavigation.Inverse
IClrCollectionAccessor? IPropertyBase.GetCollectionAccessor()
=> CollectionAccessor;
- ///
- /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
- /// the same compatibility standards as public APIs. It may be changed or removed without notice in
- /// any release. You should only use it directly in your code with extreme caution and knowing that
- /// doing so can result in application failures when updating to a new Entity Framework Core release.
- ///
- ICollectionLoader IRuntimeSkipNavigation.GetManyToManyLoader()
- => ManyToManyLoader;
+ ///
+ ICollectionLoader IRuntimeSkipNavigation.GetManyToManyLoader(IManyToManyLoaderFactory factory)
+ => NonCapturingLazyInitializer.EnsureInitialized(
+ ref _manyToManyLoader, this, factory, static (navigation, factory) =>
+ {
+ navigation.EnsureReadOnly();
+ return factory.Create(navigation);
+ });
}
diff --git a/src/EFCore/Metadata/RuntimeSkipNavigation.cs b/src/EFCore/Metadata/RuntimeSkipNavigation.cs
index 623d223101e..46c21ef98a1 100644
--- a/src/EFCore/Metadata/RuntimeSkipNavigation.cs
+++ b/src/EFCore/Metadata/RuntimeSkipNavigation.cs
@@ -24,6 +24,9 @@ public class RuntimeSkipNavigation : RuntimePropertyBase, IRuntimeSkipNavigation
private IClrCollectionAccessor? _collectionAccessor;
private bool _collectionAccessorInitialized;
private ICollectionLoader? _manyToManyLoader;
+ // An optional compiled-model delegate that creates the loader, letting a compiled model carry the
+ // concrete generic types for native AOT. Null unless set during model build.
+ private Func? _manyToManyLoaderDelegatedFactory;
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -137,6 +140,17 @@ public virtual void SetCollectionAccessor(
_collectionAccessorInitialized = true;
}
+ ///
+ /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
+ /// the same compatibility standards as public APIs. It may be changed or removed without notice in
+ /// any release. You should only use it directly in your code with extreme caution and knowing that
+ /// doing so can result in application failures when updating to a new Entity Framework Core release.
+ ///
+ [EntityFrameworkInternal]
+ public virtual void SetManyToManyLoaderFactory(
+ Func factory)
+ => _manyToManyLoaderDelegatedFactory = factory;
+
///
/// Returns a string that represents the current object.
///
@@ -204,10 +218,15 @@ bool IReadOnlySkipNavigation.IsOnDependent
: null);
///
- ICollectionLoader IRuntimeSkipNavigation.GetManyToManyLoader()
+ ICollectionLoader IRuntimeSkipNavigation.GetManyToManyLoader(IManyToManyLoaderFactory factory)
=> NonCapturingLazyInitializer.EnsureInitialized(
- ref _manyToManyLoader, this, static navigation =>
- RuntimeFeature.IsDynamicCodeSupported
- ? ManyToManyLoaderFactory.Instance.Create(navigation)
- : throw new InvalidOperationException(CoreStrings.NativeAotNoCompiledModel));
+ ref _manyToManyLoader, this, factory, static (navigation, factory) =>
+ {
+ var generated = navigation._manyToManyLoaderDelegatedFactory;
+ return generated != null
+ ? generated(factory, navigation)
+ : RuntimeFeature.IsDynamicCodeSupported
+ ? factory.Create(navigation)
+ : throw new InvalidOperationException(CoreStrings.NativeAotNoCompiledModel);
+ });
}
diff --git a/test/EFCore.Cosmos.FunctionalTests/Scaffolding/Baselines/BigModel/PrincipalBaseEntityType.cs b/test/EFCore.Cosmos.FunctionalTests/Scaffolding/Baselines/BigModel/PrincipalBaseEntityType.cs
index 64ee284a8eb..06643bad1a0 100644
--- a/test/EFCore.Cosmos.FunctionalTests/Scaffolding/Baselines/BigModel/PrincipalBaseEntityType.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/Scaffolding/Baselines/BigModel/PrincipalBaseEntityType.cs
@@ -919,6 +919,9 @@ public static RuntimeSkipNavigation CreateSkipNavigation1(RuntimeEntityType decl
(CompiledModelTestBase.PrincipalBase entity, ICollection collection) => PrincipalBaseUnsafeAccessors.Deriveds(entity) = ((ICollection)collection),
ICollection (CompiledModelTestBase.PrincipalBase entity, Action> setter) => ClrCollectionAccessorFactory.CreateAndSetHashSet, CompiledModelTestBase.PrincipalBase>(entity, setter),
ICollection () => ((ICollection)(((ICollection)(new HashSet(ReferenceEqualityComparer.Instance))))));
+ skipNavigation.SetManyToManyLoaderFactory(
+ static (factory, navigation) => factory.Create>, CompiledModelTestBase.PrincipalBase>(navigation));
+
return skipNavigation;
}
diff --git a/test/EFCore.Cosmos.FunctionalTests/Scaffolding/Baselines/BigModel/PrincipalDerivedEntityType.cs b/test/EFCore.Cosmos.FunctionalTests/Scaffolding/Baselines/BigModel/PrincipalDerivedEntityType.cs
index e9bbceea33c..edcac7de4b9 100644
--- a/test/EFCore.Cosmos.FunctionalTests/Scaffolding/Baselines/BigModel/PrincipalDerivedEntityType.cs
+++ b/test/EFCore.Cosmos.FunctionalTests/Scaffolding/Baselines/BigModel/PrincipalDerivedEntityType.cs
@@ -87,6 +87,9 @@ public static RuntimeSkipNavigation CreateSkipNavigation1(RuntimeEntityType decl
(CompiledModelTestBase.PrincipalDerived> entity, ICollection collection) => PrincipalDerivedUnsafeAccessors>.Principals(entity) = ((ICollection)collection),
ICollection (CompiledModelTestBase.PrincipalDerived> entity, Action>, ICollection> setter) => ClrCollectionAccessorFactory.CreateAndSetHashSet>, ICollection, CompiledModelTestBase.PrincipalBase>(entity, setter),
ICollection () => ((ICollection)(((ICollection)(new HashSet(ReferenceEqualityComparer.Instance))))));
+ skipNavigation.SetManyToManyLoaderFactory(
+ static (factory, navigation) => factory.Create>>(navigation));
+
return skipNavigation;
}
diff --git a/test/EFCore.InMemory.FunctionalTests/Scaffolding/Baselines/BigModel/PrincipalBaseEntityType.cs b/test/EFCore.InMemory.FunctionalTests/Scaffolding/Baselines/BigModel/PrincipalBaseEntityType.cs
index 49a781f6b6b..f150d6b05a4 100644
--- a/test/EFCore.InMemory.FunctionalTests/Scaffolding/Baselines/BigModel/PrincipalBaseEntityType.cs
+++ b/test/EFCore.InMemory.FunctionalTests/Scaffolding/Baselines/BigModel/PrincipalBaseEntityType.cs
@@ -994,6 +994,9 @@ public static RuntimeSkipNavigation CreateSkipNavigation1(RuntimeEntityType decl
(CompiledModelTestBase.PrincipalBase entity, ICollection collection) => PrincipalBaseUnsafeAccessors.Deriveds(entity) = ((ICollection)collection),
ICollection (CompiledModelTestBase.PrincipalBase entity, Action> setter) => ClrCollectionAccessorFactory.CreateAndSetHashSet, CompiledModelTestBase.PrincipalBase>(entity, setter),
ICollection () => ((ICollection)(((ICollection)(new HashSet(ReferenceEqualityComparer.Instance))))));
+ skipNavigation.SetManyToManyLoaderFactory(
+ static (factory, navigation) => factory.Create>, CompiledModelTestBase.PrincipalBase>(navigation));
+
return skipNavigation;
}
diff --git a/test/EFCore.InMemory.FunctionalTests/Scaffolding/Baselines/BigModel/PrincipalDerivedEntityType.cs b/test/EFCore.InMemory.FunctionalTests/Scaffolding/Baselines/BigModel/PrincipalDerivedEntityType.cs
index 346e58d8d13..61f3216d5d0 100644
--- a/test/EFCore.InMemory.FunctionalTests/Scaffolding/Baselines/BigModel/PrincipalDerivedEntityType.cs
+++ b/test/EFCore.InMemory.FunctionalTests/Scaffolding/Baselines/BigModel/PrincipalDerivedEntityType.cs
@@ -87,6 +87,9 @@ public static RuntimeSkipNavigation CreateSkipNavigation1(RuntimeEntityType decl
(CompiledModelTestBase.PrincipalDerived> entity, ICollection collection) => PrincipalDerivedUnsafeAccessors>.Principals(entity) = ((ICollection)collection),
ICollection (CompiledModelTestBase.PrincipalDerived> entity, Action>, ICollection> setter) => ClrCollectionAccessorFactory.CreateAndSetHashSet>, ICollection, CompiledModelTestBase.PrincipalBase>(entity, setter),
ICollection () => ((ICollection)(((ICollection)(new HashSet(ReferenceEqualityComparer.Instance))))));
+ skipNavigation.SetManyToManyLoaderFactory(
+ static (factory, navigation) => factory.Create>>(navigation));
+
return skipNavigation;
}
diff --git a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/BigModel/PrincipalBaseEntityType.cs b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/BigModel/PrincipalBaseEntityType.cs
index 3a9bcc28112..503437810e6 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/BigModel/PrincipalBaseEntityType.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/BigModel/PrincipalBaseEntityType.cs
@@ -1061,6 +1061,9 @@ public static RuntimeSkipNavigation CreateSkipNavigation1(RuntimeEntityType decl
(CompiledModelTestBase.PrincipalBase entity, ICollection collection) => PrincipalBaseUnsafeAccessors.Deriveds(entity) = ((ICollection)collection),
ICollection (CompiledModelTestBase.PrincipalBase entity, Action> setter) => ClrCollectionAccessorFactory.CreateAndSetHashSet, CompiledModelTestBase.PrincipalBase>(entity, setter),
ICollection () => ((ICollection)(((ICollection)(new HashSet(ReferenceEqualityComparer.Instance))))));
+ skipNavigation.SetManyToManyLoaderFactory(
+ static (factory, navigation) => factory.Create>, CompiledModelTestBase.PrincipalBase>(navigation));
+
return skipNavigation;
}
diff --git a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/BigModel/PrincipalDerivedEntityType.cs b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/BigModel/PrincipalDerivedEntityType.cs
index 304a4a7eb6f..808763fd89e 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/BigModel/PrincipalDerivedEntityType.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/BigModel/PrincipalDerivedEntityType.cs
@@ -100,6 +100,9 @@ public static RuntimeSkipNavigation CreateSkipNavigation1(RuntimeEntityType decl
(CompiledModelTestBase.PrincipalDerived> entity, ICollection collection) => PrincipalDerivedUnsafeAccessors>.Principals(entity) = ((ICollection)collection),
ICollection (CompiledModelTestBase.PrincipalDerived> entity, Action>, ICollection> setter) => ClrCollectionAccessorFactory.CreateAndSetHashSet>, ICollection, CompiledModelTestBase.PrincipalBase>(entity, setter),
ICollection () => ((ICollection)(((ICollection)(new HashSet(ReferenceEqualityComparer.Instance))))));
+ skipNavigation.SetManyToManyLoaderFactory(
+ static (factory, navigation) => factory.Create>>(navigation));
+
return skipNavigation;
}
diff --git a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/BigModel_with_JSON_columns/PrincipalBaseEntityType.cs b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/BigModel_with_JSON_columns/PrincipalBaseEntityType.cs
index a3ce4bf92bd..310e6a7fad7 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/BigModel_with_JSON_columns/PrincipalBaseEntityType.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/BigModel_with_JSON_columns/PrincipalBaseEntityType.cs
@@ -1089,6 +1089,9 @@ public static RuntimeSkipNavigation CreateSkipNavigation1(RuntimeEntityType decl
(CompiledModelTestBase.PrincipalBase entity, ICollection collection) => PrincipalBaseUnsafeAccessors.Deriveds(entity) = ((ICollection)collection),
ICollection (CompiledModelTestBase.PrincipalBase entity, Action> setter) => ClrCollectionAccessorFactory.CreateAndSetHashSet, CompiledModelTestBase.PrincipalBase>(entity, setter),
ICollection () => ((ICollection)(((ICollection)(new HashSet(ReferenceEqualityComparer.Instance))))));
+ skipNavigation.SetManyToManyLoaderFactory(
+ static (factory, navigation) => factory.Create>, CompiledModelTestBase.PrincipalBase>(navigation));
+
return skipNavigation;
}
diff --git a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/BigModel_with_JSON_columns/PrincipalDerivedEntityType.cs b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/BigModel_with_JSON_columns/PrincipalDerivedEntityType.cs
index e0baebdc179..95e17ad8832 100644
--- a/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/BigModel_with_JSON_columns/PrincipalDerivedEntityType.cs
+++ b/test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/BigModel_with_JSON_columns/PrincipalDerivedEntityType.cs
@@ -87,6 +87,9 @@ public static RuntimeSkipNavigation CreateSkipNavigation1(RuntimeEntityType decl
(CompiledModelTestBase.PrincipalDerived> entity, ICollection collection) => PrincipalDerivedUnsafeAccessors>.Principals(entity) = ((ICollection)collection),
ICollection (CompiledModelTestBase.PrincipalDerived> entity, Action>, ICollection> setter) => ClrCollectionAccessorFactory.CreateAndSetHashSet>, ICollection, CompiledModelTestBase.PrincipalBase>(entity, setter),
ICollection () => ((ICollection)(((ICollection)(new HashSet(ReferenceEqualityComparer.Instance))))));
+ skipNavigation.SetManyToManyLoaderFactory(
+ static (factory, navigation) => factory.Create>>(navigation));
+
return skipNavigation;
}
diff --git a/test/EFCore.Sqlite.FunctionalTests/Scaffolding/Baselines/BigModel/PrincipalBaseEntityType.cs b/test/EFCore.Sqlite.FunctionalTests/Scaffolding/Baselines/BigModel/PrincipalBaseEntityType.cs
index 2712f57ef83..ef000e05e30 100644
--- a/test/EFCore.Sqlite.FunctionalTests/Scaffolding/Baselines/BigModel/PrincipalBaseEntityType.cs
+++ b/test/EFCore.Sqlite.FunctionalTests/Scaffolding/Baselines/BigModel/PrincipalBaseEntityType.cs
@@ -981,6 +981,9 @@ public static RuntimeSkipNavigation CreateSkipNavigation1(RuntimeEntityType decl
(CompiledModelTestBase.PrincipalBase entity, ICollection collection) => PrincipalBaseUnsafeAccessors.Deriveds(entity) = ((ICollection)collection),
ICollection (CompiledModelTestBase.PrincipalBase entity, Action> setter) => ClrCollectionAccessorFactory.CreateAndSetHashSet, CompiledModelTestBase.PrincipalBase>(entity, setter),
ICollection () => ((ICollection)(((ICollection)(new HashSet(ReferenceEqualityComparer.Instance))))));
+ skipNavigation.SetManyToManyLoaderFactory(
+ static (factory, navigation) => factory.Create>, CompiledModelTestBase.PrincipalBase>(navigation));
+
return skipNavigation;
}
diff --git a/test/EFCore.Sqlite.FunctionalTests/Scaffolding/Baselines/BigModel/PrincipalDerivedEntityType.cs b/test/EFCore.Sqlite.FunctionalTests/Scaffolding/Baselines/BigModel/PrincipalDerivedEntityType.cs
index dc0e87cd11d..eb57b66fde2 100644
--- a/test/EFCore.Sqlite.FunctionalTests/Scaffolding/Baselines/BigModel/PrincipalDerivedEntityType.cs
+++ b/test/EFCore.Sqlite.FunctionalTests/Scaffolding/Baselines/BigModel/PrincipalDerivedEntityType.cs
@@ -101,6 +101,9 @@ public static RuntimeSkipNavigation CreateSkipNavigation1(RuntimeEntityType decl
(CompiledModelTestBase.PrincipalDerived> entity, ICollection collection) => PrincipalDerivedUnsafeAccessors>.Principals(entity) = ((ICollection)collection),
ICollection (CompiledModelTestBase.PrincipalDerived> entity, Action>, ICollection> setter) => ClrCollectionAccessorFactory.CreateAndSetHashSet>, ICollection, CompiledModelTestBase.PrincipalBase>(entity, setter),
ICollection () => ((ICollection)(((ICollection)(new HashSet(ReferenceEqualityComparer.Instance))))));
+ skipNavigation.SetManyToManyLoaderFactory(
+ static (factory, navigation) => factory.Create>>(navigation));
+
return skipNavigation;
}
diff --git a/test/EFCore.Sqlite.FunctionalTests/Scaffolding/Baselines/BigModel_with_JSON_columns/PrincipalBaseEntityType.cs b/test/EFCore.Sqlite.FunctionalTests/Scaffolding/Baselines/BigModel_with_JSON_columns/PrincipalBaseEntityType.cs
index 7b13cb1134c..0b1d43bb873 100644
--- a/test/EFCore.Sqlite.FunctionalTests/Scaffolding/Baselines/BigModel_with_JSON_columns/PrincipalBaseEntityType.cs
+++ b/test/EFCore.Sqlite.FunctionalTests/Scaffolding/Baselines/BigModel_with_JSON_columns/PrincipalBaseEntityType.cs
@@ -992,6 +992,9 @@ public static RuntimeSkipNavigation CreateSkipNavigation1(RuntimeEntityType decl
(CompiledModelTestBase.PrincipalBase entity, ICollection collection) => PrincipalBaseUnsafeAccessors.Deriveds(entity) = ((ICollection)collection),
ICollection (CompiledModelTestBase.PrincipalBase entity, Action> setter) => ClrCollectionAccessorFactory.CreateAndSetHashSet, CompiledModelTestBase.PrincipalBase>(entity, setter),
ICollection () => ((ICollection)(((ICollection)(new HashSet(ReferenceEqualityComparer.Instance))))));
+ skipNavigation.SetManyToManyLoaderFactory(
+ static (factory, navigation) => factory.Create>, CompiledModelTestBase.PrincipalBase>(navigation));
+
return skipNavigation;
}
diff --git a/test/EFCore.Sqlite.FunctionalTests/Scaffolding/Baselines/BigModel_with_JSON_columns/PrincipalDerivedEntityType.cs b/test/EFCore.Sqlite.FunctionalTests/Scaffolding/Baselines/BigModel_with_JSON_columns/PrincipalDerivedEntityType.cs
index 30b544eaaf9..3bb0028fffb 100644
--- a/test/EFCore.Sqlite.FunctionalTests/Scaffolding/Baselines/BigModel_with_JSON_columns/PrincipalDerivedEntityType.cs
+++ b/test/EFCore.Sqlite.FunctionalTests/Scaffolding/Baselines/BigModel_with_JSON_columns/PrincipalDerivedEntityType.cs
@@ -88,6 +88,9 @@ public static RuntimeSkipNavigation CreateSkipNavigation1(RuntimeEntityType decl
(CompiledModelTestBase.PrincipalDerived> entity, ICollection collection) => PrincipalDerivedUnsafeAccessors>.Principals(entity) = ((ICollection)collection),
ICollection (CompiledModelTestBase.PrincipalDerived> entity, Action>, ICollection> setter) => ClrCollectionAccessorFactory.CreateAndSetHashSet>, ICollection, CompiledModelTestBase.PrincipalBase>(entity, setter),
ICollection () => ((ICollection)(((ICollection)(new HashSet(ReferenceEqualityComparer.Instance))))));
+ skipNavigation.SetManyToManyLoaderFactory(
+ static (factory, navigation) => factory.Create>>(navigation));
+
return skipNavigation;
}
diff --git a/test/EFCore.Tests/ChangeTracking/ManyToManyLoaderTest.cs b/test/EFCore.Tests/ChangeTracking/ManyToManyLoaderTest.cs
new file mode 100644
index 00000000000..f557f85a9c1
--- /dev/null
+++ b/test/EFCore.Tests/ChangeTracking/ManyToManyLoaderTest.cs
@@ -0,0 +1,110 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
+using Microsoft.EntityFrameworkCore.Internal;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Metadata.Internal;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Microsoft.EntityFrameworkCore.ChangeTracking;
+
+public class ManyToManyLoaderTest
+{
+ [Fact]
+ public void Provider_can_replace_many_to_many_loader_factory()
+ {
+ using var context = new ReplacedFactoryContext();
+
+ var left = new Left();
+ var right = new Right();
+ left.Rights.Add(right);
+ context.AddRange(left, right);
+ context.SaveChanges();
+ context.ChangeTracker.Clear();
+
+ var tracked = context.Set().Single();
+
+ // Accessing the collection loader must route through the replaced factory.
+ Assert.Throws(
+ () => context.Entry(tracked).Collection(e => e.Rights).Load());
+ }
+
+ [Fact]
+ public void Generated_loader_factory_delegate_routes_through_the_runtime_factory()
+ {
+ using var context = new DelegatePathContext();
+
+ var skipNavigation = (RuntimeSkipNavigation)context.Model
+ .FindEntityType(typeof(Left))!
+ .FindSkipNavigation(nameof(Left.Rights))!;
+
+ // Simulate what the compiled-model generator emits for native AOT: a static delegate
+ // that carries the concrete generic types but defers creation to the runtime factory.
+ skipNavigation.SetManyToManyLoaderFactory(
+ static (factory, navigation) => factory.Create(navigation));
+
+ var loader = ((IRuntimeSkipNavigation)skipNavigation)
+ .GetManyToManyLoader(new CustomManyToManyLoaderFactory());
+
+ // The replaced factory governs creation even via the generated delegate.
+ Assert.IsType(loader);
+ }
+
+ private class ReplacedFactoryContext : DbContext
+ {
+ protected internal override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
+ => optionsBuilder
+ .UseInMemoryDatabase(nameof(ReplacedFactoryContext))
+ .ReplaceService();
+
+ protected internal override void OnModelCreating(ModelBuilder modelBuilder)
+ => modelBuilder.Entity().HasMany(e => e.Rights).WithMany(e => e.Lefts);
+ }
+
+ private class DelegatePathContext : DbContext
+ {
+ protected internal override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
+ => optionsBuilder.UseInMemoryDatabase(nameof(DelegatePathContext));
+
+ protected internal override void OnModelCreating(ModelBuilder modelBuilder)
+ => modelBuilder.Entity().HasMany(e => e.Rights).WithMany(e => e.Lefts);
+ }
+
+ private class CustomManyToManyLoaderFactory : IManyToManyLoaderFactory
+ {
+ public ICollectionLoader Create(ISkipNavigation skipNavigation)
+ => new CustomLoader();
+
+ public ICollectionLoader Create(ISkipNavigation skipNavigation)
+ where TEntity : class
+ where TSourceEntity : class
+ => new CustomLoader();
+ }
+
+ private class CustomLoader : ICollectionLoader
+ {
+ public void Load(InternalEntityEntry entry, LoadOptions options)
+ => throw new CustomLoaderSentinelException();
+
+ public Task LoadAsync(InternalEntityEntry entry, LoadOptions options, CancellationToken cancellationToken = default)
+ => throw new CustomLoaderSentinelException();
+
+ public IQueryable Query(InternalEntityEntry entry)
+ => throw new CustomLoaderSentinelException();
+ }
+
+ private sealed class CustomLoaderSentinelException : Exception { }
+
+ private class Left
+ {
+ public int Id { get; set; }
+ public List Rights { get; } = new();
+ }
+
+ private class Right
+ {
+ public int Id { get; set; }
+ public List Lefts { get; } = new();
+ }
+}