ChatGPT解决这个技术问题 Extra ChatGPT

How do I handle async operations in Startup.Configure?

In my ASP.NET 5 app, I want to load some data from Azure into a cache inside my Startup.Configure method. The Azure SDK exposes async methods exclusively. Typically, calling an async method is done via await inside an async method, like this:

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

    // remainder of Configure method omitted for clarity
}

However, ASP.NET 5 requires that the Configure method returns void. I could use an async void method, but my understanding is that async void methods are only supposed to be used for event handlers (as per https://msdn.microsoft.com/en-us/magazine/jj991977.aspx among many others).

I was thinking that a better way to do this would be to call the async function without await, call Wait on the returned Task, then cache the results via the Task.Results property, like this:

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 used a similar approach in a blog post earlier this year. However, it's unclear from that post if this is considered an acceptable practice. Is it?

If this is considered an acceptable practice, what - if any - error handling do I need? My understanding is that Task.Wait() will re-throw any exceptions thrown by the async operation and I haven't provided any mechanism to cancel the async operation. Is simply calling Task.Wait() sufficient?

that's an interesting question. I don't know but I think in this case doing async void has no side effects. Hope someone with more experience could answer.
async void does have side effects in this case, because it signals to whomever is calling the method that they can proceed with other work while the method is running. Since Configure deals with application setup, continuing to run other parts of the application code before it is finished can have unpredictable consequences. (We ran into this issue, and it made our application go haywire as the dependency injector injected services that weren't yet properly set up).
This another approach where u can run init code before webserver runs. stackoverflow.com/a/55707949/1912383

S
Stephen Cleary

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.


Regarding your first point, there have been various issues raised with the ASP.NET team the past few years. The current incarnation seems to be Issue #1088 and it won't be picked up any sooner than v3.0, which isn't on the roadmap yet, meaning no sooner than sometime in 2018.
I discovered it's not enough to simply use GetAwaiter() when nothing is returned. You have to use GetAwaiter().GetResult() for the async operation to "complete".
@user247702 you were correct it looks like. They did finally add better support for this in 3.x. I added an answer below showing one way to utilize these new features.
This is the newer issue tracking it github.com/dotnet/aspnetcore/issues/24142. Looks like it will not be ready for NET6 as well :(
j
joshmcode

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.


Thanks @joshmcode. Are we able to register singleton services within StartAsync? I basically want to register a singleton that acts as global application settings that need to pull data from the DB asynchronously, which is why I was looking down the path of async operations within Startup. I thought about caching data via the hosted service you detailed, and let the singleton pull from the cache, but I'm worried that if the cache expires it would cause problems. So if we can register the singleton there, it would be ideal.
@AnimaSola I believe this is possible, however it can be a little bit tricky. One possible approach you could take is to asynchronously update the Writable settings in combination with a service injection. While I don't know the full specs of your solution, I would imagine something where you inject IServiceProvider in the HostedService and get your service to access the database. Also you could inject IWritableOptions<T> and update the options. Then those IWritableOptions are available throughout the application. I haven't fully tried this, but I have done both steps separately.
The problem with this approach is that the service will be already up when the hosted service starts to execute. This can cause issues if you depend on the work being done before the application initializes, which is my case. Since hosted services start running after the app starts by definition, they cannot be used in cases where you need it to be part of the initialization.
@julealgon you're right, and that's not what this is intended for. This approach is intended to kick off async processes without blocking. I used the above to start background processes when the app begins. I don't know that the OP really specified that the process has to be finished first. They just asked how to start async processes on startup. By definition, an async process would not be the right approach for what you described.
m
mbudnik

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.


C
Chris

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

Probably, AsyncBridge will work, too.