Updated on
Every service an ASP.NET Core Web API uses is registered in Program.cs before the application starts. Left alone, that file grows into a wall of registration calls that says nothing about what the application is.
Extension methods on IServiceCollection are the fix. Each one groups the registrations for a single concern behind a name, so Program.cs reads as a list of decisions rather than a list of calls. We build a ServiceExtensions class here and use it to configure CORS, and the parts that follow add their own registrations to the same class.
If you want to see all the basic instructions and complete navigation for this series, please check the Introduction page for this tutorial.
How Do We Create the Web API Project?
After we have finished creating and populating the database in SQL Server With ASP.NET Core: Creating the Database, we are going to create a Visual Studio project for the server part of our application.
Let’s open Visual Studio and create a new ASP.NET Core Web API project and in the next window name it AccountOwnerServer.
Then in the next window, we have to provide a couple of information:
- Framework – for this series, we are going to choose .NET 10.0, but you can choose any previous version as well if you want to
- Authentication type – we’ll leave it at None
- Configure for HTTPS – we will leave it checked
- We are not going to enable Docker
- Use controllers – we want to check this since we will use controllers in our API
- Enable OpenAPI support – we are going to uncheck this since we don’t want OpenAPI or the Swagger implementation now (depending on which .NET version you are using)
After the project creation, we are going to modify the launchSettings.json file which is quite an important file for the .NET configuration. Let’s change the applicationUrl property to a new value and the launchBrowser property to false, to prevent a web browser from starting when the project starts.
In the Solution Explorer, let’s expand the Properties and double-click on the launchSettings.json file:
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "https://localhost:5001;http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
Both profiles run the project directly on Kestrel. For the sake of simplicity, we are going to use the http profile and http://localhost:5000 throughout this series, but the https profile is there whenever we want the HTTPS address on 5001 as well.
What Does Program.cs Do in a .NET Web API?
Program.cs is where an ASP.NET Core application is assembled and started. It does two things, in order, and the file is laid out to make the split visible.
First it builds. WebApplication.CreateBuilder(args) returns a builder, and every builder.Services.Add… call registers a service the application will be able to inject later. Nothing is running yet.
Then it runs. builder.Build() produces the application, each app.Use… call adds a step to the request pipeline, and app.Run() starts listening.
Everything above Build() is registration; everything below it is request handling. That single line is the seam, and most confusion about this file comes from missing it.
There is no Startup class and no Main method here. Top-level statements let the file be the program, and implicit usings supply the namespaces a web project always needs, so what remains on screen is only the decisions this application makes.
This is the file the template generates for us:
var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllers(); var app = builder.Build(); // Configure the HTTP request pipeline. app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); app.Run();
For readers who last saw ASP.NET Core in the .NET 5 days, here is the shape this replaced:
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run();
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
We can see three main changes:
- Top-level statements
- Implicit using directives
- And there is no usage of the Startup class
“Top-level statements” means the compiler generates the namespace, class, and method elements for the main program in our application. You can read more about this in our Top Level Statements article.
“Implicit using directives” means the compiler automatically adds a different set of using directives based on a project type, so we don’t have to do that manually. We can find those using directives in the generated file under obj/Debug/net10.0, named after our project with a .GlobalUsings.g.cs suffix. The folder name always tracks the target framework, so it follows the project rather than staying on one version.
Finally, we can see that now in the Program class, we can add services to the service collection right below the comment that states exactly that. In .NET 5, we would have to do this in the ConfigureServices() method. Also, we have a section (also marked with a comment) where we can add different middleware components to the application’s pipeline. In .NET 5, we had the Configure() method for this purpose.
How Do Extension Methods Keep Program.cs Readable?
An extension method is a static method that is called as if it belonged to the type it extends. Marking the first parameter with this is the whole mechanism.
In a Web API that matters because service registration accumulates. Each feature needs a handful of calls on IServiceCollection, and after a few features Program.cs is a wall of registration with no structure.
Writing ConfigureCors(this IServiceCollection services) in a static class turns that handful into one named call. Program.cs then reads as a list of concerns: configure CORS, add the controllers, and build the application.
The grouping is the point, not the syntax. A reader scanning Program.cs sees what the application is made of; a reader who wants the detail opens ServiceExtensions.
Nothing is hidden by this. The same registrations run in the same order, one indirection further away.
Let’s create a new folder Extensions in the main project and a new class inside with the name ServiceExtensions. We are going to make that class static and it will consist of our service extension methods, so it has to live in a static class:
namespace AccountOwnerServer.Extensions
{
public static class ServiceExtensions
{
}
}
How Do We Configure CORS in ASP.NET Core?
CORS is the browser rule that stops a page on one origin from reading a response from another. The server decides which origins are allowed, and ASP.NET Core expresses that decision as a named policy.
Two steps register it. AddCors() defines the policy in the service collection, and UseCors() applies it in the request pipeline. Both are needed, and the pipeline call has to sit before authorization.
The policy here allows any origin, method, and header, which is right for a sample and wrong for anything else. In production we name the origins the client actually uses.
One combination is rejected before the application starts: any origin together with credentials. ASP.NET Core throws while building the policy, so the API does not run at all until the origins are listed explicitly.
This series needs CORS because the Angular client runs on a different port from the API, and a different port is a different origin.
Our article on enabling CORS in ASP.NET Core covers the subject on its own, and the one on why any-origin and credentials cannot be combined walks through the startup failure that combination produces. So, let’s add this code to the ServiceExtensions class:
public static void ConfigureCors(this IServiceCollection services)
{
services.AddCors(options =>
{
options.AddPolicy("CorsPolicy",
builder => builder.AllowAnyOrigin()
.AllowAnyMethod()
.AllowAnyHeader());
});
}
We are using the basic settings for adding CORS policy because for this project allowing any origin, method, and header is quite enough. But we can be more restrictive with those settings if we want. Instead of the AllowAnyOrigin() method that allows requests from any source, we could use the WithOrigins("http://www.something.com") method that will allow requests just from the specified source. Also, instead of AllowAnyMethod() allowing all HTTP methods, we can use the WithMethods("POST", "GET") one that will allow only specified HTTP methods. Furthermore, we can make the same changes for the AllowAnyHeader() method by using, for example, the WithHeaders("accept", "content-type") method to allow only specified headers.
To call this extension method, we are going to modify the Program class:
using AccountOwnerServer.Extensions;
using Microsoft.AspNetCore.HttpOverrides;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.ConfigureCors();
builder.Services.AddControllers();
var app = builder.Build();
// Configure the HTTP request pipeline.
// UseForwardedHeaders goes first: every component after it has to see the client's
// scheme and address, not the proxy's, or an HTTPS redirect behind a proxy loops.
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.All
});
if (!app.Environment.IsDevelopment())
app.UseHsts();
app.UseHttpsRedirection();
app.UseCors("CorsPolicy");
app.UseAuthorization();
app.MapControllers();
app.Run();
The CORS registration goes into the first part of the file, and the CORS middleware into the second part, the one reserved for adding components to the pipeline. The important thing to notice here is that we call the UseCors() method above the UseAuthorization() method. That is not a preference: CORS must run after routing, so it can read the matched endpoint’s policy, and before authorization, so a rejected pre-flight request is not first refused for being unauthenticated.
There is one more call worth a word:
app.UseForwardedHeaders()rewrites the scheme and the client address of the current request from the proxy’s headers, which is what we need during a Linux deployment. Note that we register it beforeapp.UseHsts()andapp.UseHttpsRedirection(). Nginx forwards requests to Kestrel over plain HTTP, so until these headers are read, our app sees every request as HTTP. IfUseHttpsRedirection()runs first, it redirects requests that already arrived over HTTPS, and the browser ends up in a redirect loop.
In What Order Do Middleware Components Run?
Middleware runs in registration order, and each component decides whether the next one runs at all. The app.Use… calls in Program.cs are not configuration, they are the pipeline itself, written top to bottom.
That makes order behaviour. Moving one line changes what the application does, and the failures it produces look like bugs somewhere else entirely.
Three orderings carry most of the pain. Proxy headers are read first, or the application never learns the request arrived over HTTPS. Routing runs before anything that needs to know which endpoint was matched. Authentication precedes authorization, because deciding whether a caller may proceed requires knowing who they are.
Static files short-circuit. A matched file is served immediately and the rest of the pipeline is skipped, unless routing has already picked an endpoint for that path.
The table below is the order to start from. Departures from it are worth a comment in the code explaining why.
| Order | Middleware | Why it sits here |
|---|---|---|
| 1 | UseForwardedHeaders() | Rewrites the scheme and client IP from proxy headers. It must run before anything that reads the scheme or the client IP |
| 2 | UseExceptionHandler() / UseHsts() | Error handling wraps everything downstream; it can only catch what runs after it. In Development the framework adds the developer exception page here for us |
| 3 | UseHttpsRedirection() | Redirects plain HTTP away before any work is done on the request |
| 4 | UseStaticFiles() | Serves a matched file and short-circuits the rest of the pipeline. In minimal hosting the framework has already run routing above this line, so a path that has an endpoint goes to the endpoint and the file is not served |
| 5 | UseRouting() | Selects the endpoint. Anything that needs to know which endpoint was matched runs after this |
| 6 | UseCors() | Needs the matched endpoint to read its per-endpoint CORS policy, and must precede authorization |
| 7 | UseAuthentication() / UseAuthorization() | Establishes who the caller is, then whether they may proceed |
| 8 | MapControllers() | Runs the endpoint that routing selected |
Two of those rows are written out for reference rather than typed by us. In minimal hosting, WebApplication inserts routing at the very beginning of the pipeline and endpoint execution at the end, so our sample never calls UseRouting() and still routes correctly. Our sample does not register UseStaticFiles() either, because a controllers-only Web API has no wwwroot to serve. If we want to watch this pipeline from the inside, our article on logging from the Program class shows how to get messages out of it before the application is even running.
Conclusion
We now know how to modify the launchSettings.json file, what Program.cs actually does, how to group service registrations behind extension methods, and where each middleware component belongs in the pipeline.
From here, configuration itself is the natural next subject: where configuration values come from and how to go about binding configuration to strongly typed options.
Thank you for reading and do check out NLog in ASP.NET Core Web API: Logging to a File, in which we create our first service, the logger service, and use it for logging our messages. Two parts later we add the database, and the connection string the next parts need is worth a look before we get there.
The Ultimate ASP.NET Core Web API course builds the same project out with the configuration this series does not reach: environment-specific settings, options binding and validation, and the middleware a production API needs.
Tested with .NET 10.
