4

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?

1
  • Return of ExecuteUpdateAsync should be Task<int>, not IQueryable Commented Nov 17, 2022 at 13:21

1 Answer 1

9

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);
    }
}
Sign up to request clarification or add additional context in comments.

1 Comment

small improvement: In ValueForType replace value = Convert.ChangeType(value, desiredType) with return Expression.Convert(Expression.Constant(value), desiredType) and you can handle nullable values in the entity as well :)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.