【问题标题】:Assigning a Property to a Lambda Expression inside an Expression Tree将属性分配给表达式树中的 Lambda 表达式
【发布时间】: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&lt;T&gt;() 生成的单个bulk.ColumnPrimaryKeyExpressions 操作测试了这一点;这两种方法都可以正常工作并成功地使用桥接表添加/合并项目。

问题:

我想做的是建立全部的IncludeGraphOperationBuilder 选项的主体作为 Lambda 表达式树(类似于我处理列主键表达式的方式)。这是必要的,因为 IncludeGraphOperationBuilder 将具有不同的 BulkOperation&lt;...&gt; 部分,具体取决于要合并的基本模型类型。此外,我希望动态生成IncludeGraphOperationBuilder 的主体,这样我就不需要在每次支持数据库模型更改时添加新部分。我的问题是,当我尝试为给定 if(operation is BulkOperation&lt;T&gt;) 块的主体生成表达式时,当我尝试将 GenerateAnonTypeExpression&lt;T&gt;() 方法创建的 Lambda 表达式分配给代表 bulk.ColumnPrimaryKeyExpression 的成员表达式时,我得到一个参数例外

System.Func`2[T, Object] 类型的表达式不能用于分配给 System.Linq.Expressions.Expression`1[System.Func`2[T, Object]] 类型

我不知道这是怎么发生的,因为GenerateAnonTypeExpression&lt;T&gt;() 的返回明确属于Expression&lt;Func&lt;T, object&gt;&gt; 类型而不是Func&lt;T, object&gt;,所以我不明白试图在分配中使用的Func&lt;T, object&gt; 将在哪里出现从。

这是发生故障的完整代码:请注意,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&lt;Func&lt;T, object&gt;&gt; 类型,并且在该行中强制转换不会改变结果。也没有使用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


【解决方案1】:

回答未来的读者和我自己的参考。答案来自@Svayatoslav Danyliv 的 cmets 到原始问题。

诀窍是将原始返回的 Lambda 表达式 (keyProperties) 包装在 Expression.Constant() 调用中,这告诉系统按原样使用原始 Lambda(表达式),而不是试图将其解释为赋值的一部分。

将失败的行更新为:

var colmunPrimaryKeyPropertyAssignmentExpression = 
    Expression.Assign(columnPrimaryKeyPropertyExpression, Expression.Constant(keyProperties));

分配按预期工作,不再引发异常。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-01-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多