Skip to main content
Published / updated

ASP.NET Core Fundamentals and Program.cs

Before you start

You need: C# including async/await (Track 03).

In Visual Studio: the ASP.NET and web development workload. Add it from Tools → Get Tools and Features if the Web API template is missing.

Time: about 50 minutes, plus the practice.

Learning objective

Create an ASP.NET Core project, explain every file in it, and describe what happens between a request arriving and a response leaving.

Topics

  • What ASP.NET Core is, and how it differs from the Framework
  • Creating a project
  • Project structure
  • Program.cs line by line
  • The builder and the app
  • Kestrel and reverse proxies
  • launchSettings.json
  • The request journey

What it is

ASP.NET Core is a cross-platform framework for web applications and APIs. It replaced ASP.NET Framework, and the differences that matter day to day:

.NET FrameworkASP.NET Core
PlatformWindows onlyWindows, Linux, macOS
HostingIISKestrel, behind any proxy, or in a container
StartupGlobal.asax, Web.configProgram.cs
ConfigurationXMLJSON, environment variables, secrets
Dependency injectionBolted onBuilt in
PipelineModules and handlersMiddleware
DeploymentFramework installed on the serverSelf-contained or framework-dependent

Everything in this track targets modern .NET. The Web Forms track covers the older stack, and it is maintenance-only.

Creating a project

  1. File → New → Project, or Create a new project.
  2. Search for the template you want and pick the C# one:
TemplateGives you
ASP.NET Core Web APIController-based API — what this track uses
ASP.NET Core Web App (Model-View-Controller)MVC with views
ASP.NET Core Web AppRazor Pages
ASP.NET Core EmptyNothing but Program.cs
  1. Name it NexCoding.SchoolPortal.Api. Next.
  2. Framework .NET 9.0, Authentication None, Configure for HTTPS ticked, Use controllers ticked. Create.

Tick "Use controllers". Left unticked you get a minimal API — endpoints written directly in Program.cs — and every controller example in this track will have nowhere to go. If your new project has no Controllers folder, that box was clear.

Running it

ActionHowShortcut
Run with debuggingThe green ▶ button, which names the profileF5
Run without debuggingDebug menuCtrl+F5
Hot ReloadThe 🔥 button in the toolbar while running
Switch profile (http / https / IIS Express)The dropdown beside ▶

Use Hot Reload while developing. Edit a controller, save, and the running application picks up the change without a restart. Restarting by hand after every edit wastes a large part of the day.

The dropdown beside the ▶ button chooses the launch profile. Pick https — the track assumes HTTPS throughout, and the profiles come from launchSettings.json, covered below.

Project structure

NexCoding.SchoolPortal.Api/
├── Program.cs startup and configuration
├── appsettings.json configuration
├── appsettings.Development.json development overrides
├── Properties/
│ └── launchSettings.json local run profiles — not deployed
├── Controllers/
│ └── StudentsController.cs
├── Models/ entities and DTOs
├── Services/ business logic
├── Data/ repositories
├── wwwroot/ static files (web apps)
└── NexCoding.SchoolPortal.Api.csproj
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Dapper" Version="2.1.35" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
</ItemGroup>

</Project>

Sdk="Microsoft.NET.Sdk.Web" is what makes it a web project — it brings in the framework reference and treats wwwroot as static content.

<Nullable>enable</Nullable> should stay on. It makes the compiler warn when a reference type that could be null is dereferenced, which removes a large class of runtime failures. Turning it off in a new project to silence warnings is a false economy.

ImplicitUsings adds common using directives automatically, which is why a new file compiles without using System;.

Program.cs

var builder = WebApplication.CreateBuilder(args);

// ---- Service registration ----
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddScoped<IStudentRepository, StudentRepository>();
builder.Services.AddScoped<IStudentService, StudentService>();

var app = builder.Build();

// ---- Middleware pipeline ----
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
else
{
app.UseExceptionHandler("/error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseStaticFiles();

app.UseRouting();

app.UseAuthentication();
app.UseAuthorization();

app.MapControllers();

app.Run();

The file has exactly two halves, and confusing them is the most common startup error.

HalfBoundaryWhat belongs there
RegistrationBefore builder.Build()builder.Services.Add... — what the app can do
PipelineAfter builder.Build()app.Use..., app.Map... — what happens per request

Calling builder.Services.AddScoped(...) after Build() throws — the service collection is frozen once the container is built. The exception message is clear, and the fix is always to move the line up.

CreateBuilder

WebApplication.CreateBuilder(args) does a great deal before you write anything:

  • Sets the content root to the current directory
  • Loads configuration from appsettings.json, appsettings.{Environment}.json, user secrets in Development, environment variables, then command-line arguments — in that order, each overriding the last
  • Configures logging to console and debug
  • Registers the DI container
  • Configures Kestrel

That configuration order is worth memorising. It is why an environment variable beats appsettings.json, and why a stale environment variable on a server overrides a config change nobody can find.

Build and Run

var app = builder.Build(); // constructs the DI container
app.Run(); // starts the server and blocks until shutdown

app.Run() blocks. Code after it executes only during shutdown.

app.Run(); // default URLs from configuration
app.Run("http://localhost:5001"); // override, ignoring launchSettings

Kestrel and reverse proxies

Kestrel is the built-in cross-platform web server. It runs the application in every hosting model.

Internet → IIS / Nginx / Apache → Kestrel → Your application

Kestrel can be exposed directly, but in production it usually sits behind a reverse proxy that handles TLS termination, request buffering, static file caching and load balancing.

That arrangement creates one problem worth knowing about:

app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});

Behind a proxy, Request.Scheme is http and Request.Host is the internal address — because that is what Kestrel actually received. Generated URLs then point at the wrong place, and UseHttpsRedirection can produce a redirect loop.

UseForwardedHeaders reads the X-Forwarded-* headers the proxy set and restores the original values. It must be the first middleware, before anything that reads the scheme or host.

launchSettings.json

{
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5099",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"launchBrowser": true,
"applicationUrl": "https://localhost:7099;http://localhost:5099",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

Choose the profile from the dropdown beside the ▶ button in the toolbar.

launchSettings.json is a local development file and is never deployed. In production the environment comes from the ASPNETCORE_ENVIRONMENT environment variable, and the URLs from ASPNETCORE_URLS or the hosting configuration.

That is why "it works locally" and "the deployed app thinks it is in Production" are both true and consistent — one reads this file, the other does not.

The HTTPS profile needs a development certificate:

Visual Studio offers to create and trust one the first time you run an HTTPS project — say yes, and accept the Windows certificate prompt that follows.

If you dismissed it, the fix is a one-line command in a terminal:

dotnet dev-certs https --trust

Without it the browser refuses the connection, which looks like the application failing to start.

Minimal APIs

For a small service, endpoints can live in Program.cs directly:

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped<IStudentService, StudentService>();

var app = builder.Build();

app.MapGet("/api/students", async (IStudentService service, int schoolId, CancellationToken ct) =>
{
var students = await service.GetBySchoolAsync(schoolId, ct);
return Results.Ok(students);
});

app.MapPost("/api/students", async (
StudentCreateRequest request, IStudentService service, CancellationToken ct) =>
{
var publicId = await service.CreateAsync(request, ct);
return Results.Created($"/api/students/{publicId}", new { publicId });
});

app.Run();

Dependencies and route values are injected as parameters. Concise, and it does not scale past a handful of endpoints without organising them into extension methods.

Use minimal APIs forUse controllers for
Small services, a few endpointsMany endpoints
MicroservicesFilters and conventions
Highest throughputFamiliar structure for a team

This track uses controllers, because the structure generalises and because most existing codebases use them. Everything about routing, binding, validation and DI applies to both.

The request journey

Request arrives at Kestrel
→ HttpContext created
→ Middleware 1 (e.g. exception handler)
→ Middleware 2 (HTTPS redirection)
→ Middleware 3 (routing — endpoint selected)
→ Middleware 4 (authentication)
→ Middleware 5 (authorization)
→ Endpoint: controller action
- model binding
- validation
- action executes
- result produced
← response travels back out through each middleware
← Kestrel writes the response

Two properties of this shape explain most ASP.NET Core behaviour:

  • Middleware order is execution order. Registering authorization before authentication means the user is always anonymous.
  • Each middleware sees the request going in and the response coming out. That is how the exception handler catches everything registered after it, and how response-time logging works.

Everything else in this track fits into that journey: middleware in article 2, configuration in 3, DI in 4, routing and controllers in 5 and 6, binding and validation in 7.

Errors you will hit

MessageCauseFix
No Controllers folder in a new projectUse controllers was unticked — you got a minimal APIRecreate with it ticked
Browser refuses the HTTPS connectionDevelopment certificate not trustedAccept the prompt, or dotnet dev-certs https --trust
Unable to start Kestrel ... address already in useAnother instance still runningStop it, or change the port
The app thinks it is in Production locallyASPNETCORE_ENVIRONMENT not set in the profileCheck launchSettings.json
Swagger page is emptyNo controllers, or the wrong launch URLCheck the profile's launchUrl
InvalidOperationException: Unable to resolve serviceService not registeredAdd it in Program.cs before Build()

Registration happens before builder.Build(); the pipeline after it. Calling builder.Services.Add... after Build() throws, and the message is clearer than most.

Common mistakes

  • Registering services after builder.Build()
  • Confusing the registration half with the pipeline half
  • Expecting code after app.Run() to execute
  • Disabling nullable reference types to silence warnings
  • Expecting launchSettings.json to apply in production
  • Not trusting the development certificate, then reporting a startup failure
  • Missing UseForwardedHeaders behind a proxy, producing wrong URLs or a redirect loop
  • Not knowing the configuration precedence order
  • Not using Hot Reload, and restarting by hand after every edit

Practice

The course exercise starts with project creation and structure.

  1. Create an API project with --use-controllers. Run it and reach the default endpoint.
  2. Read Program.cs and write down, for each line, whether it is registration or pipeline.
  3. Move a builder.Services.AddScoped call to after builder.Build(). Record the exception, then move it back.
  4. Add Console.WriteLine("after run") after app.Run(). Confirm when it prints.
  5. Print app.Environment.EnvironmentName at startup. Run with each launch profile.
  6. Set ASPNETCORE_ENVIRONMENT=Production in your shell and run again. Confirm the value changed and Swagger disappeared.
  7. Add a setting to appsettings.json, override it in appsettings.Development.json, then override it again with an environment variable. Print the resolved value at each step.
  8. Accept the development certificate prompt and confirm the https profile works.
  9. Create a second Web API project with Use controllers unticked and compare its Program.cs with the controller-based one.
  10. Add one minimal API endpoint alongside your controllers and confirm both work.
  11. Run the project, edit a controller, click Hot Reload, and confirm the change applies without a restart.
  12. Right-click the project → PublishFolder, set Configuration to Release, and list what ends up in the output. Confirm launchSettings.json is not there.

Exercises 6 and 7 explain most "it works on my machine" incidents.

You can now

  • Create a controller-based Web API project in Visual Studio
  • Explain every line of Program.cs
  • Tell registration from pipeline, and say why the order matters
  • Use launch profiles and Hot Reload
  • Read launchSettings.json and say what is not deployed

Review questions

  1. What separates the two halves of Program.cs, and what happens if you mix them?
  2. In what order does configuration load, and which source wins?
  3. Why is launchSettings.json irrelevant in production?
  4. What does UseForwardedHeaders fix, and why must it come first?

Next: Middleware and the request pipeline