Stanislav Gordenko
Stanislav Gordenko

Reputation: 31

linq expression for ExecuteUpdateAsync

I have found ExecuteDeleteAsync and ExecuteUpdateAsync in EF Core 7 with great enthusiasm. They help to make my code much simpler and faster. There is no need to use self-made procedures for batch delete or update of 1-2 fields. Anyway I have situations when the exact table and field of database for update should be selected in run time.

I can use the database table:

public static IQueryable<object> Set(this DbContext context, Type entity) =>
        context.ClrQuery(context.ClrType(entity));

I have the method to make expression to filter rows:

public static IQueryable Where(this IQueryable source, string equalProperty, object value, [NotNull] Type EntityType)
{
    PropertyInfo? property = EntityType.GetProperty(equalProperty);
    if (property == null)
        throw new NotImplementedException($"Type {EntityType.Name} does not contain property {equalProperty}");

    ParameterExpression parameter = Expression.Parameter(EntityType, "r");
    MemberExpression member = Expression.MakeMemberAccess(parameter, property);
    LambdaExpression whereExpression = Expression.Lambda(Expression.Equal(member, Expression.Constant(value, property.PropertyType)), parameter);
    MethodCallExpression resultExpression = WhereCall(source, whereExpression);
    return source.Provider.CreateQuery(resultExpression);
}

So I can find the rows to make update using

IQueryable Source = db.Set(EntityType).Where(FieldName, FieldValue, EntityType);
            

I should make expression to update IQueryable ExecuteUpdateQuery = Source.ExecuteUpdateAsync(EntityType, FieldName, FieldValue);

What is the way to access to expression for SetProperty?

Upvotes: 3

Views: 6515

Answers (1)

Svyatoslav Danyliv
Svyatoslav Danyliv

Reputation: 27366

Try the following extensions. I have also corrected method signature:

var affected = anyQuery.ExecuteUpdate(FieldName, FieldValue);

var affected = await anyQuery.ExecuteUpdateAsync(FieldName, FieldValue, cancellationToken);

Updated version also supports multiple fields for update:

var affected = anyQuery.ExecuteUpdate(new Dictionary<string, object?> 
{ 
    { FieldName1, FieldValue2 }, 
    { FieldName2, FieldValue2 } 
});

And implementation:

public static class DynamicRelationalExtensions
{
    static MethodInfo UpdateMethodInfo =
        typeof(RelationalQueryableExtensions).GetMethod(nameof(RelationalQueryableExtensions.ExecuteUpdate));

    static MethodInfo UpdateAsyncMethodInfo =
        typeof(RelationalQueryableExtensions).GetMethod(nameof(RelationalQueryableExtensions.ExecuteUpdateAsync));

    public static int ExecuteUpdate(this IQueryable query, string fieldName, object? fieldValue)
    {
        var updateBody = BuildUpdateBody(query.ElementType,
            new Dictionary<string, object?> { { fieldName, fieldValue } });

        return (int)UpdateMethodInfo.MakeGenericMethod(query.ElementType).Invoke(null, new object?[] { query, updateBody });
    }

    public static Task<int> ExecuteUpdateAsync(this IQueryable query, string fieldName, object? fieldValue, CancellationToken cancellationToken = default)
    {
        var updateBody = BuildUpdateBody(query.ElementType,
            new Dictionary<string, object?> { { fieldName, fieldValue } });

        return (Task<int>)UpdateAsyncMethodInfo.MakeGenericMethod(query.ElementType).Invoke(null, new object?[] { query, updateBody, cancellationToken })!;
    }

    public static int ExecuteUpdate(this IQueryable query, IReadOnlyDictionary<string, object?> fieldValues)
    {
        var updateBody = BuildUpdateBody(query.ElementType, fieldValues);

        return (int)UpdateMethodInfo.MakeGenericMethod(query.ElementType).Invoke(null, new object?[] { query, updateBody });
    }

    public static Task<int> ExecuteUpdateAsync(this IQueryable query, IReadOnlyDictionary<string, object?> fieldValues, CancellationToken cancellationToken = default)
    {
        var updateBody = BuildUpdateBody(query.ElementType, fieldValues);

        return (Task<int>)UpdateAsyncMethodInfo.MakeGenericMethod(query.ElementType).Invoke(null, new object?[] { query, updateBody, cancellationToken })!;
    }

    static LambdaExpression BuildUpdateBody(Type entityType, IReadOnlyDictionary<string, object?> fieldValues)
    {
        var setParam = Expression.Parameter(typeof(SetPropertyCalls<>).MakeGenericType(entityType), "s");
        var objParam = Expression.Parameter(entityType, "e");

        Expression setBody = setParam;

        foreach (var pair in fieldValues)
        {
            var propExpression = Expression.PropertyOrField(objParam, pair.Key);
            var valueExpression = ValueForType(propExpression.Type, pair.Value);

            // s.SetProperty(e => e.SomeField, value)
            setBody = Expression.Call(setBody, nameof(SetPropertyCalls<object>.SetProperty),
                new[] { propExpression.Type }, Expression.Lambda(propExpression, objParam), valueExpression);

        }

        // s => s.SetProperty(e => e.SomeField, value)
        var updateBody = Expression.Lambda(setBody, setParam);

        return updateBody;
    }

    static Expression ValueForType(Type desiredType, object? value)
    {
        if (value == null)
        {
            return Expression.Default(desiredType);
        }

        if (value.GetType() != desiredType)
        {
            return Expression.Convert(Expression.Constant(value), desiredType);
        }

        return Expression.Constant(value);
    }
}

Upvotes: 8

Related Questions