Part 1: What Actually Happens When You Type a URL

Before we write any code, you MUST understand what happens when you type https://api.example.com/todos/5 in a browser:

The Journey of an HTTP Request

YOU (The Client)
│ 1. You type: https://api.example.com/todos/5
YOUR COMPUTER
│ 2. DNS Lookup: "What IP address is api.example.com?"
│ It's like asking "What's the phone number for this name?"
│ DNS Server responds: "It's 192.168.1.100"
│ 3. TCP Connection: Your computer calls that IP on port 443 (HTTPS)
│ Like dialing a phone number and waiting for someone to pick up
│ The server answers and they establish a connection
│ 4. TLS Handshake: They agree on encryption
│ Like two people agreeing on a secret code before talking
│ Now nobody can eavesdrop on the conversation
│ 5. HTTP Request Sent:
│ GET /todos/5 HTTP/1.1
│ Host: api.example.com
│ Accept: application/json
│ Authorization: Bearer eyJhbGciOi...
│ This is like saying:
│ "GET" = I want to retrieve something (not create or delete)
│ "/todos/5" = the specific thing I want
│ "Host" = which website (one server can host many)
│ "Accept" = I want the response in JSON format
│ "Authorization" = here's my ID card to prove who I am
THE SERVER (Your .NET Application Running on Some Computer)
│ 6. Kestrel Web Server Receives the Raw Bytes
│ Kestrel is .NET's built-in web server
│ It's like a receptionist that receives all incoming calls
│ It reads the raw TCP packets and reconstructs the HTTP request
│ 7. The Request Goes Through Middleware Pipeline
│ Think of middleware as a series of security checkpoints
│ Each middleware can:
│ - Examine the request
│ - Modify the request
│ - Block the request (return error)
│ - Pass it to the next middleware
│ 8. Routing Middleware Matches the URL Pattern
│ It looks at "/todos/5" and checks all registered routes
│ Finds: [Route("api/[controller]")] + [HttpGet("{id}")]
│ Matches to: TodoController.GetTodoById(int id)
│ Extracts: id = 5
│ 9. Model Binding Converts String to Int
│ URL parameter is always a string "5"
│ Model binder converts it to int 5
│ If it can't convert (like "abc"), it returns 400 Bad Request
│ 10. Controller Action Executes
│ Your GetTodoById(5) method runs
│ It calls the service
│ Service checks database/memory
│ Returns the TodoItem or null
│ 11. Action Result Gets Executed
│ If you returned Ok(todo):
│ - Sets HTTP status code to 200
│ - Serializes todo object to JSON string
│ - Adds Content-Type: application/json header
│ If you returned NotFound():
│ - Sets HTTP status code to 404
│ - Creates error response body
│ 12. Response Goes Back Through Middleware
│ Middleware can also modify the response
│ Adds headers, compresses response, logs request
│ 13. Kestrel Sends Response Back
│ Converts the response to TCP packets
│ Sends them back through the internet
YOUR COMPUTER
│ 14. Browser Receives Response
│ HTTP/1.1 200 OK
│ Content-Type: application/json
│ {
│ "id": 5,
│ "title": "Buy groceries",
│ "isComplete": false,
│ "createdAt": "2024-01-15T10:30:00Z"
│ }
│ 15. Browser Parses JSON and Renders UI

Key Understanding: A Web API is just a program that:

  1. Listens for HTTP requests (text messages in a specific format)
  2. Parses them to understand what the client wants
  3. Executes some logic
  4. Sends back HTTP responses (text messages in a specific format)

Everything else is just organization and convenience.

Part 2: Creating the Project - Every File Explained

When you run dotnet new webapi -n MyFirstApi, here's what gets created and WHY:

The .csproj File (MyFirstApi.csproj)

<Project Sdk="Microsoft.NET.Sdk.Web">
<!-- "Sdk" means Software Development Kit -->
<!-- "Microsoft.NET.Sdk.Web" includes everything needed for web apps -->
<!-- This is different from "Microsoft.NET.Sdk" which is for console apps -->
<!-- The Web SDK adds: Kestrel server, middleware, controllers, etc. -->
<PropertyGroup>
<!-- TargetFramework: Which .NET version to use -->
<TargetFramework>net6.0</TargetFramework>
<!-- Nullable: Should we warn about potential null reference exceptions? -->
<Nullable>enable</Nullable>
<!-- When enabled, the compiler WARNS you about possible null values -->
<!-- Example: string? name = null; // OK -->
<!-- Example: string name = null; // WARNING! -->
<!-- ImplicitUsings: Automatically add common using statements -->
<ImplicitUsings>enable</ImplicitUsings>
<!-- Instead of writing "using System;" everywhere -->
<!-- .NET automatically adds common namespaces -->
<!-- System, System.Collections.Generic, System.Linq, etc. -->
<!-- You can still add additional usings manually -->
</PropertyGroup>

<ItemGroup>
<!-- NuGet packages (libraries) your project needs -->
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
<!-- Swashbuckle: Provides Swagger UI for testing your API -->
<!-- Without this, you'd need Postman or curl to test -->
</ItemGroup>

</Project>

What happens when you build:

  1. dotnet build reads this .csproj file
  2. It downloads NuGet packages (like Swashbuckle)
  3. It compiles all your .cs files into IL (Intermediate Language)
  4. Creates a .dll file in bin/Debug/net6.0/
  5. When you dotnet run, it executes this .dll with the .NET runtime

Program.cs - The ENTIRE Application Startup (Deep Dive)

// ===== LINE 1: Creating the Builder =====
var builder = WebApplication.CreateBuilder(args);

/*
What is 'args'?
- These are command-line arguments passed to your program
- Example: dotnet run --environment Development --urls "http://localhost:5000"
- 'args' would contain: ["--environment", "Development", "--urls", "http://localhost:5000"]

What does CreateBuilder do INTERNALLY?
1. Creates a ConfigurationBuilder
- Reads appsettings.json
- Reads appsettings.{Environment}.json (like appsettings.Development.json)
- Reads environment variables
- Reads command-line arguments
- Merges them all together (later sources override earlier ones)

2. Creates a ServiceCollection (Dependency Injection Container)
- A big dictionary that says:
"When someone asks for ILogger, give them this Logger instance"
"When someone asks for ITodoService, create a new TodoService"

3. Creates a LoggingBuilder
- Sets up console logging, debug logging, etc.

4. Sets up Kestrel web server configuration
- What ports to listen on
- SSL/TLS certificates for HTTPS

The builder is like a "configuration object" before the app is built
You can modify it, add services, change settings
Once you call Build(), it's locked and ready to run
*/

// ===== LINE 2: Adding Services to the Container =====
builder.Services.AddControllers();

/*
What is builder.Services?
- It's an IServiceCollection - essentially a list of service registrations
- "Services" are objects that your app needs to function
- Examples: Controllers, Database Context, Email Sender, Logger

What does AddControllers() do INTERNALLY?
1. Registers all classes that inherit from ControllerBase
2. Scans your project for [ApiController] attributes
3. Sets up:
- Model binding (converting request data to C# objects)
- Model validation (checking [Required], [Range] attributes)
- JSON serialization (converting objects to/from JSON)
- Action filters (code that runs before/after controller actions)

Without this line, your controllers simply won't work
.NET won't know they exist or how to handle them

The services are NOT created yet - just REGISTERED
They'll be created when needed (lazy initialization)
*/

// ===== LINE 3 & 4: Adding API Documentation =====
builder.Services.AddEndpointsApiExplorer();
/*
What is an "Endpoint"?
- An endpoint is a combination of: HTTP Method + URL Pattern
- Example: GET /api/todos is one endpoint
- Example: POST /api/todos is another endpoint

What does AddEndpointsApiExplorer() do?
- It adds a service that can discover all your endpoints
- It scans all controllers, reads [HttpGet], [HttpPost] attributes
- It understands what parameters each endpoint expects
- It knows what response types each endpoint returns
- This information is used to generate API documentation

Without this, Swagger won't know what endpoints you have
*/

builder.Services.AddSwaggerGen();
/*
Swagger = OpenAPI = API Documentation

What does AddSwaggerGen() do?
1. Configures Swagger document generation
2. Creates a SwaggerGenerator service
3. Sets up how your API is documented:
- Title: "My API"
- Version: "v1"
- Description: "API for managing todos"

The "Gen" stands for Generation
It GENERATES a Swagger/OpenAPI specification document (JSON)
This JSON describes your entire API:
- All endpoints
- All parameters
- All response types
- Authentication requirements

When you visit /swagger/v1/swagger.json, you see this JSON
Swagger UI reads this JSON and creates the interactive documentation
*/

// ===== LINE 5: Building the Application =====
var app = builder.Build();

/*
Build() is the point of no return
Before Build():
- You're in "configuration mode"
- You can add/modify services, change settings
- Application is NOT ready to handle requests

After Build():
- You're in "execution mode"
- Services are ready (but not necessarily created yet)
- You CANNOT add new services (well, you shouldn't)
- Application is ready to start listening

What happens during Build() INTERNALLY:
1. Finalizes the service collection
2. Compiles the dependency injection container
3. Creates the middleware pipeline builder
4. Sets up the hosting environment
5. Configures Kestrel server with all settings

The returned 'app' is a WebApplication
It's a fully configured but NOT YET RUNNING application
It's like a car that's assembled but the engine isn't started
*/

// ===== LINE 6-10: Configuring the Middleware Pipeline =====
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}

/*
What is app.Environment?
- It's an IWebHostEnvironment object
- Contains information about where your app is running
- Properties:
- EnvironmentName: "Development", "Staging", "Production"
- ContentRootPath: Where your project files are
- WebRootPath: Where static files are (wwwroot folder)

Environment is set by:
1. Environment variable: ASPNETCORE_ENVIRONMENT
2. In launchSettings.json under profiles
3. Command line: dotnet run --environment Development

IsDevelopment() returns true if environment is "Development"
Why check this?
- You don't want Swagger in Production (security risk)
- Swagger exposes all your API details to attackers
- It's like showing burglars a map of your house

What does UseSwagger() do?
- Adds middleware that generates the Swagger JSON document
- Responds to requests for /swagger/v1/swagger.json
- This middleware generates the JSON ON-DEMAND
- It doesn't create a file, it creates the JSON when requested

What does UseSwaggerUI() do?
- Adds middleware that serves the Swagger UI (HTML/JS/CSS)
- Responds to requests for /swagger
- The UI is an interactive webpage
- It reads the Swagger JSON and creates "Try it out" functionality
- It's a single-page application embedded in your API
*/

// ===== LINE 11: HTTPS Redirection Middleware =====
app.UseHttpsRedirection();

/*
What this does:
- Catches any HTTP (not HTTPS) requests
- Responds with 307 Temporary Redirect to the HTTPS version
- Example: http://localhost:5000/api/todos
→ 307 Redirect to https://localhost:7001/api/todos

Why?
- HTTPS encrypts data between client and server
- HTTP sends everything in plain text (passwords visible!)
- Production apps should ALWAYS use HTTPS

How it works INTERNALLY:
1. Request arrives on HTTP port (5000 by default)
2. Middleware checks: Is this HTTP? Yes
3. It constructs the HTTPS URL (change port to 7001)
4. Returns 307 with Location header pointing to HTTPS URL
5. Client automatically makes new request to HTTPS URL

Note: In Development, you might see certificate warnings
That's because the development certificate is self-signed
Production uses real certificates from Certificate Authorities
*/

// ===== LINE 12: Authorization Middleware =====
app.UseAuthorization();

/*
Authentication vs Authorization:
- Authentication: Who are you? (Login, prove identity)
- Authorization: What are you allowed to do? (Permissions)

What UseAuthorization() does:
- Adds middleware that checks authorization policies
- Runs AFTER authentication middleware (which we haven't added yet)
- Checks [Authorize] attributes on controllers/actions

Example flow:
1. Request arrives: GET /api/admin/users
2. Authentication middleware runs first (not shown here)
- Checks JWT token/cookie
- Sets User.Identity
3. Authorization middleware runs
- Checks: Does this user have "Admin" role?
- If yes: Pass to controller
- If no: Return 403 Forbidden

Even though we haven't added authentication,
UseAuthorization() is included by default
It does nothing until you add [Authorize] attributes
*/

// ===== LINE 13: Mapping Controllers =====
app.MapControllers();

/*
This is CRUCIAL - without it, your controllers don't work!

What it does:
- Scans all classes with [ApiController] or inheriting from ControllerBase
- Reads their [Route] attributes
- Creates a mapping: URL Pattern → Controller Method
- Registers these mappings with the routing middleware

How routing works:
When a request comes in:
1. Routing middleware looks at the URL: /api/todo/5
2. It checks all registered patterns
3. Finds: api/todo/{id} matches
4. Extracts route values: { controller = "Todo", action = "GetTodo", id = 5 }
5. Creates a route context with these values
6. Passes to MVC middleware
7. MVC middleware creates the controller instance
8. Calls the correct action method with the bound parameters

Without MapControllers():
- Your controllers exist as classes
- But .NET doesn't know which URLs should reach them
- All requests would return 404 Not Found
*/

// ===== LINE 14: Starting the Application =====
app.Run();

/*
Run() does several things:
1. Starts Kestrel web server on configured ports
2. Begins listening for incoming HTTP requests
3. Enters an infinite loop (the "main loop")
- Wait for request
- Process request through middleware pipeline
- Send response
- Wait for next request
4. Blocks the main thread (program doesn't exit)

This is a BLOCKING call
Code after app.Run() would never execute
Unless the application shuts down

What ports does it listen on?
- HTTPS: 7001 (or 7000-7099 range)
- HTTP: 5001 (or 5000-5099 range)
- These are configured in launchSettings.json

How to stop:
- Press Ctrl+C in terminal
- This sends a shutdown signal
- Kestrel stops accepting new requests
- Completes ongoing requests
- Releases resources
- Program exits
*/

Part 3: The Model - Defining Data Shapes

Models/TodoItem.cs - Every Detail

// Namespace declaration
// Namespaces organize code and prevent naming conflicts
// This class's full name is: MyFirstApi.Models.TodoItem
namespace MyFirstApi.Models;

/*
Why use namespaces?
Imagine you have two "User" classes:
- One for your API models
- One for database entities
Without namespaces, they'd conflict
With namespaces: Models.User and Entities.User are different
*/

// Class declaration
public class TodoItem
/*
Why 'public'?
- Public means this class can be used by ANY other code
- Internal (default) means only code in this project can use it
- Our API controllers need to use this class
- JSON serialization needs to access it
- So it must be public
*/
{
// ===== Property 1: Id =====
public int Id { get; set; }
/*
This is an AUTO-PROPERTY
Before C# 3.0, you'd write:
private int _id;
public int GetId() { return _id; }
public void SetId(int value) { _id = value; }
Auto-properties are shorthand for the above
{ get; set; } means:
- get: Anyone can read the value
- set: Anyone can write the value
The compiler creates a HIDDEN backing field
You never see it, but it's there
Why use properties instead of fields?
1. Encapsulation - can add validation later
2. Data binding - frameworks need properties
3. Serialization - JSON serializers look for properties
4. Interfaces - can define properties, not fields
JSON example:
{
"id": 1
}
The serializer finds "Id" property, converts to "id" (camelCase)
*/
// ===== Property 2: Title =====
public string Title { get; set; }
/*
string is a REFERENCE type
- Can be null (no value)
- Stored on the heap
- Multiple variables can point to same string
Since Nullable is enabled in .csproj:
- Compiler warns if Title might be null
- You should initialize it or mark it as string? (nullable)
Better version would be:
public string Title { get; set; } = string.Empty;
This ensures it's never null, starts as empty string
Or use the 'required' keyword (C# 11):
public required string Title { get; set; }
This forces you to set Title when creating TodoItem
JSON example:
{
"title": "Learn .NET API"
}
*/
// ===== Property 3: IsComplete =====
public bool IsComplete { get; set; }
/*
bool is a VALUE type (specifically System.Boolean)
- Cannot be null (unless bool?)
- Default value is false
- Stored on the stack (when local variable)
This is a boolean flag
Represents: Is the task done? Yes/No
JSON example:
{
"isComplete": false
}
JSON uses lowercase true/false
*/
// ===== Property 4: CreatedAt =====
public DateTime CreatedAt { get; set; }
/*
DateTime is a STRUCT (value type)
- Represents a point in time
- Default is DateTime.MinValue (January 1, 0001)
- Very precise (100-nanosecond intervals)
Common DateTime operations:
- DateTime.Now: Current local time
- DateTime.UtcNow: Current UTC time (preferred for APIs)
- DateTime.Today: Midnight of current day
UTC (Coordinated Universal Time) is preferred because:
- No timezone confusion
- No daylight saving time issues
- Easy to convert to any timezone
JSON serialization:
- Default format: ISO 8601
- Example: "2024-01-15T10:30:00Z"
- Z means UTC (Zulu time)
- +05:30 would mean UTC+5:30
*/
}

/*
Data Annotations (we could add these):
using System.ComponentModel.DataAnnotations;

public class TodoItem
{
[Key] // Primary key for database
public int Id { get; set; }
[Required] // Cannot be null or empty
[StringLength(100)] // Max 100 characters
public string Title { get; set; }
[Range(typeof(bool), "false", "true")]
public bool IsComplete { get; set; }
[DataType(DataType.DateTime)]
public DateTime CreatedAt { get; set; }
}

These annotations are used for:
1. Model Validation - automatically check inputs
2. Database Generation - Entity Framework uses them
3. API Documentation - Swagger shows requirements
4. UI Generation - Could auto-generate forms
*/

Part 4: The Controller - HTTP Request Handler (Deep Dive)

Controllers/TodoController.cs - Line by Line

// ===== IMPORTS =====
using Microsoft.AspNetCore.Mvc;
/*
ASP.NET Core MVC namespace
Contains everything for building APIs:
- ControllerBase: Base class for API controllers
- ApiControllerAttribute: Marks a class as API controller
- RouteAttribute: Defines URL patterns
- HttpGetAttribute: Handles GET requests
- ActionResult<T>: Return type for API actions
- Ok(), NotFound(), CreatedAtAction(): Helper methods
- FromBodyAttribute: Bind from request body
*/

using MyFirstApi.Models;
/*
Imports our TodoItem class
Without this, we'd need to write MyFirstApi.Models.TodoItem every time
*/

// ===== NAMESPACE =====
namespace MyFirstApi.Controllers;

// ===== CLASS DECLARATION =====
[ApiController]
/*
What is an Attribute?
- Metadata added to code elements
- Enclosed in square brackets []
- Provides additional information to the compiler/runtime
- Classes, methods, properties can have attributes

What does [ApiController] do INTERNALLY?
It enables SEVERAL behaviors automatically:

1. Automatic Model Validation (HTTP 400 responses)
- Before your action runs, it validates the model
- If [Required] field is missing: Returns 400 Bad Request
- If [Range] validation fails: Returns 400 Bad Request
- You don't need to check ModelState.IsValid manually
WITHOUT ApiController:
[HttpPost]
public IActionResult Create([FromBody] TodoItem item)
{
if (!ModelState.IsValid) // Must check manually
return BadRequest(ModelState);
// ... rest of code
}
WITH ApiController:
[HttpPost]
public IActionResult Create([FromBody] TodoItem item)
{
// ModelState is already checked
// If invalid, 400 was already returned
// Your code only runs if valid
}

2. Binding Source Inference
- Automatically figures out where parameters come from
- [FromBody] for complex types (like TodoItem)
- [FromRoute] for parameters matching route template
- [FromQuery] for parameters not in route
Example:
public IActionResult Get(int id) // id comes from route
public IActionResult Create(TodoItem item) // item comes from body
// Attributes can override this inference

3. Problem Details for Error Responses
- Returns RFC 7807 compliant error responses
- Consistent error format across your API
Example 400 response:
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.1",
"title": "One or more validation errors occurred.",
"status": 400,
"traceId": "00-abc123...",
"errors": {
"Title": ["The Title field is required."]
}
}

4. Multipart/form-data request inference
- Form fields bind from [FromForm] automatically
*/
[Route("api/[controller]")]
/*
What is Routing?
- Mapping URLs to code
- Deciding which method should handle which request

[controller] is a TOKEN
- Gets replaced with controller class name MINUS "Controller"
- TodoController → "todo"
- UserController → "user"
- AdminPanelController → "AdminPanel" (preserves case)

Why "api/" prefix?
- Convention to distinguish API routes from MVC routes
- Also helps with versioning: api/v1/todo, api/v2/todo
- Clear separation from web pages (if you had any)

Complete URL examples:
- Route template: api/[controller]
- Controller: TodoController
- Base URL: https://localhost:7001/api/todo
- Specific endpoints:
- GET api/todo → GetAllTodos()
- GET api/todo/5 → GetTodoById(5)
- POST api/todo → CreateTodo(item)
- PUT api/todo/5 → UpdateTodo(5, item)
- DELETE api/todo/5 → DeleteTodo(5)

You could also hardcode the route:
[Route("api/todo")]
But [controller] is more maintainable
If you rename TodoController to TaskController
The route automatically becomes api/task
*/
public class TodoController : ControllerBase
/*
Inheriting from ControllerBase

Why ControllerBase and not Controller?
- ControllerBase: For APIs (no view support, lighter)
- Controller: For MVC with Views (Razor pages, HTML)
- Controller inherits from ControllerBase

ControllerBase provides (all inherited):
1. HttpContext: Information about current request
- Request: Headers, body, query string, cookies
- Response: Set status code, headers, cookies
- User: Authenticated user information
- Session: Session state data

2. ModelState: Validation state
- Contains validation errors
- IsValid property checks if no errors

3. Helper methods that return IActionResult:
- Ok(object): HTTP 200 with data
- Created(string uri, object): HTTP 201 Created
- NoContent(): HTTP 204 No Content
- BadRequest(): HTTP 400 Bad Request
- NotFound(): HTTP 404 Not Found
- Unauthorized(): HTTP 401 Unauthorized
- Forbid(): HTTP 403 Forbidden
- StatusCode(int): Custom status code

4. Request property shortcuts:
- Request.Body: The request body stream
- Request.Headers: All HTTP headers
- Request.Query: Query string parameters
- Request.RouteValues: URL parameters

5. User property:
- Contains ClaimsPrincipal
- Has user identity, roles, claims
*/
{
// ===== CLASS FIELDS =====
private static List<TodoItem> _todos = new List<TodoItem>();
/*
private: Only accessible within this class
static: Shared across ALL instances of this class
Why static?
- A new controller instance is created for EVERY request
- Without static, each request would have empty list
- Static ensures data persists between requests
This is TEMPORARY - for learning only!
Real applications use:
- Database (SQL Server, PostgreSQL)
- Distributed Cache (Redis)
- Message Queue (RabbitMQ)
Problems with static in-memory storage:
- Data lost on application restart
- Not scalable (multiple servers = different data)
- Memory leaks (keeps growing)
- Thread safety issues (concurrent access)
Better: Use dependency injection for data access
We'll replace this with a database later
*/
private static int _nextId = 2;
/*
Auto-incrementing ID
Starts at 2 because we already have item with ID 1
In real databases:
- IDENTITY(1,1) in SQL Server
- AUTO_INCREMENT in MySQL
- SERIAL in PostgreSQL
- Database handles this automatically
*/
// Static constructor - runs once when class is first used
static TodoController()
{
_todos.Add(new TodoItem
{
Id = 1,
Title = "Learn .NET API",
IsComplete = false,
CreatedAt = DateTime.UtcNow // Always use UTC!
});
}
// ===== GET ALL: GET /api/todo =====
[HttpGet]
/*
HttpGetAttribute
- Maps HTTP GET requests to this method
- GET requests should ONLY retrieve data
- GET should NOT modify data (no side effects)
- GET is idempotent (multiple identical requests = same result)
- GET can be cached by browsers/proxies
HTTP Methods Semantics:
GET - Retrieve (Read)
POST - Create (Create)
PUT - Replace entire resource (Update)
PATCH - Partial update (Update partially)
DELETE - Remove (Delete)
HEAD - Like GET but only headers, no body
OPTIONS - What methods are supported
RESTful convention:
- GET /todos - List all
- GET /todos/1 - Get one
- POST /todos - Create new
- PUT /todos/1 - Replace #1
- PATCH /todos/1 - Update #1 partially
- DELETE /todos/1 - Remove #1
*/
public ActionResult<IEnumerable<TodoItem>> GetAllTodos()
/*
Return Type: ActionResult<IEnumerable<TodoItem>>
What is ActionResult<T>?
- A union type representing possible HTTP responses
- Can return the data directly: return todo;
- Can return an action result: return Ok(todo);
It wraps your response in a way that:
1. Provides type information for Swagger documentation
2. Allows returning different HTTP status codes
3. Enables proper serialization
Without ActionResult:
public TodoItem Get(int id)
{
var todo = _todos.Find(t => t.Id == id);
return todo; // Returns null if not found (200 OK with null body!)
// Can't return 404!
}
With ActionResult:
public ActionResult<TodoItem> Get(int id)
{
var todo = _todos.Find(t => t.Id == id);
if (todo == null)
return NotFound(); // Returns 404
return Ok(todo); // Returns 200 with data
}
IEnumerable<TodoItem>:
- IEnumerable is the most basic collection interface
- It only supports forward-only iteration
- Any collection implements it: List, Array, HashSet
- Use it when you only need to read through items
Other collection interfaces:
- ICollection<T>: Adds Add, Remove, Count
- IList<T>: Adds index access [i]
- IQueryable<T>: For database queries (LINQ)
Using IEnumerable as return type tells callers:
"You can iterate through this, but don't modify it"
*/
{
// _todos is a List<TodoItem>
// List<TodoItem> implements IEnumerable<TodoItem>
// So we can return it directly
return Ok(_todos);
/*
Ok() method:
- Inherited from ControllerBase
- Creates OkObjectResult
- HTTP Status Code: 200
- Serializes the object to JSON
Actual HTTP Response:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Mon, 15 Jan 2024 10:30:00 GMT
Server: Kestrel
[
{
"id": 1,
"title": "Learn .NET API",
"isComplete": false,
"createdAt": "2024-01-15T10:30:00Z"
}
]
JSON serialization details:
- System.Text.Json is used by default
- Property names become camelCase
- DateTime becomes ISO 8601 string
- Null values are included by default
- Indentation is minimal (compact)
*/
}
// ===== GET BY ID: GET /api/todo/1 =====
[HttpGet("{id}")]
/*
{id} is a ROUTE TEMPLATE PARAMETER
Route parameters:
- Must match the action parameter name
- Case-insensitive by default
- Constrained by type (int here)
Route constraints:
[HttpGet("{id:int}")] - Must be integer
[HttpGet("{id:guid}")] - Must be GUID
[HttpGet("{name:alpha}")] - Alphabetic only
[HttpGet("{date:datetime}")] - DateTime only
[HttpGet("{id:min(1)}")] - Minimum value
You can have multiple parameters:
[HttpGet("{category}/{id}")]
Gets: api/todo/programming/5
Optional parameters:
[HttpGet("{id?}")]
id is optional, default value used if missing
*/
public ActionResult<TodoItem> GetTodoById(int id)
/*
int id:
- Automatically bound from URL
- Model binding converts string "5" to int 5
- If conversion fails (like "abc"), returns 400 Bad Request
Model binding process:
1. Routing extracts "5" from URL
2. Model binder sees parameter type is int
3. Attempts int.Parse("5")
4. If successful: value = 5
5. If fails: adds error to ModelState, returns 400
*/
{
// LINQ FirstOrDefault method
var todo = _todos.FirstOrDefault(t => t.Id == id);
/*
FirstOrDefault:
- Returns first element matching condition
- Returns default(T) if none found
- For reference types (classes): default is null
- For value types (int): default is 0
Lambda expression: t => t.Id == id
- t: Input parameter (each TodoItem in list)
- =>: Lambda operator ("goes to")
- t.Id == id: Expression (returns true/false)
Equivalent to:
TodoItem todo = null;
foreach (var t in _todos)
{
if (t.Id == id)
{
todo = t;
break;
}
}
Other LINQ methods:
- First(): Like FirstOrDefault, but throws if not found
- Single(): Returns exactly one, throws if 0 or many
- SingleOrDefault(): Returns one or default, throws if many
- Where(): Returns all matching items
- Any(): Returns true if any match
*/
if (todo == null)
{
return NotFound($"Todo with ID {id} not found");
/*
NotFound() method:
- Creates NotFoundObjectResult
- HTTP Status Code: 404
- Can pass a message object
Response:
HTTP/1.1 404 Not Found
Content-Type: application/problem+json
{
"type": "https://tools.ietf.org/html/rfc7231#section-6.5.4",
"title": "Not Found",
"status": 404,
"traceId": "00-abc123...",
"detail": "Todo with ID 99 not found"
}
Problem Details (RFC 7807):
- Standard error format for APIs
- Clients can reliably parse errors
- Machine-readable error information
*/
}
return Ok(todo);
// 200 OK with the todo item as JSON
}
// ===== CREATE: POST /api/todo =====
[HttpPost]
/*
POST Method characteristics:
- Creates a NEW resource
- Not idempotent (multiple posts = multiple resources)
- Request has a body (the new resource)
- Response should include Location header
POST vs PUT:
POST: Create new (server generates ID)
PUT: Replace existing (client specifies ID)
*/
public ActionResult<TodoItem> CreateTodo([FromBody] TodoItem newTodo)
/*
[FromBody] attribute:
- Tells model binder: "Read from request body"
- Body must be in JSON format
- Content-Type must be: application/json
Example request body:
{
"title": "Buy groceries",
"isComplete": false
}
What happens during binding:
1. Read the request body stream
2. Deserialize JSON into TodoItem object
3. Validate the object (if annotations used)
4. If invalid, return 400 Bad Request automatically
The client can also send:
{
"id": 999, // Will be ignored (server sets ID)
"title": "Test",
"isComplete": false,
"createdAt": "2023-01-01", // Will be overridden
"extraField": "value" // Ignored by default
}
JSON serialization is case-insensitive by default
"title", "Title", "TITLE" all work
*/
{
// Assign server-generated values
newTodo.Id = _nextId++;
newTodo.CreatedAt = DateTime.UtcNow;
_todos.Add(newTodo);
return CreatedAtAction(
nameof(GetTodoById),
new { id = newTodo.Id },
newTodo
);
/*
CreatedAtAction() method:
- HTTP Status Code: 201 Created
- Creates CreatedAtActionResult
- Includes Location header pointing to new resource
Parameters:
1. actionName: "GetTodoById" - the method to get this resource
2. routeValues: { id = 1 } - parameters for that method
3. value: newTodo - the created object (response body)
nameof(GetTodoById):
- Compile-time safe reference to method name
- If you rename GetTodoById, this updates automatically
- Better than hardcoding string "GetTodoById"
new { id = newTodo.Id }:
- Anonymous type
- Creates an object with one property: id
- Property name must match route parameter name
Full Response:
HTTP/1.1 201 Created
Location: https://localhost:7001/api/todo/2
Content-Type: application/json
{
"id": 2,
"title": "Buy groceries",
"isComplete": false,
"createdAt": "2024-01-15T11:00:00Z"
}
The Location header is IMPORTANT:
- Client knows where to find the created resource
- RESTful best practice
- Can make GET request to that URL to verify creation
*/
}
// ===== FULL UPDATE: PUT /api/todo/1 =====
[HttpPut("{id}")]
/*
PUT Method characteristics:
- Replaces the ENTIRE resource
- Idempotent (multiple identical requests = same result)
- Client sends complete replacement object
- Missing fields become default values
PUT semantics:
- "Here's the new version of this resource"
- "Replace whatever was at this URL with this"
- All fields should be provided, even unchanged ones
*/
public ActionResult UpdateTodo(int id, [FromBody] TodoItem updatedTodo)
/*
Return type: ActionResult (not ActionResult<TodoItem>)
- We don't need to return the updated item
- Just success/failure
ActionResult is the base class for all action results
It can return various HTTP status codes without a body
*/
{
var existingTodo = _todos.FirstOrDefault(t => t.Id == id);
if (existingTodo == null)
return NotFound();
// Just NotFound(), no message (optional)
// Replace ALL properties
existingTodo.Title = updatedTodo.Title;
existingTodo.IsComplete = updatedTodo.IsComplete;
// Note: We don't update Id or CreatedAt
return NoContent();
/*
NoContent() method:
- HTTP Status Code: 204 No Content
- Response has NO body
- Indicates success, nothing to return
Common uses of 204:
- Successful update (PUT/PATCH)
- Successful delete (DELETE)
- Action performed, no data to send back
Response:
HTTP/1.1 204 No Content
Date: Mon, 15 Jan 2024 11:00:00 GMT
Server: Kestrel
(No body at all)
Why no body?
- Client already has the data (it sent it)
- Saves bandwidth
- Clear indication: "Update succeeded"
Client should:
- Check status code (204 = success)
- Not expect response body
- Refresh data if needed with GET
*/
}
// ===== PARTIAL UPDATE: PATCH /api/todo/1 =====
[HttpPatch("{id}")]
/*
PATCH Method characteristics:
- Partially updates a resource
- Not necessarily idempotent
- Only changed fields are sent
- Other fields remain unchanged
PATCH vs PUT:
PUT: Send entire object
PATCH: Send only what changed
Example PATCH body:
{
"isComplete": true
}
Only IsComplete is updated, Title stays same
PUT would require:
{
"title": "Learn .NET API", // Must include even if unchanged
"isComplete": true
}
PATCH is more complex to implement correctly
JSON Patch (RFC 6902) is the standard:
[
{ "op": "replace", "path": "/isComplete", "value": true }
]
We're using a simpler merge approach here
*/
public ActionResult PartialUpdateTodo(int id, [FromBody] TodoItem partialTodo)
{
var existingTodo = _todos.FirstOrDefault(t => t.Id == id);
if (existingTodo == null)
return NotFound();
// Only update if the new value is not null
if (partialTodo.Title != null)
existingTodo.Title = partialTodo.Title;
// Problem with bool: Can't distinguish "false" from "not provided"
// Solution: Use nullable bool? or separate DTO
existingTodo.IsComplete = partialTodo.IsComplete;
/*
Better approach with DTO (Data Transfer Object):
public class UpdateTodoDto
{
public string? Title { get; set; } // nullable
public bool? IsComplete { get; set; } // nullable
}
Then:
if (dto.Title != null)
existing.Title = dto.Title;
if (dto.IsComplete.HasValue) // Check if provided
existing.IsComplete = dto.IsComplete.Value;
This clearly distinguishes:
- Title is null: Don't update
- Title is "": Update to empty string
- IsComplete is null: Don't update
- IsComplete is false: Update to false
*/
return NoContent();
}
// ===== DELETE: DELETE /api/todo/1 =====
[HttpDelete("{id}")]
/*
DELETE Method characteristics:
- Removes a resource
- Idempotent (multiple deletes = same result, resource is gone)
- First delete: 204 No Content
- Subsequent deletes: 404 Not Found (if implemented that way)
Some APIs return 404 on delete of non-existent resource
Some return 204 regardless (idempotent)
We return 404 to be explicit
*/
public ActionResult DeleteTodo(int id)
{
var todo = _todos.FirstOrDefault(t => t.Id == id);
if (todo == null)
return NotFound();
_todos.Remove(todo);
// Remove() uses reference equality
// It removes the exact object we found
return NoContent();
// 204 - Successfully deleted
// Client should remove this resource from its UI
}
// ===== SEARCH WITH QUERY PARAMETERS =====
[HttpGet("search")]
/*
Route: api/todo/search?title=learn&complete=false
Query parameters:
- Come after ? in URL
- Separated by &
- Key=value format
- Optional (method has parameters with default values)
This is STILL a GET request
Just a different route pattern
"search" is a literal string, not a parameter
*/
public ActionResult<IEnumerable<TodoItem>> SearchTodos(
[FromQuery] string? title = null,
[FromQuery] bool? isComplete = null)
/*
[FromQuery] attribute:
- Explicitly binds from query string
- Optional with ApiController (inferred by default)
- But good for documentation
string? title:
- Nullable reference type
- Can be null (not provided) or have a value
- Default value: null
bool? isComplete:
- Nullable value type
- Can be null, true, or false
- Null means "don't filter by this"
URL examples:
api/todo/search → both null, returns all
api/todo/search?title=learn → filter by title
api/todo/search?isComplete=false → filter incomplete
api/todo/search?title=learn&isComplete=false → both filters
*/
{
// Start with all todos (IQueryable-like pattern)
var query = _todos.AsEnumerable();
// Apply filters only if provided
if (title != null)
{
query = query.Where(t =>
t.Title.Contains(title, StringComparison.OrdinalIgnoreCase));
/*
Contains() with StringComparison:
- Case-insensitive search
- "Learn" matches "learn", "LEARN", "LeArN"
- OrdinalIgnoreCase: Best for programmatic comparison
Other string comparison options:
- Ordinal: Byte-by-byte comparison
- OrdinalIgnoreCase: Case-insensitive ordinal
- CurrentCulture: Culture-sensitive (for UI)
- CurrentCultureIgnoreCase: Both
Use Ordinal/OrdinalIgnoreCase for:
- IDs, codes, technical strings
- Performance matters
- Consistency across different cultures
Use CurrentCulture for:
- User-visible strings
- Sorting alphabetically for users
*/
}
if (isComplete.HasValue)
{
query = query.Where(t => t.IsComplete == isComplete.Value);
/*
HasValue and Value:
- Nullable<T> has two properties
- HasValue: true if not null
- Value: The actual value (throws if null)
Alternative:
if (isComplete != null)
query = query.Where(t => t.IsComplete == isComplete);
// Implicit conversion from bool? to bool in comparison
*/
}
var results = query.ToList();
// ToList() executes the query
// With in-memory collection, it was already executed
// With database (IQueryable), this is when SQL is generated
return Ok(results);
}
}

Part 5: Running and Testing - What Actually Happens

When You Run dotnet run

$ dotnet run

Here's the step-by-step execution:

1. .NET Runtime Starts
- Loads the .NET 6 runtime
- Initializes garbage collector
- Sets up JIT (Just-In-Time) compiler

2. Program.cs execution begins
- Creates WebApplicationBuilder
- Reads appsettings.json
- Reads appsettings.Development.json
- Reads environment variables
- Reads command-line args

3. Services are registered
- AddControllers() registers controller discovery
- AddEndpointsApiExplorer() registers API explorer
- AddSwaggerGen() registers Swagger generator

4. Build() is called
- Compiles service collection into service provider
- Service provider is the DI container
- Creates middleware pipeline builder
- Configures Kestrel with:
- Ports from launchSettings.json
- SSL certificate for HTTPS
- Request limits (max body size, etc.)

5. Middleware pipeline is configured
- Swagger middleware (if development)
- HTTPS redirection middleware
- Authorization middleware
- Controller mapping middleware

6. Run() is called
- Kestrel starts listening
- Binds to ports
- Prints: Now listening on: https://localhost:7001
- Enters request processing loop

7. Application is now running
- Waiting for HTTP requests
- Each request gets a new scope
- Controllers created per request
- Static data persists across requests

launchSettings.json Explained

{
"profiles": {
"MyFirstApi": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7001;http://localhost:5001",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
  1. launchBrowser: true: Opens browser automatically
  2. launchUrl: "swagger": Opens Swagger UI
  3. applicationUrl: Where to listen (both HTTP and HTTPS)
  4. environmentVariables: Sets Development mode

When You Make a Request to Swagger

1. Browser requests: https://localhost:7001/swagger/index.html
2. SwaggerUI middleware responds with HTML page
3. HTML loads JavaScript app (Swagger UI)
4. JS requests: https://localhost:7001/swagger/v1/swagger.json
5. Swagger middleware generates JSON document
- Scans all controllers
- Reads attributes and XML comments
- Creates OpenAPI specification
6. JS receives JSON and renders interactive UI
7. You click "Try it out" for GET /api/todo
8. JS makes fetch request to: https://localhost:7001/api/todo
9. Your controller method runs
10. Response comes back, JS displays it

Part 6: Dependency Injection - The Magic Behind Services

What is Dependency Injection (DI)?

// WITHOUT Dependency Injection - BAD
public class TodoController : ControllerBase
{
private readonly TodoService _service;
public TodoController()
{
// Controller creates its own dependency
// Tight coupling, hard to test, hard to change
_service = new TodoService(new Logger(), new Database());
}
}

// WITH Dependency Injection - GOOD
public class TodoController : ControllerBase
{
private readonly ITodoService _service;
// Dependencies are "injected" (given to us)
// Controller doesn't know HOW service works
public TodoController(ITodoService service)
{
_service = service;
}
}

How DI Works in .NET

// In Program.cs
var builder = WebApplication.CreateBuilder(args);

// Register services with different lifetimes:

// SINGLETON: One instance for the ENTIRE application
builder.Services.AddSingleton<ITodoService, TodoService>();
/*
- Created once, when first requested
- Same instance used for all requests
- Good for: Configuration, caching, stateless services
- Warning: Must be THREAD-SAFE (multiple requests use it simultaneously)
*/

// SCOPED: One instance per HTTP request
builder.Services.AddScoped<ITodoService, TodoService>();
/*
- Created once per HTTP request
- Same instance used within one request
- Different instances for different requests
- Good for: Database contexts, unit of work
- Most common for web applications
*/

// TRANSIENT: New instance every time it's requested
builder.Services.AddTransient<ITodoService, TodoService>();
/*
- Created every time someone asks for it
- Different instance for every injection
- Good for: Lightweight, stateless services
- Example: Email validators, calculators
*/

The Full DI Lifecycle

1. Application starts
2. Build() compiles ServiceCollection into ServiceProvider
3. HTTP request arrives
4. Middleware creates a new "Scope" (for Scoped services)
5. Routing determines which controller to use
6. ControllerActivator creates controller instance
- Checks constructor parameters
- For each parameter:
a. ITodoService needed
b. Check service provider for ITodoService
c. Check lifetime (Singleton/Scoped/Transient)
d. Create or retrieve instance
e. Inject into constructor
7. Controller method executes
8. Response sent
9. Scope disposed
- Disposes all Scoped services
- Database connections closed
- Resources released

Part 7: HTTP Deep Dive - The Protocol Itself

HTTP Request Structure

POST /api/todo HTTP/1.1 ← Request Line
Host: localhost:7001 ← Headers
Content-Type: application/json
Accept: application/json
Authorization: Bearer eyJhbGciOi...
User-Agent: Mozilla/5.0
Content-Length: 48
← Empty line (CRLF)
{ ← Body
"title": "New Task",
"isComplete": false
}

Request Line:

  1. Method: GET, POST, PUT, DELETE, PATCH, etc.
  2. Path: /api/todo
  3. HTTP Version: HTTP/1.1 or HTTP/2

Headers:

  1. Key-Value pairs
  2. Provide metadata about the request
  3. Case-insensitive

Body:

  1. Optional (GET requests typically don't have one)
  2. Any format (JSON, XML, form data, binary)

HTTP Response Structure

HTTP/1.1 200 OK ← Status Line
Date: Mon, 15 Jan 2024 10:30:00 GMT ← Headers
Server: Kestrel
Content-Type: application/json; charset=utf-8
Content-Length: 52
Cache-Control: no-cache
← Empty line
{ ← Body
"id": 2,
"title": "New Task",
"isComplete": false,
"createdAt": "2024-01-15T10:30:00Z"
}

Status Line:

  1. HTTP Version
  2. Status Code: 200, 404, 500, etc.
  3. Reason Phrase: OK, Not Found, Internal Server Error

Common HTTP Status Codes

2xx Success:
200 OK - Request succeeded
201 Created - Resource created
204 No Content - Success, no body

3xx Redirection:
301 Moved Permanently - Resource moved permanently
307 Temporary Redirect - Try this URL instead
308 Permanent Redirect - Use this URL from now on

4xx Client Errors:
400 Bad Request - Invalid input
401 Unauthorized - Not logged in
403 Forbidden - Logged in but no permission
404 Not Found - Resource doesn't exist
405 Method Not Allowed - Wrong HTTP method
409 Conflict - Resource state conflict
422 Unprocessable - Validation failed
429 Too Many Requests - Rate limit exceeded

5xx Server Errors:
500 Internal Server Error - Something broke
502 Bad Gateway - Upstream server error
503 Service Unavailable - Server overloaded
504 Gateway Timeout - Upstream server timeout

Part 8: Model Binding - How Parameters Work

Binding Sources

[HttpPost("api/todo/{id}")]
public IActionResult Example(
[FromRoute] int id, // From URL path: /api/todo/5
[FromQuery] string filter, // From URL query: ?filter=active
[FromBody] TodoItem item, // From request body (JSON)
[FromHeader] string apiKey, // From HTTP header: X-API-Key: abc123
[FromForm] IFormFile file, // From form data (file upload)
[FromServices] ILogger log) // From DI container

Binding Process

1. Request arrives: POST /api/todo/5?filter=active
Header: X-API-Key: abc123
Body: {"title": "Test", "isComplete": false}

2. Model binder determines source for each parameter:
- id: Route data → "5" → int.Parse("5") → 5
- filter: Query string → "active"
- item: Body → Deserialize JSON → TodoItem object
- apiKey: Headers → "abc123"
- log: DI Container → ILogger instance

3. Validation runs (if attributes present)
- Required fields check
- Range constraints check
- Custom validation

4. If all valid, action executes
If invalid, 400 Bad Request returned

This is the complete, detailed explanation of .NET 6 Web API. Each concept builds on the previous ones. The key is understanding that an API is just HTTP request/response handling, organized into controllers, models, and services.