Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Minimal APIs Advanced

Overview Questions

  • 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?

Route Groups (.NET 7+)

Basic Route Groups

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();

Route Groups với Common Configuration

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();

Nested Route Groups

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();

Endpoint Filters (.NET 7+)

Basic Filter

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();

Logging Filter

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>();

Validation Filter

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>>();

Parameter Binding

Built-in Binding

// 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);

Custom Binding

// 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);

TryParse Binding

// 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 });

Organizing Large Minimal APIs

Extension Methods Pattern

// 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();
        });
    }
}

Interface-based Pattern

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();

Minimal APIs vs Controllers

Comparison

AspectMinimal APIsControllers
Code sizeÍt hơnNhiều hơn
ComplexityĐơn giảnPhức tạp hơn
FeaturesĐầy đủ cho most casesFull MVC features
FiltersEndpoint FiltersAction/Result Filters
Model BindingTự độngAttribute-based
TestingKhó hơnDễ hơn
OrganizationExtension methodsControllers folder
Use caseSmall APIs, microservicesLarge applications

When to Use

┌─────────────────────────────────────────────────────────────────┐
│                    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                               │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘