.Net Core 2.2 web api with Entity Framework 和 Linq 无法执行异步任务?

.Net Core 2.2 web api with Entity Framework and Linq not able to do async tasks?

我有一个 .NET Core 2.2 web api,我希望在其中异步获得控制器 return 结果。在一直异步的过程中,在浏览器中调用以测试通过 id 获取并获得所有工作。

控制器单元测试也能正常工作,但是当我去创建涉及模拟上下文的服务级别单元测试时,我遇到了错误

System.AggregateException : One or more errors occurred. (The provider for the source IQueryable doesn't implement IAsyncQueryProvider. Only providers that implement IAsyncQueryProvider can be used for Entity Framework asynchronous operations.)

当我深入研究这个错误时,我发现博客和 Whosebug 文章说唯一的方法是将代码包装在 Task.FromResult.

其中一篇这样的文章是: https://expertcodeblog.wordpress.com/2018/02/19/net-core-2-0-resolve-error-the-source-iqueryable-doesnt-implement-iasyncenumerable/

这意味着 EF 实际上无法执行实际的异步工作,或者我不了解一些基本知识(第二个选项最有可能 - 但我想确认)。

代码方面,我的服务如下(只是缩小范围的 get 方法)

namespace MoneyManagerAPI.Services
{
    public class CheckingService : ICheckingService
    {
        readonly CheckbookContext context;

        public CheckingService(CheckbookContext context)
        {
            this.context = context;
        }

        public async Task<Checking[]> GetAllRecordsAsync()
        {
            return await Task.FromResult(context.Checking.OrderByDescending(m => m.Id).ToArray());
        }

        public async Task<Checking> GetByIdAsync(int id)
        {
            return await Task.FromResult(context.Checking.FirstOrDefault(c => c.Id == id));
            //return await context.Checking.FirstOrDefaultAsync(c => c.Id == id);
        }
    }
}

在 GetByIdAsync 方法中,如果注释的代码行未被注释,而另一个 return 语句被注释,代码仍然可以编译,但在测试时抛出异常方法。

我的测试class有以下代码:

namespace Unit.Services
{
    [TestFixture]
    public class CheckingServiceTests : CheckingHelper
    {
        [Test]
        public void GetAllRecordsAsync_ShouldReturnAllRecords()
        {
            // arrange
            var context = this.CreateCheckingDbContext();
            var service = new CheckingService(context.Object);
            var expectedResults = Task.FromResult(CheckingHelper.GetFakeCheckingData().ToArray());

            // act
            var task = service.GetAllRecordsAsync();

            task.Wait();
            var result = task.Result;

            // assert
            expectedResults.Result.Should().BeEquivalentTo(result);
        }

        [Test]
        public void GetByIdAsync_ShouldReturnRequestedRecord()
        {
            // arrange
            var id = 2;
            var context = this.CreateCheckingDbContext();
            var service = new CheckingService(context.Object);
            var expectedResult = CheckingHelper.GetFakeCheckingData().ToArray()[1];

            // act
            var task = service.GetByIdAsync(id);

            task.Wait();
            var result = task.Result;

            // assert
            expectedResult.Should().BeEquivalentTo(result);
        }

        Mock<CheckbookContext> CreateCheckingDbContext()
        {
            var checkingData = GetFakeCheckingData().AsQueryable();
            var dbSet = new Mock<DbSet<Checking>>();
            dbSet.As<IQueryable<Checking>>().Setup(c => c.Provider).Returns(checkingData.Provider);
            dbSet.As<IQueryable<Checking>>().Setup(c => c.Expression).Returns(checkingData.Expression);
            dbSet.As<IQueryable<Checking>>().Setup(c => c.ElementType).Returns(checkingData.ElementType);
            dbSet.As<IQueryable<Checking>>().Setup(c => c.GetEnumerator()).Returns(checkingData.GetEnumerator());

            var context = new Mock<CheckbookContext>();
            context.Setup(c => c.Checking).Returns(dbSet.Object);

            return context;
        }
    }
}

最后GetFakeCheckingData如下:

namespace Unit.Shared
{
    public class CheckingHelper
    {
        public static IEnumerable<Checking> GetFakeCheckingData()
        {
            return new Checking[3]
            {
                new Checking
                {
                    AccountBalance = 100,
                    Comment = "Deposit",
                    Confirmation = "Test Rec 1",
                    Credit = true,
                    Id = 1,
                    TransactionAmount = 100,
                    TransactionDate = new DateTime(2019, 8, 1, 10, 10, 10)
                },
                new Checking
                {
                    AccountBalance = 90,
                    Comment = "Withdrawal",
                    Confirmation = "Test Rec 2",
                    Credit = false,
                    Id = 2,
                    TransactionAmount = -10,
                    TransactionDate = new DateTime(2019, 8, 10, 10, 10, 10)
                },
                new Checking
                {
                    AccountBalance = 50,
                    Comment = "Deposit",
                    Confirmation = "Test Rec 3",
                    Credit = true,
                    Id = 3,
                    TransactionAmount = 50,
                    TransactionDate = new System.DateTime(2019, 9, 21, 10, 10, 10)
                }
            };
        }
    }
}

不要在你所在的位置使用 Task.FromResult,因为这会导致你的实时代码同步 运行(当你输入 await 一个完整的 TaskTask.FromResult 做,一切都 运行 同步)。这会损害您的生产代码。

Microsoft 有关于如何在 EF6 中处理此问题的文档 here,尽管它看起来同样适用于 EF Core。解决方案是为您的测试创建自己的异步方法。

但是,您可以考虑重构测试代码以使用内存数据库,如 EF Core 文档所述here。这样做的好处是您使用相同的 CheckbookContext 而根本不使用 Mock,因此异步方法仍应像往常一样工作。


为了将来参考,当您看到 AggregateException 时,这意味着正在抛出一个合法的异常,它只是被包裹在 AggregateException 中。如果您检查 AggregateExceptionInnerExceptions 属性,您将看到实际的异常。

为避免将真正的异常放在 AggregateException 中,请勿使用 .Wait()。使您的测试方法 async Task 并使用 await.

如果由于某种原因你不能使它们异步,那么使用.GetAwaiter().GetResult(),它仍然会阻塞线程,但会给你真正的异常。

一旦你这样做,你仍然会得到一个异常,但它会告诉你实际的问题。

您应该对应用程序流程和单元测试流程都遵循异步等待模式。您应该将代码保留为

public async Task<Checking> GetByIdAsync(int id)
{    
     return await context.Checking.FirstOrDefaultAsync(c => c.Id == id);
}

并将单元测试方法签名更改为async Task。以下编写的异步单元测试异步调用 async 方法。

    [Test]
    public async Task GetByIdAsync_ShouldReturnRequestedRecord()
    {
        // arrange
        var id = 2;
        var context = this.CreateCheckingDbContext();
        var service = new CheckingService(context.Object);
        var expectedResult = CheckingHelper.GetFakeCheckingData().ToArray()[1];

        // act
        var result = await service.GetByIdAsync(id);

        // assert
        expectedResult.Should().BeEquivalentTo(result);
    }