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.