Each HTTP request is handled independently, so durable state lives in a database or another external store. This chapter uses Entity Framework Core (EF Core) for data access, then covers input validation and the basics of authentication.
Add EF Core
EF Core is Microsoft's object-relational mapper. Add a database provider and the design-time package, then install the dotnet-ef tool:
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet tool install --global dotnet-ef
Other providers include Microsoft.EntityFrameworkCore.SqlServer and Npgsql.EntityFrameworkCore.PostgreSQL.
DbContext and entities
// Data/TodoDb.cs
using Microsoft.EntityFrameworkCore;
public class Todo
{
public int Id { get; set; }
public required string Title { get; set; }
public bool IsDone { get; set; }
}
public class TodoDb(DbContextOptions<TodoDb> options) : DbContext(options)
{
public DbSet<Todo> Todos => Set<Todo>();
}
Register the context in Program.cs. AddDbContext uses the scoped lifetime, so each request gets its own instance:
builder.Services.AddDbContext<TodoDb>(options =>
options.UseSqlite(builder.Configuration.GetConnectionString("Default")));
Migrations
Migrations turn model changes into versioned schema changes:
dotnet ef migrations add InitialCreate # generates Migrations/*.cs
dotnet ef database update # applies pending migrations
dotnet ef migrations script --idempotent -o migrate.sql
Review the generated migrations and commit them. In production, apply a SQL script or a migration bundle (dotnet ef migrations bundle) as a deployment step rather than migrating automatically at startup.
CRUD endpoints
var todos = app.MapGroup("/todos");
todos.MapGet("/", async (TodoDb db) =>
await db.Todos.AsNoTracking().ToListAsync());
todos.MapGet("/{id:int}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id) is { } todo ? Results.Ok(todo) : Results.NotFound());
todos.MapPost("/", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todos/{todo.Id}", todo);
});
todos.MapDelete("/{id:int}", async (int id, TodoDb db) =>
await db.Todos.Where(t => t.Id == id).ExecuteDeleteAsync() == 1
? Results.NoContent()
: Results.NotFound());
Use the async APIs so request threads are not blocked on I/O, and AsNoTracking() for read-only queries. Binding entities straight from the request is fine for a demo; real apps usually accept separate request types (DTOs) so clients cannot set fields such as Id.
Validation
With controllers marked [ApiController], data annotation attributes on the request type are checked automatically, and invalid input gets a 400 response with a ValidationProblemDetails body. Recent versions of ASP.NET Core bring the same behavior to minimal APIs with builder.Services.AddValidation().
using System.ComponentModel.DataAnnotations;
public class CreateTodo
{
[Required, StringLength(200)]
public string Title { get; set; } = "";
public bool IsDone { get; set; }
}
For complex rules, many teams use the FluentValidation library, or return TypedResults.ValidationProblem(errors) from their own checks.
Authentication basics
Authentication establishes who the caller is; authorization decides what they may do. APIs commonly use JWT bearer tokens:
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
dotnet user-jwts create # issues a development token and configures the app
using System.Security.Claims;
builder.Services.AddAuthentication().AddJwtBearer();
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapGet("/me", (ClaimsPrincipal user) => user.Identity?.Name)
.RequireAuthorization();
AddJwtBearer() reads its settings from the Authentication:Schemes:Bearer configuration section, which dotnet user-jwts fills in for Development. Controllers use [Authorize] and [AllowAnonymous]. Apps with server-rendered pages typically use cookie authentication and ASP.NET Core Identity for sign-in and password hashing, or delegate sign-in to an OpenID Connect provider.
Summary
- EF Core maps classes to tables; register a scoped
DbContext with AddDbContext. - Manage the schema with
dotnet ef migrations add and apply migrations as a deployment step. - Data annotations validate input automatically in
[ApiController] controllers and, with AddValidation(), in minimal APIs. - Add an authentication scheme, call
UseAuthentication and UseAuthorization, and protect endpoints with RequireAuthorization or [Authorize].
0 comments
Sign in · Sign in to leave a comment.