16

ASP.NET Core framework gives us two well defined places for initialization: 1. the Startup.ConfigureServices() method for registering DI services 2. the Startup.Configure() method for configuration of middleware pipeline

But what about other initialization steps specific to my web application? Where should those go, especially if they have dependencies that need to be injected?

For example I need to initialize database ORM based on connection string which is specified in configuration file appsettings.json. So this initialization code has dependency on IConfiguration and perhaps other custom services that are registered into DI container during Startup.ConfigureServices()

So as per recommendations from these articles:

I tried to encapsulate initialization logic in separate classes and then to create extension method for IWebHostBuilder that would execute this code, but how can I make framework inject IConfiguration and other custom dependencies into this extension methods? Also, can I be sure that this code will be executed after Startup.ConfigureServices() when all dependencies are registered?

Is there some better or recommended way to perform this kind of tasks?

mlst
  • 2,688
  • 7
  • 27
  • 57

2 Answers2

26

You can add an extension method for IWebHost (instead of IWebHostBuilder) and then use IWebHost.Services for resolving services. Here's an example of how to retrieve IConfiguration:

public static class WebHostExtensions
{
    public static IWebHost SomeExtension(this IWebHost webHost)
    {
        var config = webHost.Services.GetService<IConfiguration>();

        // Your initialisation code here.
        // ...

        return webHost;
    }
}

Usage of this extension method would look something like this:

CreateWebHostBuilder(args)
    .Build()
    .SomeExtension()
    .Run();

If you need an async version of SomeExtension, you can split up the chaining above and await the extension method. Here's what that might look like:

public static async Task SomeExtensionAsync(this IWebHost webHost)
{
    var config = webHost.Services.GetService<IConfiguration>();

    // Your initialisation code here with awaits.
    // ...
}

Usage looks something like this:

public static async Task Main(string[] args)
{
    var webHost = CreateWebHostBuilder(args)
        .Build();

    await webHost.SomeExtensionAsync();

    webHost.Run();
}

Also, can I be sure that this code will be executed after Startup.ConfigureServices() when all dependencies are registered?

With the approach I've outlined above, the answer here is yes.


Note that IWebHost.Services represents the root IServiceProvider, which will not support resolving scoped instances. IConfiguration is a singleton, so this isn't an issue for that, but if you have scoped dependencies, you'll need to create an explicit scope inside of your extension method.

Kirk Larkin
  • 84,915
  • 16
  • 214
  • 203
  • thanks for the explanation @Kirk Larkin. I was wondering what is the best practice if an error/exception occurs in this 'user-defined' initialization process? – ivanpovazan Apr 27 '21 at 16:41
  • @povke I'm not sure about a "best practice" here, but I'd expect some kind of `try/catch` around the call to `SomeExtensionAsync` (or even the entire `Main` body). This could log the exception out to a third-party service, for example, to notify you that the app failed to start. Alternatively, whatever runs your app might ship any output it sees on `stderr` to another service, to notify you in a similar fashion. – Kirk Larkin Apr 27 '21 at 19:23
  • yes, thank you @Kirk Larkin I understand that part, but I am more curious about what should be done with the webhost? Should it be started or just opted-out from the app? I know it depends on the hosting environment, and what got me worried/confused is this [topic](https://github.com/dotnet/aspnetcore/issues/22507). Do you have any input on this? :) – ivanpovazan Apr 27 '21 at 19:46
  • 1
    @povke I've not experienced that problem myself, but I think I'd skip the host and let the process exit with a non-zero status code. I expect the solution might be different for different deployment scenarios. – Kirk Larkin Apr 28 '21 at 18:20
  • Thank you very much, I will probably have to test both scenarios on Azure to see how it behaves – ivanpovazan Apr 28 '21 at 18:47
5

In Program.cs you have the following code for your Main method:

public static void Main(string[] args)
{
    CreateWebHostBuilder(args).Build().Run();
}

After the Build() part has run, you have a fully configured host. As such, you can simply do something like the following:

var host = CreateWebHostBuilder(args).Build();

// do something with host

host.Run();

The host has a member, Services, which is an instance of IServiceProvider, so you can pull any services you need from that, i.e.

var config = host.Services.GetRequiredService<IConfiguration>();

Just bear in mind that at this point, there is no inherent scope, so if you need scoped services, you'll need to create one:

using (var scope = host.Services.CreateScope())
{
    var myScopedService = scope.ServiceProvider.GetRequiredService<MyScopedService>();
    // do something with myScopedService
}
Chris Pratt
  • 232,153
  • 36
  • 385
  • 444