Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Not suported with EF7: ExecuteDeleteAsync and ExecuteUpdateAsync #66

Open
SerhiyBalan opened this issue Jan 24, 2023 · 10 comments
Open

Comments

@SerhiyBalan
Copy link

SerhiyBalan commented Jan 24, 2023

Hello!

I've been using MockQueryable with .NET6 a lot
Recently my project migrated to .NET 7 (EF Core 7)
And I started to use new EF7 features:

  • ExecuteDeleteAsync
            ...
            .Where(user => user.Id == id)
            .ExecuteDeleteAsync();

ExecuteDeleteAsync implementation is here
https://github.com/dotnet/efcore/blob/main/src/EFCore.Relational/Extensions/RelationalQueryableExtensions.cs#L316

  • ExecuteUpdateAsync
            .Where(user => user.Id == id)
            .ExecuteUpdateAsync(updater => updater
                .SetProperty(
                    user => user.Status,
                    user => status)
                .SetProperty(
                    user => user.ModifiedBy,
                    user => _userContextProvider.GetUserId())
                .SetProperty(
                    user => user.ModifiedOn,
                    user => DateTimeOffset.UtcNow));

ExecuteUpdateAsync implementation is here
https://github.com/dotnet/efcore/blob/main/src/EFCore.Relational/Extensions/RelationalQueryableExtensions.cs#L372

I use MockQueryable using a standard pattern:

           var mock = users.BuildMock();
           _userRepository.Setup(x => x.GetQueryable()).Returns(mock);

ToListAsync(), FirstOrDefaultAsync() and others works perfectly with this pattern.

Unfortunately ExecuteUpdateAsync / ExecuteDeleteAsync doesn't work at all :(

When I try to run a unit test, a mocked GetQueryable method works nicely as usual, but it throws an exception on ExecuteDeleteAsync

 <System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation.
 ---> System.InvalidOperationException: There is no method 'ExecuteDelete' on type 'Microsoft.EntityFrameworkCore.RelationalQueryableExtensions' that matches the specified arguments
   at System.Linq.EnumerableRewriter.FindMethod(Type type, String name, ReadOnlyCollection`1 args, Type[] typeArgs)
   at System.Linq.EnumerableRewriter.VisitMethodCall(MethodCallExpression m)
   at System.Linq.EnumerableExecutor`1.Execute()
   at System.Linq.EnumerableQuery`1.System.Linq.IQueryProvider.Execute[TElement](Expression expression)
   at Microsoft.EntityFrameworkCore.RelationalQueryableExtensions.ExecuteDelete[TSource](IQueryable`1 source)
   at lambda_method34(Closure)
   at MockQueryable.Core.TestQueryProvider`1.CompileExpressionItem[TResult](Expression expression)
   at MockQueryable.Core.TestQueryProvider`1.Execute[TResult](Expression expression)
   at System.RuntimeMethodHandle.InvokeMethod(Object target, Void** arguments, Signature sig, Boolean isConstructor)
   at System.Reflection.MethodInvoker.Invoke(Object obj, IntPtr* args, BindingFlags invokeAttr)
   --- End of inner exception stack trace ---
   at System.Reflection.MethodInvoker.Invoke(Object obj, IntPtr* args, BindingFlags invokeAttr)
   at System.Reflection.RuntimeMethodInfo.Invoke(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
   at System.Reflection.MethodBase.Invoke(Object obj, Object[] parameters)
   at MockQueryable.EntityFrameworkCore.TestAsyncEnumerableEfCore`1.ExecuteAsync[TResult](Expression expression, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.RelationalQueryableExtensions.ExecuteDeleteAsync[TSource](IQueryable`1 source, CancellationToken cancellationToken)

Exception throws here on .Invoke(this, new object[] { expression });
https://github.com/romantitov/MockQueryable/blob/master/src/MockQueryable/MockQueryable.EntityFrameworkCore/TestQueryProviderEfCore.cs#L25

Same exception throws at ExecuteUpdateAsync (but it says There is no method 'ExecuteUpdate' on type 'Microsoft.EntityFrameworkCore.RelationalQueryableExtensions' that matches the specified arguments)

The custom logic pattern doesn't work because ExecuteUpdateAsync / ExecuteDeleteAsync are IQueryable extensions

Thank you very much

@SerhiyBalan SerhiyBalan changed the title Troubles with EF7: ExecuteDeleteAsync and ExecuteUpdateAsync Not suported with EF7: ExecuteDeleteAsync and ExecuteUpdateAsync Jan 25, 2023
@StuartBale-Xero
Copy link

Is there a suggested approach on how to support this?
@SerhiyBalan - did you have a workaround?

@SerhiyBalan
Copy link
Author

SerhiyBalan commented Jun 14, 2023

@StuartBale-Xero

My project uses UnitOfWork / GenericRepository pattern

So instead of using native EF's ExecuteUpdateAsync / ExecuteDeleteAsync extensions I use own methods at the Generic Repository:

public class GenericRepository<TEntity> : IRepository<TEntity>
    where TEntity : class, new()
{
    public GenericRepository(DbContext dbContext)
    {
        ArgumentNullException.ThrowIfNull(dbContext);
        DbSet = dbContext.Set<TEntity>();
    }

    protected DbSet<TEntity> DbSet { get; }

    ...

    public virtual Task<int> ExecuteUpdateAsync(
        Expression<Func<TEntity, bool>> predicate,
        Expression<Func<SetPropertyCalls<TEntity>, SetPropertyCalls<TEntity>>> setPropertyCalls,
        CancellationToken cancellationToken = default)
        => DbSet
            .AsQueryable()
            .Where(predicate)
            .ExecuteUpdateAsync(setPropertyCalls, cancellationToken);

    public virtual Task<int> ExecuteDeleteAsync(
        Expression<Func<TEntity, bool>> predicate,
        CancellationToken cancellationToken = default)
        => DbSet
            .AsQueryable()
            .Where(predicate)
            .ExecuteDeleteAsync(cancellationToken);
}

In code instead of

        await _unitOfWork
            .GetRepository<User>()
            .AsQueryable()
            .Where(user => user.Id == id)
            .ExecuteDeleteAsync(); // Native Entity Framework extension

I use my own methods:

        await _unitOfWork
            .GetRepository<User>()
            .ExecuteDeleteAsync(user => user.Id == id); // My method in GenericRepository

Since they are no more extensions, I can easily mock them

@marcoatribeiro
Copy link

marcoatribeiro commented Jul 19, 2023

Hi, guys!

Any update on this? I don't use the UoW / repository and it would be really interesting if MockQueryable could support these methods.

@tiagoseminotti
Copy link

Hi, guys!

Any update on this? I don't use the UoW / repository and it would be really interesting if MockQueryable could support these methods.

same here...

@romantitov
Copy link
Owner

Hello. Thanks for you contribution. Sorry for the late answer. Unfortunately I'm very busy at the moment. If you provide a pull request with fix of the issue I would be happy to include it to the next release. Please don't forget to cover the case by additional tests to minimize possibility of regressions for the future. Thanks for the understanding.

@hpaygu-icims
Copy link

hpaygu-icims commented Aug 26, 2024

@StuartBale-Xero

My project uses UnitOfWork / GenericRepository pattern

So instead of using native EF's ExecuteUpdateAsync / ExecuteDeleteAsync extensions I use own methods at the Generic Repository:

public class GenericRepository<TEntity> : IRepository<TEntity>
    where TEntity : class, new()
{
    public GenericRepository(DbContext dbContext)
    {
        ArgumentNullException.ThrowIfNull(dbContext);
        DbSet = dbContext.Set<TEntity>();
    }

    protected DbSet<TEntity> DbSet { get; }

    ...

    public virtual Task<int> ExecuteUpdateAsync(
        Expression<Func<TEntity, bool>> predicate,
        Expression<Func<SetPropertyCalls<TEntity>, SetPropertyCalls<TEntity>>> setPropertyCalls,
        CancellationToken cancellationToken = default)
        => DbSet
            .AsQueryable()
            .Where(predicate)
            .ExecuteUpdateAsync(setPropertyCalls, cancellationToken);

    public virtual Task<int> ExecuteDeleteAsync(
        Expression<Func<TEntity, bool>> predicate,
        CancellationToken cancellationToken = default)
        => DbSet
            .AsQueryable()
            .Where(predicate)
            .ExecuteDeleteAsync(cancellationToken);
}

In code instead of

        await _unitOfWork
            .GetRepository<User>()
            .AsQueryable()
            .Where(user => user.Id == id)
            .ExecuteDeleteAsync(); // Native Entity Framework extension

I use my own methods:

        await _unitOfWork
            .GetRepository<User>()
            .ExecuteDeleteAsync(user => user.Id == id); // My method in GenericRepository

Since they are no more extensions, I can easily mock them

@SerhiyBalan
But does it work well? since ExecuteUpdate doesn't follow UoW and repository pattern. My project has same scenario where existing code is using repository pattern and UoW since I need to implement bulk update action, I'm clueless how to do it if I already have repository pattern in place

@romantitov
Copy link
Owner

Hello everyone. @lazaro-ansaldi made a great contribution for the issue in his pull request. Thanks @lazaro-ansaldi for that!
The source code is currently available in a separate branch and in the release 7.0.4-beta. Please feel free to give me your feedback. Once everyone will be happy I will merge these changes into the master branch and create a regular release.

@icnocop
Copy link

icnocop commented Sep 24, 2024

Thank you, Roman.

I recently made a comment to the PR here and want to know if my assumption of how the mocked ExecuteUpdate and ExecuteUpdateAsync methods are expected to work is correct.

I expected the underlying data to be updated.

For example, given a mocked DbSet<User>, which was created from a list containing one User with Id 1, when dbSet.Where(x => x.Id == 1).ExecuteUpdate(x => x.SetProperty(x => x.FirstName, "New first name")); is called, then I expected the user's first name to be updated.

If that's the wrong assumption, is there a way to update the underlying data when ExecuteUpdate or ExecuteUpdateAsync is called such that the entities are updated according to the SetProperty method?

Thank you.

@mithun-shirali
Copy link

mithun-shirali commented Nov 6, 2024

Hello everyone. @lazaro-ansaldi made a great contribution for the issue in his pull request. Thanks @lazaro-ansaldi for that! The source code is currently available in a separate branch and in the release 7.0.4-beta. Please feel free to give me your feedback. Once everyone will be happy I will merge these changes into the master branch and create a regular release.

Thanks @romantitov & @lazaro-ansaldi . This worked for me with testing ExecuteUpdateAsync()!

@pooyahadavi
Copy link

Big thanks to @romantitov and @lazaro-ansaldi. I also used the new feature for testing ExecuteUpdateAsync

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants