ChatGPT解决这个技术问题 Extra ChatGPT

如何在 Startup.Configure 中处理异步操作?

在我的 ASP.NET 5 应用程序中,我想将一些数据从 Azure 加载到我的 Startup.Configure 方法中的缓存中。 Azure SDK 专门公开异步方法。通常,调用异步方法是通过异步方法中的 await 完成的,如下所示:

public async Task Configure(IApplicationBuilder app, IMemoryCache cache)
{
    Data dataToCache = await DataSource.LoadDataAsync();
    cache.Set("somekey", dataToCache);

    // remainder of Configure method omitted for clarity
}

但是,ASP.NET 5 要求 Configure 方法返回 void。我可以使用 async void 方法,但我的理解是 async void 方法只应该用于事件处理程序(根据 https://msdn.microsoft.com/en-us/magazine/jj991977.aspx 以及许多其他方法)。

我在想更好的方法是在没有等待的情况下调用异步函数,在返回的任务上调用等待,然后通过 Task.Results 属性缓存结果,如下所示:

public void Configure(IApplicationBuilder app, IMemoryCache cache)
{
    Task<Data> loadDataTask = DataSource.LoadDataAsync();
    loadDataTask.Wait();
    cache.Set("somekey", loadDataTask.Result);

    // remainder of Configure method omitted for clarity
}

今年早些时候,Stephen Walther 在blog post中使用了类似的方法。但是,从该帖子中不清楚这是否被认为是可接受的做法。是吗?

如果这被认为是可接受的做法,我需要什么(如果有的话)错误处理?我的理解是 Task.Wait() 将重新抛出异步操作抛出的任何异常,并且我没有提供任何机制来取消异步操作。简单地调用 Task.Wait() 就足够了吗?

这是一个有趣的问题。我不知道,但我认为在这种情况下做 async void 没有副作用。希望有经验的大侠解答一下。
async void 确实在这种情况下会产生副作用,因为它会向调用该方法的任何人发出信号,表明他们可以在该方法运行时继续进行其他工作。由于 Configure 处理应用程序设置,因此在完成之前继续运行应用程序代码的其他部分可能会产生不可预知的后果。 (我们遇到了这个问题,它使我们的应用程序变得混乱,因为依赖注入器注入了尚未正确设置的服务)。
这是另一种方法,您可以在网络服务器运行之前运行初始化代码。 stackoverflow.com/a/55707949/1912383

S
Stephen Cleary

您链接到的博客中的示例代码仅使用异步同步来使用示例数据填充数据库;该调用不会存在于生产应用程序中。

首先,我想说,如果您真的需要 Configure 是异步的,那么您应该向 ASP.NET 团队提出一个问题,以便他们关注。此时(即在发布之前)添加对 ConfigureAsync 的支持对他们来说并不难。

其次,您有几种解决问题的方法。您可以使用 task.Wait(或者更好的是 task.GetAwaiter().GetResult(),这样可以在发生错误时避免使用 AggregateException 包装器)。或者,您可以缓存 task 而不是任务的 result (如果 IMemoryCache 更像是一个字典而不是一些奇怪的 serialize-into-binary-array -内存中的东西 - 我在看着你,以前版本的 ASP.NET)。

如果这被认为是可接受的做法,我需要什么(如果有的话)错误处理?

使用 GetAwaiter().GetResult() 会导致异常(如果有)传播到 Configure 之外。不过,如果 configuring 应用程序失败,我不确定 ASP.NET 会如何响应。

我没有提供任何机制来取消异步操作。

我不确定如何“取消”应用程序的设置,所以我不会担心它的那部分。


关于您的第一点,过去几年 ASP.NET 团队提出了各种问题。当前版本似乎是 Issue #1088,并且不会早于 v3.0,它还没有出现在 the roadmap 上,这意味着不会早于 2018 年的某个时间。
我发现在没有返回任何内容时仅使用 GetAwaiter() 是不够的。您必须使用 GetAwaiter().GetResult() 才能“完成”异步操作。
@user247702 你是对的,它看起来像。他们最终在 3.x 中为此添加了更好的支持。我在下面添加了一个答案,展示了利用这些新功能的一种方法。
这是跟踪它的较新问题 github.com/dotnet/aspnetcore/issues/24142。看起来它也不会为 NET6 做好准备:(
j
joshmcode

Dotnet Core 3.x 对此提供了更好的支持。

首先,您可以为缓存过程创建一个类。让它像下面那样实现 IHostedService。只需实现两个功能:

    private readonly IServiceProvider _serviceProvider;
    public SetupCacheService(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        // Perform your caching logic here. 
        // In the below example I omit the caching details for clarity and 
        // instead show how to get a service using the service provider scope. 
        using (var scope = _serviceProvider.CreateScope())
        {
            // Example of getting a service you registered in the startup
            var sampleService = scope.ServiceProvider.GetRequiredService<IYourService>();

            // Perform the caching or database or whatever async work you need to do. 
            var results = sampleService.DoStuff();
            var cacheEntryOptions = new MemoryCacheEntryOptions(){ // cache options };

            // finish caching setup..
        }
    }

    public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;

现在,在 Starup.cs

    public virtual void ConfigureServices(IServiceCollection services)
    {
        // Normal service registration stuff. 
        // this is just an example. There are 1000x ways to do this. 
        services.AddTransient(IYourService, ConcreteService);

       // Here you register the async work from above, which will 
       // then be executed before the app starts running
       services.AddHostedService<SetupCacheService>();
    }

就是这样。请注意,我对此的解决方案很大程度上依赖于 Andrew Lock's article。我非常感谢他花时间把它写下来。

从我由 Andrew Lock 发布的链接中,

这些服务将在启动时按照它们添加到 DI 容器的相同顺序执行,即稍后在 ConfigureServices 中添加的服务将在启动时稍后执行。

希望这可以帮助任何寻找 Dotnet core 3.x+ 方法的人。


谢谢@joshmcode。我们可以在 StartAsync 中注册单例服务吗?我基本上想注册一个作为全局应用程序设置的单例,需要从数据库中异步提取数据,这就是为什么我在 Startup 中查找异步操作路径的原因。我考虑过通过您详述的托管服务缓存数据,并让单例从缓存中提取,但我担心如果缓存过期会导致问题。因此,如果我们可以在那里注册单例,那将是理想的。
@AnimaSola 我相信这是可能的,但它可能有点棘手。您可以采取的一种可能方法是结合服务注入异步更新 Writable 设置。虽然我不知道您的解决方案的完整规格,但我想您在 HostedService 中注入 IServiceProvider 并让您的服务访问数据库。您也可以注入 IWritableOptions<T> 并更新选项。然后那些 IWritableOptions 在整个应用程序中都可用。我还没有完全尝试过,但我已经分别完成了这两个步骤。
这种方法的问题在于,当托管服务开始执行时,该服务已经启动。如果您依赖于在应用程序初始化之前完成的工作,这可能会导致问题,这就是我的情况。由于托管服务根据定义在应用程序启动后开始运行,因此在您需要它作为初始化的一部分的情况下不能使用它们。
@julealgon 你是对的,这不是它的目的。这种方法旨在在不阻塞的情况下启动异步进程。当应用程序启动时,我使用上述方法启动后台进程。我不知道 OP 是否真的指定该过程必须首先完成。他们只是问如何在启动时启动异步进程。根据定义,异步进程不会是您所描述的正确方法。
m
mbudnik

你可以做一些异步工作,但方法是同步的,你不能改变它。这意味着您需要同步等待异步调用完成。

如果启动尚未完成,您不想从 Startup 方法返回,对吗?您的解决方案似乎没问题。

至于异常处理:如果没有某项工作,您的应用程序将无法正常运行,您应该让 Startup 方法失败(参见Fail-fast)。如果不是很重要,我会将相关部分包含在 try catch 块中,然后将问题记录下来以供以后检查。


C
Chris

如果您的异步代码进行进一步的异步调用,特别是如果这些是回调,那么这里的答案并不总是正确的,那么您可能会发现代码死锁。

这对我来说发生过很多次,并且使用 Nito.AsyncEx 效果很好。

using Nito.AsyncEx;

AsyncContext.Run(async () => { await myThing.DoAsyncTask(); });

可能,AsyncBridge 也会起作用。