- Create a Web API project and explain the structure of
Program.cs(services, middleware, endpoints) - Write CRUD endpoints with EF Core in both minimal API and controller style
- Choose DI lifetimes, read configuration and test the API with curl and OpenAPI
In the previous lesson you learned to keep data in a database with EF Core. Now let's open it to the world: a mobile app, a website or another server should be able to ask for the list of students and add a new student over HTTP. ASP.NET Core is the fast, cross-platform, open-source web framework of .NET. In this lesson you will use it to build a full CRUD API on top of EF Core and learn dependency injection, middleware, configuration and the OpenAPI document.
- 1Create the project
dotnet new webapi -n SchoolApicreates a minimal API by default; add--use-controllersfor controllers. - 2Add EF Core
Add the
Microsoft.EntityFrameworkCore.Sqlitepackage and write the model and context classes. - 3Configuration and database
Put the connection string into
appsettings.json, then rundotnet ef migrations addanddotnet ef database updateas in the previous lesson. - 4Run and test
dotnet run --urls http://localhost:5080starts the server; send requests with curl, an.httpfile or Swagger UI.
dotnet new webapi -n SchoolApi
cd SchoolApi
dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet run --urls http://localhost:5080info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://localhost:5080
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.Program.cs: services and middleware
In a minimal API the whole application is set up in Program.cs, which has three parts. First, services are registered in builder.Services — this is .NET's built-in dependency injection (DI) container: AddDbContext adds the SchoolContext, AddOpenApi the document generator and AddValidation attribute-based validation. Then builder.Build() creates the app and the middleware pipeline is set up. Finally come the endpoints — methods bound to URLs.
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Http.HttpResults;
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
// 1. services for dependency injection
builder.Services.AddDbContext<SchoolContext>(options =>
options.UseSqlite(builder.Configuration.GetConnectionString("School")));
builder.Services.AddOpenApi();
builder.Services.AddValidation(); // .NET 10: checks [Required], [Range]...
var app = builder.Build();
// 2. the middleware pipeline
if (app.Environment.IsDevelopment())
app.MapOpenApi(); // the document at /openapi/v1.json
app.Use(async (context, next) => // our own middleware: request timing
{
var watch = System.Diagnostics.Stopwatch.StartNew();
await next(context);
app.Logger.LogInformation("{Method} {Path} -> {Status} in {Ms} ms", context.Request.Method,
context.Request.Path, context.Response.StatusCode, watch.ElapsedMilliseconds);
});Program.cs, part 1: services and middleware (requires the EF Core and OpenAPI packages)Middleware is a link in the chain that every HTTP request passes through. Each link can look at the request, change it, answer it itself or call next to pass the request to the next link. Our own middleware above starts a stopwatch, waits for the rest of the chain with await next(context) and then logs the status and the time taken. The order of the links matters and is usually this:
UseExceptionHandler— catches errors from all the links below itUseHttpsRedirection— redirects HTTP requests to HTTPSUseCors— decides which websites may call the APIUseAuthentication— “who are you?”UseAuthorization— “what are you allowed to do?”- Endpoints:
MapGet,MapPost,MapControllers
CRUD endpoints with EF Core
public class Student
{
public int Id { get; set; }
public required string Name { get; set; }
public int Score { get; set; }
}
public class SchoolContext(DbContextOptions<SchoolContext> options) : DbContext(options)
{
public DbSet<Student> Students => Set<Student>();
}
public record StudentDto([Required] string Name, [Range(0, 100)] int Score);// 3. endpoints
var students = app.MapGroup("/api/students");
students.MapGet("/", async (SchoolContext db) =>
await db.Students.AsNoTracking().OrderBy(s => s.Id).ToListAsync());
students.MapGet("/{id:int}", async Task<Results<Ok<Student>, NotFound>> (int id, SchoolContext db) =>
await db.Students.FindAsync(id) is Student s ? TypedResults.Ok(s) : TypedResults.NotFound());
students.MapPost("/", async (StudentDto dto, SchoolContext db) =>
{
var student = new Student { Name = dto.Name, Score = dto.Score };
db.Students.Add(student);
await db.SaveChangesAsync();
return TypedResults.Created($"/api/students/{student.Id}", student);
});
students.MapPut("/{id:int}", async Task<Results<NoContent, NotFound>> (int id, StudentDto dto, SchoolContext db) =>
await db.Students.Where(s => s.Id == id)
.ExecuteUpdateAsync(set => set.SetProperty(s => s.Name, dto.Name).SetProperty(s => s.Score, dto.Score)) == 0
? TypedResults.NotFound() : TypedResults.NoContent());
students.MapDelete("/{id:int}", async Task<Results<NoContent, NotFound>> (int id, SchoolContext db) =>
await db.Students.Where(s => s.Id == id).ExecuteDeleteAsync() == 0
? TypedResults.NotFound() : TypedResults.NoContent());
app.Run();Program.cs, part 2: five endpoints — read, create, update, deleteMapGroup lets you write the common /api/students prefix once. An endpoint's parameters are filled in automatically: id from the URL ({id:int} accepts only whole numbers), StudentDto from the JSON body and SchoolContext from the DI container. TypedResults return the right statuses — Created gives 201 and a Location header, NoContent 204, NotFound 404 — and the Results<...> type documents every possible answer, so OpenAPI sees them too. Names in JSON become camelCase automatically: Name → name.
{
"ConnectionStrings": {
"School": "Data Source=school.db"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}appsettings.json: GetConnectionString("School") reads this value; appsettings.Development.json and environment variables can override itTesting with curl
curl -X POST localhost:5080/api/students -H 'Content-Type: application/json' -d '{"name":"Aysel","score":95}'
curl -X POST localhost:5080/api/students -H 'Content-Type: application/json' -d '{"name":"Murad","score":78}'
curl localhost:5080/api/students
curl -s -o /dev/null -w '%{http_code}\n' -X PUT localhost:5080/api/students/2 \
-H 'Content-Type: application/json' -d '{"name":"Murad","score":84}'
curl -s -o /dev/null -w '%{http_code}\n' -X DELETE localhost:5080/api/students/2
curl -s -o /dev/null -w '%{http_code}\n' localhost:5080/api/students/2
curl -X POST localhost:5080/api/students -H 'Content-Type: application/json' -d '{"name":"Elvin","score":120}'{"id":1,"name":"Aysel","score":95}
{"id":2,"name":"Murad","score":78}
[{"id":1,"name":"Aysel","score":95},{"id":2,"name":"Murad","score":78}]
204
204
404
{"title":"One or more validation errors occurred.","errors":{"Score":["The field Score must be between 0 and 100."]}}Controllers, DI lifetimes and OpenAPI
Minimal APIs are handy for small and medium services. Large projects often use controllers: endpoints are grouped in classes derived from ControllerBase, and routes are written with attributes. dotnet new webapi --use-controllers creates such a project. Both styles use the same DI, middleware and EF Core, and they can even be combined in one project.
[ApiController]
[Route("api/[controller]")] // -> /api/students
public class StudentsController(SchoolContext db) : ControllerBase
{
[HttpGet]
public async Task<List<Student>> GetAll() =>
await db.Students.AsNoTracking().OrderBy(s => s.Id).ToListAsync();
[HttpGet("{id:int}")]
public async Task<ActionResult<Student>> Get(int id) =>
await db.Students.FindAsync(id) is Student s ? s : NotFound();
[HttpPost]
public async Task<ActionResult<Student>> Create(StudentDto dto)
{
var student = new Student { Name = dto.Name, Score = dto.Score };
db.Students.Add(student);
await db.SaveChangesAsync();
return CreatedAtAction(nameof(Get), new { id = student.Id }, student);
}
}
// Program.cs: builder.Services.AddControllers(); ... app.MapControllers();SchoolContext comes through the primary constructor, and [ApiController] returns 400 automatically on validation errors| Method | Lifetime | Used for |
|---|---|---|
AddSingleton | one instance for the whole app | configuration, an in-memory cache |
AddScoped | one instance per HTTP request | DbContext and the services that use it |
AddTransient | a new instance every time | lightweight, stateless helpers |
builder.Services.AddDbContext<SchoolContext>(...); // scoped: one per request
builder.Services.AddSingleton<ReportService>(); // ReportService needs SchoolContext
// error at startup (Development):
// Cannot consume scoped service '...SchoolContext' from singleton '...ReportService'.builder.Services.AddDbContext<SchoolContext>(...); // scoped: one per request
builder.Services.AddScoped<ReportService>(); // the same lifetime as its dependencyDbContext would be shared by all requestsOpenAPI (formerly called Swagger) is a machine-readable description of an API: which URLs exist, which JSON they expect and which statuses they return. Since .NET 9 the template uses AddOpenApi() and MapOpenApi(), and the document is served at /openapi/v1.json. For an interactive page in the browser, add the Swashbuckle.AspNetCore.SwaggerUI package and write app.UseSwaggerUI(o => o.SwaggerEndpoint("/openapi/v1.json", "School API")): on the /swagger page you can try every endpoint with a button. Client code for mobile apps can also be generated from this document automatically.
Key points
Program.cs: services (DI) →Build()→ middleware → endpoints →Run().- Minimal APIs:
MapGroup,MapGet/MapPost/MapPut/MapDelete; parameters come from the URL, the body and DI, andTypedResultsreturn 200/201/204/404. - DI lifetimes:
Singleton,Scoped(DbContext),Transient; never inject a scoped service into a singleton. - Configuration comes from
appsettings.json, environment-specific files and environment variables;GetConnectionStringreads the connection string. - DTOs and
AddValidationfor input; the OpenAPI document is at/openapi/v1.json, and Swagger UI is added with a package.
Check yourself
10 questions. Every correct answer earns XP.
AddDbContext register the DbContext?