Skip to main content

11.1 - HTTP and ASP.NET Core Basics

Recap

In 10.2 - async/await the console AI became responsive with Task and CancellationToken, and in 08.1 - Repository & DI you hid IGameRepository and IConfigRepository behind a DI container so that ConsoleUI never creates a DbContext by hand. This week the same class libraries get a second front-end: a browser. Nothing in GameEngine, DAL.Json or DAL.EF changes — we only add a new UI project, WebApp.

By the end of this lecture you should be able to:

  • Read an HTTP request and response (method, path, headers, status code, body) in the browser's network tab.
  • Explain what Kestrel is and what Program.cs does in a minimal-hosting ASP.NET Core app.
  • Describe the middleware pipeline and why its order matters.
  • Create a Razor Pages project, add a page with a PageModel, and predict its URL from its file path.
  • Run the app with dotnet run or dotnet watch over HTTPS with a trusted development certificate.
Demo code

Lecture demos: csharp-2026-fall

HTTP in ten minutes

HTTP is a plain-text protocol. The browser sends a request (a verb, a path, some headers, optionally a body); the server answers with a response (a status code, some headers, optionally a body). That is the whole model — every web framework, including ASP.NET Core, is just a nicer way of producing responses.

A GET request and its response

Typing https://localhost:7123/Configs into the address bar produces roughly this (headers trimmed):

GET /Configs HTTP/1.1
Host: localhost:7123
Accept: text/html,application/xhtml+xml
Accept-Language: en-US,en;q=0.9,et;q=0.8
Cookie: .AspNetCore.Antiforgery.abc=CfDJ8...
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Content-Length: 2317

<!DOCTYPE html>
<html lang="en">
<head>
<title>Configurations - WebApp</title>
...

The first line is the request line: method, path, protocol version. Then headers as Key: Value pairs, an empty line, and (for GET) no body. The response starts with the status line, then headers, an empty line, then the body — here HTML.

A POST request and its response

Submitting the "create configuration" form sends the field values in the body, URL-encoded:

POST /Configs/Create HTTP/1.1
Host: localhost:7123
Content-Type: application/x-www-form-urlencoded
Content-Length: 97

Name=Classic&BoardWidth=3&BoardHeight=3&WinLength=3&__RequestVerificationToken=CfDJ8...
HTTP/1.1 302 Found
Location: /Configs
Content-Length: 0

Note the body: key=value&key=value. Those keys are exactly the name attributes of the form's inputs, and next lecture you will see ASP.NET Core map them onto C# properties. The response has no body at all — 302 plus Location tells the browser "now GET this other URL". That redirect is deliberate; we will come back to it as Post-Redirect-Get.

Methods

MethodMeaningHas bodyIn Razor Pages
GETread; must not change server statenoOnGet — every page load, every link
POSTsend data, change stateyesOnPost — every form submit
PUT, PATCH, DELETEupdate / delete a resourceyes / noWeb API only — HTML forms cannot send them
HEAD, OPTIONSheaders only / capabilitiesnohandled by the framework

An HTML <form> can only do GET and POST, so in this course those two are all you need. Search forms use GET (the values go into the query string, the URL is bookmarkable); anything that creates, changes or deletes uses POST.

Status codes

  • 1xx informational — you will not see these.
  • 2xx success — 200 OK, 201 Created, 204 No Content.
  • 3xx redirection — 301 Moved Permanently, 302 Found (what RedirectToPage sends), 304 Not Modified (cache hit).
  • 4xx the client did something wrong — 400 Bad Request (e.g. antiforgery token missing), 401 Unauthorized, 403 Forbidden, 404 Not Found.
  • 5xx the server broke — 500 Internal Server Error is an unhandled exception in your code.
tip

A 404 on a page you are sure exists is almost always a file-name / URL mismatch. A 400 right after submitting a form is almost always a missing antiforgery token. A 500 in development shows you the exception page with the stack trace.

Statelessness and cookies

HTTP has no memory. Two requests from the same browser are, to the server, two unrelated events — it does not know it is "the same user" or "the same game". If you want continuity, the client has to carry an identifier on every request. Our game puts the game id in the URL (/Games/Play/3f2a...), which is stateless and shareable. The other mechanism is cookies: a Set-Cookie response header stores a small value in the browser, and the browser sends it back as a Cookie header on every request to that host. ASP.NET Core uses cookies for the antiforgery token and for TempData; you will not write login code in this course (a per-player secret link is enough for A6).

ASP.NET Core overview

ASP.NET Core is Microsoft's cross-platform web framework. It is a console applicationdotnet run starts a process that opens a TCP port and serves HTTP. There is no separate web server to install.

  • Kestrel — the built-in web server. Fast, small, cross-platform. It takes over the whole ip:port, so in production it usually sits behind a reverse proxy (nginx, Apache, IIS) that handles TLS termination and hosting several apps on one machine. For development, Kestrel alone is all you need.
  • HostingWebApplication.CreateBuilder(args) wires up Kestrel, configuration, logging and the DI container; app.Run() blocks until the process is stopped.
  • The request pipeline — a chain of middleware components that each request flows through. Razor Pages is the last link in that chain.

Which flavour?

ASP.NET Core ships several ways to produce HTML or JSON. They share the same hosting, DI, configuration and middleware — they differ in how you write the endpoint.

Razor PagesMVCMinimal APIBlazor
Unit of codea page: .cshtml + PageModelcontroller class with action methods + viewsa lambda per endpointa component .razor
Outputserver-rendered HTMLserver-rendered HTML or JSONJSONinteractive UI (server or WebAssembly)
Routingby file pathby convention or attributesexplicit MapGet / MapPost@page in component
Good forpage-focused apps, CRUD, our gamelarge apps, REST servicessmall REST servicesSPA-style apps
In this courseyes — the only one we usenext course (ICD0024)next courseno

Razor Pages is the simplest of the four and covers everything A6 needs. MVC and Web API are the subject of Web Applications with C# next semester; you will find that most of what you learn here (Razor syntax, tag helpers, model binding, DI) carries over unchanged.

Creating the project

Inside the existing solution folder:

dotnet new webapp -n WebApp
dotnet sln add WebApp
dotnet add WebApp reference GameEngine DAL.EF DAL.Json
dotnet run --project WebApp

webapp is the Razor Pages template. The last line starts Kestrel and prints the URLs it listens on.

Project structure

WebApp/
├── Pages/
│ ├── Index.cshtml ← page markup (Razor)
│ ├── Index.cshtml.cs ← page code (PageModel)
│ ├── Privacy.cshtml
│ ├── Error.cshtml
│ ├── Shared/
│ │ ├── _Layout.cshtml ← common HTML skeleton
│ │ └── _ValidationScriptsPartial.cshtml
│ ├── _ViewImports.cshtml ← directives shared by every page
│ └── _ViewStart.cshtml ← sets Layout for every page
├── wwwroot/ ← static files served as-is: css/, js/, lib/
├── Properties/launchSettings.json
├── appsettings.json
├── appsettings.Development.json
├── Program.cs
└── WebApp.csproj
  • Pages/ — one page = one .cshtml file plus, usually, a .cshtml.cs code-behind. The folder structure is the URL structure.
  • wwwroot/ — everything here is reachable from the browser at /: wwwroot/css/site.css is https://localhost:7123/css/site.css. Put your board CSS here; never put code here.
  • _ViewImports.cshtml@using and @addTagHelper directives applied to every page in the folder and below.
  • _ViewStart.cshtml — runs before every page; the template uses it to set Layout = "_Layout".
  • Shared/_Layout.cshtml — the <html>, <head>, navigation bar and footer that wrap every page.

Program.cs

The whole application is configured in one file, top to bottom:

var builder = WebApplication.CreateBuilder(args);

// 1. Services go into the DI container (same container as lecture 08.1)
builder.Services.AddRazorPages();

var app = builder.Build();

// 2. Middleware pipeline — order matters
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Error");
app.UseHsts();
}

app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();

app.MapStaticAssets(); // serve wwwroot (UseStaticFiles() in older templates)
app.MapRazorPages()
.WithStaticAssets();

app.Run();

Two halves. Before Build() you register services — things pages will ask for through their constructors. After Build() you assemble the pipeline — what happens to each request, in the order you write it. In Development the builder adds the developer exception page automatically, which is why the explicit handler is only wired for other environments.

Middleware pipeline

Each app.Use… call adds one link. A request enters at the top; each middleware can act, pass the request on, or stop right there and answer.

  • Exception handler wraps everything below it — that is why it must be first. Any unhandled exception further down becomes a friendly /Error page (or the stack trace page in Development).
  • HTTPS redirection answers an http:// request with a 302 to the https:// URL and stops.
  • Static files checks wwwroot; if the path matches a file it is sent and the pipeline ends. No page code runs for a stylesheet.
  • Routing decides which page (if any) matches the URL. Authorization would enforce [Authorize] — unused in this course but harmless.
  • Razor Pages finally runs your OnGet / OnPost and renders the .cshtml.
warning

Put UseStaticFiles / MapStaticAssets before the page endpoints and UseRouting before UseAuthorization. Swapping lines in Program.cs produces confusing failures — a 404 for a CSS file, or every page returning 401.

Environments

ASPNETCORE_ENVIRONMENT selects the environment: Development, Staging or Production (default). Properties/launchSettings.json sets it for dotnet run and for the IDE run configuration:

{
"profiles": {
"https": {
"commandName": "Project",
"launchBrowser": true,
"applicationUrl": "https://localhost:7123;http://localhost:5123",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

app.Environment.IsDevelopment() in Program.cs reads the same value. A published app on a server has no launchSettings.json — it runs as Production unless you set the variable.

Configuration

appsettings.json holds settings; appsettings.Development.json overrides them when the environment is Development. Environment variables and command-line arguments override both. The classic use is the connection string:

{
"ConnectionStrings": {
"Default": "Data Source=game.db"
},
"Logging": { "LogLevel": { "Default": "Information" } }
}
var connectionString = builder.Configuration.GetConnectionString("Default")
?? throw new InvalidOperationException("Connection string 'Default' missing");

The DI container

The same IServiceCollection you used in ConsoleUI in lecture 08.1 is builder.Services here. Registrations look identical; what changes is the scope: ASP.NET Core creates a new DI scope for every HTTP request, so a Scoped service — the DbContext, a repository — lives exactly as long as one request. Transient is created on every resolve; Singleton lives as long as the process.

builder.Services.AddDbContext<AppDbContext>(o => o.UseSqlite(connectionString));
builder.Services.AddScoped<IConfigRepository, ConfigRepositoryEf>();
builder.Services.AddScoped<IGameRepository, GameRepositoryEf>();

A PageModel receives these through its constructor, exactly like your console MenuSystem did. Wiring all of this up properly is the subject of lecture 13.1; for now it is enough to know the container is the same one.

Razor Pages basics

A page is two files that share a name:

  • Pages/<pagename>.cshtml — HTML with Razor markup. The view.
  • Pages/<pagename>.cshtml.cs — a C# class deriving from PageModel that holds the page's data and handler methods for GET and POST. The model (code-behind).

Razor is Microsoft's syntax for mixing C# into HTML. The default language in a .cshtml file is HTML; the @ character switches to C# for one expression or one block, and there is no closing marker — the parser knows where C# ends. @page on the first line is what makes a file a page (routable) rather than a plain view.

Simplest page

@page
<h1>Hello, world!</h1>

Save that as Pages/Hello.cshtml, open /Hello, done. No code file needed.

Razor page result

URL ↔ file matching

The route of a page is its path under Pages/, without the extension. Index is optional at the end.

FileURL
Pages/Index.cshtml/ or /Index
Pages/Configs/Index.cshtml/Configs or /Configs/Index
Pages/Configs/Create.cshtml/Configs/Create
Pages/Games/Play.cshtml/Games/Play — plus a game id from the route template, next lecture

Files whose names start with an underscore (_Layout.cshtml, _ViewImports.cshtml) are never routable — they are helpers that other pages pull in.

First page with a PageModel

The code-behind gives the page data. @model declares the class; @Model (capital M) is the instance at run time, so the view is strongly typed and the IDE completes Model. for you.

@page
@model WebApp.Pages.IndexModel
@{
ViewData["Title"] = "Home";
}

<h1>Hello, world!</h1>
<h2>@Model.Message</h2>
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace WebApp.Pages;

public class IndexModel : PageModel
{
public string Message { get; private set; } = "Hello from code: ";

public void OnGet()
{
Message += $"server time is {DateTime.Now:HH:mm:ss}";
}
}

The framework instantiates IndexModel (through DI, so a constructor with parameters is fine), calls OnGet() because the request was a GET, and then renders the .cshtml with Model pointing at that instance. One request, one PageModel instance — properties set in OnGet are visible in the view, and gone when the response is sent.

Page with data from the model

Layout and the universal files

Most sites share a header, a navigation bar and a footer. Razor puts the shared skeleton into Pages/Shared/_Layout.cshtml, and every page renders only its own middle part.

Web layout

The smallest working layout:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - WebApp</title>
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
</head>
<body>
<main class="container">
@RenderBody()
</main>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>

@RenderBody() is where the page's output goes. The template's layout adds Bootstrap, a navbar and jQuery-based client-side validation — you will keep most of it. _ViewStart.cshtml contains only @{ Layout = "_Layout"; }, which is why no page has to mention its layout, and _ViewImports.cshtml contains the @using WebApp and @addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers lines that make asp-… attributes work everywhere. Next lecture covers all three in detail.

Running the app

dotnet run and dotnet watch

dotnet run --project WebApp           # build, start Kestrel, print the URLs
dotnet watch --project WebApp # same, plus rebuild / hot-reload on every save

dotnet watch is the one you want while working on pages: edit a .cshtml, save, refresh the browser. Changes to Program.cs or to a class library trigger a full restart, which takes a few seconds. Stop with Ctrl+C.

HTTPS development certificate

The https profile needs a certificate the browser trusts. The SDK creates a self-signed one; you trust it once per machine:

dotnet dev-certs https --trust

On Windows and macOS this pops up a system dialog. On Linux the trust step depends on the distribution and browser — if the browser keeps warning, use the http profile for local work; nothing in A6 depends on TLS.

Browser developer tools

Press F12 and open the Network tab before you click anything. Every row is one request: method, URL, status code, size, time. Click a row to see request headers, response headers, the form body of a POST, and the raw response. When something "does nothing", the Network tab tells you whether the browser sent what you think it sent and what the server actually answered. Keep it open for the whole of A6.

info

dotnet run prints Now listening on: https://localhost:7123. The port number comes from launchSettings.json and is random per project, so yours will differ. If you get "address already in use", another instance is still running.

Self preparation QA

Be prepared to explain topics like these:

  1. What are the parts of an HTTP request and an HTTP response? — Request: method + path + version, headers, optional body. Response: status line with a code, headers, optional body.
  2. Why does a successful form POST answer with 302 instead of 200? — So that the browser makes a fresh GET of the result page; a refresh then repeats the harmless GET instead of re-submitting the form (Post-Redirect-Get).
  3. HTTP is stateless — how does the server know which game a request belongs to? — The client carries the identifier: in our app the game id is part of the URL; cookies are the other mechanism and ASP.NET Core uses them for antiforgery and TempData.
  4. What is Kestrel and why is it usually behind a reverse proxy in production? — The built-in cross-platform web server inside the app process; a proxy adds TLS termination, multiple sites per machine and hardening that Kestrel does not aim to provide.
  5. What are the two halves of Program.cs? — Before Build(): service registrations into the DI container; after Build(): the middleware pipeline, in execution order, ending with the page endpoints.
  6. Why must the static-files middleware come before the Razor Pages endpoints? — So a request for /css/site.css is answered directly from wwwroot and never reaches routing; a swapped order costs a lookup on every asset or breaks it entirely.
  7. How is the URL of a Razor page determined? — From its path under Pages/ without the extension; Index may be omitted; underscore-prefixed files are not routable.
  8. What is the difference between @model and @Model?@model is a directive declaring the PageModel type of the view; @Model is the run-time instance the framework created for this request and populated in OnGet / OnPost.