返回 302 重定向的异步控制器操作是下一个请求的 'overtaken'

Async controller action returning a 302 redirect, is 'overtaken' by the next request

我们正在构建 ASP.Net Core 3.1 应用程序。它包含一个页面,一个工作项列表和一个允许将某些项目分配给某人的表单。如果控制器操作成功,它将以重定向到再次显示工作项的操作结束。

它注意到有时列表会重新加载一些具有旧分配的项目,然后刷新会获得更新后的列表。

列表的控制器 GET 操作是异步的,等待获取项目列表的代码,一些嵌套代码通过 Linq to Entities 查询从数据库中获取数据 ToListAsync() .

public async Task<IActionResult> Index()
{
    _log.Debug("Getting work items");
    List<Item> model = await _itemData.GetItems();
    return View(model);
}

控制器 POST 操作也是异步的,但没有任何等待。

public async Task<IActionResult> Index(int employeeId, int[] itemIds)
{
    _itemData.Assign(employeeId, itemIds);

    _log.Debug("Redirecting to work items");
    return RedirectToAction(nameof(Index));
}

但是,被调用的代码包含这样的结构:

public bool Assign(int employeeId, int[] itemIds)
{
    _log.Debug("Start assigning items");
    Array.ForEach(itemIds, async itemId =>
    {
        _log.Debug($"Done assigning item {itemId}");
        using (var scope = _serviceScopeFactory.CreateScope())
        {
            var db = scope.ServiceProvider.GetService<context>();

            Item item = await db.Items.Where(i => i.Id == itemId).FirstOrDefaultAsync();
            item .EmployeeId= employeeId;
            await db.SaveChangesAsync();
            _log.Debug($"Done assigning item {itemId}");
        }
    });

    _log.Debug("Done assigning items");
    return true;
}

我已经大大简化了代码,希望我没有引入与之不一致的地方...

我想知道这实际上是如何工作的。

循环使用 async 指令处理。此代码等待 FirstOrDefaultAsync()SaveChangesAsync(),但仅在循环内。日志记录显示方法本身 returns 在所有等待的任务完成之前。

日志

Getting work items
Start assigning items
Start assigning item 1234
Start assigning report 2345
Done assigning item
Redirecting to work items
Getting work items
Done assigning item 1234
Done assigning item 2345

这是预期的行为吗?

我试图让 Assign() 方法异步并等待它的调用,但正如我所料,这并没有改变结果。

我能否结合异步分配并以故障安全方式等待完整结果,还是最好简单地更改为完全同步的解决方案?

您的 Assign 方法需要异步:

public async Task<bool> Assign(int employeeId, int[] itemIds)
{
    _log.Debug("Start assigning items");

    foreach (var itemId in itemIds)
    {
        _log.Debug($"Done assigning item {itemId}");
        using (var scope = _serviceScopeFactory.CreateScope())
        {
            var db = scope.ServiceProvider.GetService<context>();

            Item item = await db.Items
                .Where(i => i.Id == itemId)
                .FirstOrDefaultAsync();
            item .EmployeeId= employeeId;
            await db.SaveChangesAsync();
            _log.Debug($"Done assigning item {itemId}");
        }
    }

    _log.Debug("Done assigning items");
    return true;
}

Array.ForEach 接受一个 Action<T>。这意味着当您使用 async 委托时,它将把它变成 async void。由于签名是 void 而不是 Task,无法等待委托。因此,将其标记为 async 并在内部使用 await 会给您一种错误的正确感,而实际上恰恰相反。您必须将 Assign 方法更改为 async Task,然后实施@Paulo Morgado 的建议。

您正在将异步委托传递给 Array.ForEach,但您并未等待生成的任务完成。

正如@JohanP 所暗示的那样,Array.ForEach 不适用于异步代码,因为它接受一个 Action<T> 委托,它没有 return 值,这意味着任务是迷路了。

让你的 Assign 方法 async:

public async Task<bool> Assign(int employeeId, int[] itemIds)
{
    _log.Debug("Start assigning items");
    using (var scope = _serviceScopeFactory.CreateScope())
    {
        var db = scope.ServiceProvider.GetService<context>();
        var tasks = itemIds.Select(async itemId =>
        {
            _log.Debug($"Done assigning item {itemId}");

            Item item = await db.Items.Where(i => i.Id == itemId).FirstOrDefaultAsync();
            item.EmployeeId = employeeId;
            _log.Debug($"Done assigning item {itemId}");
        });

        await Task.WhenAll(tasks);
        await db.SaveChangesAsync();
    }

    _log.Debug("Done assigning items");
    return true;
}

请注意 Task.WhenAll 如何确保在允许您的方法 return.

之前等待所有任务

使用 Enumerable.Select 将允许异步工作并行进行,这与 @Paulo 建议的 foreach 方法不同。

SaveChangesAsync 应在所有任务完成后调用一次,以避免并发问题;这种方法还将减少数据库的负载。

然后在你的控制器中:

public async Task<IActionResult> Index(int employeeId, int[] itemIds)
{
    await _itemData.Assign(employeeId, itemIds);

    _log.Debug("Redirecting to work items");
    return RedirectToAction(nameof(Index));
}