Route many-to-many loading through an injectable IManyToManyLoaderFactory (#38363)#38411
Open
ajcvickers wants to merge 1 commit into
Open
Route many-to-many loading through an injectable IManyToManyLoaderFactory (#38363)#38411ajcvickers wants to merge 1 commit into
ajcvickers wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
This PR enables providers and compiled (Native AOT) runtime models to control how many-to-many collection loaders are created, rather than always using EF Core’s static factory.
Changes:
- Introduces
IManyToManyLoaderFactoryas a singleton service and wires it intoDbContextdependencies. - Updates skip-navigation loader creation to route through the injected factory and optionally a compiled-model delegate (
SetManyToManyLoaderFactory). - Adds a targeted test and updates scaffolding baselines/codegen to emit the compiled delegate for Native AOT.
Reviewed changes
Copilot reviewed 24 out of 24 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| test/EFCore.Tests/ChangeTracking/ManyToManyLoaderReplacementTest.cs | Adds coverage verifying service replacement and compiled-delegate routing for many-to-many loaders. |
| test/EFCore.Sqlite.FunctionalTests/Scaffolding/Baselines/BigModel_with_JSON_columns/PrincipalDerivedEntityType.cs | Baseline update to include generated SetManyToManyLoaderFactory call. |
| test/EFCore.Sqlite.FunctionalTests/Scaffolding/Baselines/BigModel_with_JSON_columns/PrincipalBaseEntityType.cs | Baseline update to include generated SetManyToManyLoaderFactory call. |
| test/EFCore.Sqlite.FunctionalTests/Scaffolding/Baselines/BigModel/PrincipalDerivedEntityType.cs | Baseline update to include generated SetManyToManyLoaderFactory call. |
| test/EFCore.Sqlite.FunctionalTests/Scaffolding/Baselines/BigModel/PrincipalBaseEntityType.cs | Baseline update to include generated SetManyToManyLoaderFactory call. |
| test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/BigModel_with_JSON_columns/PrincipalDerivedEntityType.cs | Baseline update to include generated SetManyToManyLoaderFactory call. |
| test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/BigModel_with_JSON_columns/PrincipalBaseEntityType.cs | Baseline update to include generated SetManyToManyLoaderFactory call. |
| test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/BigModel/PrincipalDerivedEntityType.cs | Baseline update to include generated SetManyToManyLoaderFactory call. |
| test/EFCore.SqlServer.FunctionalTests/Scaffolding/Baselines/BigModel/PrincipalBaseEntityType.cs | Baseline update to include generated SetManyToManyLoaderFactory call. |
| test/EFCore.InMemory.FunctionalTests/Scaffolding/Baselines/BigModel/PrincipalDerivedEntityType.cs | Baseline update to include generated SetManyToManyLoaderFactory call. |
| test/EFCore.InMemory.FunctionalTests/Scaffolding/Baselines/BigModel/PrincipalBaseEntityType.cs | Baseline update to include generated SetManyToManyLoaderFactory call. |
| test/EFCore.Cosmos.FunctionalTests/Scaffolding/Baselines/BigModel/PrincipalDerivedEntityType.cs | Baseline update to include generated SetManyToManyLoaderFactory call. |
| test/EFCore.Cosmos.FunctionalTests/Scaffolding/Baselines/BigModel/PrincipalBaseEntityType.cs | Baseline update to include generated SetManyToManyLoaderFactory call. |
| src/EFCore/Metadata/RuntimeSkipNavigation.cs | Adds compiled-model delegate hook and updates runtime loader creation to use an injected factory. |
| src/EFCore/Metadata/Internal/SkipNavigation.cs | Switches runtime loader creation to use an injected factory and caches the result. |
| src/EFCore/Metadata/Internal/IRuntimeSkipNavigation.cs | Updates the internal API to accept an IManyToManyLoaderFactory. |
| src/EFCore/Internal/ManyToManyLoaderFactory.cs | Converts the factory into a DI service implementing IManyToManyLoaderFactory and preserves override behavior on reflection path. |
| src/EFCore/Internal/IManyToManyLoaderFactory.cs | Introduces a new internal interface for loader factory customization. |
| src/EFCore/Internal/IDbContextDependencies.cs | Exposes IManyToManyLoaderFactory via context dependencies. |
| src/EFCore/Internal/DbContextDependencies.cs | Plumbs IManyToManyLoaderFactory through dependency construction. |
| src/EFCore/Infrastructure/EntityFrameworkServicesBuilder.cs | Registers IManyToManyLoaderFactory as a core singleton service. |
| src/EFCore/DbContext.cs | Implements the new dependency property for IManyToManyLoaderFactory. |
| src/EFCore/ChangeTracking/CollectionEntry.cs | Uses the injected factory when loading many-to-many collections. |
| src/EFCore.Design/Scaffolding/Internal/CSharpRuntimeModelCodeGenerator.cs | Emits SetManyToManyLoaderFactory for Native AOT runtime models. |
…tory (dotnet#38363) TL;DR - Adds IManyToManyLoaderFactory as a replaceable singleton service so providers can override how skip-navigation (many-to-many) collection loaders are created. - Removes the static ManyToManyLoaderFactory.Instance; loaders are now resolved from DI via IDbContextDependencies. - Loaders are cached per skip-navigation on the (singleton) model, so the factory and the loaders it returns must be thread-safe and capture no scoped state. - Native AOT / compiled models emit a SetManyToManyLoaderFactory delegate carrying the concrete generic types, still routed through the runtime factory. - Adds Create<TEntity, TSourceEntity>() for a reflection-free creation path; the reflection path now forwards to it so overrides are honored either way. ## Background Many-to-many collection loaders for skip navigations were created by a static singleton, `ManyToManyLoaderFactory.Instance`, hard-wired into `SkipNavigation`/`RuntimeSkipNavigation`. Because creation was static there was no way for a provider to substitute its own loader, and the reflection-based generic dispatch (`MakeGenericMethod`) was not overridable and is unavailable to fully reflection-free native AOT. ## Change Introduce `IManyToManyLoaderFactory` as a core service registered with a `Singleton` lifetime, implemented by `ManyToManyLoaderFactory`. The factory is exposed on `IDbContextDependencies` and consumed by `CollectionEntry` when building the target loader, replacing the static `Instance`. Providers can swap the implementation via `ReplaceService<IManyToManyLoaderFactory, ...>`. `IRuntimeSkipNavigation.GetManyToManyLoader` now takes the factory as a parameter; both `SkipNavigation` and `RuntimeSkipNavigation` lazily create and cache the loader through it. A new `Create<TEntity, TSourceEntity>()` overload provides a reflection-free creation path; the non-generic `Create` still uses `MakeGenericMethod` but now forwards to the virtual `Create<,>` (via the private `CreateManyToMany`) so a provider override is honored on both paths. For native AOT, `CSharpRuntimeModelCodeGenerator` emits a `RuntimeSkipNavigation.SetManyToManyLoaderFactory(...)` call carrying a static delegate with the concrete generic type arguments. At runtime `GetManyToManyLoader` prefers that generated delegate (still invoking the injected factory) and otherwise falls back to the reflection path when dynamic code is supported, throwing `NativeAotNoCompiledModel` when it is not. Compiled-model `BigModel` baselines are regenerated accordingly. ## Tests `ManyToManyLoaderReplacementTest`: - `Provider_can_replace_many_to_many_loader_factory` — a context that replaces the service routes collection loading through the custom factory. - `Generated_loader_factory_delegate_routes_through_the_runtime_factory` — the compiled-model delegate path still defers loader creation to the injected factory.
b8fa5b4 to
03a096c
Compare
| 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<IManyToManyLoaderFactory, ISkipNavigation, ICollectionLoader>? _manyToManyLoaderFactory; |
Member
There was a problem hiding this comment.
Rename to _manyToManyLoaderDelegatedFactory
|
|
||
| namespace Microsoft.EntityFrameworkCore.ChangeTracking; | ||
|
|
||
| public class ManyToManyLoaderReplacementTest |
Member
There was a problem hiding this comment.
Rename to ManyToManyLoaderTest so more tests can be added here in the future
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #38363.
Background
Many-to-many collection loaders for skip navigations were created by a static singleton,
ManyToManyLoaderFactory.Instance, hard-wired intoSkipNavigation/RuntimeSkipNavigation. Because creation was static there was no way for a provider to substitute its own loader, and the reflection-based generic dispatch (MakeGenericMethod) was not overridable and is unavailable to fully reflection-free native AOT.Change
Introduce
IManyToManyLoaderFactoryas a core service registered with aSingletonlifetime, implemented byManyToManyLoaderFactory. The factory is exposed onIDbContextDependenciesand consumed byCollectionEntrywhen building the target loader, replacing the staticInstance. Providers can swap the implementation viaReplaceService<IManyToManyLoaderFactory, ...>.IRuntimeSkipNavigation.GetManyToManyLoadernow takes the factory as a parameter; bothSkipNavigationandRuntimeSkipNavigationlazily create and cache the loader through it. A newCreate<TEntity, TSourceEntity>()overload provides a reflection-free creation path; the non-genericCreatestill usesMakeGenericMethodbut now forwards to the virtualCreate<,>(via the privateCreateManyToMany) so a provider override is honored on both paths.For native AOT,
CSharpRuntimeModelCodeGeneratoremits aRuntimeSkipNavigation.SetManyToManyLoaderFactory(...)call carrying a static delegate with the concrete generic type arguments. At runtimeGetManyToManyLoaderprefers that generated delegate (still invoking the injected factory) and otherwise falls back to the reflection path when dynamic code is supported, throwingNativeAotNoCompiledModelwhen it is not. Compiled-modelBigModelbaselines are regenerated accordingly.Tests
ManyToManyLoaderReplacementTest:Provider_can_replace_many_to_many_loader_factory— a context that replaces the service routes collection loading through the custom factory.Generated_loader_factory_delegate_routes_through_the_runtime_factory— the compiled-model delegate path still defers loader creation to the injected factory.