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.csline 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 Framework | ASP.NET Core | |
|---|---|---|
| Platform | Windows only | Windows, Linux, macOS |
| Hosting | IIS | Kestrel, behind any proxy, or in a container |
| Startup | Global.asax, Web.config | Program.cs |
| Configuration | XML | JSON, environment variables, secrets |
| Dependency injection | Bolted on | Built in |
| Pipeline | Modules and handlers | Middleware |
| Deployment | Framework installed on the server | Self-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
- File → New → Project, or Create a new project.
- Search for the template you want and pick the C# one:
| Template | Gives you |
|---|---|
| ASP.NET Core Web API | Controller-based API — what this track uses |
| ASP.NET Core Web App (Model-View-Controller) | MVC with views |
| ASP.NET Core Web App | Razor Pages |
| ASP.NET Core Empty | Nothing but Program.cs |
- Name it
NexCoding.SchoolPortal.Api. Next. - 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
| Action | How | Shortcut |
|---|---|---|
| Run with debugging | The green ▶ button, which names the profile | F5 |
| Run without debugging | Debug menu | Ctrl+F5 |
| Hot Reload | The 🔥 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.
| Half | Boundary | What belongs there |
|---|---|---|
| Registration | Before builder.Build() | builder.Services.Add... — what the app can do |
| Pipeline | After 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 for | Use controllers for |
|---|---|
| Small services, a few endpoints | Many endpoints |
| Microservices | Filters and conventions |
| Highest throughput | Familiar 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
| Message | Cause | Fix |
|---|---|---|
No Controllers folder in a new project | Use controllers was unticked — you got a minimal API | Recreate with it ticked |
| Browser refuses the HTTPS connection | Development certificate not trusted | Accept the prompt, or dotnet dev-certs https --trust |
Unable to start Kestrel ... address already in use | Another instance still running | Stop it, or change the port |
| The app thinks it is in Production locally | ASPNETCORE_ENVIRONMENT not set in the profile | Check launchSettings.json |
| Swagger page is empty | No controllers, or the wrong launch URL | Check the profile's launchUrl |
InvalidOperationException: Unable to resolve service | Service not registered | Add 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.jsonto apply in production - Not trusting the development certificate, then reporting a startup failure
- Missing
UseForwardedHeadersbehind 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.
- Create an API project with
--use-controllers. Run it and reach the default endpoint. - Read
Program.csand write down, for each line, whether it is registration or pipeline. - Move a
builder.Services.AddScopedcall to afterbuilder.Build(). Record the exception, then move it back. - Add
Console.WriteLine("after run")afterapp.Run(). Confirm when it prints. - Print
app.Environment.EnvironmentNameat startup. Run with each launch profile. - Set
ASPNETCORE_ENVIRONMENT=Productionin your shell and run again. Confirm the value changed and Swagger disappeared. - Add a setting to
appsettings.json, override it inappsettings.Development.json, then override it again with an environment variable. Print the resolved value at each step. - Accept the development certificate prompt and confirm the https profile works.
- Create a second Web API project with Use controllers unticked and compare its
Program.cswith the controller-based one. - Add one minimal API endpoint alongside your controllers and confirm both work.
- Run the project, edit a controller, click Hot Reload, and confirm the change applies without a restart.
- Right-click the project → Publish → Folder, set Configuration to Release, and list what ends up in the output. Confirm
launchSettings.jsonis 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.jsonand say what is not deployed
Review questions
- What separates the two halves of
Program.cs, and what happens if you mix them? - In what order does configuration load, and which source wins?
- Why is
launchSettings.jsonirrelevant in production? - What does
UseForwardedHeadersfix, and why must it come first?