ℹ️ This feature is not available for Oracle and MySQL providers due to limitations in retrieving generated IDs.
This library supports bulk inserting entire object graphs, including entities with their related navigation properties.
await dbContext.ExecuteBulkInsertAsync(blogs, options =>
{
options.IncludeGraph = true;
});- The library traverses all reachable entities via navigation properties
- Entities are sorted in topological order (parents before children) to respect foreign key constraints
- Each entity type is bulk inserted in dependency order
- Generated IDs (identity columns) are propagated to foreign key properties
- Many-to-many join tables are populated automatically
| 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 |
- ✅ 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)
- Graph insert is inherently slower than flat insert due to FK propagation overhead
- For entities with identity columns, the library uses
ExecuteBulkInsertReturnEntitiesAsyncinternally to retrieve generated IDs - Consider using client-generated keys (GUIDs with
ValueGeneratedNever()) to avoid ID propagation overhead - Use
MaxGraphDepthto limit traversal for large/deep graphs - Use
IncludeNavigationsorExcludeNavigationsto reduce the scope of insertions
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.Idvar 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.Idvar 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" };
});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;
});- 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.