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ự:
- Form values - POST form data
- Route values - URL route parameters
- Query string - URL query parameters
- 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");
}
// ...
}