Search code examples
c#entity-frameworklambdaexpression-treesef-database-first

Assigning a Property to a Lambda Expression inside an Expression Tree


Background Context:

I am working on a database project that builds models from parsed datasets and then merges these models with the database using Entity and the Entity Framework Extensions (for bulk operations). Entity Framework Extensions allow for overriding the primary keys used when performing the merge/insert/etc by providing a delegate that specifies an anonymous type to use where the anonymous type only has the properties to use for the primary key.

Example:

context.BulkMerge<T>(IEnumerable<T>,options => options.ColumnPrimaryKeyExpression = x => new { x.Id, x.Name });

I have a function Expression<Func<T, object>> GenerateAnonTypeExpression<T>(this IEnumerable<PropertyInfo> fields) which I use to generate this anonymous type lambda as an Expression<Func<T, object>> that I can pass to the BulkMerge as the column primary key expression like so:

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;
        });
    }
}

This works correctly without any issues. However, this only works for entities of a single table and does not merge child entities, which results in having to call BulkMergeCollection() for all entities and all child entities on a type-by-type basis (typically around 12-13 calls at the moment). While this is usually doable, I have started working in a Database First context rather than Code First, meaning that bridge tables do not exist in the .edmx as models that can be instantiated and instead only exist as child entities of the left and right entities. This means that when using BulkMerge in this way, my bridge tables are not getting populated (even though the left and right entities have values for the child entities).

Fortunately, Entity Framework Extensions also has an Include Graph option that allows for child entities to be merged along with parent entities when calling BulkMerge(). This requires that the column primary key option be specified for all types that are being merged though.

Example for Person and Address models with a Many:Many relationship

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
                    };
                }
            }
        }
    }
}

I have tested this out with the operations being both hardcoded (as above) and with the individual bulk.ColumnPrimaryKeyExpressions being generated by GenerateAnonTypeExpression<T>(); both methods work correctly and successfully add/merge items with the bridge table.

Problem:

What I am trying to do is to build the Entire body of the IncludeGraphOperationBuilder option as a Lambda Expression Tree (similar to how I am handling the Column Primary Key Expressions). This is necessary as the IncludeGraphOperationBuilder will have different BulkOperation<...> sections depending on the base model type being merged. Additionally, I'd just like the body of the IncludeGraphOperationBuilder to be dynamically generated so that I do not need to add new sections every time the backing database model changes. My issue is that, when trying to generate the expressions for the body of a given if(operation is BulkOperation<T>) block, when I try to assign the Lambda Expression created by the GenerateAnonTypeExpression<T>() method to the Member Expression representing the bulk.ColumnPrimaryKeyExpression, I get an Argument Exception of

Expression of type System.Func`2[T, Object] cannot be used for assignment to type System.Linq.Expressions.Expression`1[System.Func`2[T, Object]]

I do not know how this is occurring as the return of GenerateAnonTypeExpression<T>() is explicitly of type Expression<Func<T, object>> and not of Func<T, object>, so I do not understand where the Func<T, object> that is trying to be used in the assignment is coming from.

Here is the full code where the failure is occuring: Note that IModelItem is an interface that that allows for retrieval of the Unique Identification properties from the model via reflection

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
        );
}

Everything I've tried just results in the same error and I cannot find any documentation online regarding that particular error. Stepping through the code while debugging shows that keyProperties is of type Expression<Func<T, object>> before the failing line and casting it in that line does not change the result. Neither does using Expression.Member instead of Expression.Property.

I am at a bit of a loss here and clearly do not understand Expressions well enough. Can someone explain what I am doing wrong?


Solution

  • Answering for future readers and my own reference. Answer is from @Svayatoslav Danyliv's comments to original question.

    The trick is to wrap the original returned Lambda expression (keyProperties) in an Expression.Constant() call, which tells the system to use the original Lambda as-is (an expression) rather than trying to interpret it as part of the assignment.

    After updating the failing line to:

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

    the assignment works as intended and no longer throws the exception.