Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,8 @@ await dbContext.ExecuteBulkInsertAsync(entities, onConflict: new OnConflictOptio
## Roadmap

- [ ] [Add support for navigation properties](https://github.com/PhenX/PhenX.EntityFrameworkCore.BulkInsert/issues/2)
- [ ] [Add support for complex types](https://github.com/PhenX/PhenX.EntityFrameworkCore.BulkInsert/issues/3)
- [ ] Add support for owned types
- [x] [Add support for complex types](https://github.com/PhenX/PhenX.EntityFrameworkCore.BulkInsert/issues/3)
- [x] Add support for owned types
- [ ] Add support for shadow properties
- [ ] Add support for TPT (Table Per Type) inheritance
- [ ] Add support for TPC (Table Per Concrete Type) inheritance
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ public override string BuildMoveDataSql<T>(
{
matchColumns = GetColumns(target, onConflictTyped.Match);
}
else if (target.PrimaryKey.Count > 0)
else if (target.PrimaryKey.Length > 0)
{
matchColumns = target.PrimaryKey.Select(x => x.QuotedColumName);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ protected override void AppendConflictMatch<T>(StringBuilder sql, TableMetadata
{
base.AppendConflictMatch(sql, target, conflict);
}
else if (target.PrimaryKey.Count > 0)
else if (target.PrimaryKey.Length > 0)
{
sql.Append(' ');
sql.AppendLine("(");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ public override string BuildMoveDataSql<T>(
{
matchColumns = GetColumns(target, onConflictTyped.Match);
}
else if (target.PrimaryKey.Count > 0)
else if (target.PrimaryKey.Length > 0)
{
matchColumns = target.PrimaryKey.Select(x => x.QuotedColumName);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,39 @@

namespace PhenX.EntityFrameworkCore.BulkInsert.Metadata;

internal sealed class ColumnMetadata(IProperty property, SqlDialectBuilder dialect)
internal sealed class ColumnMetadata
{
private readonly Func<object, object?> _getter = BuildGetter(property);
public ColumnMetadata(IProperty property, SqlDialectBuilder dialect, IComplexProperty? complexProperty = null)
{
StoreObjectIdentifier? ownerTable = complexProperty != null
? StoreObjectIdentifier.Table(complexProperty.DeclaringType.GetTableName()!, complexProperty.DeclaringType.GetSchema())
: null;

_getter = BuildGetter(property, complexProperty);
Property = property;
PropertyName = property.Name;
ColumnName = ownerTable == null ? property.GetColumnName() : property.GetColumnName(ownerTable.Value)!;
QuotedColumName = dialect.Quote(ColumnName);
StoreDefinition = GetStoreDefinition(property);
ClrType = property.ClrType;
IsGenerated = property.ValueGenerated != ValueGenerated.Never;
}

private readonly Func<object, object?> _getter;

public IProperty Property { get; } = property;
public IProperty Property { get; }

public string PropertyName { get; } = property.Name;
public string PropertyName { get; }

public string ColumnName { get; } = property.GetColumnName();
public string ColumnName { get; }

public string QuotedColumName { get; } = dialect.Quote(property.GetColumnName());
public string QuotedColumName { get; }

public string StoreDefinition { get; } = GetStoreDefinition(property);
public string StoreDefinition { get; }

public Type ClrType { get; } = property.ClrType;
public Type ClrType { get; }

public bool IsGenerated { get; } = property.ValueGenerated != ValueGenerated.Never;
public bool IsGenerated { get; }

public object GetValue(object entity, BulkInsertOptions options)
{
Expand All @@ -43,15 +59,15 @@ public object GetValue(object entity, BulkInsertOptions options)
return result ?? DBNull.Value;
}

private static Func<object, object?> BuildGetter(IProperty property)
private static Func<object, object?> BuildGetter(IProperty property, IComplexProperty? complexProperty)
{
var valueConverter =
property.GetValueConverter() ??
property.GetTypeMapping().Converter;

var propInfo = property.PropertyInfo!;

return PropertyAccessor.CreateGetter(propInfo, valueConverter?.ConvertToProviderExpression);
return PropertyAccessor.CreateGetter(propInfo, complexProperty, valueConverter?.ConvertToProviderExpression);
}

private static string GetStoreDefinition(IProperty property)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,64 +1,94 @@
using System.Linq.Expressions;
using System.Reflection;

using Microsoft.EntityFrameworkCore.Metadata;

namespace PhenX.EntityFrameworkCore.BulkInsert.Metadata;

internal static class PropertyAccessor
{
public static Func<object, object?> CreateGetter(PropertyInfo propertyInfo, LambdaExpression? converter = null)
public static Func<object, object?> CreateGetter(
PropertyInfo propertyInfo,
IComplexProperty? complexProperty = null,
LambdaExpression? converter = null)
{
ArgumentNullException.ThrowIfNull(propertyInfo);
var getMethod = propertyInfo.GetMethod ?? throw new ArgumentException("Property does not have a getter.");

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

// Convert object to the declaring type
Expression typedInstance = propertyInfo.DeclaringType!.IsValueType
? Expression.Unbox(instanceParam, propertyInfo.DeclaringType)
: Expression.Convert(instanceParam, propertyInfo.DeclaringType);
Expression body;

if (complexProperty == null)
{
var propDeclaringType = propertyInfo.DeclaringType!;

// Convert object to the declaring type
var typedInstance = GetTypedInstance(propDeclaringType, instanceParam);

// instance => ((TEntity)instance).Property
body = Expression.Property(typedInstance, propertyInfo);
}
else
{
// Nested access: ((TEntity)instance).ComplexProp.Property
var complexPropInfo = complexProperty.PropertyInfo!;
var complexPropDeclaringType = complexPropInfo.DeclaringType!;

var typedInstance = GetTypedInstance(complexPropDeclaringType, instanceParam);

// Call Getter
Expression getterExpression = Expression.Call(typedInstance, getMethod);
// instance => ((TEntity)instance).ComplexProp
Expression complexAccess = Expression.Property(typedInstance, complexPropInfo);

var propertyType = propertyInfo.PropertyType;
// instance => ((TEntity)instance).ComplexProp.Property
body = Expression.Property(complexAccess, propertyInfo);
}

// If the converter is provided, we call it
if (converter != null)
{
// Validate the converter input type matches property type
var converterParamType = converter.Parameters[0].Type;
if (!converterParamType.IsAssignableFrom(propertyType) && !propertyType.IsAssignableFrom(converterParamType))
if (!converterParamType.IsAssignableFrom(body.Type) && !body.Type.IsAssignableFrom(converterParamType))
{
throw new ArgumentException($"Converter input must be assignable from property type ({propertyType} -> {converterParamType})");
throw new ArgumentException($"Converter input must be assignable from property type ({body.Type} -> {converterParamType})");
}

// If property type != converter param, convert
var converterInput = getterExpression;
if (converterParamType != propertyType)
Expression converterInput = body;
if (converterParamType != body.Type)
{
converterInput = Expression.Convert(getterExpression, converterParamType);
// instance => converter((TConverterType)body)
converterInput = Expression.Convert(body, converterParamType);
}

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

if (propertyType.IsClass)
if (body.Type.IsClass)
{
var nullCondition = Expression.Equal(getterExpression, Expression.Constant(null, propertyType));
var nullResult = Expression.Constant(null, converter.ReturnType);
getterExpression = Expression.Condition(nullCondition, nullResult, invokeConverter);
// instance => body == null ? null : converter(body)
var nullCondition = Expression.Equal(body, Expression.Constant(null, body.Type));
var nullResult = Expression.Constant(null, invokeConverter.Type);

body = Expression.Condition(nullCondition, nullResult, invokeConverter);
}
else
{
getterExpression = invokeConverter;
body = invokeConverter;
}

propertyType = getterExpression.Type;
}

var finalExpression = propertyType.IsValueType
? Expression.Convert(getterExpression, typeof(object))
: getterExpression;
var finalExpression = body.Type.IsValueType
? Expression.Convert(body, typeof(object))
: body;

return Expression.Lambda<Func<object, object?>>(finalExpression, instanceParam).Compile();
}

private static UnaryExpression GetTypedInstance(Type propDeclaringType, ParameterExpression instanceParam)
{
return propDeclaringType.IsValueType
? Expression.Unbox(instanceParam, propDeclaringType)
: Expression.Convert(instanceParam, propDeclaringType);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,53 +5,76 @@

namespace PhenX.EntityFrameworkCore.BulkInsert.Metadata;

internal sealed class TableMetadata(IEntityType entityType, SqlDialectBuilder dialect)
internal sealed class TableMetadata
{
private IReadOnlyList<ColumnMetadata>? _notGeneratedColumns;
private IReadOnlyList<ColumnMetadata>? _primaryKeys;
private ColumnMetadata[]? _notGeneratedColumns;
private ColumnMetadata[]? _primaryKeys;

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

public string TableName { get; } =
entityType.GetTableName() ?? throw new InvalidOperationException("Canot determine table name.");
public string QuotedTableName { get; }

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

public IReadOnlyList<ColumnMetadata> PrimaryKey =>
_primaryKeys ??= GetPrimaryKey();
private ColumnMetadata[] Columns { get; }

public IReadOnlyList<ColumnMetadata> GetColumns(bool includeGenerated = true)
public TableMetadata(IEntityType entityType, SqlDialectBuilder dialect)
{
_entityType = entityType;
TableName = entityType.GetTableName() ?? throw new InvalidOperationException("Cannot determine table name.");
QuotedTableName = dialect.QuoteTableName(entityType.GetSchema(), TableName);
Columns = GetColumns(entityType, dialect);
}

private static ColumnMetadata[] GetColumns(IEntityType entityType, SqlDialectBuilder dialect)
{
var properties = entityType.GetProperties()
.Where(p => !p.IsShadowProperty())
.Select(x => new ColumnMetadata(x, dialect));

var complexProperties = entityType.GetComplexProperties()
.SelectMany(cp => cp.ComplexType
.GetProperties()
.Where(p => !p.IsShadowProperty())
.Select(x => new ColumnMetadata(x, dialect, cp)));

return properties.Concat(complexProperties).ToArray();
}

public ColumnMetadata[] PrimaryKey => _primaryKeys ??= GetPrimaryKey();

public ColumnMetadata[] GetColumns(bool includeGenerated = true)
{
if (includeGenerated)
{
return Columns;
}

return _notGeneratedColumns ??= Columns.Where(x => !x.IsGenerated).ToList();
return _notGeneratedColumns ??= Columns.Where(x => !x.IsGenerated).ToArray();
}

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

return property.QuotedColumName;
}

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

return property.ColumnName;
}

private List<ColumnMetadata> GetPrimaryKey()
private ColumnMetadata[] GetPrimaryKey()
{
var primaryKey = entityType.FindPrimaryKey()?.Properties ?? [];
var primaryKey = _entityType.FindPrimaryKey()?.Properties ?? [];

return Columns.Where(x => primaryKey.Any(y => x.PropertyName == y.Name)).ToList();
return Columns
.Where(x => primaryKey.Any(y => x.PropertyName == y.Name))
.ToArray();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ public void IterationSetup()
? expression
: null;

return PropertyAccessor.CreateGetter(propertyInfo, converter);
return PropertyAccessor.CreateGetter(propertyInfo, converter: converter);
})
.ToArray();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext;

public class JsonDbObject
public class OwnedObject
{
public int Code { get; set; }

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public class TestDbContext : TestDbContextBase
public DbSet<TestEntityWithJson> TestEntitiesWithJson { get; set; } = null!;
public DbSet<TestEntityWithGuidId> TestEntitiesWithGuidId { get; set; } = null!;
public DbSet<TestEntityWithConverters> TestEntitiesWithConverter { get; set; } = null!;
public DbSet<TestEntityWithComplexType> TestEntitiesWithComplexType { get; set; } = null!;

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
Expand All @@ -26,6 +27,13 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
builder.Property(e => e.Id)
.ValueGeneratedNever();
});

modelBuilder.Entity<TestEntityWithComplexType>(builder =>
{
builder
.ComplexProperty(e => e.OwnedComplexType)
.IsRequired();
});
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;

namespace PhenX.EntityFrameworkCore.BulkInsert.Tests.DbContext;

[Table("test_entity_complex_type")]
public class TestEntityWithComplexType : TestEntityBase
{
[Key]
public int Id { get; set; }

public OwnedObject OwnedComplexType { get; set; } = null!;
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ public class TestEntityWithJson : TestEntityBase

public List<int> JsonArray { get; set; } = [];

public JsonDbObject? JsonObject { get; set; }
public OwnedObject? JsonObject { get; set; }
}
Loading