Skip to content

Commit 0410e40

Browse files
author
fabien.menager
committed
Add optimized property accessors for entity metadata
1 parent 463dd4b commit 0410e40

3 files changed

Lines changed: 157 additions & 66 deletions

File tree

src/PhenX.EntityFrameworkCore.BulkInsert/Graph/GraphBulkInsertOrchestrator.cs

Lines changed: 48 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
using System.Collections.Concurrent;
2-
using System.Linq.Expressions;
31
using System.Reflection;
42

53
using Microsoft.EntityFrameworkCore;
@@ -17,8 +15,6 @@ namespace PhenX.EntityFrameworkCore.BulkInsert.Graph;
1715
/// </summary>
1816
internal sealed class GraphBulkInsertOrchestrator
1917
{
20-
private static readonly ConcurrentDictionary<(Type, string), Action<object, object?>> PropertySetters = new();
21-
private static readonly ConcurrentDictionary<(Type, string), Func<object, object?>> PropertyGetters = new();
2218

2319
private readonly DbContext _context;
2420
private readonly MetadataProvider _metadataProvider;
@@ -76,15 +72,15 @@ public async Task<GraphInsertResult<T>> InsertGraph<T>(
7672
PropagateParentForeignKeys(entitiesToInsert, entityType, graphMetadata);
7773

7874
// Insert entities of this type
79-
await InsertEntitiesOfType(sync, _context, entityType, entitiesToInsert, options, provider, ctk);
75+
await InsertEntitiesOfType(sync, _context, entityType, entitiesToInsert, options, provider, graphMetadata, ctk);
8076

8177
totalInserted += entitiesToInsert.Count;
8278
}
8379

8480
// 3. Insert join table records for many-to-many relationships
8581
if (collectionResult.JoinRecords.Count > 0)
8682
{
87-
await InsertJoinRecords(sync, _context, collectionResult.JoinRecords, options, provider, ctk);
83+
await InsertJoinRecords(sync, _context, collectionResult.JoinRecords, options, provider, graphMetadata, ctk);
8884
}
8985

9086
// Return root entities
@@ -110,6 +106,12 @@ private static void PropagateParentForeignKeys(
110106
return;
111107
}
112108

109+
var entityMetadata = graphMetadata.GetEntityMetadata(entityType);
110+
if (entityMetadata == null)
111+
{
112+
return;
113+
}
114+
113115
// For each FK relationship, propagate PK values from parent entities
114116
foreach (var fk in efEntityType.GetForeignKeys())
115117
{
@@ -125,12 +127,18 @@ private static void PropagateParentForeignKeys(
125127
foreach (var entity in entities)
126128
{
127129
// Get the parent entity via navigation property
128-
var parentEntity = GetPropertyValue(entity, navigationPropertyName);
130+
var parentEntity = entityMetadata.GetPropertyValue(entity, navigationPropertyName);
129131
if (parentEntity == null)
130132
{
131133
continue;
132134
}
133135

136+
var parentMetadata = graphMetadata.GetEntityMetadata(parentEntity.GetType());
137+
if (parentMetadata == null)
138+
{
139+
continue;
140+
}
141+
134142
// Copy PK values from parent to FK properties on this entity
135143
var fkProperties = fk.Properties;
136144
var pkProperties = fk.PrincipalKey.Properties;
@@ -147,8 +155,8 @@ private static void PropagateParentForeignKeys(
147155
continue;
148156
}
149157

150-
var pkValue = GetPropertyValue(parentEntity, pkProp.Name);
151-
SetPropertyValue(entity, fkProp.Name, pkValue);
158+
var pkValue = parentMetadata.GetPropertyValue(parentEntity, pkProp.Name);
159+
entityMetadata.SetPropertyValue(entity, fkProp.Name, pkValue);
152160
}
153161
}
154162
}
@@ -161,14 +169,15 @@ private async Task InsertEntitiesOfType(
161169
List<object> entities,
162170
BulkInsertOptions options,
163171
IBulkInsertProvider provider,
172+
GraphMetadata graphMetadata,
164173
CancellationToken ctk)
165174
{
166175
// Use reflection to call the generic BulkInsert method
167176
var method = typeof(GraphBulkInsertOrchestrator)
168177
.GetMethod(nameof(InsertEntitiesGenericAsync), BindingFlags.NonPublic | BindingFlags.Instance)!
169178
.MakeGenericMethod(entityType);
170179

171-
var task = (Task)method.Invoke(this, [context, entities, options, provider, ctk])!;
180+
var task = (Task)method.Invoke(this, [context, entities, options, provider, graphMetadata, ctk])!;
172181
await task;
173182
}
174183

@@ -177,6 +186,7 @@ private async Task InsertEntitiesGenericAsync<TEntity>(
177186
List<object> entities,
178187
BulkInsertOptions options,
179188
IBulkInsertProvider provider,
189+
GraphMetadata graphMetadata,
180190
CancellationToken ctk) where TEntity : class
181191
{
182192
var typedEntities = entities.Cast<TEntity>().ToList();
@@ -202,7 +212,11 @@ private async Task InsertEntitiesGenericAsync<TEntity>(
202212
}
203213

204214
// Copy generated IDs back to original entities
205-
CopyGeneratedIds(typedEntities, insertedEntities, tableInfo);
215+
var entityMetadata = graphMetadata.GetEntityMetadata(typeof(TEntity));
216+
if (entityMetadata != null)
217+
{
218+
CopyGeneratedIds(typedEntities, insertedEntities, tableInfo, entityMetadata);
219+
}
206220
}
207221
else
208222
{
@@ -214,7 +228,8 @@ private async Task InsertEntitiesGenericAsync<TEntity>(
214228
private void CopyGeneratedIds<TEntity>(
215229
List<TEntity> originalEntities,
216230
List<TEntity> insertedEntities,
217-
TableMetadata tableInfo) where TEntity : class
231+
TableMetadata tableInfo,
232+
EntityMetadata entityMetadata) where TEntity : class
218233
{
219234
if (originalEntities.Count != insertedEntities.Count)
220235
{
@@ -241,8 +256,8 @@ private void CopyGeneratedIds<TEntity>(
241256

242257
foreach (var pkProp in pkProps)
243258
{
244-
var value = GetPropertyValue(inserted, pkProp.PropertyName);
245-
SetPropertyValue(original, pkProp.PropertyName, value);
259+
var value = entityMetadata.GetPropertyValue(inserted, pkProp.PropertyName);
260+
entityMetadata.SetPropertyValue(original, pkProp.PropertyName, value);
246261
}
247262
}
248263
}
@@ -253,6 +268,7 @@ private async Task InsertJoinRecords(
253268
List<JoinRecord> joinRecords,
254269
BulkInsertOptions options,
255270
IBulkInsertProvider provider,
271+
GraphMetadata graphMetadata,
256272
CancellationToken ctk)
257273
{
258274
// Group join records by join entity type
@@ -278,11 +294,23 @@ private async Task InsertJoinRecords(
278294
continue;
279295
}
280296

297+
// Get entity metadata for join type
298+
var joinEntityMetadata = graphMetadata.GetEntityMetadata(joinEntityType);
299+
281300
// Create join table entries
282301
var joinEntities = new List<object>();
283302

284303
foreach (var record in records)
285304
{
305+
// Get metadata for left and right entities
306+
var leftMetadata = graphMetadata.GetEntityMetadata(record.LeftEntity.GetType());
307+
var rightMetadata = graphMetadata.GetEntityMetadata(record.RightEntity.GetType());
308+
309+
if (leftMetadata == null || rightMetadata == null)
310+
{
311+
continue;
312+
}
313+
286314
// Create a dictionary-based join entity
287315
var joinEntry = Activator.CreateInstance(joinEntityType);
288316
if (joinEntry == null)
@@ -304,14 +332,14 @@ private async Task InsertJoinRecords(
304332
var fkProp = fk.Properties[i];
305333
var pkProp = fk.PrincipalKey.Properties[i];
306334

307-
var pkValue = GetPropertyValue(record.LeftEntity, pkProp.Name);
335+
var pkValue = leftMetadata.GetPropertyValue(record.LeftEntity, pkProp.Name);
308336
if (dictEntry != null)
309337
{
310338
dictEntry[fkProp.Name] = pkValue!;
311339
}
312-
else
340+
else if (joinEntityMetadata != null)
313341
{
314-
SetPropertyValue(joinEntry, fkProp.Name, pkValue);
342+
joinEntityMetadata.SetPropertyValue(joinEntry, fkProp.Name, pkValue);
315343
}
316344
}
317345

@@ -321,14 +349,14 @@ private async Task InsertJoinRecords(
321349
var fkProp = inverseFk.Properties[i];
322350
var pkProp = inverseFk.PrincipalKey.Properties[i];
323351

324-
var pkValue = GetPropertyValue(record.RightEntity, pkProp.Name);
352+
var pkValue = rightMetadata.GetPropertyValue(record.RightEntity, pkProp.Name);
325353
if (dictEntry != null)
326354
{
327355
dictEntry[fkProp.Name] = pkValue!;
328356
}
329-
else
357+
else if (joinEntityMetadata != null)
330358
{
331-
SetPropertyValue(joinEntry, fkProp.Name, pkValue);
359+
joinEntityMetadata.SetPropertyValue(joinEntry, fkProp.Name, pkValue);
332360
}
333361
}
334362

@@ -374,50 +402,4 @@ private async Task InsertJoinEntities(
374402
}
375403
await task;
376404
}
377-
378-
private static object? GetPropertyValue(object entity, string propertyName)
379-
{
380-
var key = (entity.GetType(), propertyName);
381-
var getter = PropertyGetters.GetOrAdd(key, k =>
382-
{
383-
var property = k.Item1.GetProperty(k.Item2, BindingFlags.Public | BindingFlags.Instance);
384-
if (property == null)
385-
{
386-
return _ => null;
387-
}
388-
389-
var param = Expression.Parameter(typeof(object), "obj");
390-
var cast = Expression.Convert(param, k.Item1);
391-
var access = Expression.Property(cast, property);
392-
var convertResult = Expression.Convert(access, typeof(object));
393-
394-
return Expression.Lambda<Func<object, object?>>(convertResult, param).Compile();
395-
});
396-
397-
return getter(entity);
398-
}
399-
400-
private static void SetPropertyValue(object entity, string propertyName, object? value)
401-
{
402-
var key = (entity.GetType(), propertyName);
403-
var setter = PropertySetters.GetOrAdd(key, k =>
404-
{
405-
var property = k.Item1.GetProperty(k.Item2, BindingFlags.Public | BindingFlags.Instance);
406-
if (property == null || !property.CanWrite)
407-
{
408-
return (_, _) => { };
409-
}
410-
411-
var param = Expression.Parameter(typeof(object), "obj");
412-
var valueParam = Expression.Parameter(typeof(object), "value");
413-
var cast = Expression.Convert(param, k.Item1);
414-
var access = Expression.Property(cast, property);
415-
var convertValue = Expression.Convert(valueParam, property.PropertyType);
416-
var assign = Expression.Assign(access, convertValue);
417-
418-
return Expression.Lambda<Action<object, object?>>(assign, param, valueParam).Compile();
419-
});
420-
421-
setter(entity, value);
422-
}
423405
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
using System.Collections.Concurrent;
2+
3+
using Microsoft.EntityFrameworkCore.Metadata;
4+
5+
namespace PhenX.EntityFrameworkCore.BulkInsert.Metadata;
6+
7+
/// <summary>
8+
/// Metadata for an entity type with optimized property accessors.
9+
/// Getters and setters are computed on demand and cached.
10+
/// </summary>
11+
internal sealed class EntityMetadata
12+
{
13+
private readonly ConcurrentDictionary<string, Func<object, object?>> _getters = new();
14+
private readonly ConcurrentDictionary<string, Action<object, object?>> _setters = new();
15+
private readonly IEntityType _entityType;
16+
17+
public EntityMetadata(IEntityType entityType)
18+
{
19+
_entityType = entityType;
20+
ClrType = entityType.ClrType;
21+
}
22+
23+
public Type ClrType { get; }
24+
25+
/// <summary>
26+
/// Gets the value of a property from an entity using an optimized getter.
27+
/// The getter is created on first access and cached for subsequent calls.
28+
/// Returns null if the property is not found or is a shadow property.
29+
/// </summary>
30+
public object? GetPropertyValue(object entity, string propertyName)
31+
{
32+
var getter = _getters.GetOrAdd(propertyName, static (name, entityType) =>
33+
{
34+
var property = entityType.FindProperty(name);
35+
if (property == null || property.IsShadowProperty())
36+
{
37+
// Try to find a navigation property
38+
var navigation = entityType.FindNavigation(name);
39+
if (navigation?.PropertyInfo != null)
40+
{
41+
return PropertyAccessor.CreateGetter(navigation.PropertyInfo);
42+
}
43+
44+
return _ => null;
45+
}
46+
47+
var propertyInfo = property.PropertyInfo;
48+
if (propertyInfo == null)
49+
{
50+
return _ => null;
51+
}
52+
53+
return PropertyAccessor.CreateGetter(propertyInfo);
54+
}, _entityType);
55+
56+
return getter(entity);
57+
}
58+
59+
/// <summary>
60+
/// Sets the value of a property on an entity using an optimized setter.
61+
/// The setter is created on first access and cached for subsequent calls.
62+
/// Does nothing if the property is not found, is a shadow property, or is not writable.
63+
/// </summary>
64+
public void SetPropertyValue(object entity, string propertyName, object? value)
65+
{
66+
var setter = _setters.GetOrAdd(propertyName, static (name, entityType) =>
67+
{
68+
var property = entityType.FindProperty(name);
69+
if (property == null || property.IsShadowProperty())
70+
{
71+
return (_, _) => { };
72+
}
73+
74+
var propertyInfo = property.PropertyInfo;
75+
if (propertyInfo == null || !propertyInfo.CanWrite)
76+
{
77+
return (_, _) => { };
78+
}
79+
80+
return PropertyAccessor.CreateSetter(propertyInfo);
81+
}, _entityType);
82+
83+
setter(entity, value);
84+
}
85+
}
86+
87+

src/PhenX.EntityFrameworkCore.BulkInsert/Metadata/GraphMetadata.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ internal sealed class GraphMetadata
1212
{
1313
private readonly Dictionary<Type, IEntityType> _entityTypes;
1414
private readonly Dictionary<Type, List<NavigationMetadata>> _navigationsByType;
15+
private readonly Dictionary<Type, EntityMetadata> _entityMetadataByType = [];
1516
private readonly BulkInsertOptions _options;
1617

1718
public GraphMetadata(DbContext context, BulkInsertOptions options)
@@ -59,6 +60,27 @@ public IReadOnlyList<NavigationMetadata> GetNavigations(Type clrType)
5960
: [];
6061
}
6162

63+
/// <summary>
64+
/// Gets or creates the entity metadata with optimized property accessors for a CLR type.
65+
/// </summary>
66+
public EntityMetadata? GetEntityMetadata(Type clrType)
67+
{
68+
if (_entityMetadataByType.TryGetValue(clrType, out var metadata))
69+
{
70+
return metadata;
71+
}
72+
73+
var entityType = GetEntityType(clrType);
74+
if (entityType == null)
75+
{
76+
return null;
77+
}
78+
79+
metadata = new EntityMetadata(entityType);
80+
_entityMetadataByType[clrType] = metadata;
81+
return metadata;
82+
}
83+
6284
/// <summary>
6385
/// Determines the topological insertion order for a set of types based on FK dependencies.
6486
/// Parents are inserted before children to satisfy FK constraints.

0 commit comments

Comments
 (0)