If you want to upgrade your .NET Core/ASP.NET 5 website to .NET 6, you will need to consider what you will want to do with your
Startup.cs files 🤔
Within .NET 6,
Startup.cs has been sunsetted. In the new world, it is still possible to make use a `Startup.cs, however, it is not a mandatory class anymore. This means when upgrading, you do not need to do anything if you so wish, however, if you want your code to use some of the latest framework features, you will need to perform some refactoring by munging the two files together.
Without some proper thought and consideration, it is very easy for your
Program.cs to become a complete jumble and mess. Within ASP.NET 6, we now have several new ways of structuring classes, including minimal APIs, file scope namespaces, the removal of top-level statements and global using to name a few. You can prevent the quality of your
Program.cs from decaying over time, by applying some simple patterns.
If you want to learn how to create a production-ready
Program.cs that combines all the code from a
Startup.cs file, you have come to the right place. In this tutorial, I will convert an API based on the Microsoft template using the new minimal style and refactor it so that afterwards it is production-ready and scalable. If you want to master
Program.cs, read on 🔥🔥🔥
Program.cs And Startup.cs
Every application needs a starting point and this is where
Startup.cs comes into the mix.
Program.cs is the first class that will be called whenever your application is initiated.
Program.cs is responsible for bootstrapping the rest of your application. In .NET 5 and Core, within
Program.cs you had to define a
Main() method which looked like this:
The second class,
Startup.cs, was the class where you could add the code to configure how your application worked. A typical
Startup.cs had two main purposes.
The class's first purpose was to register services with the dependency injection container. To register stuff you needed to add code into a method called
Services(), you are free to register anything, ranging from services required by a third-party NuGet package, services required by some Microsoft framework features, as well as custom services related to your application. By using methods like
AddTransient() you have complete control over how the container instantiates new dependencies.
The second purpose that
Startup.cs solved was to be the class that defines how the application responds to incoming HTTP requests. Any code that defines how your application's HTTP pipeline works should be added to a method called
Configure() inside of
The main responsibility of
Configure() is to register what frameworks and middleware you want to be applied to those incoming HTTP requests.
After successful registration, these middlewares will be applied to all HTTP requests made to your application 😊
As of .NET 6, a few new features have been released that change how these two files have historically worked. After upgrading to .NET 6, you will have access to new features like file-scoped named spaces, a new minimal
Startup.cs is optional, global using statements, and minimal APIs.
I won't cover any of these features in detail here, however, if you are new to .NET 6 and want to learn more about these features I have created some useful resources on these topics here:
- ASP.NET 6 Minimal APIs: Will They Make Your Life Easier?
- The Best Features Of C# 10 That You Should Start Using Today
- Install ASP.NET 7 And Get Started With C#11 Today!
Combining all these things together means that the way we can construct
Program.cs has changed pretty substantially. One cool thing about .NET is that it has good backwards compatibility. When upgrading an app, if you cant be arsed, you can simply update the
csproj and leave your
Startup.cs alone. If you want to use these features, this raises the question, how do you refactor your code to work with .NET 6?
The first takeaway is that in the new world, it is now possible to bootstrap your application, with a single file rather than two files 🤔. This file can also be created with a lot less noise, as can be seen below:
The code in the snippet above is everything you need to add within
Program.cs to successfully start your application. This is a big reduction in boilerplate code compared to .NET 5.
While the simplification is nice for simple apps, it can cause a second much worse issue, large classes. In terms of good software development patterns, large classes are frowned upon in. When your application scales, without some careful consideration your
Program.cs will quickly become a god class. Within a production application, you will want to design your
Program.cs from the outset so that it can scale. This is what we will look at now 💥
Designing a production-ready application!
First, I want to point out that if you want to play with a working example of the code within this tutorial, you can clone my Chuck Norris API for free from here. You're welcome 😉
In order to make a production-ready
Program.cs you need to make sure that it is easy to understand. Secondly, you need to make sure it can scale. Going back to software development 101, in order to make something scalable we need to follow the single-responsibility pattern. Creating lots of little classes that do one thing well. If we want to apply this rule to this situation, we need to consider the three main functions of
- Register services
- Register middleware
- Register end-points
Program.cs has three main functions, we can create three additional classes to abstract some of the logic out of
Progam.cs. As we are writing code directly within
Program.cs, we can not use dependency injection to gain access to these additional classes.
Program.cs is the file where we register things for dependency injection, rather than the file where we can get access-to-things auto-magically. This means we will need to either access these new classes by instantiating them directly within
Prorgam.cs, access the classes using
static, or, access the classes by exposing an extension method.
In this scenario, to make the most readable and concise code, I recommend that you make use of the extension method feature. Normally, if we were working at the controller level, I would not recommend creating custom extension methods. Typically, I will always recommend accessing classes in code using dependency injection, however,
Program.cs is the exception to this rule. To create an extension method to register new services, you can pass in
IServiceCollection into a class and use the
Using this class means that within
Program.cs you could construct a class that looks something like this:
The other responsibility within
Startup.cs is to configure the middleware/HTTP pipeline. This code can also be extracted into a separate class using an extension method. The difference in this scenario is that you need to pass over a different type,
WebApplication. The code to do that looks like this:
When it comes to writing code, naming is important. That is why I call my extension methods,
ConfigureMiddleware(). You will want to name these methods to whatever makes you happy, IMHO these make sense.
If you are building an API within .NET 6 that uses the new minimal API structure, the final consideration is how you register the API endpoints. In minimal API you do not use controllers but define everything within
Program.cs as well.
As API registration is also done at the incoming HTTP level, this means you can abstract the end-point registration code into a separate class as well using an extension method of type
WebApplication. To create an extension method to add end-points you can use this code:
Combining all these extension classes together, you can then create a minimal
Program.cs class that looks like this:
I personally think that this code is easy to understand, uncluttered from boilerplate code, and more importantly, it won't grow unyieldly over time. This gives you an overview of how to create a framework on top of
Program.cs that scales, let us now drill into the detail 🔨
How To Register Dependencies and Services
ServiceInitializer: Within the
ServiceInitializer class, you will want to add all the codes to register your service with the DI container. The main reason people will come to this file while building your application is to add new custom dependencies with the dependency injection framework. As this will be the code that changes the most, I recommend adding this into its own method. Often the order in which things are registered is important. Often you will need to ensure third-party services are registered before you register your own custom dependencies. Even though I add the code to register custom dependencies last, the method should be the second one in your class structure:
When it comes to registering third-party services, to make things light-weight, you can abstract the code required for each service into its own method. In the example above, I've added all the swagger-related code into a method called
RegisterSwagger. I hope you agree that the class above is super-easy to understand.
In almost all applications, you will need to make some routing and redirect rules. These rules can include registering end-points. The code to do that looks like this:
Startup.cs into a single file, mixed with minimal API can mean your
Program.cs class can turn into a car wreck. With a little bit of thought, some good naming conventions and some abstractions, spitting up your
Program.cs so that it is scalable and production-ready is easy enough.
Using the simple pattern outlined above, when you are upgrading from .NET 5 to .NET 6, most of the effort to combine
Startup.cs is simply a task of copying and pasting code to the correct methods in the new world. The pattern outlined above is meant for some food for thought, use the names and approaches that make you happy. Happy Coding 🤘