Skip to content

Commit 463dd4b

Browse files
author
fabien.menager
committed
Enhance inverse navigation handling in GraphEntityCollector and NavigationMetadata
1 parent a181197 commit 463dd4b

3 files changed

Lines changed: 68 additions & 18 deletions

File tree

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

Lines changed: 6 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using System.Collections;
2-
using System.Reflection;
32

43
using Microsoft.EntityFrameworkCore;
54

@@ -146,29 +145,18 @@ private void CollectEntity(object? entity, int depth)
146145

147146
private static void SetInverseNavigation(object parentEntity, object childEntity, NavigationMetadata navigation)
148147
{
149-
// For one-to-many navigations, find and set the inverse navigation property
148+
// For one-to-many navigations, set the inverse navigation property
150149
// (e.g., if Blog.Posts is the navigation, set Post.Blog = blog)
151-
var nav = navigation.Navigation;
152-
if (nav is not Microsoft.EntityFrameworkCore.Metadata.INavigation regularNav)
150+
if (!navigation.HasInverseSetter)
153151
{
154152
return;
155153
}
156154

157-
var inverse = regularNav.Inverse;
158-
if (inverse == null)
155+
// Check if the inverse navigation is already set
156+
var currentValue = navigation.GetInverseValue(childEntity);
157+
if (currentValue == null)
159158
{
160-
return;
161-
}
162-
163-
// Set the inverse navigation property on the child
164-
var inversePropertyInfo = childEntity.GetType().GetProperty(inverse.Name, BindingFlags.Public | BindingFlags.Instance);
165-
if (inversePropertyInfo != null && inversePropertyInfo.CanWrite)
166-
{
167-
var currentValue = inversePropertyInfo.GetValue(childEntity);
168-
if (currentValue == null)
169-
{
170-
inversePropertyInfo.SetValue(childEntity, parentEntity);
171-
}
159+
navigation.SetInverseValue(childEntity, parentEntity);
172160
}
173161
}
174162
}

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,21 @@ public NavigationMetadata(INavigationBase navigation)
2424

2525
_getter = PropertyAccessor.CreateGetter(propertyInfo);
2626

27+
// Build inverse navigation metadata if available
28+
if (navigation is INavigation { Inverse: not null } regularNav)
29+
{
30+
var inverse = regularNav.Inverse;
31+
var inversePropertyInfo = inverse.DeclaringEntityType.ClrType.GetProperty(
32+
inverse.Name,
33+
BindingFlags.Public | BindingFlags.Instance);
34+
35+
if (inversePropertyInfo != null && inversePropertyInfo.CanWrite)
36+
{
37+
_inverseGetter = PropertyAccessor.CreateGetter(inversePropertyInfo);
38+
_inverseSetter = PropertyAccessor.CreateSetter(inversePropertyInfo);
39+
}
40+
}
41+
2742
if (navigation is ISkipNavigation skipNavigation)
2843
{
2944
IsManyToMany = true;
@@ -40,6 +55,8 @@ public NavigationMetadata(INavigationBase navigation)
4055
}
4156

4257
private readonly Func<object, object?> _getter;
58+
private readonly Func<object, object?>? _inverseGetter;
59+
private readonly Action<object, object?>? _inverseSetter;
4360

4461
/// <summary>
4562
/// The underlying EF Core navigation.
@@ -91,6 +108,23 @@ public NavigationMetadata(INavigationBase navigation)
91108
/// </summary>
92109
public object? GetValue(object entity) => _getter.Invoke(entity);
93110

111+
/// <summary>
112+
/// Gets the value of the inverse navigation property from the entity using an optimized getter.
113+
/// Returns null if there is no inverse navigation.
114+
/// </summary>
115+
public object? GetInverseValue(object entity) => _inverseGetter?.Invoke(entity);
116+
117+
/// <summary>
118+
/// Sets the value of the inverse navigation property on the entity using an optimized setter.
119+
/// Does nothing if there is no inverse navigation.
120+
/// </summary>
121+
public void SetInverseValue(object entity, object? value) => _inverseSetter?.Invoke(entity, value);
122+
123+
/// <summary>
124+
/// Returns true if this navigation has an inverse navigation with a setter.
125+
/// </summary>
126+
public bool HasInverseSetter => _inverseSetter != null;
127+
94128
public override string ToString()
95129
{
96130
var relationshipType = IsManyToMany ? "ManyToMany" : (IsCollection ? "OneToMany" : "OneToOne");

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,34 @@ internal static class PropertyAccessor
8686
return Expression.Lambda<Func<object, object?>>(finalExpression, instanceParam).Compile();
8787
}
8888

89+
public static Action<object, object?> CreateSetter(PropertyInfo propertyInfo)
90+
{
91+
ArgumentNullException.ThrowIfNull(propertyInfo);
92+
93+
if (!propertyInfo.CanWrite)
94+
{
95+
throw new ArgumentException($"Property '{propertyInfo.Name}' is not writable", nameof(propertyInfo));
96+
}
97+
98+
// (instance, value) => { }
99+
var instanceParam = Expression.Parameter(typeof(object), "instance");
100+
var valueParam = Expression.Parameter(typeof(object), "value");
101+
102+
var propDeclaringType = propertyInfo.DeclaringType!;
103+
104+
// Convert object to the declaring type
105+
var typedInstance = GetTypedInstance(propDeclaringType, instanceParam);
106+
107+
// Convert value to property type (both value types and reference types need conversion from object)
108+
var typedValue = Expression.Convert(valueParam, propertyInfo.PropertyType);
109+
110+
// ((TEntity)instance).Property = (TProperty)value
111+
var propertyAccess = Expression.Property(typedInstance, propertyInfo);
112+
var assignment = Expression.Assign(propertyAccess, typedValue);
113+
114+
return Expression.Lambda<Action<object, object?>>(assignment, instanceParam, valueParam).Compile();
115+
}
116+
89117
private static UnaryExpression GetTypedInstance(Type propDeclaringType, ParameterExpression instanceParam)
90118
{
91119
return propDeclaringType.IsValueType

0 commit comments

Comments
 (0)