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


71

在我的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。我可以使用异步无效方法,但是我的理解是异步无效方法仅应用于事件处理程序(如https://msdn.microsoft.com/zh-cn/magazine/jj991977.aspx等) )。

我在想,一种更好的方法是在不等待的情况下调用异步函数,对返回的Task调用Wait,然后通过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在今年早些时候的博客中使用了类似的方法。但是,从该职位尚不清楚这是否可以接受。是吗?

如果这被认为是可以接受的做法,那么我需要什么(如果有)错误处理?我的理解是Task.Wait()将重新引发异步操作引发的所有异常,而我没有提供任何取消异步操作的机制。仅仅调用Task.Wait()就足够了吗?


1
这是一个有趣的问题。我不知道,但我认为在这种情况下执行异步无效操作没有副作用。希望有更多经验的人可以回答。
Bart Calixto 2015年

async void 在这种情况下确实有副作用,因为它向正在调用该方法的任何人发出信号,表示他们可以在该方法运行时继续进行其他工作。由于Configure处理应用程序设置,因此在未完成之前继续运行应用程序代码的其他部分可能会带来不可预测的后果。(我们遇到了这个问题,因为依赖注入程序注入了尚未正确设置的服务,这使我们的应用程序陷入了麻烦)。
Daniel Saner

Answers:


40

您链接到的博客中的示例代码仅使用sync-over-async来用示例数据填充数据库。该调用在生产应用中将不存在。

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

其次,您有两种解决问题的方法。您可以使用task.Wait(或更好的方法,如果确实发生错误task.GetAwaiter().GetResult(),则避免使用AggregateException包装器)。或者,您可以缓存任务,而不是任务结果(如果IMemoryCache更多的是字典而不是某些奇怪的序列化为内存中的二进制数组的东西的话,它可以工作-我在找您,以前的ASP版本。净)。

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

使用GetAwaiter().GetResult()会导致异常(如果有)从中传播出去Configure。但是,我不确定如果配置应用程序失败,ASP.NET将如何响应。

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

我不确定如何“取消”应用程序的设置,因此我不必担心它的这一部分。


7
关于您的第一点,在过去几年中,ASP.NET团队提出了许多问题。目前的化身似乎是问题#1088,它不会被拾起任何超过3.0,这是不是越早路线图然而,在2018年意味着不早于某个
user247702

我发现仅GetAwaiter()返回任何内容时仅使用它是不够的。您必须使用GetAwaiter().GetResult()异步操作才能“完成”。
gregsonian

@ user247702你是正确的,看起来像。他们终于在3.x中为此添加了更好的支持。我在下面添加了一个答案,显示了一种利用这些新功能的方法。
joshmcode

4

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的文章。我非常感谢他抽出宝贵的时间来写下这些内容。

在我由安德鲁·洛克(Andrew Lock)发布的链接中,

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

希望这对寻找Dotnet core 3.x +方法的人有帮助。


1

您可以执行一些异步工作,但是该方法是同步的,您无法更改它。这意味着您需要同步等待异步调用完成。

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

至于异常处理:如果您的应用程序无法正常运行,您应该让Startup方法失败(请参阅Fail-fast)。如果不是很关键,我会将相关部分放在try catch块中,并记录问题以供以后检查。


0

如果您的异步代码进行了进一步的异步调用(尤其是那些回调),则此处的答案并不总是能正常工作,那么您可能会发现代码死锁。

对我而言,这种情况已经发生过很多次,并且使用Nito.AsyncEx效果非常好。

using Nito.AsyncEx;

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

可能,AsyncBridge也将起作用。
Uwe Keim
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.