AspNet Core Nested Applications

Given any application of a reasonable size, to reason about it and manage complexity one generally applies modular programming along clear and well defined boundaries. Recently I was seeking to do this with AspNet Core where I wanted to compose several independent applications, potentially developed by separate teams, within the one host.

Nested Apps

Out-of-the box this is achieved with middleware, however this still means there is a single dependency injection container whose service registration code gets large and leaky. Occasionally there were classes and configurations that clashed. While useful for a lot of scenarios, AspNetCore middleware wasn't giving me the isolation I desired: defined controllers, own static content, auth middleware settings, independent policy definition, focused service registration and more. Basically I want multiple StartUp classes representing the different applications and connect them to a path in a host application.

Prior to AspNet Core this was easy to achieve with OWIN. Nested applications were just an AppFunc that you build and connected when wiring up an an app.MapPath("path", appFunc). With AspNet.Core we need to take another approach.

Isolated Nested Apps

From the aspnet-contrib project there is an IApplicationBuilder extension that allows adding a nested (isolated) application to a pipeline. This is a single class file that is small enough to be copied into your code base (as opposed to adding a dependency). Internally, it creates it's own independent service collection that registers a minimal set of global services resolved from the root/parent container.

Let's look at a minimal example:

public class RootStartup
{
    public void ConfigureServices(IServiceCollection services)
    {
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        app.IsolatedMap<NestedStartup>("/nested");
        app.Run(async context => await context.Response.WriteAsync("Hello World!"));
    }
}

public class NestedStartup
{
    public void ConfigureServices(IServiceCollection services)
    {
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        app.Run(async context => await context.Response.WriteAsync("Hello from Nested App!"));
    }
}

Here, requests to /nested will be routed to the application defined in NestedStartup. Neat.

There is a small gotcha though that isn't obvious in this API. As I was exploring getting this to work with IdentityServer4 I was getting NullReferenceExceptions from deep insider it with respect to accessing the HttpContext. When registering IdentityServer it registers IHttpContextAccessor. The HttpContext is owned by the root application and because this service is not registered in the root startup's, nested applications won't receive it either. To fix this I needed to register the service in the root container for it to 'flow' into nested apps:

public class RootStartup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        app.IsolatedMap<NestedStartup>("/nested");
        app.Run(async context => await context.Response.WriteAsync("Hello World!"));
    }
}

public class NestedStartup
{
    public void ConfigureServices(IServiceCollection services)
    {
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        app.Run(async context => await context.Response.WriteAsync("Hello from Nested App!"));
    }
}

If the root app has IHttpContextAccessor registered, the IsolatedMap extension will then register it in the nested app's container.

This issue describes why IHttpContextAccesor is not registered by default. At this time, I am not aware of any other services that might need explicit registering in the root app container to supported nested apps.

Injecting Service into Nested Startup

Many nested applications may need a service that needs to be injected into their startup class. To support such, just register the service in the root app container and it will be injected when the nested app's startup is activated:

public class RootStartup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.TrysAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
        services.AddSingleton(new NestedAppSettings());
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        app.IsolatedMap<NestedStartup>("/nested");
        app.Run(async context => await context.Response.WriteAsync("Hello World!"));
    }
}


public class NestedAppSettings { }

public class NestedStartup
{
    private readonly NestedAppSettings _settings;

    public NestedStartup(NestedAppSettings settings)
    {
        _settings = settings;
    }

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton(_settings);
    }

    public void Configure(IApplicationBuilder app, IHostingEnvironment env)
    {
        app.Run(async context => await context.Response.WriteAsync("Hello from Nested App!"));
    }
}

Controller discovery

Something to be aware of is that AspNetCore MVC will assembly scan to discover controllers. Want we absolutely don't want happening is any nested apps discovering controllers from other nestesd apps. Therefore you must override this behavior with more explicit registration. Refer to the section Handling MVC in this blog post that describes an approach.s This post also describes a mechanism to host independent apps ("Pipelines") however I think the aspnet-contrib approach described in this post is superior.

Wrapping up

My advice on when to use this is only when your application is sufficiently large / partitioned. In other words, sparingly. I also think that AspNet Core should have first class support for this model.

The sample code is on github: https://github.com/damianh/lab/tree/master/dotnet/AspNetCoreNestedApps

Hat tip to Kévin Chalet for developing and publishing this very useful utility.