1

I'm creating a generic async update method for Entity Framework 6.x on .NET Framework 4.8. Here's the class:

public class GenericUpdate<TEntity, TId, TDto>
    where TEntity : class
    where TId : IConvertible
    where TDto : class
{
    public async Task UpdateSingleAsync(string searchPropertyName, TId searchPropertyValue, TDto dto)
    {
        try
        {
            var tableType = typeof(TEntity);

            // https://stackoverflow.com/questions/30029230/dynamic-lambda-expression-for-singleordefault
            var param = Expression.Parameter(tableType, "m");
            var searchProperty = Expression.PropertyOrField(param, searchPropertyName);
            var constSearchValue = Expression.Constant(searchPropertyValue);

            var body = Expression.Equal(searchProperty, constSearchValue);

            var lambda = Expression.Lambda(body, param);

            using (var context = new MyContext())
            {
                var dbTable = context.Set(tableType);

                var genericSingleOrDefaultAsyncMethod =
                    typeof(QueryableExtensions).GetMethods().First(m => m.Name == "SingleOrDefaultAsync" && m.GetParameters().Length == 2);
                var specificSingleOrDefaultAsync = genericSingleOrDefaultAsyncMethod.MakeGenericMethod(tableType);

                // https://stackoverflow.com/a/16153317/177416
                var result = (Task<TEntity>) specificSingleOrDefault.Invoke(null, new object[] { dbTable, lambda });
                await result;

                context.Entry(result).CurrentValues.SetValues(dto);
                await context.SaveChangesAsync();
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine(ex.ToString());
            throw;
        }
    }
}

On the result, it blows up with:

System.ArgumentException: Object of type 'System.Linq.Expressions.Expression [System.Func[MyEntities.Models.SomeEntity,System.Boolean]]' cannot be converted to type 'System.Threading.CancellationToken'.

How do you call Invoke on an async method? What am I doing wrong?

Update 1: I used this answer to add the (Task) to my code but obviously I'm doing something wrong.

Update 2: Following @StephenCleary, made this change:

dynamic dbTable = context.Set(tableType);
var result = await QueryableExtensions.SingleOrDefaultAsync(dbTable, lambda);

The first suggestion didn't work as dbTable didn't have a SingleOrDefaultAsync.

Now getting this error:

The best overloaded method match for 'System.Data.Entity.QueryableExtensions.SingleOrDefaultAsync(System.Linq.IQueryable, System.Threading.CancellationToken)' has some invalid arguments

Update 3 & solution: Thanks to @StephenCleary, this solution works like a charm:

dynamic lambda = Expression.Lambda(body, param);

using (var context = new MyContext())
{
    dynamic dbTable = context.Set(tableType);
    var result = await QueryableExtensions.SingleOrDefaultAsync(dbTable, lambda);

    if(result != null)
    {
        context.Entry(result).CurrentValues.SetValues(dto);
        await context.SaveChangesAsync();
    }
}
Alex
  • 34,699
  • 13
  • 75
  • 158
  • See: [How do you call invoke on an async method](https://stackoverflow.com/questions/16153047/net-invoke-async-method-and-await) – Wyck Apr 24 '20 at 16:51
  • @Wyck, that's where I got the `(Task)` code and it's exactly what I'm doing, isn't it? Please see my update – Alex Apr 24 '20 at 16:55

1 Answers1

2

generic async update method

I do have to question whether one horribly complex method is really more maintainable than a half-dozen or so clean and obvious methods.

That said, the error you're getting indicates you're retrieving the wrong overload of SingleOrDefaultAsync. Indeed, the code typeof(QueryableExtensions).GetMethods().First(m => m.Name == "SingleOrDefaultAsync" && m.GetParameters().Length == 2) just returns the first method of that name that has two parameters. This is an insufficient amount of information to ensure you get the correct method.

You can either add more checks to ensure the parameters are the types you expect, or you can just drop the reflection completely, since it doesn't appear to be necessary:

var dbTable = context.Set(tableType);
var result = await dbTable.SingleOrDefaultAsync(lambda);

This is assuming that dbTable is of an appropriate type. If it's some unspecified type (e.g., object), you can still use the compiler's overload resolution by making it dynamic:

dynamic dbTable = context.Set(tableType);
var result = await QueryableExtensions.SingleOrDefaultAsync(dbTable, lambda);
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
  • Now getting the error indicated in Update 2 above, @StephenCleary. – Alex Apr 24 '20 at 18:03
  • 1
    Try making the `lambda` `dynamic`. – Stephen Cleary Apr 24 '20 at 18:12
  • Thanks, @StephenCleary, that worked. But why? What is `dynamic` doing here? – Alex Apr 24 '20 at 18:44
  • 1
    Lambda expressions in reflection are weird. The [overload you're trying to call](https://learn.microsoft.com/en-us/dotnet/api/system.data.entity.queryableextensions.singleordefaultasync?view=entity-framework-6.2.0#System_Data_Entity_QueryableExtensions_SingleOrDefaultAsync__1_System_Linq_IQueryable___0__System_Linq_Expressions_Expression_System_Func___0_System_Boolean___) has type `Expression>` but `lambda` is of type `LambdaExpression`. So `dynamic` is doing "magic compiler stuff". I don't even know. – Stephen Cleary Apr 24 '20 at 19:39