- Route Groups là gì và khi nào sử dụng?
- Endpoint Filters hoạt động như thế nào?
- Làm sao để organize large Minimal APIs?
- Parameter binding trong Minimal APIs có gì đặc biệt?
- So sánh Minimal APIs vs Controllers khi nào dùng cái nào?
var app = builder.Build();
// Group với common prefix
var products = app.MapGroup("/api/products");
products.MapGet("/", () => new[] { "Product 1", "Product 2" });
products.MapGet("/{id}", (int id) => new { Id = id, Name = "Product" });
products.MapPost("/", (Product product) => Results.Created($"/api/products/{product.Id}", product));
products.MapPut("/{id}", (int id, Product product) => Results.NoContent());
products.MapDelete("/{id}", (int id) => Results.NoContent());
app.Run();
var app = builder.Build();
// Group với common middleware
var products = app.MapGroup("/api/products")
.RequireAuthorization()
.WithTags("Products")
.WithOpenApi();
products.MapGet("/", () => new[] { "Product 1" });
products.MapGet("/{id}", (int id) => new { Id = id });
products.MapPost("/", (Product product) => Results.Created($"/api/products/{product.Id}", product));
app.Run();
var app = builder.Build();
var api = app.MapGroup("/api")
.RequireAuthorization();
var products = api.MapGroup("/products")
.WithTags("Products");
products.MapGet("/", () => new[] { "Product 1" });
products.MapGet("/{id}", (int id) => new { Id = id });
var orders = api.MapGroup("/orders")
.WithTags("Orders");
orders.MapGet("/", () => new[] { "Order 1" });
orders.MapGet("/{id}", (int id) => new { Id = id });
app.Run();
var app = builder.Build();
app.MapGet("/api/products/{id}", (int id, AppDbContext db) =>
{
return db.Products.Find(id);
})
.AddEndpointFilter(async (context, next) =>
{
var id = context.GetArgument<int>(0);
if (id <= 0)
{
return Results.BadRequest("Invalid ID");
}
return await next(context);
});
app.Run();
public class LoggingFilter : IEndpointFilter
{
private readonly ILogger<LoggingFilter> _logger;
public LoggingFilter(ILogger<LoggingFilter> logger)
{
_logger = logger;
}
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
var startTime = DateTime.UtcNow;
_logger.LogInformation("Starting {Method} {Path}",
context.HttpContext.Request.Method,
context.HttpContext.Request.Path);
var result = await next(context);
_logger.LogInformation("Completed in {Elapsed}ms",
(DateTime.UtcNow - startTime).TotalMilliseconds);
return result;
}
}
// Đăng ký
app.MapGet("/api/products", async (AppDbContext db) =>
{
return await db.Products.ToListAsync();
})
.AddEndpointFilter<LoggingFilter>();
public class ValidationFilter<T> : IEndpointFilter
{
public async ValueTask<object?> InvokeAsync(
EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
// Find the argument to validate
for (var i = 0; i < context.Arguments.Count; i++)
{
if (context.Arguments[i] is T item)
{
var validationContext = new ValidationContext<T>(item);
var validator = context.HttpContext.RequestServices
.GetRequiredService<IValidator<T>>();
var result = await validator.ValidateAsync(validationContext);
if (!result.IsValid)
{
var errors = result.Errors
.GroupBy(e => e.PropertyName)
.ToDictionary(
g => g.Key,
g => g.Select(e => e.ErrorMessage).ToArray());
return Results.ValidationProblem(errors);
}
}
}
return await next(context);
}
}
// Sử dụng
app.MapPost("/api/products", (Product product, AppDbContext db) =>
{
db.Products.Add(product);
db.SaveChanges();
return Results.Created($"/api/products/{product.Id}", product);
})
.AddEndpointFilter<ValidationFilter<Product>>();
// FromRoute
app.MapGet("/api/products/{id}", (int id) => id);
// FromQuery
app.MapGet("/api/products", (int page, int pageSize) => new { page, pageSize });
// FromBody
app.MapPost("/api/products", (Product product) => product);
// FromServices
app.MapGet("/api/products", (AppDbContext db) => db.Products.ToList());
// FromHeader
app.MapGet("/api/secure", ([FromHeader(Name = "X-API-Key")] string apiKey) => apiKey);
// HttpContext
app.MapGet("/api/context", (HttpContext context) => context.Request.Path);
// HttpRequest/HttpResponse
app.MapGet("/api/request", (HttpRequest request) => request.Headers);
// Bind từ query string vào complex type
app.MapGet("/api/products", ([AsParameters] PagingParams p) =>
{
return new { p.Page, p.PageSize, p.Search };
});
public record PagingParams(
int Page = 1,
int PageSize = 10,
string? Search = null);
// Custom type với TryParse
public class ProductId
{
public int Value { get; }
public ProductId(int value) => Value = value;
public static bool TryParse(string? value, out ProductId? result)
{
if (int.TryParse(value, out var id))
{
result = new ProductId(id);
return true;
}
result = null;
return false;
}
}
// Binding tự động qua TryParse
app.MapGet("/api/products/{id}", (ProductId id) => new { id.Value });
// Program.cs
var app = builder.Build();
app.MapProductEndpoints();
app.MapOrderEndpoints();
app.MapUserEndpoints();
app.Run();
// ProductEndpoints.cs
public static class ProductEndpoints
{
public static void MapProductEndpoints(this IEndpointRouteBuilder routes)
{
var group = routes.MapGroup("/api/products")
.WithTags("Products")
.WithOpenApi();
group.MapGet("/", async (AppDbContext db) =>
await db.Products.ToListAsync());
group.MapGet("/{id}", async (int id, AppDbContext db) =>
await db.Products.FindAsync(id) is Product product
? Results.Ok(product)
: Results.NotFound());
group.MapPost("/", async (Product product, AppDbContext db) =>
{
db.Products.Add(product);
await db.SaveChangesAsync();
return Results.Created($"/api/products/{product.Id}", product);
});
group.MapPut("/{id}", async (int id, Product product, AppDbContext db) =>
{
var existing = await db.Products.FindAsync(id);
if (existing == null) return Results.NotFound();
existing.Name = product.Name;
existing.Price = product.Price;
await db.SaveChangesAsync();
return Results.NoContent();
});
group.MapDelete("/{id}", async (int id, AppDbContext db) =>
{
var product = await db.Products.FindAsync(id);
if (product == null) return Results.NotFound();
db.Products.Remove(product);
await db.SaveChangesAsync();
return Results.NoContent();
});
}
}
public interface IEndpoint
{
void MapEndpoint(IEndpointRouteBuilder app);
}
public class ProductEndpoint : IEndpoint
{
public void MapEndpoint(IEndpointRouteBuilder app)
{
var group = app.MapGroup("/api/products");
group.MapGet("/", () => new[] { "Product 1" });
group.MapGet("/{id}", (int id) => new { Id = id });
}
}
// Program.cs
var app = builder.Build();
var scope = app.Services.CreateScope();
var endpoints = scope.ServiceProvider.GetServices<IEndpoint>();
foreach (var endpoint in endpoints)
{
endpoint.MapEndpoint(app);
}
app.Run();
| Aspect | Minimal APIs | Controllers |
| Code size | Ít hơn | Nhiều hơn |
| Complexity | Đơn giản | Phức tạp hơn |
| Features | Đầy đủ cho most cases | Full MVC features |
| Filters | Endpoint Filters | Action/Result Filters |
| Model Binding | Tự động | Attribute-based |
| Testing | Khó hơn | Dễ hơn |
| Organization | Extension methods | Controllers folder |
| Use case | Small APIs, microservices | Large applications |
┌─────────────────────────────────────────────────────────────────┐
│ WHEN TO USE │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Minimal APIs: │
│ - Microservices │
│ - Small to medium APIs │
│ - Quick prototypes │
│ - Serverless functions │
│ - Simple CRUD operations │
│ │
│ Controllers: │
│ - Large enterprise applications │
│ - Complex business logic │
│ - Need MVC features (Views, Razor Pages) │
│ - Team với nhiều developers │
│ - Need extensive filter pipeline │
│ │
└─────────────────────────────────────────────────────────────────┘