Course based on the CityInfo.API project — a REST API that exposes cities and their points of interest.
Table of Contents
- Introduction and Core Concepts
- Creating the API and Returning Resources
- Customizing API Responses
- Manipulating Resources and Input Validation
- Full and Partial Updates
- Dependency Injection and Configuration
- Quick Reference
1. Introduction and Core Concepts
What Is a REST API?
An API (Application Programming Interface) is a contract between a service provider (the server) and a consumer (the client). A REST API (Representational State Transfer) is an API that adheres to REST architectural constraints, making it a very popular architectural style for web services.
Key REST characteristics:
- Stateless: each request contains all the information needed; the server does not maintain session state on the server side.
- Resources identified by URIs: e.g.
/api/cities,/api/cities/1. - Representations: resources are transferred as representations (JSON, XML…).
- HTTP methods: GET, POST, PUT, PATCH, DELETE.
ASP.NET Core 10 Web API
ASP.NET Core is a cross-platform, open-source, high-performance web framework developed by Microsoft. It implements the MVC (Model-View-Controller) pattern, but for an API, the View layer is removed and the focus is on Controllers that return data.
graph TD
A[HTTP Client / Browser / App] -->|HTTP Request| B[ASP.NET Core Web API]
B -->|HTTP Response JSON/XML| A
B --> C[Controller]
C --> D[Service / Business Logic]
D --> E[Data Store / Database]
E --> D
D --> C
C --> B
Overall Architecture of an ASP.NET Core Web API Project
graph LR
subgraph CityInfo.API
P[Program.cs\nEntry Point]
P --> MW[Middleware Pipeline]
MW --> CR[Controller Routing]
CR --> CC[CitiesController]
CR --> PC[PointsOfInterestController]
CR --> FC[FilesController]
CC --> DS[CitiesDataStore]
PC --> DS
PC --> MS[IMailService]
MS --> LS[LocalMailService]
MS --> CS[CloudMailService]
end
subgraph Models
CDto[CityDto]
PDto[PointOfInterestDto]
PCreate[PointOfInterestForCreationDto]
PUpdate[PointOfInterestForUpdateDto]
end
CC --> CDto
PC --> PDto
PC --> PCreate
PC --> PUpdate
Project Structure
CityInfo.API/
├── Program.cs ← Entry point, DI and middleware configuration
├── CityInfo.API.csproj ← .NET project file
├── appsettings.json ← Configuration (dev + prod)
├── appsettings.Development.json ← Development-specific configuration
├── CitiesDataStore.cs ← In-memory data store
├── Controllers/
│ ├── CitiesController.cs ← GET /api/cities endpoint
│ ├── PointsOfInterestController.cs ← CRUD /api/cities/{id}/pointsofinterest
│ └── FilesController.cs ← File upload/download
├── Models/
│ ├── CityDto.cs
│ ├── PointOfInterestDto.cs
│ ├── PointOfInterestForCreationDto.cs
│ └── PointOfInterestForUpdateDto.cs
└── Services/
├── IMailService.cs
├── LocalMailService.cs
└── CloudMailService.cs
2. Creating the API and Returning Resources
The Middleware Pipeline
In ASP.NET Core, every HTTP request travels through a middleware pipeline configured in Program.cs. Each middleware can process the request, pass it to the next one, or short-circuit the pipeline.
sequenceDiagram
participant Client
participant HTTPS as UseHttpsRedirection
participant Auth as UseAuthorization
participant Router as MapControllers
participant Controller
Client->>HTTPS: HTTP Request
HTTPS->>Auth: Redirects to HTTPS if needed
Auth->>Router: Checks authorizations
Router->>Controller: Routes to the correct controller
Controller-->>Router: ActionResult (200, 404, etc.)
Router-->>Auth: Response
Auth-->>HTTPS: Response
HTTPS-->>Client: HTTP Response
Program.cs — Application Entry Point
using CityInfo.API;
using CityInfo.API.Services;
using Microsoft.AspNetCore.StaticFiles;
var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders();
builder.Logging.AddConsole();
// Register services in the DI container
builder.Services.AddControllers()
.AddXmlDataContractSerializerFormatters(); // XML support in addition to JSON
builder.Services.AddOpenApi(); // OpenAPI documentation (Swagger)
builder.Services.AddProblemDetails(options =>
options.CustomizeProblemDetails = ctx =>
{
ctx.ProblemDetails.Extensions
.Add("additionalInfo", "Additional info example");
ctx.ProblemDetails.Extensions
.Add("server", Environment.MachineName);
});
builder.Services.AddSingleton<FileExtensionContentTypeProvider>();
#if DEBUG
builder.Services.AddTransient<IMailService, LocalMailService>();
#else
builder.Services.AddTransient<IMailService, CloudMailService>();
#endif
builder.Services.AddSingleton<CitiesDataStore>();
var app = builder.Build();
// Configure the HTTP pipeline (middleware)
if (app.Environment.IsDevelopment())
{
app.MapOpenApi();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers(); // Enables routing to controllers
app.Run();
Key insight:
builderconfigures services (dependency injection), andappconfigures the pipeline (middlewares). This is the fundamental separation in ASP.NET Core.
HTTP Request Flow
sequenceDiagram
participant Client
participant API as ASP.NET Core API
participant Controller
participant DataStore
Client->>API: GET /api/cities
API->>Controller: Route → CitiesController.GetCities()
Controller->>DataStore: citiesDataStore.Cities
DataStore-->>Controller: List<CityDto>
Controller-->>API: Ok(cities) → 200 OK
API-->>Client: JSON Response
Models (DTOs — Data Transfer Objects)
A DTO is an object whose sole purpose is to transport data between the client and the API. It contains no business logic.
Important distinction: What the API returns (DTO) is NOT the same as what is stored in the database (Entity). This separation is essential.
// CityDto.cs — what the API returns to the client
namespace CityInfo.API.Models;
public class CityDto
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
// Calculated property: no need to store it, it is derived
public int NumberOfPointsOfInterest => PointsOfInterest.Count;
// Initialized to an empty list to avoid NullReferenceException
public ICollection<PointOfInterestDto> PointsOfInterest { get; set; } = [];
}
// PointOfInterestDto.cs — read representation of a point of interest
namespace CityInfo.API.Models;
public class PointOfInterestDto
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string? Description { get; set; }
}
The CitiesDataStore — In-Memory Data
For this course, data is stored in memory (no database). This is sufficient to learn API concepts. In production, Entity Framework Core with a real database would be used.
// CitiesDataStore.cs
using CityInfo.API.Models;
namespace CityInfo.API;
public class CitiesDataStore
{
public List<CityDto> Cities { get; set; }
public CitiesDataStore()
{
Cities =
[
new()
{
Id = 1,
Name = "New York City",
Description = "The one with that big park.",
PointsOfInterest = [
new() {
Id = 1,
Name = "Central Park",
Description = "The most visited urban park in the United States." },
new() {
Id = 2,
Name = "Empire State Building",
Description = "A 102-story skyscraper located in Midtown Manhattan." }
]
},
new()
{
Id = 2,
Name = "Antwerp",
Description = "The one with the cathedral that was never really finished.",
PointsOfInterest = [
new() {
Id = 3,
Name = "Cathedral of Our Lady",
Description = "A Gothic style cathedral, conceived by architects Jan and Pieter Appelmans." },
new() {
Id = 4,
Name = "Antwerp Central Station",
Description = "The finest example of railway architecture in Belgium." }
]
},
new()
{
Id = 3,
Name = "Paris",
Description = "The one with that big tower.",
PointsOfInterest = [
new() {
Id = 5,
Name = "Eiffel Tower",
Description = "A wrought iron lattice tower on the Champ de Mars, named after engineer Gustave Eiffel." },
new() {
Id = 6,
Name = "The Louvre",
Description = "The world's largest museum." }
]
}
];
}
}
The CitiesController
// Controllers/CitiesController.cs
using CityInfo.API.Models;
using Microsoft.AspNetCore.Mvc;
namespace CityInfo.API.Controllers;
[ApiController] // Enables API-friendly behaviors (auto validation, etc.)
[Route("api/cities")] // Route prefix for all actions in this controller
public class CitiesController(CitiesDataStore citiesDataStore) : ControllerBase
{
// GET /api/cities
[HttpGet()]
public ActionResult<IEnumerable<CityDto>> GetCities()
{
return Ok(citiesDataStore.Cities);
}
// GET /api/cities/{id}
[HttpGet("{id}")]
public ActionResult<CityDto> GetCity(int id)
{
var cityToReturn = citiesDataStore
.Cities.FirstOrDefault(c => c.Id == id);
if (cityToReturn == null)
{
return NotFound(); // 404 Not Found
}
return Ok(cityToReturn); // 200 OK
}
}
Routing Attributes
| Attribute | HTTP Method | Usage |
|---|---|---|
[HttpGet] | GET | Read one or more resources |
[HttpPost] | POST | Create a new resource |
[HttpPut] | PUT | Full update of a resource |
[HttpPatch] | PATCH | Partial update of a resource |
[HttpDelete] | DELETE | Delete a resource |
[Route("template")] | — | Define a route prefix at the controller level |
Tip: The
[Route]attribute at the controller level defines the common prefix. You can use[controller]as a placeholder that automatically takes the controller name without the “Controller” suffix. However, it is better to write the name explicitly so URIs remain stable even if the class is renamed.
ActionResult and IActionResult
ActionResult<T> is the recommended return type. It allows returning either a typed result (e.g. CityDto) or an HTTP result with a status code.
| Helper method | HTTP Code | When to use |
|---|---|---|
Ok(value) | 200 | Successful request with content |
Created(uri, value) | 201 | Resource created successfully |
CreatedAtRoute(name, routeValues, value) | 201 | Creation with Location header link |
NoContent() | 204 | Success with no content (DELETE, PUT) |
BadRequest() | 400 | Invalid client-side request |
NotFound() | 404 | Resource not found |
Unauthorized() | 401 | Not authenticated |
Forbid() | 403 | Not authorized |
StatusCode(500) | 500 | Generic server error |
3. Customizing API Responses
HTTP Status Codes — Why They Matter
Status codes are the only way for the API consumer to know whether their request succeeded. Here are the main levels:
graph LR
A[HTTP Status Codes] --> B[1xx Informational]
A --> C[2xx Success ✅]
A --> D[3xx Redirection]
A --> E[4xx Client Error ⚠️]
A --> F[5xx Server Error ❌]
C --> C1[200 OK]
C --> C2[201 Created]
C --> C3[204 No Content]
E --> E1[400 Bad Request]
E --> E2[401 Unauthorized]
E --> E3[403 Forbidden]
E --> E4[404 Not Found]
E --> E5[409 Conflict]
E --> E6[422 Unprocessable Entity]
F --> F1[500 Internal Server Error]
Golden rule: An error caused by the client (bad request, resource not found) → 4xx codes. An error caused by the server (bug, outage) → 5xx codes. Never return 200 OK for an error!
Problem Details — RFC Standard
When an error occurs, ASP.NET Core automatically returns a response that follows the RFC 7807 Problem Details for HTTP APIs standard. This standard defines a common JSON format for errors.
Example Problem Details response for a 404:
{
"type": "https://tools.ietf.org/html/rfc7807",
"title": "Not Found",
"status": 404,
"traceId": "00-abc123...",
"additionalInfo": "Additional info example",
"server": "SAMUEL"
}
Customization is done in Program.cs:
builder.Services.AddProblemDetails(options =>
options.CustomizeProblemDetails = ctx =>
{
ctx.ProblemDetails.Extensions
.Add("additionalInfo", "Additional info example");
ctx.ProblemDetails.Extensions
.Add("server", Environment.MachineName); // Useful in multi-server environments
});
Content Negotiation
Content negotiation is the process by which the client and server agree on the response format. The client sends an Accept header with the desired media type.
sequenceDiagram
participant Client
participant API
Client->>API: GET /api/cities\nAccept: application/json
API-->>Client: 200 OK\nContent-Type: application/json\n[JSON data]
Client->>API: GET /api/cities\nAccept: application/xml
API-->>Client: 200 OK\nContent-Type: application/xml\n[XML data]
Client->>API: GET /api/cities\nAccept: application/csv
API-->>Client: 406 Not Acceptable\n(format not supported)
Enabling XML support:
builder.Services.AddControllers()
.AddXmlDataContractSerializerFormatters(); // Adds XML for both input and output
| Type | Description | Example |
|---|---|---|
| Output Formatter | Serializes the response (server → client) | JSON, XML |
| Input Formatter | Deserializes the request body (client → server) | JSON, XML |
Accept header | Client requests a response format | Accept: application/xml |
Content-Type header | Client indicates the format of the sent body | Content-Type: application/json |
Returning a File
// Controllers/FilesController.cs
[Route("api/files")]
[ApiController]
public class FilesController(FileExtensionContentTypeProvider fileExtensionContentTypeProvider)
: ControllerBase
{
[HttpGet("{fileId}")]
public async Task<ActionResult> GetFile(string fileId)
{
var pathToFile = "sample-file.pdf";
if (!System.IO.File.Exists(pathToFile))
{
return NotFound();
}
// Automatically determine the Content-Type based on the extension
if (!fileExtensionContentTypeProvider.TryGetContentType(pathToFile,
out var contentType))
{
contentType = "application/octet-stream"; // Generic binary type
}
// async/await for I/O operations (file read = I/O)
var bytes = await System.IO.File.ReadAllBytesAsync(pathToFile);
return File(bytes, contentType, Path.GetFileName(pathToFile));
}
}
async / await — Asynchronous Programming
graph TD
A[Incoming HTTP request] --> B[Thread pool allocates a thread]
B --> C{I/O operation?}
C -->|No - CPU computation| D[Thread blocked until done]
C -->|Yes - file, DB, network| E[await releases the thread]
E --> F[Thread returns to pool]
F --> G[Other requests use this thread]
G --> H[I/O done: thread recovered from pool]
H --> I[Response sent to client]
Fundamental rules:
asyncis a modifier on the method — it allowsawaitto be used inside.awaitis an operator — it waits for an asynchronous task to complete without blocking the thread.- An
asyncmethod must returnvoid,Task, orTask<T>. - Use
async/awaitonly for I/O operations (files, network, database). For purely in-memory computations, it is unnecessary and consumes more memory.
// Synchronous (blocks the thread)
var bytes = System.IO.File.ReadAllBytes(pathToFile);
// Asynchronous (releases the thread during the read)
var bytes = await System.IO.File.ReadAllBytesAsync(pathToFile);
4. Manipulating Resources and Input Validation
Binding Source Attributes — Where Do Parameters Come From?
ASP.NET Core needs to know where to find the values for an action’s parameters. The [ApiController] attribute configures smart default rules.
| Attribute | Data source | Example |
|---|---|---|
[FromRoute] | URL segment | /api/cities/{id} → id |
[FromQuery] | Query string | /api/cities?page=1 → page |
[FromBody] | Request body (JSON) | JSON body → DTO object |
[FromHeader] | HTTP header | Accept: application/json |
[FromForm] | HTML form / files | IFormFile |
[FromServices] | DI container | Injected service |
Behavior with [ApiController]:
- Complex types (classes) →
[FromBody]inferred automatically IFormFile/IFormFileCollection→[FromForm]inferred automatically- Simple parameters (int, string) matching a route template →
[FromRoute]inferred - Other simple parameters →
[FromQuery]inferred
Separate DTOs for Each Operation
Golden rule: Use a distinct DTO for each type of operation (create, update, read). Even if the fields are identical.
graph LR
A[Operation] --> B[DTO used]
B1[Read GET] --> C1[PointOfInterestDto\nId + Name + Description]
B2[Create POST] --> C2[PointOfInterestForCreationDto\nName + Description\nno Id - server assigns it]
B3[Update PUT/PATCH] --> C3[PointOfInterestForUpdateDto\nName + Description\nwith validations]
Why?
- The client does not choose the resource ID — the server assigns it.
- Validations for creation may differ from those for updates.
- Makes refactoring easier without breaking the API.
Data Annotations — Declarative Validation
// Models/PointOfInterestForCreationDto.cs
using System.ComponentModel.DataAnnotations;
namespace CityInfo.API.Models;
public class PointOfInterestForCreationDto
{
[Required(ErrorMessage = "You must provide a name value.")]
[MaxLength(50)]
public string Name { get; set; } = string.Empty;
[MaxLength(200)]
public string? Description { get; set; }
}
| Attribute | Description |
|---|---|
[Required] | Field is mandatory |
[MaxLength(n)] | Maximum string length |
[MinLength(n)] | Minimum string length |
[Range(min, max)] | Numeric value between min and max |
[EmailAddress] | Validates email format |
[Url] | Validates URL format |
[RegularExpression(pattern)] | Validates against a regex |
[StringLength(max)] | Maximum length with message |
With the [ApiController] attribute, ModelState validation is automatic: if annotations fail, the API automatically returns a 400 Bad Request with validation error details. No need to manually check ModelState.IsValid.
Creating a Resource — POST
// Excerpt from PointsOfInterestController.cs
[HttpPost]
public ActionResult<PointOfInterestDto> CreatePointOfInterest(
int cityId,
PointOfInterestForCreationDto pointOfInterest) // [FromBody] inferred automatically
{
var city = citiesDataStore.Cities.FirstOrDefault(c => c.Id == cityId);
if (city is null)
{
return NotFound();
}
// Calculate the next ID (in-memory, no DB)
var maxPointOfInterestId = citiesDataStore.Cities
.SelectMany(c => c.PointsOfInterest).Max(p => p.Id);
var finalPointOfInterest = new PointOfInterestDto()
{
Id = ++maxPointOfInterestId,
Name = pointOfInterest.Name,
Description = pointOfInterest.Description
};
city.PointsOfInterest.Add(finalPointOfInterest);
// 201 Created + Location header pointing to the new resource
return CreatedAtRoute("GetPointOfInterest",
new { cityId, pointOfInterestId = finalPointOfInterest.Id },
finalPointOfInterest);
}
REST standard: A successful creation returns
201 Createdwith aLocationheader pointing to the URI of the new resource. Example:Location: /api/cities/1/pointsofinterest/7
Deleting a Resource — DELETE
[HttpDelete("{pointOfInterestId}")]
public ActionResult DeletePointOfInterest(int cityId, int pointOfInterestId)
{
var city = citiesDataStore.Cities
.FirstOrDefault(c => c.Id == cityId);
if (city is null) return NotFound();
var pointOfInterestFromStore = city.PointsOfInterest
.FirstOrDefault(c => c.Id == pointOfInterestId);
if (pointOfInterestFromStore is null) return NotFound();
city.PointsOfInterest.Remove(pointOfInterestFromStore);
mailService.Send("Point of interest deleted.",
$"Point of interest {pointOfInterestFromStore.Name} with id {pointOfInterestFromStore.Id} was deleted.");
return NoContent(); // 204 No Content — success with no response body
}
Uploading a File — POST with IFormFile
[HttpPost]
public async Task<ActionResult> CreateFile(IFormFile file)
{
// Essential security validation!
// 1. Check size (prevent large-file attacks - max 20MB here)
// 2. Check Content-Type (prevent malicious files)
if (file.Length is 0
|| file.Length > 20971520 // 20 MB
|| file.ContentType != "application/pdf")
{
return BadRequest("No file or an invalid one has been inputted.");
}
// ⚠️ SECURITY: Never store files in the API directory
// Use a separate directory without execute permissions
var path = Path.Combine(
Directory.GetCurrentDirectory(),
$"uploaded_file_{Guid.NewGuid()}.pdf");
using (var stream = new FileStream(path, FileMode.Create))
{
await file.CopyToAsync(stream);
}
return Ok("Your file has been uploaded successfully!");
}
⚠️ File security: File names and Content-Types can be forged by attackers. Always store files in a secure directory, without execute permissions, and preferably outside the web application tree.
5. Full and Partial Updates
PUT — Full Update
The PUT method performs a full update: the client must send all fields of the resource, even those it does not want to modify. If a field is omitted, it will be set to null or its default value.
// Models/PointOfInterestForUpdateDto.cs
using System.ComponentModel.DataAnnotations;
public class PointOfInterestForUpdateDto
{
[Required(ErrorMessage = "You should provide a name value.")]
[MaxLength(50)]
public string Name { get; set; } = string.Empty;
[MaxLength(200)]
public string? Description { get; set; }
}
[HttpPut("{pointOfInterestId}")]
public ActionResult UpdatePointOfInterest(
int cityId,
int pointOfInterestId,
PointOfInterestForUpdateDto pointOfInterest)
{
var city = citiesDataStore.Cities.FirstOrDefault(c => c.Id == cityId);
if (city is null) return NotFound();
var pointOfInterestFromStore = city.PointsOfInterest
.FirstOrDefault(c => c.Id == pointOfInterestId);
if (pointOfInterestFromStore is null) return NotFound();
// Full update: all fields are overwritten
pointOfInterestFromStore.Name = pointOfInterest.Name;
pointOfInterestFromStore.Description = pointOfInterest.Description;
return NoContent(); // 204 — success with no body
}
PATCH — Partial Update with JSON Patch
The PATCH method allows modifying only the specified fields. It uses the JSON Patch (RFC 6902) standard.
JSON Patch Document Format
[
{
"op": "replace",
"path": "/name",
"value": "New Name"
},
{
"op": "replace",
"path": "/description",
"value": "New description"
}
]
Operation (op) | Description |
|---|---|
add | Adds a value or element to an array |
remove | Removes a value |
replace | Replaces an existing value |
copy | Copies a value from one path to another |
move | Moves a value from one path to another |
test | Verifies a value matches before applying |
Required NuGet package:
Microsoft.AspNetCore.JsonPatch.SystemTextJson
[HttpPatch("{pointOfInterestId}")]
public ActionResult PartiallyUpdatePointOfInterest(
int cityId,
int pointOfInterestId,
JsonPatchDocument<PointOfInterestForUpdateDto> patchDocument)
{
var city = citiesDataStore.Cities.FirstOrDefault(c => c.Id == cityId);
if (city is null) return NotFound();
var pointOfInterestFromStore = city.PointsOfInterest
.FirstOrDefault(c => c.Id == pointOfInterestId);
if (pointOfInterestFromStore is null) return NotFound();
// Convert to update DTO (not PointOfInterestDto
// to prevent modification of the ID)
var pointOfInterestToPatch = new PointOfInterestForUpdateDto()
{
Name = pointOfInterestFromStore.Name,
Description = pointOfInterestFromStore.Description
};
// Apply the patch document — errors are added to ModelState
patchDocument.ApplyTo(pointOfInterestToPatch, jsonPatchError =>
{
var key = jsonPatchError.AffectedObject.GetType().Name;
ModelState.AddModelError(key, jsonPatchError.ErrorMessage);
});
// Validate ModelState after patch application
if (!ModelState.IsValid) return BadRequest(ModelState);
// Validate Data Annotations on the resulting DTO
if (!TryValidateModel(pointOfInterestToPatch)) return BadRequest(ModelState);
// Apply changes to the resource
pointOfInterestFromStore.Name = pointOfInterestToPatch.Name;
pointOfInterestFromStore.Description = pointOfInterestToPatch.Description;
return NoContent(); // 204
}
PUT vs PATCH Comparison
graph LR
subgraph PUT Full Update
A1[Client sends ALL fields\neven unmodified ones]
A2[Resource fully replaced]
A1 --> A2
end
subgraph PATCH Partial Update
B1[Client sends only\nchanges via JSON Patch]
B2[Only specified fields\nare modified]
B1 --> B2
end
6. Dependency Injection and Configuration
Dependency Injection (DI) — Principle
Dependency injection is a fundamental design pattern in ASP.NET Core. Rather than creating dependencies directly (new MyService()), a class receives them as parameters (typically through the constructor). The framework creates and manages the instances.
Advantages:
- Loose coupling between classes
- Ease of unit testing (mocks can be injected)
- Centralized lifetime management of objects
graph LR
subgraph Without DI
C1[PointsOfInterestController] -->|new LocalMailService| M1[LocalMailService]
end
subgraph With DI
C2[PointsOfInterestController] -->|IMailService injected| I[IMailService interface]
I --> M2[LocalMailService in dev]
I --> M3[CloudMailService in prod]
end
Mail Service Interface and Implementations
// Services/IMailService.cs
namespace CityInfo.API.Services;
public interface IMailService
{
void Send(string subject, string message);
}
// Services/LocalMailService.cs
namespace CityInfo.API.Services;
public class LocalMailService(IConfiguration configuration) : IMailService
{
// Reading configuration via IConfiguration
private string _mailTo = configuration["mailSettings:mailToAddress"]
?? throw new ArgumentNullException("mailSettings:mailToAddress");
private string _mailFrom = configuration["mailSettings:mailFromAddress"]
?? throw new ArgumentNullException("mailSettings:mailFromAddress");
public void Send(string subject, string message)
{
// In development: output to console
Console.WriteLine($"Mail from {_mailFrom} to {_mailTo}, with {nameof(LocalMailService)}.");
Console.WriteLine($"Subject: {subject}");
Console.WriteLine($"Message: {message}");
}
}
Service Lifetimes in the DI Container
| Method | Lifetime | When to use |
|---|---|---|
AddSingleton<T>() | Single instance for the entire app lifetime | Shared resources, caches, in-memory stores |
AddScoped<T>() | One instance per HTTP request | Entity Framework DbContext, request-scoped services |
AddTransient<T>() | New instance on every injection | Lightweight, stateless services |
// In Program.cs — service registration
builder.Services.AddSingleton<FileExtensionContentTypeProvider>();
// Based on build configuration (DEBUG or Release)
#if DEBUG
builder.Services.AddTransient<IMailService, LocalMailService>();
#else
builder.Services.AddTransient<IMailService, CloudMailService>();
#endif
// Data store is Singleton: one in-memory instance for the entire app
builder.Services.AddSingleton<CitiesDataStore>();
Injection in a Controller via Primary Constructor
In C# 12 and .NET 8+, the Primary Constructor syntax simplifies dependency injection:
// Old C# syntax (still valid)
public class PointsOfInterestController : ControllerBase
{
private readonly ILogger<PointsOfInterestController> _logger;
private readonly IMailService _mailService;
private readonly CitiesDataStore _citiesDataStore;
public PointsOfInterestController(
ILogger<PointsOfInterestController> logger,
IMailService mailService,
CitiesDataStore citiesDataStore)
{
_logger = logger;
_mailService = mailService;
_citiesDataStore = citiesDataStore;
}
}
// New C# 12 syntax — Primary Constructor (used in this course)
public class PointsOfInterestController(
ILogger<PointsOfInterestController> logger,
IMailService mailService,
CitiesDataStore citiesDataStore) : ControllerBase
{
// Parameters are directly available in all methods
}
Logging with ILogger
ASP.NET Core provides a built-in logging system via ILogger<T>. Logs have several severity levels:
| Level | Method | Usage |
|---|---|---|
| Trace | LogTrace() | Very detailed diagnostics |
| Debug | LogDebug() | Debug information |
| Information | LogInformation() | Normal events |
| Warning | LogWarning() | Abnormal but non-critical situation |
| Error | LogError() | Error requiring attention |
| Critical | LogCritical() | Severe failure |
// Using the logger in the controller
[HttpGet]
public ActionResult<IEnumerable<PointOfInterestDto>> GetPointsOfInterest(int cityId)
{
var city = citiesDataStore.Cities.FirstOrDefault(c => c.Id == cityId);
if (city is null)
{
// Structured message with named parameter {CityId} — avoids string interpolation
logger.LogInformation(
"City with id {CityId} wasn't found when accessing points of interest.", cityId);
return NotFound();
}
return Ok(city.PointsOfInterest);
}
Configuration — appsettings.json
// appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"mailSettings": {
"mailToAddress": "admin@mycompany.com",
"mailFromAddress": "noreply@mycompany.com"
}
}
Configuration is accessible via IConfiguration injected into services:
// In a service
public class LocalMailService(IConfiguration configuration) : IMailService
{
private string _mailTo = configuration["mailSettings:mailToAddress"] ?? "default@mail.com";
// The key uses ":" as a JSON hierarchy separator
}
7. Quick Reference
REST Conventions
| Operation | HTTP Method | URI | Body | Success Status Code |
|---|---|---|---|---|
| List all cities | GET | /api/cities | — | 200 OK |
| Get a city | GET | /api/cities/{id} | — | 200 OK |
| List POIs for a city | GET | /api/cities/{id}/pointsofinterest | — | 200 OK |
| Get a POI | GET | /api/cities/{id}/pointsofinterest/{poiId} | — | 200 OK |
| Create a POI | POST | /api/cities/{id}/pointsofinterest | JSON | 201 Created |
| Full update | PUT | /api/cities/{id}/pointsofinterest/{poiId} | JSON | 204 No Content |
| Partial update | PATCH | /api/cities/{id}/pointsofinterest/{poiId} | JSON Patch | 204 No Content |
| Delete a POI | DELETE | /api/cities/{id}/pointsofinterest/{poiId} | — | 204 No Content |
| Download a file | GET | /api/files/{fileId} | — | 200 OK |
| Upload a file | POST | /api/files | multipart/form-data | 200 OK |
Essential ASP.NET Core Attributes
| Attribute | Level | Description |
|---|---|---|
[ApiController] | Controller | Enables API behaviors (auto validation, inferred binding) |
[Route("template")] | Controller / Action | Defines the route |
[HttpGet] / [HttpPost] / etc. | Action | Maps an HTTP method |
[Required] | DTO property | Mandatory field |
[MaxLength(n)] | DTO property | Max length |
[FromBody] | Parameter | Request body |
[FromRoute] | Parameter | URL segment |
[FromQuery] | Parameter | Query string |
NuGet Packages Used in This Course
| Package | Usage |
|---|---|
Microsoft.AspNetCore.OpenApi | OpenAPI / Swagger documentation |
Microsoft.AspNetCore.JsonPatch.SystemTextJson | JSON Patch support (PATCH requests) |
Summary: Full POST Request Flow
sequenceDiagram
participant Client
participant Middleware
participant Controller
participant DataStore
Client->>Middleware: POST /api/cities/1/pointsofinterest\nBody: {"name":"Times Square","description":"..."}
Middleware->>Middleware: HTTPS Redirect
Middleware->>Middleware: Authorization
Middleware->>Controller: Route → PointsOfInterestController.CreatePointOfInterest(cityId=1, dto)
Note over Controller: [ApiController] automatically validates\nData Annotations on the DTO
Controller->>DataStore: Cities.FirstOrDefault(c => c.Id == 1)
DataStore-->>Controller: CityDto found
Controller->>DataStore: city.PointsOfInterest.Add(newPOI)
Controller-->>Middleware: CreatedAtRoute(201) + Location header
Middleware-->>Client: HTTP 201 Created\nLocation: /api/cities/1/pointsofinterest/7\nBody: {id:7, name:"Times Square",...}
Search Terms
asp.net · core · web · api · apis · c# · .net · development · attributes · data · injection · patch · post · configuration · dependency · dtos · flow · http · json · partial · put · request · resource · resources