如何在启动中处理异步操作?

在我的 ASP.NET 5应用程序中,我想从 Azure 加载一些数据到启动程序的缓存中。配置方法。Azure SDK 专门公开异步方法。通常,调用异步方法是通过在异步方法内部等待来完成的,如下所示:

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。我可以使用一个异步 void 方法,但是我的理解是,异步 void 方法只应该用于事件处理程序(根据 https://msdn.microsoft.com/en-us/magazine/jj991977.aspx和其他许多方法)。

我认为更好的方法是不等待地调用异步函数,调用等待返回的 Task,然后通过 Task 缓存结果。结果属性,如下所示:

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 在今年早些时候的 博客文章中使用了类似的方法。然而,从那篇文章来看,这是否被认为是一种可以接受的做法还不清楚。是吗?

如果这被认为是一种可接受的实践,那么我需要什么样的错误处理(如果有的话)呢?我的理解是,任务。Wait ()将重新抛出由异步操作引发的任何异常,我还没有提供任何取消异步操作的机制。只是调用 Task。等够了吗?

25940 次浏览

You can do some asynchronous work, but the method is synchronous and you cannot change that. This means you need synchronously wait for async calls to be completed.

You don't want to return from a Startup method if the startup is not finished yet, right? Your solution seems to be all right.

As for exception handling: If there's a piece of work that your application can't run properly without, you should let the Startup method fail (see Fail-fast). If it isn't something critical I would enclose the relevant part in a try catch block and just log the problem for later inspection.

The example code in the blog you linked to was only using sync-over-async to populate a database with example data; that call wouldn't exist in a production app.

First, I'd say that if you truly need Configure to be asynchronous, then you should raise an issue with the ASP.NET team so it's on their radar. It would not be too difficult for them to add support for a ConfigureAsync at this point (that is, before release).

Second, you've got a couple of approaches to the problem. You could use task.Wait (or better yet, task.GetAwaiter().GetResult(), which avoids the AggregateException wrapper if an error does occur). Or, you could cache the task rather than the result of the task (which works if IMemoryCache is more of a dictionary than some weird serialize-into-binary-array-in-memory thing - I'm looking at you, previous versions of ASP.NET).

If this is considered an acceptable practice, what - if any - error handling do I need?

Using GetAwaiter().GetResult() would cause the exception (if any) to propagate out of Configure. I'm not sure how ASP.NET would respond would be if configuring the application failed, though.

I haven't provided any mechanism to cancel the async operation.

I'm not sure how you can "cancel" the setup of an application, so I wouldn't worry about that part of it.

The answers in here do not always work correctly if your async code makes further async calls, especially if those are callbacks, then you may find the code deadlocks.

This has happens on numerous occasions for me and have used the Nito.AsyncEx with great effect.

using Nito.AsyncEx;


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

Dotnet Core 3.x offers better support for this.

First, you could create a class for your caching process. Have it implement IHostedService like below. There are just two functions to implement:

    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;

Now, in 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>();
}

And that's it. Note that my solution to this relied heavily on Andrew Lock's article. I'm very grateful for him taking the time to write that up.

From the link I posted by Andrew Lock,

The services will be executed at startup in the same order they are added to the DI container, i.e. services added later in ConfigureServices will be executed later on startup.

Hopefully this helps anyone looking for Dotnet core 3.x+ approach.