-
-
Notifications
You must be signed in to change notification settings - Fork 10
Add IncludeGraph support for full object graph bulk insert #91
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Copilot
wants to merge
27
commits into
main
Choose a base branch
from
copilot/add-include-graph-support
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
27 commits
Select commit
Hold shift + click to select a range
c0b4240
Initial plan
Copilot e1aa4de
Add IncludeGraph support for full object graph bulk insert
Copilot 570bd6a
Address code review comments: add debug logging and fix docs
Copilot 1b4553f
Remove tests for Oracle and MySQL as they don't support returing ids
2514aac
Add note about Oracle and MySQL limitations for IncludeGraph feature
9f337a9
Address PR review feedback: add tests, validation, and docs improvements
Copilot 5f74e09
Improve dictionary cast in join entity handling
Copilot 8e4dfe6
Add tests for entity linking with generated and client-generated keys
Copilot f811609
Add UseIncludeGraph property to benchmark project
Copilot c06aee2
Update benchmark to generate entities with children when UseIncludeGr…
Copilot b20d8cd
Merge branch 'main' into copilot/add-include-graph-support
d1f0632
Refactor GraphTestsBase to use IDbContextFactory for context creation
6b6b003
Code cleanup and handle providers that don't support inserted ids whe…
a181197
Optimize navigation getters
463dd4b
Enhance inverse navigation handling in GraphEntityCollector and Navig…
0410e40
Add optimized property accessors for entity metadata
6283440
Add support for IncludeGraph option in benchmarks
2c994ae
Fix benchmark to really use EFCoreBulkExtensions
d927f1f
Address code review feedback for IncludeGraph feature
Copilot 3e636b1
Fix minor code review issues: null check, redundant condition, grammar
Copilot f21b3a8
Allow IncludeGraph on Oracle/MySQL when keys are client-generated, in…
Copilot 2ab4e87
Rename variable to hasAnyDatabaseGeneratedKeys for clarity
Copilot 092ed1f
Apply suggestions from code review
PhenX 29eb67b
Merge branch 'main' into copilot/add-include-graph-support
97e239e
Wrap full graph insert with a transaction
fef0db3
Restore original PKs on failure
e2fd19b
Handle deduplication of join records
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,137 @@ | ||
| # Graph Insert (Navigation Properties) | ||
|
|
||
| > ℹ️ Graph inserts that require database-generated key propagation are not supported for Oracle and MySQL providers due to limitations in retrieving generated IDs. Graph inserts using client-generated keys (e.g., GUIDs with `ValueGeneratedNever()`) are supported on all providers. | ||
|
|
||
| This library supports bulk inserting entire object graphs, including entities with their related navigation properties. | ||
|
|
||
| ## Enabling Graph Insert | ||
|
|
||
| ```csharp | ||
| await dbContext.ExecuteBulkInsertAsync(blogs, options => | ||
| { | ||
| options.IncludeGraph = true; | ||
| }); | ||
| ``` | ||
|
|
||
| ## How It Works | ||
|
|
||
| 1. The library traverses all reachable entities via navigation properties | ||
| 2. Entities are sorted in topological order (parents before children) to respect foreign key constraints | ||
| 3. Each entity type is bulk inserted in dependency order | ||
| 4. Generated IDs (identity columns) are propagated to foreign key properties | ||
| 5. Many-to-many join tables with explicit join entity types are populated automatically (see Limitations below) | ||
|
|
||
| ## Options | ||
|
|
||
| | Option | Default | Description | | ||
| |--------|---------|-------------| | ||
| | `IncludeGraph` | `false` | Enable graph traversal | | ||
| | `MaxGraphDepth` | `0` (unlimited) | Maximum depth to traverse. Use 0 for unlimited. | | ||
| | `IncludeNavigations` | `null` (all) | Specific navigation property names to include | | ||
| | `ExcludeNavigations` | `null` (none) | Navigation property names to exclude | | ||
|
|
||
| ## Supported Relationship Types | ||
|
|
||
| - ✅ One-to-Many (e.g., Blog → Posts) | ||
| - ✅ Many-to-One (e.g., Post → Blog) | ||
| - ✅ One-to-One (e.g., Blog → BlogSettings) | ||
| - ✅ Many-to-Many with join table (e.g., Post ↔ Tags) | ||
| - ✅ Self-referencing/Hierarchies (e.g., Category → Parent/Children) | ||
|
|
||
| ## Performance Considerations | ||
|
|
||
| - Graph insert is inherently slower than flat insert due to FK propagation overhead | ||
| - For entities with identity columns, the library uses `ExecuteBulkInsertReturnEntitiesAsync` internally to retrieve generated IDs | ||
| - Consider using client-generated keys (GUIDs with `ValueGeneratedNever()`) to avoid ID propagation overhead | ||
| - Use `MaxGraphDepth` to limit traversal for large/deep graphs | ||
| - Use `IncludeNavigations` or `ExcludeNavigations` to reduce the scope of insertions | ||
|
|
||
| ## Example | ||
|
|
||
| ### One-to-Many Relationship | ||
|
|
||
| ```csharp | ||
| var blog = new Blog | ||
| { | ||
| Name = "My Blog", | ||
| Posts = new List<Post> | ||
| { | ||
| new Post { Title = "First Post" }, | ||
| new Post { Title = "Second Post" } | ||
| } | ||
| }; | ||
|
|
||
| await dbContext.ExecuteBulkInsertAsync(new[] { blog }, o => o.IncludeGraph = true); | ||
|
|
||
| // After insert: | ||
| // - blog.Id is populated | ||
| // - blog.Posts[0].BlogId == blog.Id | ||
| // - blog.Posts[1].BlogId == blog.Id | ||
| ``` | ||
|
|
||
| ### One-to-One Relationship | ||
|
|
||
| ```csharp | ||
| var blog = new Blog | ||
| { | ||
| Name = "My Blog", | ||
| Settings = new BlogSettings { EnableComments = true } | ||
| }; | ||
|
|
||
| await dbContext.ExecuteBulkInsertAsync(new[] { blog }, o => o.IncludeGraph = true); | ||
|
|
||
| // After insert: | ||
| // - blog.Id is populated | ||
| // - blog.Settings.BlogId == blog.Id | ||
| ``` | ||
|
|
||
| ### Selective Navigation Inclusion | ||
|
|
||
| ```csharp | ||
| var blog = new Blog | ||
| { | ||
| Name = "My Blog", | ||
| Posts = new List<Post> { new Post { Title = "Post" } }, | ||
| Settings = new BlogSettings { EnableComments = true } | ||
| }; | ||
|
|
||
| // Only insert Posts, not Settings | ||
| await dbContext.ExecuteBulkInsertAsync(new[] { blog }, o => | ||
| { | ||
| o.IncludeGraph = true; | ||
| o.IncludeNavigations = new HashSet<string> { "Posts" }; | ||
| }); | ||
| ``` | ||
|
|
||
| ### Limiting Graph Depth | ||
|
|
||
| ```csharp | ||
| var blog = new Blog | ||
| { | ||
| Name = "My Blog", | ||
| Posts = new List<Post> | ||
| { | ||
| new Post | ||
| { | ||
| Title = "Post", | ||
| Tags = new List<Tag> { new Tag { Name = "EF Core" } } // Won't be inserted | ||
| } | ||
| } | ||
| }; | ||
|
|
||
| // MaxGraphDepth = 1 means only Blog and direct children (Posts) | ||
| await dbContext.ExecuteBulkInsertAsync(new[] { blog }, o => | ||
| { | ||
| o.IncludeGraph = true; | ||
| o.MaxGraphDepth = 1; | ||
| }); | ||
| ``` | ||
|
|
||
| ## Limitations | ||
|
|
||
| - **Shadow foreign keys**: Currently not supported. Add a CLR property for foreign keys. | ||
| - **Circular references**: Handled gracefully by tracking visited entities, but may result in incomplete graphs. | ||
| - **Owned entities**: Owned entity types are not included in graph traversal and are not inserted when using `IncludeGraph = true`. | ||
| - **Self-referencing hierarchies**: Multi-level self-referencing hierarchies (e.g., Category → Children) require multiple insert operations. Root entities can be inserted, but nested children with FK references to other entities of the same type within the same batch are not supported. | ||
| - **Many-to-many join tables**: Entities on both sides of many-to-many relationships are traversed and inserted. However, automatic join table population only works with explicit join entity types (not `Dictionary<string, object>` shared-type entities). | ||
| - **OnConflict/Upsert**: Not currently supported with `IncludeGraph = true`. | ||
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
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
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
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
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
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
28 changes: 28 additions & 0 deletions
28
src/PhenX.EntityFrameworkCore.BulkInsert/Graph/EntityPairEqualityComparer.cs
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,28 @@ | ||
| using System.Runtime.CompilerServices; | ||
|
|
||
| namespace PhenX.EntityFrameworkCore.BulkInsert.Graph; | ||
|
|
||
| /// <summary> | ||
| /// Compares pairs of entity references for equality using reference equality. | ||
| /// Used for deduplicating many-to-many join records. | ||
| /// </summary> | ||
| internal sealed class EntityPairEqualityComparer : IEqualityComparer<(object Left, object Right)> | ||
| { | ||
| public static readonly EntityPairEqualityComparer Instance = new(); | ||
|
|
||
| private EntityPairEqualityComparer() { } | ||
|
|
||
| public bool Equals((object Left, object Right) x, (object Left, object Right) y) | ||
| { | ||
| return ReferenceEquals(x.Left, y.Left) && ReferenceEquals(x.Right, y.Right); | ||
| } | ||
|
|
||
| public int GetHashCode((object Left, object Right) obj) | ||
| { | ||
| return HashCode.Combine( | ||
| RuntimeHelpers.GetHashCode(obj.Left), | ||
| RuntimeHelpers.GetHashCode(obj.Right) | ||
| ); | ||
| } | ||
| } | ||
|
|
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.