I want to talk about this point made about Keeping a Clean Startup.cs in Asp.Net Core by K. Scott Alan. Mostly I want to agree and elaborate on it.

(Warning! I used the British English customised below. If this doesn’t match your sensibilities, just use customize instead.)

The Idea

The basic idea is that we have a Startup.cs file, where we configure our application. This becomes a dumping ground for all sorts of configuration of both services, and pipeline if we let it. This is a simple pattern to avoid the mess and organise your code.

It is a simple ‘what’ vs ‘how’ situation. In your Startup.cs you want to see the ‘what’. The ‘how’ should be abstracted away to go look at separately.

The simple example given in the original example by OdeToCode is the final code that looks like this:

public void ConfigureServices(IServiceCollection services)
{
    // ...
    services.AddCustomizedMvc();
    services.AddCustomizedIdentity();
    services.AddDataStores();
    // ...
}

Unpacking the idea

I have a few specific implementation details I have taken away from this.

Naming

The first is a naming convention. We already have extension methods for IApplicationBuilder in Configure and IServiceCollection in ConfigureServices. These follow a convention of AddX and UseX and are pipelines that take and return the Builder/Services. If you are building extensions in a library, this convention should be followed for these.

If you are creating these application-specific Extensions, the naming convention UseCustomisedX and AddCustomisedX works well, both showing their intent while distinguishing local vs library items.

public static IServiceCollection AddCustomisedMvc(this IServiceCollection services)
{
    // ...
    return services;
}

public static IApplicationBuilder UseCustomisedMvc(this IApplicationBuilder app)
{
    // ...
    return app;
}

Cohesion

Most, but not all, components added to Startup have both an Add and a Use. That is, you register services, and you add to the pipeline. Now that we pull the ‘how’ of these out into extensions methods, we can group them together in the same file. It just makes sense. So if we have our MVC customisations, we create a MvcExtensions class.

public static class MvcExtensions
{
    public static IServiceCollection AddCustomisedMvc(this IServiceCollection services)
    {
        services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

        return services;
    }

    public static IApplicationBuilder UseCustomisedMvc(this IApplicationBuilder app)
    {
        app.UseMvc(routes =>
        {
            routes.MapRoute(
                name: "default",
                template: "{controller=Home}/{action=Index}/{id?}");
        });

        return app;
    }
}

Namespacing

I would love to put these Extensions in a folder called Startup. But it is bad practice to have a Class and Namespace called the same thing. So instead I use StartupExtensions as the namespace/folder. inside are all the XExtensions classes that our system is using.

Pass through dependencies

You may find that your extension needs IConfiguration or IHostingEnvironment. That is fine. You can pass these into the method as necessary in the normal way.

Piping

If you correctly build your customised methods as both Extension Methods, and as pipes, you can keep the code even cleaner. You could use the lambda syntax to create a functional version of each method like this:


public void ConfigureServices(IServiceCollection services) => services
    .AddCustomisedCookies()
    .AddCustomisedMvc();

public void Configure(IApplicationBuilder app, IHostingEnvironment env) => app
    .UseCustomisedErrorHandling(env)
    .UseHttpsRedirection()
    .UseStaticFiles()
    .UseCustomisedMvc();

Conditional Flow

You may have conditional flow in your middleware pipeline. I have two different options, you can pick the right one for the right situation.

Put it in the Extensions

If the logic fits nicely into one particular aspect of your setup (such as Error Handling and Development mode) you can just keep the conditions you need but inside that extension.

public static IApplicationBuilder UseCustomisedErrorHandling(this IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
    }

    return app;
}

Branching middleware

If your middleware has complex pipeline branching, you probably should consider that more of a what than a how. I would recommend keeping this in the Startup, still. An example of this might be:

app.UseCustomisedErrorHandling();
app.MapWhen(
    ctx => IsAdminArea(ctx.Request.Path),
    builder =>
    {
        builder.UseCustomisedAuthorisation();
        builder.UseCustomisedMvc();
    });
app.UseCustomisedMvc();

What you can see here at a high level is ‘what’ is configured for each of the branches, while still abstracting away the ‘how’.

However, if you have a more aspect-specific MapWhen, (such as “When the URL is /ping return a 200”) this can probably still be pulled out into an Extension. The above only really applies when you have branched complex middleware pipelines.

Sample

I ran a dotnet new mvc to produce a new project. I created a before and after version and you can see an example of the refactoring following the above rules. This can be found at github.com/csMacnzBlog/CleanStartup. There is also a Full Pipeline branched version available.