1

Is there a native mechanism in AspNet Core that allows splitting the work being done inside a monolithic Startup class, in a way as to improve readability/maintainability/scalability in the long run? If so, how does it work?

We have a somewhat small .Net Core MVC WebAPI project that abstracts some product catalog concerns, but the Startup class is growing fast and getting hard to read and maintain in my opinion.

Here are some statistics:

  • 244 lines of code
  • 32 using namespace directives
  • ~50 lines of manual domain-level container registrations

While this may not sound like a big deal, compared to a few classes following the SOLID principles in the rest of the project this can be daunting (especially the number of different namespaces included which are a good indication of SRP violation).

I could create a few additional .AddX() extension methods to reduce a good part of the manual DI registration code (for example, something on a "per module" basis or closely resembling Registry/Module from Autofac or Structuremap) like what is described here, but even then I'll be left with a good chunk of unrelated and somewhat complex logic for registering/configuring stuff like:

  • Mvc (including custom filters, serialization options, OData routes, OData EDM model builder)
  • Swagger (again including customizations and various settings)
  • ApiVersioning
  • Cors configuration
  • Complex IConfiguration builder using an external configuration system
  • explicit IsDevelopment check for configuring default exception page

These all seem like completely isolated, independent concerns, and I feel like I'm violating SRP by putting them together into the same class.

Is there a known mechanism I could leverage to split the work being done inside Startup into separate classes, to more closely follow SRP for example? Would that be advisable?

Even if aspnet core only supports a single Startup class (I have found no confirmation on this) I think I could come up with some sort of composite implementation with child Startup classes each dealing with one of these concerns, but I didn't want to reinvent the wheel or increase the complexity too much if a similar mechanism was already widely available and built for that purpose.

The fact that the class is so big also makes it much harder to have clean "per-environment" configurations, which are natively supported by the convention system, due to potential massive code duplication it would cause.

For instance, we have this small code section inside Configure method:

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    // lots of code here

    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Home/Error");
    }

    // ...and lots of code here
}

If this logic was abstracted in a completely isolated configuration class, we could have something like this instead:

public class ErrorPageConfigurationStartup
{   
    private readonly IApplicationBuilder _app;

    public ErrorPageConfigurationStartup(IApplicationBuilder app)
    {
        _app = app;
    }

    public void Configure()
    {
        app.UseExceptionHandler("/Home/Error");           
    }

    public void ConfigureDevelopment()
    {
        app.UseDeveloperExceptionPage();
    }
}

Or even this, leveraging method-level injection:

public class ErrorPageConfigurationStartup
{   
    public void Configure(IApplicationBuilder app)
    {
        app.UseExceptionHandler("/Home/Error");           
    }

    public void ConfigureDevelopment(IApplicationBuilder app)
    {
        app.UseDeveloperExceptionPage();
    }
}

I could come up with similarly small classes for most of the concerns listed above, which would result in drastically simpler logic overall due to reduced dependencies/responsibilities.

I'm looking for ways to achieve this without having to create a significant amount of custom infrastructure code to support it.

julealgon
  • 7,072
  • 3
  • 32
  • 77
  • What is growing hard? Is it DI registrations? These can (and should) be abstracted away in their own extension method. same applies for everything else actually – Tseng Nov 05 '18 at 21:27
  • Yes, DI registrations is one aspect that of course will tend to grow with the functionality in the API itself @Tseng. I'm not quite sure I follow you when you say everything can be refactored into extension methods similarly though. How do you refactor the MVC/Cors/OData/ApiVersioning/etc concerns I mentioned? I mean, they are already mostly extension methods themselves. – julealgon Nov 05 '18 at 21:31
  • Looks like I got some downvotes. I'd really appreciate if you could share the reasons so that I can improve the question. – julealgon Nov 05 '18 at 22:53

1 Answers1

1

Our startup file has grown quite a bit, but most of it is abstracted away behind classes and helper methods:

DI > Startup has configure method > goes to a DI bootstrapper > goes to a file I named IocConfig.cs that contains the composite root. Swapping the container took a couple hours last time I did it with this as a bonus.

For .NET Core, the config is called directly as the container is built in, see : https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-2.1

Swagger > when you install the nuget it should have given you a config file already, again w config 1 line in startup.

If this isn't the case in .net Core, I'd still create the config file by hand and move my code.

Past that it's all doing more of the same, and is language agnostic, make a method or a provider class to abstract out the logic and have it fire off a line or two in startup.

There's no standard here from what I can tell, how far you choose to go is up to you on abstracting out the code. For example, my oauth configure methods are methods at the bottom of startup.cs (they then call more classes) weighing in about a dozen lines each, so moving them to their own classes doesn't make a lot of sense, however the caching singleton is a bit more complex, so it gets a cachingprovider.cs file.

RandomUs1r
  • 4,010
  • 1
  • 24
  • 44
  • Thanks for your answer. Not marking as the actual answer yet as I wanted to wait for a definition on this: "There's no standard here from what I can tell..." – julealgon Nov 05 '18 at 21:39
  • @julealgon I think you mean clarification... ;) but I gotcha. As with all design patterns there's competing concerns. SRP competes with encapsulation for example. So, all I'm saying here is use the level of abstraction that makes the most sense to your problem at hand. Provider class vs helper method vs inline startup code. Hope that makes sense! – RandomUs1r Nov 05 '18 at 22:18
  • Thanks for the follow-up, but what I meant was that I'd wait for a definitive answer about aspnet core supporting the split via a native mechanism. Your answer seemed to imply you were not aware it existed, just that you didn't know about it (as I don't). If that's not the case, and you know for sure such mechanism really doesn't exist, I kindly ask you to update your answer (preferably with sources) so that I can mark it as accepted. – julealgon Nov 05 '18 at 22:51
  • @julealgon Ah, yeah I'm not aware of one, and we're looking at migration to .net Core 2 is where we're at, however see my answer for the update on the differences I'm aware of. I don't think there's an overarching technical mechanism for SOLID principles in .net Core, just various individual implementations of providers. – RandomUs1r Nov 05 '18 at 23:01