背景上下文:
我正在从事一个数据库项目,该项目从解析的数据集构建模型,然后使用实体和实体框架扩展(用于批量操作(将这些模型与数据库合并。实体框架扩展允许通过提供指定要使用的匿名类型的委托来覆盖执行合并/插入等时使用的主键,其中匿名类型只有用于主键的属性。
示例:
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次左右(。虽然这通常是可行的,但我已经开始在数据库优先上下文而不是代码优先上下文中工作,这意味着桥接表在.edmx中不作为可以实例化的模型存在,而只作为左右实体的子实体存在。这意味着,当以这种方式使用BulkMerge时,我的桥接表不会被填充(即使左右实体都有子实体的值(。
幸运的是,实体框架扩展还有一个包含图选项,允许在调用BulkMerge()
时将子实体与父实体合并。这要求为正在合并的所有类型指定列主键选项。
具有多对多关系的人员和地址模型示例
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.ColumnPrimaryKeyExpression
测试了这一点;这两种方法都能正常工作,并成功地将项添加/合并到桥接表中。
问题:
我要做的是将IncludeGraphOperationBuilder
选项的整个主体构建为Lambda表达式树(类似于我处理列主键表达式的方式(。这是必要的,因为IncludeGraphOperationBuilder
将根据要合并的基本模型类型具有不同的BulkOperation<...>
部分。此外,我只希望IncludeGraphOperationBuilder
的主体是动态生成的,这样我就不需要每次后台数据库模型更改时都添加新的部分。我的问题是,当我试图为给定if(operation is BulkOperation<T>)
块的主体生成表达式时,当我尝试将GenerateAnonTypeExpression<T>()
方法创建的Lambda表达式分配给表示bulk.ColumnPrimaryKeyExpression
的Member表达式时,我得到的Argument Exception
类型
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
也不行。
我在这里有点不知所措,显然对表达式理解得不够好。有人能解释一下我做错了什么吗?
为未来的读者和我自己的参考答案。答案来自@Svayatoslav Danyliv对原问题的评论。
技巧是将原始返回的Lambda表达式(keyProperties
(包装在Expression.Constant()
调用中,这告诉系统使用原始Lambda as-is(一个表达式(,而不是试图将其解释为赋值的一部分。
将故障线路更新为:
var colmunPrimaryKeyPropertyAssignmentExpression =
Expression.Assign(columnPrimaryKeyPropertyExpression, Expression.Constant(keyProperties));
赋值按预期工作,不再抛出异常。