-
Notifications
You must be signed in to change notification settings - Fork 10.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support an OnApplicationStart[Async] method or similar pattern #5890
Comments
I think we had a bug on this a million years ago but who knows where that bug is... I've always liked the middleware approach to this. It's kind of like the IIS warmup module. |
Just there, sir: aspnet/Hosting#29 😄 |
Yeah that's just about a million years ago! 😄 |
@Tratcher and recent thoughts on this? |
What about ApplicationStarted? |
@Tragetaschen the scenario is an application warmup scenario, such as hydrating a cache, etc. So the requirement is to have some sort of "event" that is blocking and async. Not sure that cancellation token fits the bill. |
I think this really belongs in Startup.Configure.
The middleware approach is not good for app initialization because it is only triggered by the first request, and that request needs to block all other requests while it does it's work. When it's done that middleware stays in the pipeline and runs on every request. |
I don't know that the scopes can really be "fixed" because there's nothing actually wrong with it. The problem is that the app's "warmup" code requires those services, but is not running within the desired scope. The idea of a middleware (though perhaps it's not the right solution) is that the middleware would obviously run in the right scope (request scope). I agree that actually being async is meaningless because it's blocking, but it allows the app developer to write non-ugly code (avoid calling As far as running on every request, well, the cost would presumably be negligible... |
Any updates about this? @Eilon I agree that the cost of initialization blocking would be negligible, however It doesn't seem to be nice if I should write a blocking middleware for one-time initialization. |
No new features for now. Backlog. |
Is this still visible and on the backlog? I notice the tag says "rc2". |
No, there's no plan to do this. |
Our server sends a bunch of http requests during startup to get all kind of configuration, so this feature definitely makes sense (though I have no preference on the API shape specifically). |
This would be very helpful, especially when using platforms such as Contentful where in my case a lot of configuration lives. I need it at Startup but it takes an async network request to fetch it. |
I have a (albiet hacky) workaround for this using IApplicationLifetime. using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace TGWebhooks.Core
{
static class ApplicationBuilderExtensions
{
public static void UseAsyncInitialization<TInitializer>(this IApplicationBuilder app, Func<TInitializer, CancellationToken, Task> asyncInitializer)
{
var applicationLifetime = app.ApplicationServices.GetRequiredService<IApplicationLifetime>();
Task initializationTask = null;
applicationLifetime.ApplicationStarted.Register(() =>
{
var toInitialize = app.ApplicationServices.GetRequiredService<TInitializer>();
initializationTask = asyncInitializer(toInitialize, applicationLifetime.ApplicationStopping);
});
app.Use(async (context, next) =>
{
await initializationTask;
await next.Invoke();
});
}
}
} They must be the first handlers in the pipeline, TInitializer must be a singleton service, and block on the first request until finished. But it allows you to write code like this: public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseAsyncInitialization<IPluginManager>((pluginManager, cancellationToken) => pluginManager.Initialize(cancellationToken));
if (env.IsDevelopment())
app.UseDeveloperExceptionPage();
app.UseMvc();
} UpdateI made it more flexible. (Cyberboss/AspNetCore.AsyncInitializer)(Nuget) |
The problem on async startup is still pending. Would you consider it for v3? |
I don't think there is any practical benefit in allowing start-up to be async. I like to refer to a discussion about this I had with Stephen Cleary. My main point is:
Although some application types might deadlock while blocking async calls, this is not the case in ASP.NET Core. There would therefore not be any benefit in adding all sorts of asynchronous start-up abstractions. |
Isn't this solved with IHostedService now? |
Two things
|
I agree with both of @Drawaes points. My main concern is number 2. There is definitely a message problem here, exactly as he states. If you decide to not do it it would be a good idea to publish docs bringing up some best practices on how to do this correctly. I still have nightmares with deadlocks from |
Given that It is easy to call sync code from async, but not vice-versa. Let's support the path of least resistance. There may be some hidden details in the core libs that make this a significant effort to implement, but when it comes to the user code (implementations), having |
Are there any known workarounds to this? |
@CoskunSunali I've been using AspNetCore.AsyncInitialization and it suits my needs. There's a blog post from the author that explains it |
On one of my apps it was suggested to use an https://github.com/Eilon/Hubbup/blob/master/src/Hubbup.Web/Program.cs#L12-L24 This is from @Tratcher , so credit to him on this. |
@matteocontrini, @Eilon Thank you for the replies. Both of the links show how to call async methods within I have plugins that need to register services during So I am trying to achieve something similar to: public class Startup {
public async Task Configure(IServiceCollection services) {
var plugins = await LoadPlugins();
foreach (var plugin in plugins) {
await plugin.ConfigureAsync(services);
}
}
} That being said, I am aware of being able to run an async method synchronously but not sure it that breaks the further (inner?) asynchronous calls. The workaround turns to be: public class Startup {
public void Configure(IServiceCollection services) {
Task.WaitAll(Task.Run(async () => await ConfigurePluginServicesAsync(services)));
}
private async Task ConfigurePluginServicesAsync(IServiceCollection services) {
var plugins = await LoadPlugins();
foreach (var plugin in plugins) {
await plugin.ConfigureAsync(services);
}
}
} I would be glad if you can provide any insights. |
@CoskunSunali async Startup is not supported, and blocking is highly discouraged. |
Just |
That code that does a Task.Run followed by .Wait doesn’t make much sense. There’s no fear of deadlocking in ASP.NET Core applications so the additional Task.Run is kinda pointless. We should look at async startup but today you can use an IHostedService to do you async initialization |
The point is to be on the safe side of the equation, to always use same code on the sync-to-async boundary. Diversification of approaches in this case leads to mistakes. Any overhead is insignificant as the code runs just once. |
I think it gives a far sense of security. It’s usually presented as “the right way to make async code synchronous”. I’ve seen all of the ways and there are all different tradeoffs. If I saw this code in an application I would guess it was copy pasta and the developer didn’t actually understand what was going on. |
Might not be necessary now with the current DI system and public static class Program
{
public static async Task Main(string[] args)
{
using (var host = CreateWebHostBuilder().Build())
{
// get a service from the DI container and use it
await host.Services.GetService<SomeService>().Start();
// get and start multiple services at the same time
await Task.WhenAll(
host.Services.GetService<SomeService>().Start1(),
host.Services.GetService<SomeOtherService>().Start2()
);
// use a scope if you need scoped services
var sp = host.Services.GetService<IServiceScope>();
using (var scope = sp.ServiceProvider.CreateScope())
{
scope.ServiceProvider.GetService<SomeScopedService>().RunSomething();
}
// start the actual webhost and run it
await host.RunAsync();
// webhost also has other methods for more control
// this is basically what's inside RunAsync above
await host.StartAsync();
await host.WaitForShutdownAsync();
await host.StopAsync();
// all services implementing IDisposable will be disposed
// when the WebHost is disposed and in reverse order from instantiation
// so you can control the dependencies
}
}
} You can wrap this with extension methods or a subclass to make it cleaner. We use this code with several apps that have startup requirements for config, messaging, caching, etc. that need to be available before the app can serve requests, while also shutting down those services properly after requests are stopped. |
Adding another use case for my requirements is to fetch config used to register services: private async Task RegisterHttpClientsAsync(IServiceCollection services, Uri configUri)
{
using var configClient = new HttpClient { BaseAddress = configUri };
var httpClients = await configClient.GetFromJsonAsync<IEnumerable<KeyValuePair<string, Uri>>>("foo");
foreach (var client in httpClients)
{
services.AddHttpClient(client.Key, c => { c.BaseAddress = client.Value; });
}
} This can only be done before |
Closing in favor of #24142 |
Applications often need to run code once at application startup per web server, e.g. to setup or initialize some external resource to be used by the application. There are two main issues with doing so in
Startup.Configure()
:One general idea that has been discussed is to have a middleware that will run this code on first request but use a lock to make sure it is only done once.
The main example where this have come up is dotnet/efcore#3070 "Pattern for seeding database with EF7 and ASP.NET 5". Note that we cannot recommend a pattern like this in general because it is not safe unless you know there is only one web server, but for many customers this can be enough. Note also that MusicStore currently uses a workaround.
Another possibly related scenario is described at dotnet/efcore#3070 (comment). Although it is not about code that needs to run at application start, it is about how to get services to be activated with the right lifetime on code that doesn't execute as part of a request:
cc @Eilon
The text was updated successfully, but these errors were encountered: