Search code examples
c#entity-framework.net-corelambdaexpression

How to pass multiple values to SetPropertyCalls for ExecuteUpdate in EF 7


I have the following query:

           IQueryable<Location> query = unitOfWork.LocationRepository.Query()
                .Where(x => x.ID == "{some-id}");

I want to update the Location properties with values:

            List<(Func<Location, object>, object)> parameters = new List<(Func<Location, object>, object)>
            {
                (x => x.Name, "New Name"), // string
                (x => x.Date, DateTime.Now) // DateTime
            };

How to build SetProperty dynamically from the list above?

            query.ExecuteUpdate(sett => sett.SetProperty(... here??? ...));

I started with simple:

Func<Location, string> func = x => x.Name;

            query.ExecuteUpdate(sett => sett.SetProperty(func, "abc123"));

But this fails and I get System.InvalidCastException: 'Unable to cast object of type 'System.Linq.Expressions.TypedParameterExpression' to type 'System.Linq.Expressions.LambdaExpression'.'

When I use lambda directly:


// This works
query.ExecuteUpdate(sett => sett.SetProperty(x => x.Name, "abc123"));

So how to pass multiple Func<> to the SetProperty?

Update 7/7: Context added:

I am creating an extension method to improve the performance of the app. Instead of loading entity, changing single property and saving it I want to set property value and save it quickly with EF7 ExecuteUpdate(). There is some logic implemented in DbContext.SaveChanges() which updates the entity when saving (setting current user name, last updated time, ...) and I want the same for ExecuteUpdate(). I expect several properties to be saved in the same time (max. 5 to have it efficient), that's why I need a List.

public static async Task<int> ExecuteUpdateCustom<T, V>(this IQueryable<T> source,
    List<(Func<T, V> propertyExpression, V value)> propertiesAndValuesSet) where T : class
{
    if (source is IHasUserDetails)
    {
        List<(Func<T, V>, V)> parameters = new List<(Func<T, V>, V)>
        {
            (x => x.UpdatedBy, getCurrentUser()),
            (x => x.ModifiedTime, DateTime.UtcNow)
        };

        // Chain
        propertiesAndValuesSet.AddRange(parameters);
    }

    return await source.ExecuteUpdateAsync(setters => setters.SetProperty(...propertiesAndValuesSet...));
}

Solution

  • You can declare the list like this

    List<Expression<Func<SetPropertyCalls<TSource>, SetPropertyCalls<TSource>>>> parameters = new()
    {
        { sett => sett.SetProperty(x => x.Name, "New Name") }, // string
        { sett => sett.SetProperty(x => x.Date, DateTime.Now) }, // DateTime
    };
    

    Combining them is a little tricky. you essentially need to manufacture a new Expression for each of those, which does return firstExpression(secondExpression);

    I think the following code should work:

    Expression<Func<SetPropertyCalls<TSource>, SetPropertyCalls<TSource>>> CombineSetters(
        IEnumerable<Expression<Func<SetPropertyCalls<TSource>, SetPropertyCalls<TSource>>>> setters
    )
    {
        Expression<Func<SetPropertyCalls<TSource>, SetPropertyCalls<TSource>>> expr = sett => sett;
    
        foreach (var expr2 in setters)
        {
            var call = (MethodCallExpression)expr2.Body;
            expr = Expression.Lambda<Func<SetPropertyCalls<TSource>, SetPropertyCalls<TSource>>>(
                Expression.Call(expr.Body, call.Method, call.Arguments),
                expr2.Parameters
            );
        }
    
        return expr;
    }
    

    You can then do

    await source.ExecuteUpdateAsync(finalExpression);