【发布时间】:2022-10-05 23:23:18
【问题描述】:
背景背景:
我正在开发一个数据库项目,该项目从解析的数据集构建模型,然后使用实体和实体框架扩展(用于批量操作)将这些模型与数据库合并。实体框架扩展允许覆盖执行合并/插入/等时使用的主键,方法是提供一个指定要使用的匿名类型的委托,其中匿名类型仅具有用于主键的属性。
例子:
context.BulkMerge<T>(IEnumerable<T>,options => options.ColumnPrimaryKeyExpression = x => new { x.Id, x.Name });
我有一个函数Expression<Func<T, object>> GenerateAnonTypeExpression<T>(this IEnumerable<PropertyInfo> fields),我用它来生成这个匿名类型 lambda 作为Expression<Func<T, object>>,我可以将它作为列主键表达式传递给 BulkMerge,如下所示:
void BulkMergeCollection<T>(IEnumerable<T> entities)
{
IEnumerable<PropertyInfo> keyProperties = entites.GetKeyProperties() //Returns a list of PropertyInfos from type T to use as the primary key for entites of type T
var KeyPropertiesExpression = keyProperties.GenerateAnonTypeExpression<T>()
using (var ctx = new Context())
{
ctx.BulkMerge(entities, options =>
{
options.ColumnPrimaryKeyExpression = keyPropertiesExpression;
});
}
}
这可以正常工作,没有任何问题。但是,这仅适用于单个表的实体并且不会合并子实体,这导致必须逐个类型地为所有实体和所有子实体调用 BulkMergeCollection()(通常在 12-13 次左右调用此时此刻)。虽然这通常是可行的,但我已经开始在 Database First 上下文而不是 Code First 环境中工作,这意味着 .edmx 中不存在桥表作为可以实例化的模型,而是仅作为左右实体的子实体存在.这意味着当以这种方式使用 BulkMerge 时,我的桥接表不会被填充(即使左右实体具有子实体的值)。
幸运的是,Entity Framework Extensions 还有一个 Include Graph 选项,允许在调用BulkMerge() 时将子实体与父实体合并。这要求为所有正在合并的类型指定列主键选项。
具有 Many:Many 关系的 Person 和 Address 模型示例
public class Person
{
public Person()
{
this.Addresses = new HashSet<Address>();
}
public int Id { get; set; } //Identity column assigned by the database
public string FirstName { get; set; }
public string LastName { get; set; }
public string SSN { get; set; } //Unique identifier.
public virtual ICollection<Address> Addresses { get; set; }
}
public class Address
{
public Address()
{
this.Inhabitants = new HashSet<Person>();
}
public int Id { get; set; } //Identity column assigned by the database
public int HouseNumber { get; set; } //Unique Identifier with StreetName and Zip
public string StreetName { get; set; } //Unique Identifier with HouseNumber and Zip
public string Unit { get; set; }
public int Zipcode { get; set; } //Unique Identifier with StreetName and HouseNumber
public virtual ICollection<Person> Inhabitants { get; set; }
}
public void TestFunction()
{
Address home = new Address()
{
HouseNumber = 1234;
StreetName = \"1st St\";
Zipcode = 99999;
}
Person john = new Person()
{
FirstName = \"John\";
LastName = \"Smith\";
SSN = 123456789;
}
john.Addresses.Add(home);
IEnumerable<Person> persons = new List<Person>() { john };
BulkMergePersonsAndAddresses(persons);
}
public void BulkMergePersonsAndAddresses(IEnumerable<Person> persons)
{
using (var ctx = new Context())
{
BulkMerge(persons, options =>
{
options.IncludeGraph = true;
options.IncludeGraphOperationBuilder = operation =>
{
if(operation is BulkOperation<Person>)
{
var bulk = (BulkOperation<Person>)operation;
bulk.ColumnPrimaryKeyExpression = x => new { x.SSN };
}
else if(operation is BulkOperation<Address>)
{
var bulk = (BulkOperation<Address>)operation;
bulk.ColumnPrimaryKeyExpression = x => new
{
x.HouseNumber,
x.StreetName,
x.Zipcode
};
}
}
}
}
}
我已经通过硬编码(如上)和GenerateAnonTypeExpression<T>() 生成的单个bulk.ColumnPrimaryKeyExpressions 操作测试了这一点;这两种方法都可以正常工作并成功地使用桥接表添加/合并项目。
问题:
我想做的是建立全部的IncludeGraphOperationBuilder 选项的主体作为 Lambda 表达式树(类似于我处理列主键表达式的方式)。这是必要的,因为 IncludeGraphOperationBuilder 将具有不同的 BulkOperation<...> 部分,具体取决于要合并的基本模型类型。此外,我希望动态生成IncludeGraphOperationBuilder 的主体,这样我就不需要在每次支持数据库模型更改时添加新部分。我的问题是,当我尝试为给定 if(operation is BulkOperation<T>) 块的主体生成表达式时,当我尝试将 GenerateAnonTypeExpression<T>() 方法创建的 Lambda 表达式分配给代表 bulk.ColumnPrimaryKeyExpression 的成员表达式时,我得到一个参数例外
System.Func`2[T, Object]类型的表达式不能用于分配给System.Linq.Expressions.Expression`1[System.Func`2[T, Object]]类型我不知道这是怎么发生的,因为
GenerateAnonTypeExpression<T>()的返回明确属于Expression<Func<T, object>>类型而不是Func<T, object>,所以我不明白试图在分配中使用的Func<T, object>将在哪里出现从。这是发生故障的完整代码:请注意,IModelItem 是一个接口,允许通过反射从模型中检索唯一标识属性
public static void GenerateAnonTypeGraphExpression<T>(this IEnumerable<T> models) where T : class, IModelItem { Expression<Func<T, object> keyProperties = models.GetUniqueIdProperties().GenerateAnonTypeExpression<T>(); ParameterExpression bulkVar = Expression.Variable(typeof(BulkOperaton<T>), \"bulk\"); //Creates the variable \"var bulk\" ParameterExpression operation = Expression.Parameter(typeof(BulkOperation), \"operation\"); var castExpression = Expression.Convert(operation, typeof(BulkOperation<T>)); //\"(BulkOperation<T>) operation\" var bulkLineExpression = Expression.Assign(bulkVar, castExpression); //\"var bulk = (BulkOperation<T>) operation\" var columnPrimaryKeyProperty = typeof(BulkOperation<T>).GetProperties().Where(p => p.Name == \"ColumnPrimaryKeyExpression\").FirstOrDefault(); var colmunPrimaryKeyPropertyExpression = Expression.Property(bulkVar, columnPrimaryKeyProperty); //\"bulk.ColumnPrimaryKeyExpression\" //The next line is where it blows up with the above Argument Exception var colmunPrimaryKeyPropertyAssignmentExpression = Expression.Assign(columnPrimaryKeyPropertyExpression, keyProperties); //\"bulk.ColumnPrimayKeyExpression = keyProperties\" var bodyExpression = Expression.Block( bulkLineExpression, columnPrimaryKeyPropertyAssignmentExpression ); }我尝试过的所有操作都会导致相同的错误,并且我无法在线找到有关该特定错误的任何文档。在调试时单步执行代码表明
keyProperties在失败行之前属于Expression<Func<T, object>>类型,并且在该行中强制转换不会改变结果。也没有使用Expression.Member代替Expression.Property。我在这里有点茫然,显然对表达式的理解不够好。有人可以解释我做错了什么吗?
-
试试
Expression.Assign(columnPrimaryKeyPropertyExpression, Expression.Constant(keyProperties)) -
@SvyatoslavDanyliv 这正是我需要做的,谢谢!不过,我对为什么必须让表达式成为常量感到有些困惑。
-
因为
LambdaExpression.Compile会将你的keyProperties编译成函数。但是Expression.Constant告诉我们必须按原样使用keyProperties作为表达式。 -
@SvyatoslavDanyliv 即使
LambdaExpression.Compile从未被调用(初始返回只是调用Expression.Lambda()但从未编译它)?无论哪种情况,感谢您的解释!我将使用它为这个问题创建一个答案并将其标记为已接受。
标签: c# entity-framework lambda expression-trees ef-database-first