Nguon: Microsoft Learn · .NET 8.0
Hướng dẫn: Tạo Minimal API với ASP.NET Core
Tổng quan
Minimal APIs (API tối giản) được thiết kế để tạo HTTP API với các dependency (phụ thuộc) tối thiểu. Chúng lý tưởng cho microservices và các ứng dụng chỉ muốn bao gồm các file, tính năng và dependency tối thiểu trong ASP.NET Core.
Các API Endpoint được tạo
| API | Mô tả | Request body | Response body |
|---|---|---|---|
GET /todoitems | Lấy tất cả todo items | Không có | Mảng todo items |
GET /todoitems/complete | Lấy các todo items đã hoàn thành | Không có | Mảng todo items |
GET /todoitems/{id} | Lấy một item theo ID | Không có | Todo item |
POST /todoitems | Thêm item mới | Todo item | Todo item |
PUT /todoitems/{id} | Cập nhật item hiện có | Todo item | Không có |
PATCH /todoitems/{id} | Cập nhật một phần item | Todo item một phần | Không có |
DELETE /todoitems/{id} | Xóa một item | Không có | Không có |
Yêu cầu trước
Visual Studio
- Visual Studio 2022 với workload (bộ công việc) ASP.NET and web development
Visual Studio Code
- Visual Studio Code
- C# Dev Kit cho Visual Studio Code
- .NET 8 SDK (hoặc .NET 9)
Tạo Project
Dùng Visual Studio
- Tạo project mới bằng template ASP.NET Core Empty
- Đặt tên project là TodoApi
- Chọn .NET 8.0
- Bỏ chọn Do not use top-level statements
Dùng Visual Studio Code
dotnetcli
dotnet new web -o TodoApi cd TodoApi code -r ../TodoApi
Program.cs ban đầu
csharp
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();Thêm các gói NuGet
dotnetcli
dotnet add package Microsoft.EntityFrameworkCore.InMemory dotnet add package Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
Cho Visual Studio Code với Swagger:
dotnetcli
dotnet add package NSwag.AspNetCore
Model và Database Context
Todo.cs
csharp
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
}TodoDb.cs
csharp
using Microsoft.EntityFrameworkCore;
class TodoDb : DbContext
{
public TodoDb(DbContextOptions<TodoDb> options)
: base(options) { }
public DbSet<Todo> Todos => Set<Todo>();
}Triển khai API - Phiên bản cơ bản
csharp
using Microsoft.EntityFrameworkCore;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<TodoDb>(opt => opt.UseInMemoryDatabase("TodoList"));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();
var app = builder.Build();
app.MapGet("/todoitems", async (TodoDb db) =>
await db.Todos.ToListAsync());
app.MapGet("/todoitems/complete", async (TodoDb db) =>
await db.Todos.Where(t => t.IsComplete).ToListAsync());
app.MapGet("/todoitems/{id}", async (int id, TodoDb db) =>
await db.Todos.FindAsync(id)
is Todo todo
? Results.Ok(todo)
: Results.NotFound());
app.MapPost("/todoitems", async (Todo todo, TodoDb db) =>
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return Results.Created($"/todoitems/{todo.Id}", todo);
});
app.MapPut("/todoitems/{id}", async (int id, Todo inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return Results.NoContent();
});
app.MapDelete("/todoitems/{id}", async (int id, TodoDb db) =>
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return Results.NoContent();
}
return Results.NotFound();
});
app.Run();Cấu hình Swagger (Visual Studio Code)
csharp
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddOpenApiDocument(config =>
{
config.DocumentName = "TodoAPI";
config.Title = "TodoAPI v1";
config.Version = "v1";
});
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseOpenApi();
app.UseSwaggerUi(config =>
{
config.DocumentTitle = "TodoAPI";
config.Path = "/swagger";
config.DocumentPath = "/swagger/{documentName}/swagger.json";
config.DocExpansion = "list";
});
}Endpoint PATCH
TodoPatchDto.cs
csharp
public class TodoPatchDto
{
public string? Name { get; set; }
public bool? IsComplete { get; set; }
}Mã Endpoint PATCH
csharp
app.MapPatch("/todoitems/{id}", async (int id, TodoPatchDto inputTodo, TodoDb db) =>
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return Results.NotFound();
if (inputTodo.Name is not null) todo.Name = inputTodo.Name;
if (inputTodo.IsComplete is not null) todo.IsComplete = inputTodo.IsComplete.Value;
await db.SaveChangesAsync();
return Results.NoContent();
});Dùng MapGroup API
csharp
var todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);Dùng TypedResults API
csharp
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.ToArrayAsync());
}
static async Task<IResult> GetCompleteTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).ToListAsync());
}
static async Task<IResult> GetTodo(int id, TodoDb db)
{
return await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(todo)
: TypedResults.NotFound();
}
static async Task<IResult> CreateTodo(Todo todo, TodoDb db)
{
db.Todos.Add(todo);
await db.SaveChangesAsync();
return TypedResults.Created($"/todoitems/{todo.Id}", todo);
}
static async Task<IResult> UpdateTodo(int id, Todo inputTodo, TodoDb db)
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return TypedResults.NotFound();
todo.Name = inputTodo.Name;
todo.IsComplete = inputTodo.IsComplete;
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
return TypedResults.NotFound();
}Ngăn chặn Over-posting (gửi dữ liệu thừa) bằng DTOs
TodoItemDTO.cs
csharp
public class TodoItemDTO
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public TodoItemDTO() { }
public TodoItemDTO(Todo todoItem) =>
(Id, Name, IsComplete) = (todoItem.Id, todoItem.Name, todoItem.IsComplete);
}Model Todo được cập nhật (có trường Secret)
csharp
public class Todo
{
public int Id { get; set; }
public string? Name { get; set; }
public bool IsComplete { get; set; }
public string? Secret { get; set; }
}Các Endpoint được cập nhật dùng DTO
csharp
var todoItems = app.MapGroup("/todoitems");
todoItems.MapGet("/", GetAllTodos);
todoItems.MapGet("/complete", GetCompleteTodos);
todoItems.MapGet("/{id}", GetTodo);
todoItems.MapPost("/", CreateTodo);
todoItems.MapPut("/{id}", UpdateTodo);
todoItems.MapDelete("/{id}", DeleteTodo);
static async Task<IResult> GetAllTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.Select(x => new TodoItemDTO(x)).ToArrayAsync());
}
static async Task<IResult> GetCompleteTodos(TodoDb db)
{
return TypedResults.Ok(await db.Todos.Where(t => t.IsComplete).Select(x => new TodoItemDTO(x)).ToListAsync());
}
static async Task<IResult> GetTodo(int id, TodoDb db)
{
return await db.Todos.FindAsync(id)
is Todo todo
? TypedResults.Ok(new TodoItemDTO(todo))
: TypedResults.NotFound();
}
static async Task<IResult> CreateTodo(TodoItemDTO todoItemDTO, TodoDb db)
{
var todoItem = new Todo
{
IsComplete = todoItemDTO.IsComplete,
Name = todoItemDTO.Name
};
db.Todos.Add(todoItem);
await db.SaveChangesAsync();
todoItemDTO = new TodoItemDTO(todoItem);
return TypedResults.Created($"/todoitems/{todoItem.Id}", todoItemDTO);
}
static async Task<IResult> UpdateTodo(int id, TodoItemDTO todoItemDTO, TodoDb db)
{
var todo = await db.Todos.FindAsync(id);
if (todo is null) return TypedResults.NotFound();
todo.Name = todoItemDTO.Name;
todo.IsComplete = todoItemDTO.IsComplete;
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
static async Task<IResult> DeleteTodo(int id, TodoDb db)
{
if (await db.Todos.FindAsync(id) is Todo todo)
{
db.Todos.Remove(todo);
await db.SaveChangesAsync();
return TypedResults.NoContent();
}
return TypedResults.NotFound();
}Các khái niệm chính
Giá trị trả về
- ASP.NET Core tự động serialize (tuần tự hóa) các đối tượng thành JSON
- Mã response mặc định là 200 OK
- Các exception chưa được xử lý chuyển thành lỗi 5xx
- Có thể trả về các mã trạng thái khác nhau (404 NotFound, 201 Created, 204 NoContent)
TypedResults vs Results
- TypedResults có khả năng kiểm thử tốt hơn
- Tự động trả về metadata loại response cho OpenAPI
- Cách tiếp cận được khuyến nghị cho code production
DTOs (Data Transfer Objects - Đối tượng truyền dữ liệu)
- Ngăn chặn over-posting (gửi dữ liệu thừa)
- Ẩn các thuộc tính nhạy cảm
- Giảm kích thước payload (tải trọng dữ liệu)
- Làm phẳng các object graph (đồ thị đối tượng)
MapGroup API
- Giảm code lặp lại
- Cho phép tùy chỉnh nhóm endpoint bằng một lần gọi
- Các phương thức:
RequireAuthorization(),WithMetadata()
Kiểm thử
Visual Studio
- Dùng Endpoints Explorer và file .http
- Có thể tạo request trực tiếp từ các endpoint
Visual Studio Code
- Dùng Swagger UI tại
https://localhost:<port>/swagger - Giao diện kiểm thử tương tác