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 Program.cs and 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 Program.cs and 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(). Within 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 AddScoped(), AddSingleton(), or 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 Startup.cs.

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 Program.cs structure, 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:

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 Program.cs and 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 Program.cs:

  • Register services
  • Register middleware
  • Register end-points

As 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 this operator:

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, RegisterApplicationServices() and 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:


Combining Program.cs and 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 Program.cs and 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 🤘