Trong cơ chế Model Binding của ASP.NET Core chúng ta sẽ học cách làm sao để truyền dữ liệu từ View lên Controller. Chúng ta cũng sẽ tìm hiểu về Model Binding và cơ chế hoạt động của nó. ASP.NET Core cho phép chúng ta bind dữ liệu từ nhiều nguồn khác nhau như HTML Form sử dụng [FromForm], từ giá trị route [FromRoute], từ query string [FromQuery], từ  body của request [FromBody] và từ Header của request [FromHeader].

Một số bài hướng dẫn trước đây có thể tham khảo:

  • Model và ViewModel trong ASP.NET Core MVC
  • Truyền dữ liệu từ Controller sang View trong ASP.NET Core
  • Xây dựng HTML Form trong ASP.NET Core
  • Strongly Typed View trong ASP.NET Core
  • Tag Helpers trong ASP.NET Core MVC

Model Binding là gì?

Model Binding là cơ chế map dữ liệu được gửi qua HTTP Request vào các tham số của action method trong Controller. HTTP Request có thể chứa dữ liệu từ nhiều định dạng. Dữ liệu có thể chứa trong HTML Form. Nó có thể là một phần của route value hoặc trên query string hay có thể là một body của request.

Cơ chế ASP.NET Core model binding cho phép chúng ta dễ dàng bind các giá trị này vào các tham số của action method. Các tham số này có thể là kiểu nguyên thủy hoặc kiểu đối tượng phức tạp.

Lấy dữ liệu từ Form Data trong Controller

Trong bài Tag Helper, chúng ta đã tạo một form cơ bản cho phép nhận một đối tượng Product. Khi người dùng click nút Submit thì dữ liệu sẽ được post lên phương thức Create trên Controller. Trong project đó chúng ta cũng tạo một ProductEditModel class chứa chi tiết của sản phẩm cần được tạo hoặc chỉnh sửa:

public class ProductEditModel
{
  public int ID{ get; set; }
  public string Name { get; set; }
  public decimal Rate { get; set; }
  public int Rating { get; set; }
}

Một form được tạo chứa 3 field: NameRate và Rating:

<form action="/home/Create" method="post">
    <label for="Name">Name</label>
    <input type="text" name="Name" />

    <label for="Rate">Rate</label>
    <input type="text" name="Rate" />
    <label for="Rating">Rating</label>
    <input type="text" name="Rating" />
    <input type="submit" name="submit" />
</form>

Action method Create trong HomeController:

[HttpPost]
public IActionResult Create(ProductEditModel model)
{
    string message = "";

    if (ModelState.IsValid)
    {
        message = "product " + model.Name + " created successfully" ;
    }
    else
    {
        message = "Failed to create the product. Please try again";
    }
    return Content(message);
}

Một submit của form trên, các giá trị trong form sẽ tự động được map vào đối tượng ProductEditModel trong Action method của controller:

public IActionResult Create(ProductEditModel model)

Cơ chế này tự động xảy ra đằng sau được gọi là Model Binding. Model Binder sẽ tìm các trường tương ứng giữa tham số ProductEditModel với trường trong form, route value hoặc query string…

Cơ chế Model Binding làm việc như thế nào?

Hình dưới đây minh họa cơ chế làm việc của Model binding:

 

Khi người dùng click vào nút Submit thì một request Post được gửi lên server với Form Data, QueryString, Route Parameter…MVCRouteHandler của Routing Engine sẽ xử lý request đến và có trách nhiệm gọi action method tương ứng. Model Bindler sẽ được kích hoạt trước khi action method được gọi. Nó tìm dữ liệu thỏa mãn trong form data, querys tring và request parameter trong HTTP Request. Sau đso nó sẽ binding các giá trị vào tham số của action method qua tên.

Ví dụ, trường “name” trong form sẽ được map vào thuộc tính “Name” trong ProductEditModel. Rate trong form sẽ được map vào thuộc tính Rate…

 

Để Model binding làm việc đúng:

  • Thuộc tính Name phải match với Request Data
  • Cấc thuộc tính phải đặt public set

Model Binder

Model Binder có trách nhiệm gán dữ liệu vào các tham số của action method. Model Binder được tạo mởi model binder provider. Model binder phải được implement inteface IModelBinderProvider. Nghĩa là bạn có thể tạo một Model Binder của riêng mình hoặc mở rộng nó bằng cách triển khai inteface IModelBinderProvider. Custom model binder phải được đăng ký trong ModelBinderProviders trong Startup.cs.

services.AddMvc(options =>
{
    options.ModelBinderProviders.Add(new CustomModelBinderProvider());
});

ModelState

Nếu Model binder không thành công trong việc bind dữ liệu từ Request vào thuộc tính model tương ứng, nó sẽ không đưa ra bất cứ thông báo lỗi nào. Nhưng nó sẽ update đối tượng ModelState với danh sách lỗi và set thuộc tính IsValid là false.

Vì thế kiểm tra ModelState.IsValid sẽ cho chúng ta thấy quá trình binding có thành công hay không.

Ví dụ: Trong ví dụ trên khi click nút submit mà không nhập bất cứ dữ liệu gì trên form thì kết quả sẽ ra validate thất bại và vì thế ModelState.IsValid sẽ là false.

Nếu không dùng model binding thì sao?

Trước khi chúng ta tìm hiểu sâu hơn về Model binding, chúng ta cần hiểu nếu không có model binding thì chúng ta sẽ truy cập đến dữ liệu từ request kiểu gì? Xem lại code mà chúng ta đã tạo trong phần trước. Thêm mới action method NoModelBinding:

[HttpPost]
public IActionResult NoModelBinding()
{

    ProductEditModel model = new ProductEditModel();
    string message = "";

    model.Name = Request.Form["Name"].ToString();
    model.Rate = Convert.ToDecimal( Request.Form["Rate"]);
    model.Rating =Convert.ToInt32( Request.Form["Rateing"]);

    message = "product " + model.Name + " created successfully";
    return Content(message);
}

Và thay đổi thành:

<form action="/home/NoModelBinding" method="post">

Truy cập trực tiếp đến query string

Tương tự như thế, bạn có thể truy cập đến các giá trị trên query string sử dụng Request.Query. Lấy các giá trị trên query string.

Ví dụ, Request.Query[“id”].ToString() trả về giá trị của id trên query string. Sử dụng Request.QueryString.HasValue sẽ cho bạn biết nếu có giá trị query string trên URL hiện tại hay không và Request.QueryString.Value sẽ trả về giá trị thô của query string.

Truy cập trực tiếp đến Request Headers

Tương tự như thế, bạn có thể sử dụng Request.Headers để truy cập các giá trị được gửi lên thông qua HTTP Header.

Truy cập đến Route Data

Để truy cập đến route bạn cần ghi đè phương thức OnActionExecuting:

using Microsoft.AspNetCore.Mvc.Filters;

public override void OnActionExecuting(ActionExecutingContext context)
{
    string id = context.RouteData.Values["id"].ToString();
    base.OnActionExecuting(context);
}

Bạn thấy rằng có rất nhiều code để lấy giá trị được post lên HTTP Request. ASP.NET Core model binding sẽ làm giúp bạn các việc này mà dùng ít code hơn.

Các nguồn cho Model binding

Như đã nhắc đến trước đây, model binder có thể lấy dữ liệu từ rất nhiều nơi khác nhau. Đây là danh sách các nguồn dữ liệu theo thứ tự mà model binding sẽ tìm:

  • HTML Form Value
  • Route Value
  • Query String

Model binder cũng có thể tìm dữ liệu từ các nguồn sau, nhưng chúng ta cần chỉ ra nguồn nào cần lấy một cách tường minh:

  • Request Body
  • Request Header
  • Services

 Lấy dữ liệu từ Form và Query String

Hãy thử binding action parameter với cả form và query string. Phương thức FormAndQuery sẽ như sau:

[HttpGet]
public IActionResult FormAndQuery()
{
    return View();
}

[HttpPost]
public IActionResult FormAndQuery(string name,ProductEditModel model)
{
    string message = "";

    if (ModelState.IsValid)
    {
        message = "Query string "+ name + " product " + model.Name + " Rate " + model.Rate + " Rating " + model.Rating ;
    }
    else
    {
        message = "Failed to create the product. Please try again";
     }
     return Content(message);
}

Chú ý rằng action method FormAndQuery có hai tham số name và ProductEditModel.

public IActionResult FormAndQuery(string name,ProductEditModel model)

Tiếp theo, chúng ta tạo view FormAndQuery như sau:

<form action="/home/FormAndQuery/?name=Test" method="post">
    <label for="Name">Name</label>
    <input type="text" name="Name" />

    <label for="Rate">Rate</label>
    <input type="text" name="Rate" />
    <label for="Rating">Rating</label>
    <input type="text" name="Rating" />
    <input type="submit" name="submit" />
</form>

Form có tên trường là “name“. Chúng ta cũng gửi “name=test” qua query string đến controller action:

<form action="/home/FormAndQuery/?name=Test" method="post">

Trong ví dụ trên, tham số “name” xuất hiện 2 lần như là một phần của query string. Khi form được submit, tham số “name” luôn map đến trường trong form chứ không phải query string. Bởi vì model binder luôn sử dụng thứ tự map dữ liệu theo thứ tự:

  1. Form Values
  2. Route Values
  3. Query Strings

Vì thế các giá trị trong form có trường name, tham số name luôn được gán giá trị. Chúng ta có thể thay đổi hành vi này bằng cách thêm thuộc tính [FromQuery]. Sử dụng [FromQuery] như sau:

public IActionResult FormAndQuery([FromQuery] string name,ProductEditModel model)

Giờ nếu submit form thì tham số name sẽ lấy giá trị từ query string, trong khi ProductEditModel sẽ lấy giá trị từ form.

Điều khiển Binding Source

Trong ví dụ trước, chúng ta sử dụng [FromQuery] để bắt buộc model binder đổi hành vi mặc định và sử dụng query string làm nguồn cho binding. ASP.NET Core cung cấp cho chúng ta một số thuộc tính điều khiển và chọn nguồn nào sẽ được nhận khi binding:

  1. [FromForm]
  2. [FromRoute]
  3. [FromQuery]
  4. [FromBody]
  5. [FromHeader]
  6. [FromServices]

[FromForm]

[FromForm] ép model binder bind tham số vào các trường của HTML Form.

[HttpPost]
public IActionResult Create([FromForm] ProductEditModel model)
{
}

[FromRoute]

[FromRoute] ép model binder bind tham số vào route data từ request.

Ví dụ: Tạo một action method FromRoute, cho phép một giá trị id và ProductEditModel. Chúng ta có 2 tham số id. MVC mặc định sẽ có tham số Id qua route và nó là tuỳ chọn. ProductEditModel cũng có thuộc tính id.

[HttpGet]
public IActionResult FromRoute()
{
    return View();
}

[HttpPost]
public IActionResult FromRoute(string id, ProductEditModel model)
{
    string message = "";

    if (ModelState.IsValid)
    {
        message = "Route " + id + " Product id " + model.id + " product " + model.Name + " Rate " + model.Rate + " Rating " + model.Rating;
    }
    else
    {
        message = "Failed to create the product. Please try again";
    }
    return Content(message);
}

Tạo ra view FromRoute

<form action="/home/FromRoute/Test" method="post">

    <label for="id">id</label>
    <input type="text" name="id" />

    <label for="Name">Name</label>
    <input type="text" name="Name" />

    <label for="Rate">Rate</label>
    <input type="text" name="Rate" />
    <label for="Rating">Rating</label>
    <input type="text" name="Rating" />
    <input type="submit" name="submit" />
</form>

Chú ý là chúng ta đang gọi controller action bằng cách sử dụng “test” như là giá trị route:

<form action="/home/FromRoute/Test" method="post">

Giờ khi submit form thì tham số id luôn map vào id từ form thay vì từ route value. Giờ hãy mở HomeController ra và áp dụng [FromRoute] trên tham số id.

public IActionResult FromRoute([FromRoute] string id, ProductEditModel model)

Khi submit form thì id sẽ được nhận là “test”

Binding query string sử dụng [FromQuery]

[FromQuery] ép model binder bind giá trị vào tham số từ giá trị lấy từ query string.

Binding đến Request body sử dụng [FromBody]

[FromBody] ép model binder bind dữ liệu từ request body. Formatter sẽ chọn dựa trên content-type của request. Dữ liệu trong request body có nhiều format khác nhau như JSON, XML…Model binder sẽ tìm thuộc tính content-type trên header và chọn formatter để chuyển dữ liệu về kiểu mong muốn.

Ví dụ, khi content-type là ‘application/json”, model binder sử dụng JsonInputFormatter class để chuyển request body và map vào tham số.

Binding từ Request Header sử dụng [FromHeader]

[FromHeader] map các giá trị từ request header vào tham số của action:

public IActionResult FromHeader( [FromHeader] string Host)
{
    return Content(Host);
}

Vô hiệu hoá binding với [BindNever]

Để cho model binder biết rằng không bind cho thuộc tính nào đó.

[BindNever]
public int Rating { get; set; }

Giờ thì model binder sẽ bỏ qua thuộc tính Rating ngay cả khi form có trường Rating.

Bắt buộc Binding với [BindRequuired]

Cái này ngược hẳn với [BindNever]. Trường nào được đánh dấu là BindRequired phải luôn hiển thị trên form và binding phải thực hiện nếu không thì ModelState.IsValid sẽ false.