Skip to content

Commit 5bd2fd0

Browse files
authored
Merge pull request #1901 from Lion-Ch/feature/issue-1780
[Issue #1780] Bug: IncludeGraph fails when first entity has null navigation property
2 parents 6a28e82 + 5400131 commit 5bd2fd0

File tree

5 files changed

+224
-21
lines changed

5 files changed

+224
-21
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/GraphDbContext.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,15 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
2525
cfg.HasKey(y => y.Id);
2626

2727
cfg.HasOne(y => y.ParentAsset).WithMany(y => y.ChildAssets);
28-
cfg.HasMany(y => y.WorkOrders).WithOne(y => y.Asset).IsRequired();
28+
cfg.HasMany(y => y.WorkOrders).WithOne(y => y.Asset);
2929
});
3030

3131
modelBuilder.Entity<WorkOrder>(cfg =>
3232
{
3333
cfg.HasKey(y => y.Id);
3434

3535
cfg.HasMany(y => y.WorkOrderSpares).WithOne(y => y.WorkOrder);
36-
cfg.HasOne(y => y.Asset).WithMany(y => y.WorkOrders).IsRequired();
36+
cfg.HasOne(y => y.Asset).WithMany(y => y.WorkOrders);
3737
});
3838

3939
modelBuilder.Entity<WorkOrderSpare>(cfg =>

EFCore.BulkExtensions.Tests/IncludeGraph/IncludeGraphTests.cs

Lines changed: 124 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
11
using EFCore.BulkExtensions.SqlAdapters;
22
using EFCore.BulkExtensions.Tests.IncludeGraph.Model;
33
using EFCore.BulkExtensions.Tests.ShadowProperties;
4+
45
using Microsoft.EntityFrameworkCore;
6+
57
using System;
68
using System.Collections.Generic;
79
using System.Linq;
810
using System.Threading.Tasks;
11+
912
using Xunit;
1013

1114
namespace EFCore.BulkExtensions.Tests.IncludeGraph;
1215

1316
public class IncludeGraphTests : IDisposable
1417
{
15-
private readonly static WorkOrder WorkOrder1 = new ()
18+
private readonly static WorkOrder WorkOrder1 = new()
1619
{
1720
Description = "Fix belt",
1821
Asset = new Asset
@@ -45,7 +48,7 @@ public class IncludeGraphTests : IDisposable
4548
}
4649
};
4750

48-
private static readonly WorkOrder WorkOrder2 = new ()
51+
private static readonly WorkOrder WorkOrder2 = new()
4952
{
5053
Description = "Fix toilets",
5154
Asset = new Asset
@@ -78,6 +81,7 @@ public class IncludeGraphTests : IDisposable
7881
}
7982
};
8083

84+
8185
[Theory]
8286
[InlineData(SqlType.SqlServer)]
8387
//[InlineData(DbServer.Sqlite)]
@@ -100,11 +104,14 @@ public async Task BulkInsertOrUpdate_EntityWithNestedObjectGraph_SavesGraphToDat
100104
wos.WorkOrder = WorkOrder2;
101105
}
102106

103-
WorkOrder1.Asset.WorkOrders.Add(WorkOrder1);
104-
WorkOrder2.Asset.WorkOrders.Add(WorkOrder2);
107+
if (WorkOrder1.Asset != null && WorkOrder2.Asset != null)
108+
{
109+
WorkOrder1.Asset.WorkOrders.Add(WorkOrder1);
110+
WorkOrder2.Asset.WorkOrders.Add(WorkOrder2);
105111

106-
WorkOrder1.Asset.ParentAsset = WorkOrder2.Asset;
107-
WorkOrder2.Asset.ChildAssets.Add(WorkOrder1.Asset);
112+
WorkOrder1.Asset.ParentAsset = WorkOrder2.Asset;
113+
WorkOrder2.Asset.ChildAssets.Add(WorkOrder1.Asset);
114+
}
108115

109116
var testData = GetTestData().ToList();
110117
var bulkConfig = new BulkConfig
@@ -136,6 +143,117 @@ private static IEnumerable<WorkOrder> GetTestData()
136143
yield return WorkOrder2;
137144
}
138145

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+
139257
public void Dispose()
140258
{
141259
using var db = new GraphDbContext(ContextUtil.GetOptions<GraphDbContext>(databaseName: $"{nameof(EFCoreBulkTest)}_Graph"));
Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
using System.Collections.Generic;
2-
3-
namespace EFCore.BulkExtensions.Tests.IncludeGraph.Model;
1+
using System.Collections.Generic;
42

5-
public class WorkOrder
6-
{
7-
public int Id { get; set; }
8-
public string Description { get; set; } = null!;
9-
10-
public Asset Asset { get; set; } = null!;
11-
public ICollection<WorkOrderSpare> WorkOrderSpares { get; set; } = new HashSet<WorkOrderSpare>();
12-
}
3+
namespace EFCore.BulkExtensions.Tests.IncludeGraph.Model;
4+
5+
public class WorkOrder
6+
{
7+
public int Id { get; set; }
8+
public string Description { get; set; } = null!;
9+
10+
public int? AssetId { get; set; }
11+
public Asset? Asset { get; set; }
12+
public ICollection<WorkOrderSpare> WorkOrderSpares { get; set; } = new HashSet<WorkOrderSpare>();
13+
}

0 commit comments

Comments
 (0)