Resolving ASP.NET Core Startup class from the DI container

Β· 976 words Β· 5 minutes to read

In ASP.NET Core, the most common setup is characterized by having a standalone Startup class, responsible for bootstrapping the services needed by your application, as well as setting up the application pipeline.

What most users of ASP.NET Core do not realize, is that at runtime, the Startup instance is actually being resolved from the DI container. This allows you to control some interesting aspects of how your application is bootstrapped, which can be really important i.e. in integration testing scenarios.

Let’s have a look.

The IStartup interface πŸ”—

Pretty much everyone working on ASP.NET Core applications is used to writing the application startup code in the following manner:

public class Program

{

    public static void Main(string[] args)

    {

        var host = new WebHostBuilder()

            .UseKestrel()

            .UseContentRoot(Directory.GetCurrentDirectory())

            .UseIISIntegration()

            .UseStartup<Startup>()

            .UseApplicationInsights()

            .Build();



        host.Run();

    }

}



public class Startup

{

    public Startup(IHostingEnvironment env)

    {

        var builder = new ConfigurationBuilder()

            .SetBasePath(env.ContentRootPath)

            .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)

            .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)

            .AddEnvironmentVariables();

        Configuration = builder.Build();

    }



    public IConfigurationRoot Configuration { get; set; }



    public void ConfigureServices(IServiceCollection services)

    {

        // Add framework services.

    }



    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)

    {

        // Set up application pipeline

    }

}

This is also how the templates generate the code - a WebHostBuilder pointed at a Startup class which is entirely convention based (no type safety enforced via interfaces or base classes). This approach is actually a carry over from the OWIN/Katana days that pre-dates ASP.NET Core.

However, turns out the startup code can also be written as an implementation of an IStartup interface. This little known interface is publicly available in the Microsoft.AspNetCore.Hosting namespace.

namespace Microsoft.AspNetCore.Hosting

{

    public interface IStartup

    {

        IServiceProvider ConfigureServices(IServiceCollection services);



        void Configure(IApplicationBuilder app);

    }

}

The way WebHostBuilder approaches the initialization of your application is that it will pick up the candidate Startup type (the one configured against the WebHostBuilder) and check if it implements IStartup interface. If that’s the case, it will just use it straight up. Otherwise, it uses convention based startup logic, where the candidate Startup is inspected using reflection and the relevant configuration methods are discovered. Then delegates representing them are stuffed into a type called ConventionBasedStartup, which ends up doing the thing your Startup never did in the first place - implementing IStartup - and that instance is then registered in the DI.

All of that logic can be viewed in the source code of the WebHostBuilder.

So what does it take for us to use IStartup? IStartup doesn’t perfectly match our original Startup class, but we could very easily rewrite our original code to fit into the constraints of IStartup by doing just 2 or 3 small changes. This is shown below:

public class Startup : IStartup

{

    public Startup(IHostingEnvironment env)

    {

        var builder = new ConfigurationBuilder()

            .SetBasePath(env.ContentRootPath)

            .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)

            .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)

            .AddEnvironmentVariables();

        Configuration = builder.Build();

    }



    public IConfigurationRoot Configuration { get; set; }



    public IServiceProvider ConfigureServices(IServiceCollection services)

    {

        // Add framework services

        return services.BuildServiceProvider();

    }



    public void Configure(IApplicationBuilder app)

    {

        var env = app.ApplicationServices.GetService<IHostingEnvironment>();

        var loggerFactory = app.ApplicationServices.GetService<ILoggerFactory>();



        // Set up application pipeline

    }

}

In order to conform to the contract defined by IStartup we just need to build the IServiceProvider manually at the end of the ConfigureServices method, and drop the injection of dependencies into the Configure method. Instead, we have to resolve the necessary dependencies from the container by hand. And that’s it.

On a side note, there is also an abstract StartupBase class which you could subclass to achieve the same thing. If we use it, our implementation would look identical though.

The benefits πŸ”—

There are two main benefits of writing your startup code this way.

First, you will avoid runtime errors if your Startup class cannot be parsed into the convention based startup approach (for example if you had a typo in the method name). So no more unexpected runtime crashes - as the compiler would help us avoid them.

Second, it gives you interesting possibilities in mocking/overriding both IHostingEnvironment and the configuration - something that is rather difficult when using convention based startup.

Let’s consider a simple integration test scenario - because, as we all know ASP.NET Core is testable using the elegant in memory infrastructure.

A typical in-memory testing setup is shown below:

// Startup doesn't implement IStartup

var builder = new WebHostBuilder().UseStartup<Startup>();

var server = new TestServer(builder);

With such a setup, the testing infrastructure will wire in everything configured in your Startup and allow you to use HttpClient to interact with the in memory instance of your ASP.NET Core application.

Now imagine the following - what if your application uses some configuration settings that you’d like to override in those tests. This could be anything - for example a DB connection string, that should be changed to SQLite in memory string in case you are running integration tests?

Surely, there are plenty of ways of achieving this in ASP.NET Core - you could i.e. use environment variables to pass in the necessary values.

That said, I find it very useful to be able to tinker with configuration directly in the tests and inject things on demand directly from code level. This becomes possible when using startup classes that implement IStartup.

The example is shown:

// Startup implements IStartup

var myStartup = new Startup();



// copy existing config into memory

var existingConfig = new MemoryConfigurationSource();

existingConfig.InitialData = myStartup.Configuration;



// create new configuration from existing config

// and override whatever needed

var testConfigBuilder = new ConfigurationBuilder()

    .Add(existingConfig)

    .AddInMemoryCollection(new Dictionary<string, string>()

    {

        { "DbContext:ConnectionString", "DataSource=:memory:" }

    });





myStartup.Configuration = testConfigBuilder.Build();



var builder = new WebHostBuilder()

    .ConfigureServices(services =>

    {

         services.AddSingleton<IStartup>(myStartup);

    });



var server = new TestServer(builder);

The reason why this is possible, is that we can now interact with the instance of our startup class, before it’s consumed by the ASP.NET Core web host infrastructure, rather than relying on the convention based startup.

Hope this helps!

About


Hi! I'm Filip W., a software architect from ZΓΌrich πŸ‡¨πŸ‡­. I like Toronto Maple Leafs πŸ‡¨πŸ‡¦, Rancid and quantum computing. Oh, and I love the Lowlands 🏴󠁧󠁒󠁳󠁣󠁴󠁿.

You can find me on Github, on Mastodon and on Bluesky.

My Introduction to Quantum Computing with Q# and QDK book
Microsoft MVP