
Configuring service container and setting up request pipeline in ASP.NET Core can eat up a lot of lines of code, especially for more complex projects. A well-established way of doing this is using Startup.cs and its ConfigureServices()
and Configure()
methods. Although complete application setup can be packed into a single type, we must not forget about Single-responsibility principle. I wanted to show you a way to prevent startup from growing uncontrollably, by keeping different concerns separate from each other.
One disclaimer though. ASP.NET Core 6 has rolled in and removed the need of having Startup.cs altogether. Despite that fact, I am sure the ideas presented in this post will be relevant, even in the new era.
This post is part of C# Advent Calendar 2021. Cheers to Matthew D. Groves for letting me participate!
Basic principles
We will apply two .NET features: Options pattern and Extension methods. Similar to how ASP.NET Core uses plugin architecture, we will utilize these features to separate concerns and leave heavy lifting to dependency injection container.
One design standard, to keep in mind, is that almost all parts of ASP.NET Core are configured via IOptionsIOptions<TOptions>
instances are part of service container, we can use IConfigureOptions
MVC
Whether you are using Controllers only, or including Views or Razor Pages, MVC setup is more or less the same. The goal is to call AddControllers(), AddControllersWithViews() or AddRazorPages(), based on your scenario, and define configuration for MvcOptions, and possibly RazorPagesOptions. If you are using System.Text.Json or Newtonsoft Json.NET for model serialization, you could also configure them in a similar manner. Whole setup would look like this:
1 | public static class MvcExtensions |
Authentication
Completely driving authentication by configuration is difficult. You probably need to know what type of authentication your application uses in advance. Good news is that, once baseline is established, you can use above mentioned technique to configure individual parts. For example, you would use CookieAuthenticationOptions to configure cookies, and JwtBearerOptions for token based authentication. Extension and individual configuration options, including an example of injecting IConfiguration into one of them, would look like this:
1 | public static class AuthenticationExtensions |
Authorization
Configuring authorization can get quite tedious, if number or complexity of policies increases. Luckily, setup process is pretty straightforward, as the only concern is injecting configuration via AuthorizationOptions type.
1 | public static class AuthorizationExtensions |
OpenAPI
If you care about your REST API consumers, you are publishing service description, using OpenAPI specification. And, since we are talking about ASP.NET Core, you are probably using awesome Swashbuckle package. As with previously mentioned framework parts, this extension was designed on same principles, and can be setup in a similar fashion. Whether its generating definition file using SwaggerGenOptions
, or adjusting swagger user interface via SwaggerUIOptions
, everything can be broken down and put into separate types.
1 | public static class OpenApiExtensions |
Application Insights
How about some Azure extensions? Most of the time you will not even need them, as ASP.NET Core seamlessly integrates with most of Azure resources. However, one particular extension I recommend using are Application Insights. You want to have application telemetry under control, and this package allows you to fine tune metrics and data that will be ingested by Azure. From setup point of view, there is nothing surprising:
1 | public static class TelemetryExtensions |
Extras
You’ve probably gotten the gist of it by now; use extension methods to extract service definitions, and options to do actual service configuration. For completeness, let me give you the rest of commonly used ASP.NET Core features, including types used for configuration.
- Default files middleware via UseDefaultFiles() - DefaultFilesOptions
- Static files middleware via UseStaticFiles() - StaticFileOptions
- Cross-Origin Resource Sharing via AddCors() - CorsOptions
- API Versioning services via AddApiVersioning() and AddVersionedApiExplorer() - ApiExplorerOptions and ApiVersioningOptions
Application code
Last but not least, what about your own application code? I highly recommend following the same principles, in order to avoid having application setup in a single place. If you are using clean architecture, and have your code decoupled into layers, initialization could be as simple as:
1 | public static class ApplicationExtensions |
Conclusion
I have shown you one neat little trick that I use all the time, when setting up any kind of application. It is important to remember Single-responsibility principle, even for this type of code. Having Startup.cs consisting of several hundreds, or even thousands lines, is nothing unheard of. And, sadly, is an obvious code smell.
All code shown in this post was published to GitHub, as a single ASP.NET Core 6 project.
Comments