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

Model Binding

Khái niệm

Model Binding là quá trình ASP.NET Core chuyển đổi dữ liệu từ HTTP request thành các tham số của action method.

┌─────────────────────────────────────────────────────────────────┐
│                    MODEL BINDING SOURCES                         │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐  ┌──────────┐       │
│  │   Form   │  │   Route  │  │  Query   │  │   Body   │       │
│  │  Values  │  │  Values  │  │  String  │  │  (JSON)  │       │
│  └────┬─────┘  └────┬─────┘  └────┬─────┘  └────┬─────┘       │
│       │             │             │             │              │
│       └─────────────┴─────────────┴─────────────┘              │
│                         │                                      │
│                         ▼                                      │
│              ┌─────────────────────┐                           │
│              │   Model Binder      │                           │
│              │  (Type Conversion)  │                           │
│              └─────────┬───────────┘                           │
│                        │                                       │
│                        ▼                                       │
│              ┌─────────────────────┐                           │
│              │   Action Method     │                           │
│              │   Parameters        │                           │
│              └─────────────────────┘                           │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

Binding Sources

[FromBody]

// JSON request body
[HttpPost]
public IActionResult Create([FromBody] Product product)
{
    // product được deserialize từ JSON body
    return Ok(product);
}

// Request:
// POST /api/products
// Content-Type: application/json
// {"name": "Laptop", "price": 999.99}

[FromQuery]

// Query string parameters
[HttpGet]
public IActionResult GetProducts(
    [FromQuery] int page = 1,
    [FromQuery] int pageSize = 10,
    [FromQuery] string? search = null)
{
    // /api/products?page=2&pageSize=20&search=laptop
    return Ok(new { page, pageSize, search });
}

// Hoặc binding vào object
[HttpGet]
public IActionResult GetProducts([FromQuery] PagingRequest request)
{
    return Ok(request);
}

public class PagingRequest
{
    public int Page { get; set; } = 1;
    public int PageSize { get; set; } = 10;
    public string? Search { get; set; }
}

[FromRoute]

// Route data
[HttpGet("{id}")]
public IActionResult GetById([FromRoute] int id)
{
    // /api/products/123 -> id = 123
    return Ok(id);
}

// Multiple route parameters
[HttpGet("{category}/{id}")]
public IActionResult GetByCategoryAndId(
    [FromRoute] string category,
    [FromRoute] int id)
{
    // /api/products/electronics/123
    return Ok(new { category, id });
}

[FromHeader]

[HttpGet]
public IActionResult GetWithHeader([FromHeader] string apiKey)
{
    // Lấy từ header: X-API-Key: abc123
    return Ok(apiKey);
}

// Hoặc lấy tất cả headers
[HttpGet]
public IActionResult GetHeaders([FromHeader] IHeaderDictionary headers)
{
    return Ok(headers);
}

[FromForm]

// Form data (multipart/form-data)
[HttpPost]
public IActionResult CreateForm([FromForm] ProductForm form)
{
    return Ok(form);
}

public class ProductForm
{
    public string Name { get; set; }
    public decimal Price { get; set; }
    public IFormFile Image { get; set; }
}

Binding Order

ASP.NET Core tìm kiếm giá trị theo thứ tự:

  1. Form values - POST form data
  2. Route values - URL route parameters
  3. Query string - URL query parameters
  4. Request body - JSON/XML (cho complex types)
// Không cần attribute cho complex types (mặc định là [FromBody])
[HttpPost]
public IActionResult Create(Product product) // Tự động [FromBody]
{
    return Ok(product);
}

// Cần attribute cho simple types
[HttpGet]
public IActionResult Get(int id) // Tự động [FromQuery]
{
    return Ok(id);
}

Custom Model Binding

Custom Model Binder

public class CommaSeparatedListBinder : IModelBinder
{
    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName).FirstValue;
        
        if (string.IsNullOrEmpty(value))
        {
            bindingContext.Result = ModelBindingResult.Success(new List<string>());
            return Task.CompletedTask;
        }

        var list = value.Split(',', StringSplitOptions.RemoveEmptyEntries)
                       .Select(x => x.Trim())
                       .ToList();
        
        bindingContext.Result = ModelBindingResult.Success(list);
        return Task.CompletedTask;
    }
}

// Sử dụng
[HttpGet]
public IActionResult GetTags([ModelBinder(typeof(CommaSeparatedListBinder))] List<string> tags)
{
    // /api/products?tags=csharp,dotnet,web -> ["csharp", "dotnet", "web"]
    return Ok(tags);
}

Model Binder Provider

public class CommaSeparatedListBinderProvider : IModelBinderProvider
{
    public IModelBinder? GetBinder(ModelBinderProviderContext context)
    {
        if (context.Metadata.ModelType == typeof(List<string>))
        {
            return new BinderTypeModelBinder(typeof(CommaSeparatedListBinder));
        }
        return null;
    }
}

// Đăng ký
builder.Services.AddControllers(options =>
{
    options.ModelBinderProviders.Insert(0, new CommaSeparatedListBinderProvider());
});

Binding Complex Types

Nested Objects

public class Address
{
    public string Street { get; set; }
    public string City { get; set; }
    public string Country { get; set; }
}

public class Customer
{
    public string Name { get; set; }
    public Address Address { get; set; }
    public List<string> Tags { get; set; }
}

// JSON binding
// {
//   "name": "John",
//   "address": {
//     "street": "123 Main St",
//     "city": "NYC",
//     "country": "USA"
//   },
//   "tags": ["vip", "active"]
// }

Collections

[HttpPost]
public IActionResult CreateMany([FromBody] List<Product> products)
{
    // [
    //   {"name": "Product 1", "price": 100},
    //   {"name": "Product 2", "price": 200}
    // ]
    return Ok(products.Count);
}

Binding với Minimal APIs

// Tự động binding
app.MapPost("/api/products", (Product product) => Results.Ok(product));

// Binding từ query string
app.MapGet("/api/products", (int page, int pageSize) => 
    Results.Ok(new { page, pageSize }));

// Binding từ route
app.MapGet("/api/products/{id}", (int id) => Results.Ok(id));

// Custom binding
app.MapGet("/api/products", ([FromQuery] PagingRequest request) => 
    Results.Ok(request));

// Binding từ header
app.MapGet("/api/secure", ([FromHeader(Name = "X-API-Key")] string apiKey) =>
{
    if (apiKey != "secret") return Results.Unauthorized();
    return Results.Ok("Authorized");
});

Best Practices

1. Sử dụng đúng binding source

// ✅ Rõ ràng
public IActionResult Get(
    [FromRoute] int id,
    [FromQuery] string? search,
    [FromBody] Product product)

// ❌ Không rõ ràng
public IActionResult Get(int id, string search, Product product)

2. Sử dụng nullable cho optional parameters

// ✅ Good
public IActionResult Get([FromQuery] string? search = null)

// ❌ Bad - sẽ throw nếu không có query param
public IActionResult Get([FromQuery] string search)

3. Validate binding

[HttpPost]
public IActionResult Create([FromBody] Product product)
{
    if (product == null)
    {
        return BadRequest("Invalid request body");
    }
    // ...
}