Skip to content

Commit 1592da1

Browse files
authored
Add support for complex types (#60)
1 parent e243c66 commit 1592da1

13 files changed

Lines changed: 191 additions & 64 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,8 +153,8 @@ await dbContext.ExecuteBulkInsertAsync(entities, onConflict: new OnConflictOptio
153153
## Roadmap
154154

155155
- [ ] [Add support for navigation properties](https://github.com/PhenX/PhenX.EntityFrameworkCore.BulkInsert/issues/2)
156-
- [ ] [Add support for complex types](https://github.com/PhenX/PhenX.EntityFrameworkCore.BulkInsert/issues/3)
157-
- [ ] Add support for owned types
156+
- [x] [Add support for complex types](https://github.com/PhenX/PhenX.EntityFrameworkCore.BulkInsert/issues/3)
157+
- [x] Add support for owned types
158158
- [ ] Add support for shadow properties
159159
- [ ] Add support for TPT (Table Per Type) inheritance
160160
- [ ] Add support for TPC (Table Per Concrete Type) inheritance

src/PhenX.EntityFrameworkCore.BulkInsert.Oracle/OracleDialectBuilder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ public override string BuildMoveDataSql<T>(
4040
{
4141
matchColumns = GetColumns(target, onConflictTyped.Match);
4242
}
43-
else if (target.PrimaryKey.Count > 0)
43+
else if (target.PrimaryKey.Length > 0)
4444
{
4545
matchColumns = target.PrimaryKey.Select(x => x.QuotedColumName);
4646
}

src/PhenX.EntityFrameworkCore.BulkInsert.PostgreSql/PostgreSqlDialectBuilder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ protected override void AppendConflictMatch<T>(StringBuilder sql, TableMetadata
2222
{
2323
base.AppendConflictMatch(sql, target, conflict);
2424
}
25-
else if (target.PrimaryKey.Count > 0)
25+
else if (target.PrimaryKey.Length > 0)
2626
{
2727
sql.Append(' ');
2828
sql.AppendLine("(");

src/PhenX.EntityFrameworkCore.BulkInsert.SqlServer/SqlServerDialectBuilder.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ public override string BuildMoveDataSql<T>(
4848
{
4949
matchColumns = GetColumns(target, onConflictTyped.Match);
5050
}
51-
else if (target.PrimaryKey.Count > 0)
51+
else if (target.PrimaryKey.Length > 0)
5252
{
5353
matchColumns = target.PrimaryKey.Select(x => x.QuotedColumName);
5454
}

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

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,39 @@
66

77
namespace PhenX.EntityFrameworkCore.BulkInsert.Metadata;
88

9-
internal sealed class ColumnMetadata(IProperty property, SqlDialectBuilder dialect)
9+
internal sealed class ColumnMetadata
1010
{
11-
private readonly Func<object, object?> _getter = BuildGetter(property);
11+
public ColumnMetadata(IProperty property, SqlDialectBuilder dialect, IComplexProperty? complexProperty = null)
12+
{
13+
StoreObjectIdentifier? ownerTable = complexProperty != null
14+
? StoreObjectIdentifier.Table(complexProperty.DeclaringType.GetTableName()!, complexProperty.DeclaringType.GetSchema())
15+
: null;
16+
17+
_getter = BuildGetter(property, complexProperty);
18+
Property = property;
19+
PropertyName = property.Name;
20+
ColumnName = ownerTable == null ? property.GetColumnName() : property.GetColumnName(ownerTable.Value)!;
21+
QuotedColumName = dialect.Quote(ColumnName);
22+
StoreDefinition = GetStoreDefinition(property);
23+
ClrType = property.ClrType;
24+
IsGenerated = property.ValueGenerated != ValueGenerated.Never;
25+
}
26+
27+
private readonly Func<object, object?> _getter;
1228

13-
public IProperty Property { get; } = property;
29+
public IProperty Property { get; }
1430

15-
public string PropertyName { get; } = property.Name;
31+
public string PropertyName { get; }
1632

17-
public string ColumnName { get; } = property.GetColumnName();
33+
public string ColumnName { get; }
1834

19-
public string QuotedColumName { get; } = dialect.Quote(property.GetColumnName());
35+
public string QuotedColumName { get; }
2036

21-
public string StoreDefinition { get; } = GetStoreDefinition(property);
37+
public string StoreDefinition { get; }
2238

23-
public Type ClrType { get; } = property.ClrType;
39+
public Type ClrType { get; }
2440

25-
public bool IsGenerated { get; } = property.ValueGenerated != ValueGenerated.Never;
41+
public bool IsGenerated { get; }
2642

2743
public object GetValue(object entity, BulkInsertOptions options)
2844
{
@@ -43,15 +59,15 @@ public object GetValue(object entity, BulkInsertOptions options)
4359
return result ?? DBNull.Value;
4460
}
4561

46-
private static Func<object, object?> BuildGetter(IProperty property)
62+
private static Func<object, object?> BuildGetter(IProperty property, IComplexProperty? complexProperty)
4763
{
4864
var valueConverter =
4965
property.GetValueConverter() ??
5066
property.GetTypeMapping().Converter;
5167

5268
var propInfo = property.PropertyInfo!;
5369

54-
return PropertyAccessor.CreateGetter(propInfo, valueConverter?.ConvertToProviderExpression);
70+
return PropertyAccessor.CreateGetter(propInfo, complexProperty, valueConverter?.ConvertToProviderExpression);
5571
}
5672

5773
private static string GetStoreDefinition(IProperty property)
Lines changed: 55 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,94 @@
11
using System.Linq.Expressions;
22
using System.Reflection;
33

4+
using Microsoft.EntityFrameworkCore.Metadata;
5+
46
namespace PhenX.EntityFrameworkCore.BulkInsert.Metadata;
57

68
internal static class PropertyAccessor
79
{
8-
public static Func<object, object?> CreateGetter(PropertyInfo propertyInfo, LambdaExpression? converter = null)
10+
public static Func<object, object?> CreateGetter(
11+
PropertyInfo propertyInfo,
12+
IComplexProperty? complexProperty = null,
13+
LambdaExpression? converter = null)
914
{
1015
ArgumentNullException.ThrowIfNull(propertyInfo);
11-
var getMethod = propertyInfo.GetMethod ?? throw new ArgumentException("Property does not have a getter.");
1216

17+
// instance => { }
1318
var instanceParam = Expression.Parameter(typeof(object), "instance");
1419

15-
// Convert object to the declaring type
16-
Expression typedInstance = propertyInfo.DeclaringType!.IsValueType
17-
? Expression.Unbox(instanceParam, propertyInfo.DeclaringType)
18-
: Expression.Convert(instanceParam, propertyInfo.DeclaringType);
20+
Expression body;
21+
22+
if (complexProperty == null)
23+
{
24+
var propDeclaringType = propertyInfo.DeclaringType!;
25+
26+
// Convert object to the declaring type
27+
var typedInstance = GetTypedInstance(propDeclaringType, instanceParam);
28+
29+
// instance => ((TEntity)instance).Property
30+
body = Expression.Property(typedInstance, propertyInfo);
31+
}
32+
else
33+
{
34+
// Nested access: ((TEntity)instance).ComplexProp.Property
35+
var complexPropInfo = complexProperty.PropertyInfo!;
36+
var complexPropDeclaringType = complexPropInfo.DeclaringType!;
37+
38+
var typedInstance = GetTypedInstance(complexPropDeclaringType, instanceParam);
1939

20-
// Call Getter
21-
Expression getterExpression = Expression.Call(typedInstance, getMethod);
40+
// instance => ((TEntity)instance).ComplexProp
41+
Expression complexAccess = Expression.Property(typedInstance, complexPropInfo);
2242

23-
var propertyType = propertyInfo.PropertyType;
43+
// instance => ((TEntity)instance).ComplexProp.Property
44+
body = Expression.Property(complexAccess, propertyInfo);
45+
}
2446

2547
// If the converter is provided, we call it
2648
if (converter != null)
2749
{
2850
// Validate the converter input type matches property type
2951
var converterParamType = converter.Parameters[0].Type;
30-
if (!converterParamType.IsAssignableFrom(propertyType) && !propertyType.IsAssignableFrom(converterParamType))
52+
if (!converterParamType.IsAssignableFrom(body.Type) && !body.Type.IsAssignableFrom(converterParamType))
3153
{
32-
throw new ArgumentException($"Converter input must be assignable from property type ({propertyType} -> {converterParamType})");
54+
throw new ArgumentException($"Converter input must be assignable from property type ({body.Type} -> {converterParamType})");
3355
}
3456

35-
// If property type != converter param, convert
36-
var converterInput = getterExpression;
37-
if (converterParamType != propertyType)
57+
Expression converterInput = body;
58+
if (converterParamType != body.Type)
3859
{
39-
converterInput = Expression.Convert(getterExpression, converterParamType);
60+
// instance => converter((TConverterType)body)
61+
converterInput = Expression.Convert(body, converterParamType);
4062
}
4163

64+
// instance => converter(body)
4265
var invokeConverter = Expression.Invoke(converter, converterInput);
4366

44-
if (propertyType.IsClass)
67+
if (body.Type.IsClass)
4568
{
46-
var nullCondition = Expression.Equal(getterExpression, Expression.Constant(null, propertyType));
47-
var nullResult = Expression.Constant(null, converter.ReturnType);
48-
getterExpression = Expression.Condition(nullCondition, nullResult, invokeConverter);
69+
// instance => body == null ? null : converter(body)
70+
var nullCondition = Expression.Equal(body, Expression.Constant(null, body.Type));
71+
var nullResult = Expression.Constant(null, invokeConverter.Type);
72+
73+
body = Expression.Condition(nullCondition, nullResult, invokeConverter);
4974
}
5075
else
5176
{
52-
getterExpression = invokeConverter;
77+
body = invokeConverter;
5378
}
54-
55-
propertyType = getterExpression.Type;
5679
}
5780

58-
var finalExpression = propertyType.IsValueType
59-
? Expression.Convert(getterExpression, typeof(object))
60-
: getterExpression;
81+
var finalExpression = body.Type.IsValueType
82+
? Expression.Convert(body, typeof(object))
83+
: body;
6184

6285
return Expression.Lambda<Func<object, object?>>(finalExpression, instanceParam).Compile();
6386
}
87+
88+
private static UnaryExpression GetTypedInstance(Type propDeclaringType, ParameterExpression instanceParam)
89+
{
90+
return propDeclaringType.IsValueType
91+
? Expression.Unbox(instanceParam, propDeclaringType)
92+
: Expression.Convert(instanceParam, propDeclaringType);
93+
}
6494
}

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

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -5,53 +5,76 @@
55

66
namespace PhenX.EntityFrameworkCore.BulkInsert.Metadata;
77

8-
internal sealed class TableMetadata(IEntityType entityType, SqlDialectBuilder dialect)
8+
internal sealed class TableMetadata
99
{
10-
private IReadOnlyList<ColumnMetadata>? _notGeneratedColumns;
11-
private IReadOnlyList<ColumnMetadata>? _primaryKeys;
10+
private ColumnMetadata[]? _notGeneratedColumns;
11+
private ColumnMetadata[]? _primaryKeys;
1212

13-
public string QuotedTableName { get; } =
14-
dialect.QuoteTableName(entityType.GetSchema(), entityType.GetTableName()!);
13+
private readonly IEntityType _entityType;
1514

16-
public string TableName { get; } =
17-
entityType.GetTableName() ?? throw new InvalidOperationException("Canot determine table name.");
15+
public string QuotedTableName { get; }
1816

19-
public IReadOnlyList<ColumnMetadata> Columns { get; } =
20-
entityType.GetProperties().Where(p => !p.IsShadowProperty()).Select(x => new ColumnMetadata(x, dialect)).ToList();
17+
public string TableName { get; }
2118

22-
public IReadOnlyList<ColumnMetadata> PrimaryKey =>
23-
_primaryKeys ??= GetPrimaryKey();
19+
private ColumnMetadata[] Columns { get; }
2420

25-
public IReadOnlyList<ColumnMetadata> GetColumns(bool includeGenerated = true)
21+
public TableMetadata(IEntityType entityType, SqlDialectBuilder dialect)
22+
{
23+
_entityType = entityType;
24+
TableName = entityType.GetTableName() ?? throw new InvalidOperationException("Cannot determine table name.");
25+
QuotedTableName = dialect.QuoteTableName(entityType.GetSchema(), TableName);
26+
Columns = GetColumns(entityType, dialect);
27+
}
28+
29+
private static ColumnMetadata[] GetColumns(IEntityType entityType, SqlDialectBuilder dialect)
30+
{
31+
var properties = entityType.GetProperties()
32+
.Where(p => !p.IsShadowProperty())
33+
.Select(x => new ColumnMetadata(x, dialect));
34+
35+
var complexProperties = entityType.GetComplexProperties()
36+
.SelectMany(cp => cp.ComplexType
37+
.GetProperties()
38+
.Where(p => !p.IsShadowProperty())
39+
.Select(x => new ColumnMetadata(x, dialect, cp)));
40+
41+
return properties.Concat(complexProperties).ToArray();
42+
}
43+
44+
public ColumnMetadata[] PrimaryKey => _primaryKeys ??= GetPrimaryKey();
45+
46+
public ColumnMetadata[] GetColumns(bool includeGenerated = true)
2647
{
2748
if (includeGenerated)
2849
{
2950
return Columns;
3051
}
3152

32-
return _notGeneratedColumns ??= Columns.Where(x => !x.IsGenerated).ToList();
53+
return _notGeneratedColumns ??= Columns.Where(x => !x.IsGenerated).ToArray();
3354
}
3455

3556
public string GetQuotedColumnName(string propertyName)
3657
{
3758
var property = Columns.FirstOrDefault(x => x.PropertyName == propertyName)
38-
?? throw new InvalidOperationException($"Property {propertyName} not found in entity type {entityType.Name}.");
59+
?? throw new InvalidOperationException($"Property {propertyName} not found in entity type {_entityType.Name}.");
3960

4061
return property.QuotedColumName;
4162
}
4263

4364
public string GetColumnName(string propertyName)
4465
{
4566
var property = Columns.FirstOrDefault(x => x.PropertyName == propertyName)
46-
?? throw new InvalidOperationException($"Property {propertyName} not found in entity type {entityType.Name}.");
67+
?? throw new InvalidOperationException($"Property {propertyName} not found in entity type {_entityType.Name}.");
4768

4869
return property.ColumnName;
4970
}
5071

51-
private List<ColumnMetadata> GetPrimaryKey()
72+
private ColumnMetadata[] GetPrimaryKey()
5273
{
53-
var primaryKey = entityType.FindPrimaryKey()?.Properties ?? [];
74+
var primaryKey = _entityType.FindPrimaryKey()?.Properties ?? [];
5475

55-
return Columns.Where(x => primaryKey.Any(y => x.PropertyName == y.Name)).ToList();
76+
return Columns
77+
.Where(x => primaryKey.Any(y => x.PropertyName == y.Name))
78+
.ToArray();
5679
}
5780
}

tests/PhenX.EntityFrameworkCore.BulkInsert.Benchmark/GetValueComparator.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ public void IterationSetup()
7676
? expression
7777
: null;
7878

79-
return PropertyAccessor.CreateGetter(propertyInfo, converter);
79+
return PropertyAccessor.CreateGetter(propertyInfo, converter: converter);
8080
})
8181
.ToArray();
8282

tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/JsonDbObject.cs renamed to tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/OwnedObject.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext;
22

3-
public class JsonDbObject
3+
public class OwnedObject
44
{
55
public int Code { get; set; }
66

tests/PhenX.EntityFrameworkCore.BulkInsert.Tests/DbContext/TestDbContext.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ public class TestDbContext : TestDbContextBase
1010
public DbSet<TestEntityWithJson> TestEntitiesWithJson { get; set; } = null!;
1111
public DbSet<TestEntityWithGuidId> TestEntitiesWithGuidId { get; set; } = null!;
1212
public DbSet<TestEntityWithConverters> TestEntitiesWithConverter { get; set; } = null!;
13+
public DbSet<TestEntityWithComplexType> TestEntitiesWithComplexType { get; set; } = null!;
1314

1415
protected override void OnModelCreating(ModelBuilder modelBuilder)
1516
{
@@ -26,6 +27,13 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
2627
builder.Property(e => e.Id)
2728
.ValueGeneratedNever();
2829
});
30+
31+
modelBuilder.Entity<TestEntityWithComplexType>(builder =>
32+
{
33+
builder
34+
.ComplexProperty(e => e.OwnedComplexType)
35+
.IsRequired();
36+
});
2937
}
3038
}
3139

0 commit comments

Comments
 (0)