Skip to content

Commit 5400131

Browse files
author
Чуприн Лев
committed
additional graph sorting by depth and tests
1 parent 3cf0337 commit 5400131

File tree

3 files changed

+197
-2
lines changed

3 files changed

+197
-2
lines changed

EFCore.BulkExtensions.Core/DbContextBulkTransactionGraphUtil.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ private static async Task ExecuteWithGraphAsync(DbContext context, IEnumerable<o
4040
// If this is set to false, wont' be able to support some code first model types as EFCore uses shadow properties when a relationship's foreign keys arent explicitly defined
4141
bulkConfig.EnableShadowProperties = true;
4242

43-
var graphNodes = GraphUtil.GetTopologicallySortedGraph(context, entities);
43+
var graphNodes = GraphUtil.GetSortedGraph(context, entities);
4444

4545
if (graphNodes == null)
4646
return;

EFCore.BulkExtensions.Core/Util/GraphUtil.cs

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using Microsoft.EntityFrameworkCore;
22
using Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
33
using Microsoft.EntityFrameworkCore.Metadata;
4+
45
using System;
56
using System.Collections.Generic;
67
using System.Linq;
@@ -9,7 +10,7 @@ namespace EFCore.BulkExtensions;
910

1011
internal class GraphUtil
1112
{
12-
public static IEnumerable<GraphNode>? GetTopologicallySortedGraph(DbContext dbContext, IEnumerable<object> entities)
13+
public static IEnumerable<GraphNode>? GetSortedGraph(DbContext dbContext, IEnumerable<object> entities)
1314
{
1415
if (!entities.Any())
1516
{
@@ -26,6 +27,16 @@ internal class GraphUtil
2627

2728
// Sort these entities so the first entity is the least dependendant
2829
var topologicalSorted = TopologicalSort(dependencies.Keys, y => dependencies[y].DependsOn.Select(y => y.entity));
30+
31+
var withAdditionalSorting = true;
32+
33+
if (withAdditionalSorting) return GetNodesWithSortingByDepth(dependencies, topologicalSorted);
34+
35+
return GetNodes(dependencies, topologicalSorted);
36+
}
37+
38+
private static IEnumerable<GraphNode> GetNodes(Dictionary<object, GraphDependency> dependencies, IEnumerable<object> topologicalSorted)
39+
{
2940
var result = new List<GraphNode>();
3041

3142
foreach (var s in topologicalSorted)
@@ -40,6 +51,79 @@ internal class GraphUtil
4051
return result;
4152
}
4253

54+
private static IEnumerable<GraphNode> GetNodesWithSortingByDepth(Dictionary<object, GraphDependency> dependencies, IEnumerable<object> topologicalSorted)
55+
{
56+
var entitiesDepth = CalculateEntitiesDepth(dependencies, topologicalSorted.ToList());
57+
58+
var typesDepth = CalculateTypesDepth(entitiesDepth);
59+
60+
var result = new List<GraphNode>();
61+
62+
foreach (var type in typesDepth.OrderByDescending(x => x.Value))
63+
{
64+
foreach (var s in topologicalSorted)
65+
{
66+
if (s.GetType() != type.Key) continue;
67+
68+
result.Add(new GraphNode
69+
{
70+
Entity = s,
71+
Dependencies = dependencies[s]
72+
});
73+
}
74+
}
75+
76+
return result;
77+
}
78+
79+
/// <summary>
80+
/// Calculates the depth of each entity from the entity to the top of the dependency graph. Returns a dictionary mapping entities to their depth.
81+
/// </summary>
82+
private static Dictionary<object, int> CalculateEntitiesDepth(
83+
Dictionary<object, GraphDependency> graph,
84+
IReadOnlyList<object> topologicalSorted)
85+
{
86+
var depthDict = graph.Keys.ToDictionary(k => k, v => 0);
87+
88+
for (int i = graph.Count - 1; i >= 0; i--)
89+
{
90+
var node = topologicalSorted[i];
91+
92+
var nodeDeps = graph
93+
.Where(x => x.Value.DependsOn.Any(d => d.entity == node))
94+
.Select(x => x.Key)
95+
.ToList();
96+
97+
int max = 0;
98+
foreach (var childNode in nodeDeps)
99+
max = Math.Max(max, depthDict[childNode] + 1);
100+
101+
depthDict[node] = max;
102+
}
103+
104+
return depthDict;
105+
}
106+
107+
/// <summary>
108+
/// Calculates the maximum depth from entity of a given type to the top. Returns a dictionary mapping types to their maximum depth.
109+
/// </summary>
110+
private static Dictionary<Type, int> CalculateTypesDepth(Dictionary<object, int> entitiesDepth)
111+
{
112+
var typesDepth = new Dictionary<Type, int>();
113+
114+
foreach (var entityDepth in entitiesDepth)
115+
{
116+
var type = entityDepth.Key.GetType();
117+
118+
if (typesDepth.TryGetValue(type, out int value))
119+
typesDepth[type] = Math.Max(value, entityDepth.Value);
120+
else
121+
typesDepth.Add(type, entityDepth.Value);
122+
}
123+
124+
return typesDepth;
125+
}
126+
43127
private static GraphDependency? GetFlatGraph(DbContext dbContext, object graphEntity, IDictionary<object, GraphDependency> result)
44128
{
45129
var entityType = dbContext.Model.FindEntityType(graphEntity.GetType());

EFCore.BulkExtensions.Tests/IncludeGraph/IncludeGraphTests.cs

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,117 @@ private static IEnumerable<WorkOrder> GetTestData()
143143
yield return WorkOrder2;
144144
}
145145

146+
[Theory]
147+
[MemberData(nameof(GetTestData2))]
148+
public async Task BulkInsertOrUpdate_EntityWithNestedNullableObjectGraph_SavesGraphToDatabase1(IEnumerable<WorkOrder> orders)
149+
{
150+
ContextUtil.DatabaseType = SqlType.SqlServer;
151+
152+
using (var db = new GraphDbContext(ContextUtil.GetOptions<GraphDbContext>(databaseName: $"{nameof(EFCoreBulkTest)}_Graph")))
153+
{
154+
db.Database.EnsureDeleted();
155+
await db.Database.EnsureCreatedAsync();
156+
157+
var bulkConfig = new BulkConfig
158+
{
159+
IncludeGraph = true,
160+
CalculateStats = true,
161+
};
162+
await db.BulkInsertOrUpdateAsync(orders, bulkConfig);
163+
164+
Assert.NotNull(bulkConfig.StatsInfo);
165+
Assert.Equal(
166+
orders.Count() + orders.Where(x => x.Asset != null).Count(),
167+
bulkConfig.StatsInfo.StatsNumberInserted);
168+
}
169+
170+
using (var db = new GraphDbContext(ContextUtil.GetOptions<GraphDbContext>(databaseName: $"{nameof(EFCoreBulkTest)}_Graph")))
171+
{
172+
var ordersFromDb = db.WorkOrders
173+
.Include(y => y.Asset)
174+
.OrderBy(x => x.Id)
175+
.ToList();
176+
177+
foreach (var orderFromDb in ordersFromDb)
178+
{
179+
var order = orders.First(x => x.Description == orderFromDb.Description);
180+
181+
if (order.Asset == null)
182+
Assert.Null(orderFromDb.Asset);
183+
else
184+
Assert.NotNull(orderFromDb.Asset);
185+
}
186+
}
187+
}
188+
189+
public static IEnumerable<object[]> GetTestData2()
190+
=>
191+
[
192+
[GetTestDataWithNullInEnd()],
193+
[GetTestDataWithNullInMiddle()],
194+
[GetTestDataWithNullInFirst()],
195+
];
196+
197+
private static IEnumerable<WorkOrder> GetTestDataWithNullInEnd()
198+
{
199+
var baseData = GetBaseTestData();
200+
201+
baseData.Last().Asset = null;
202+
203+
return baseData;
204+
}
205+
206+
private static IEnumerable<WorkOrder> GetTestDataWithNullInMiddle()
207+
{
208+
var baseData = GetBaseTestData();
209+
210+
baseData[1].Asset = null;
211+
212+
return baseData;
213+
}
214+
215+
private static IEnumerable<WorkOrder> GetTestDataWithNullInFirst()
216+
{
217+
var baseData = GetBaseTestData();
218+
219+
baseData.First().Asset = null;
220+
221+
return baseData;
222+
}
223+
224+
private static List<WorkOrder> GetBaseTestData()
225+
{
226+
return
227+
[
228+
new WorkOrder()
229+
{
230+
Description = "Fix belt",
231+
Asset = new Asset
232+
{
233+
Description = "MANU-1",
234+
Location = "WAREHOUSE-1"
235+
},
236+
},
237+
new WorkOrder()
238+
{
239+
Description = "Fix toilets",
240+
Asset = new Asset
241+
{
242+
Description = "FLUSHMASTER-1",
243+
Location = "GYM-BLOCK-3"
244+
},
245+
},
246+
new WorkOrder()
247+
{
248+
Description = "Fix door",
249+
Asset = new Asset
250+
{
251+
Location = "OFFICE-12"
252+
},
253+
}
254+
];
255+
}
256+
146257
public void Dispose()
147258
{
148259
using var db = new GraphDbContext(ContextUtil.GetOptions<GraphDbContext>(databaseName: $"{nameof(EFCoreBulkTest)}_Graph"));

0 commit comments

Comments
 (0)