ASP.NET Core: Model Binding trong ASP.NET Core


Khóa học qua video:
Lập trình Python All Lập trình C# All SQL Server All Lập trình C All Java PHP HTML5-CSS3-JavaScript
Đăng ký Hội viên
Tất cả các video dành cho hội viên

Bài viết này giải thích model binding là gì, cách thức hoạt động và cách tùy chỉnh hành vi của nó.

Model binding là gì

Các trang Controller và Razor hoạt động với dữ liệu đến từ các yêu cầu HTTP. Ví dụ: dữ liệu route có thể cung cấp khóa bản ghi và các trường biểu mẫu đã đăng có thể cung cấp giá trị cho các property của model. Viết code để truy xuất từng giá trị này và chuyển đổi chúng từ chuỗi sang kiểu .NET sẽ rất tẻ nhạt và dễ xảy ra lỗi. Model binding sẽ tự động hóa quá trình này. Hệ thống model binding:

  • Truy xuất dữ liệu từ nhiều nguồn khác nhau như dữ liệu route, trường form và chuỗi truy vấn.
  • Cung cấp dữ liệu cho controller và trang Razor trong các tham số phương thức và property dạng public.
  • Chuyển đổi dữ liệu chuỗi thành kiểu .NET.
  • Cập nhật property của các kiểu phức tạp.

Ví dụ

Giả sử bạn có phương thức hành động sau:

[HttpGet("{id}")]
public ActionResult<Pet> GetById(int id, bool dogsOnly)

Và ứng dụng nhận được yêu cầu có URL này:

https://contoso.com/api/pets/2?DogsOnly=true

Model binding trải qua các bước sau sau khi hệ thống route chọn phương thức hành động:

  • Tìm tham số đầu tiên của GetById, một số nguyên có tên id.
  • Xem qua các nguồn có sẵn trong yêu cầu HTTP và tìm thấy id = "2" trong dữ liệu route.
  • Chuyển đổi chuỗi "2" thành số nguyên 2.
  • Tìm tham số tiếp theo của GetById, một boolean có tên dogsOnly.
  • Xem qua các nguồn và tìm thấy "DogsOnly=true" trong chuỗi truy vấn. Khớp tên không phân biệt chữ hoa chữ thường.
  • Chuyển đổi chuỗi "true" thành boolean true.

Sau đó, framework sẽ gọi phương thức GetById, truyền vào 2 cho tham số id và true cho tham số dogsOnly.

Trong ví dụ trên, các mục tiêu model binding là các tham số phương thức có kiểu đơn giản. Mục tiêu cũng có thể là property của một kiểu phức tạp. Sau khi mỗi property được liên kết thành công, quá trình xác thực model sẽ diễn ra đối với property đó. Bản ghi về dữ liệu nào được liên kết với model và mọi lỗi liên kết hoặc xác thực được lưu trữ trong ControllerBase.ModelState hoặc PageModel.ModelState. Để tìm hiểu xem quá trình này có thành công hay không, ứng dụng sẽ kiểm tra cờ ModelState.IsValid.

Mục tiêu

Model binding cố gắng tìm giá trị cho các kiểu mục tiêu sau:

  • Các tham số của phương thức action của controller mà yêu cầu được route đến.
  • Các tham số của phương thức xử lý Razor Pages mà yêu cầu được route tới.
  • Property public của controller hoặc lớp PageModel, nếu được chỉ định bởi attribute.

Attribute [BindProperty]

Có thể được áp dụng cho property public của controller hoặc lớp PageModel để khiến model binding nhắm mục tiêu property đó:

public class EditModel : PageModel
{
    [BindProperty]
    public Instructor? Instructor { get; set; }

    // ...
}

Attribute [BindProperties]

Có thể được áp dụng cho controller hoặc lớp PageModel để yêu cầu model binding nhắm mục tiêu tất cả các property public của lớp:

[BindProperties]
public class CreateModel : PageModel
{
    public Instructor? Instructor { get; set; }

    // ...
}

Model binding cho các yêu cầu HTTP GET

Theo mặc định, các property không bị ràng buộc đối với các yêu cầu HTTP GET. Thông thường, tất cả những gì bạn cần cho yêu cầu GET là tham số ID bản ghi. ID bản ghi được sử dụng để tra cứu mục trong cơ sở dữ liệu. Do đó, không cần phải liên kết một property chứa thể hiện của model. Trong trường hợp bạn muốn các property được liên kết với dữ liệu từ các yêu cầu GET, hãy đặt property SupportsGet thành true:

[BindProperty(Name = "ai_user", SupportsGet = true)]
public string? ApplicationInsightsCookie { get; set; }

Model binding các kiểu đơn giản và phức tạp

Model binding sử dụng các định nghĩa cụ thể cho các kiểu mà nó hoạt động. Một kiểu đơn giản được chuyển đổi từ một chuỗi bằng cách sử dụng TypeConverter hoặc một phương thức TryParse. Một kiểu phức tạp được chuyển đổi từ nhiều giá trị đầu vào. Framework xác định sự khác biệt dựa trên sự tồn tại của TypeConverter hoặc TryParse. Bạn nên tạo một trình chuyển đổi kiểu hoặc sử dụng TryParse để chuyển đổi string sang kiểu SomeType không yêu cầu tài nguyên bên ngoài hoặc nhiều đầu vào.

Nguồn

Theo mặc định, model binding nhận dữ liệu ở dạng cặp key-value từ các nguồn sau trong yêu cầu HTTP:

  1. Trường form
  2. Phần thân yêu cầu (Dành cho các controller có attribute [ApiController].)
  3. Dữ liệu route
  4. Tham số chuỗi truy vấn
  5. Tệp đã tải lên

Đối với mỗi tham số hoặc property đích, các nguồn được quét theo thứ tự được chỉ ra trong danh sách trước đó. Có một vài trường hợp ngoại lệ:

  • Dữ liệu route và giá trị chuỗi truy vấn chỉ được sử dụng cho các kiểu đơn giản.
  • Các tệp đã tải lên chỉ bị ràng buộc với các kiểu mục tiêu triển khai IFormFile hoặc IEnumerable<IFormFile>.

Nếu nguồn mặc định không chính xác, hãy sử dụng một trong các attribute sau để chỉ định nguồn:

  • [FromQuery] - Nhận các giá trị từ chuỗi truy vấn.
  • [FromRoute] - Nhận giá trị từ dữ liệu tuyến đường.
  • [FromForm] - Nhận giá trị từ các trường biểu mẫu đã đăng.
  • [FromBody] - Nhận các giá trị từ nội dung yêu cầu.
  • [FromHeader] - Nhận giá trị từ header HTTP.

Những attribute này:

  • Được thêm vào các property model riêng lẻ chứ không phải vào lớp model, như trong ví dụ sau:
public class Instructor
{
    public int Id { get; set; }

    [FromQuery(Name = "Note")]
    public string? NoteFromQueryString { get; set; }

    // ...
}
  • Tùy chọn chấp nhận giá trị tên model trong hàm tạo. Tùy chọn này được cung cấp trong trường hợp tên property không khớp với giá trị trong yêu cầu. Ví dụ: giá trị trong yêu cầu có thể là tiêu đề có dấu gạch nối trong tên của nó, như trong ví dụ sau:
public void OnGet([FromHeader(Name = "Accept-Language")] string language)

Attribute [FromBody]

Áp dụng attribute [FromBody] cho một tham số để điền các property của nó từ nội dung của yêu cầu HTTP. Thời gian chạy ASP.NET Core ủy quyền trách nhiệm đọc nội dung cho bộ định dạng đầu vào. Các định dạng đầu vào sẽ được giải thích sau trong bài viết này.

Khi [FromBody] được áp dụng cho một tham số kiểu phức tạp, mọi attribute nguồn liên kết được áp dụng cho các property của nó đều bị bỏ qua. Ví dụ: action Create sau đây chỉ định rằng tham số pet của nó được điền từ nội dung:

public ActionResult<Pet> Create([FromBody] Pet pet)

Lớp này  Pet chỉ định rằng  Breed thuộc tính của nó được điền từ tham số chuỗi truy vấn:

public class Pet
{
    public string Name { get; set; } = null!;

    [FromQuery] // Attribute is ignored.
    public string Breed { get; set; } = null!;
}

Trong ví dụ trên:

  • Attribute [FromQuery] bị bỏ qua.
  • Property Breed không được điền từ tham số chuỗi truy vấn.

Trình định dạng đầu vào chỉ đọc phần nội dung và không hiểu các attribute nguồn liên kết. Nếu tìm thấy một giá trị phù hợp trong nội dung, giá trị đó sẽ được sử dụng để điền vào property Breed.

Không áp dụng [FromBody] cho nhiều tham số cho mỗi phương thức action. Sau khi trình định dạng đầu vào đọc luồng yêu cầu, luồng yêu cầu đó sẽ không thể đọc lại để liên kết các tham số [FromBody] khác nữa.

Nguồn bổ sung

Dữ liệu nguồn được cung cấp cho hệ thống model binding bởi các nhà cung cấp giá trị. Bạn có thể viết và đăng ký các nhà cung cấp giá trị tùy chỉnh để lấy dữ liệu cho việc model binding từ các nguồn khác. Ví dụ: bạn có thể muốn dữ liệu từ cookie hoặc trạng thái phiên. Để lấy dữ liệu từ một nguồn mới:

  • Tạo một lớp thực thi IValueProvider.
  • Tạo một lớp thực thi IValueProviderFactory.
  • Đăng ký lớp factory trong Program.cs.

Mẫu bao gồm nhà cung cấp giá trị và ví dụ về factory nhận giá trị từ cookie. Đăng ký các nhà máy cung cấp giá trị tùy chỉnh tại Program.cs:

builder.Services.AddControllers(options =>
{
    options.ValueProviderFactories.Add(new CookieValueProviderFactory());
});

Đoạn code trên đặt nhà cung cấp giá trị tùy chỉnh sau tất cả các nhà cung cấp giá trị tích hợp. Để đặt nó ở vị trí đầu tiên trong danh sách, hãy gọi Insert(0, new CookieValueProviderFactory()) thay vì Add.

Không có nguồn cho property model

Theo mặc định, lỗi trạng thái model không được tạo nếu không tìm thấy giá trị nào cho property model. Property được đặt thành null hoặc giá trị mặc định:

  • Các kiểu đơn giản nullable được đặt thành null.
  • Các kiểu giá trị non-nullable được đặt thành default(T). Ví dụ: một tham số int id được đặt thành 0.
  • Đối với các Kiểu phức tạp, thì model binding sẽ tạo một thể hiện bằng cách sử dụng hàm tạo mặc định mà không cần thiết lập property.
  • Mảng được đặt thành Array.Empty<T>(), ngoại trừ mảng byte[] được đặt thành null.

Nếu trạng thái model bị vô hiệu hóa khi không tìm thấy gì trong các trường form cho property model, hãy sử dụng attribute [BindRequired].

Lưu ý rằng hành vi [BindRequired] áp dụng cho model binding từ dữ liệu form đã post, không áp dụng cho dữ liệu JSON hoặc XML trong nội dung yêu cầu. Dữ liệu nội dung yêu cầu được xử lý bởi các bộ định dạng đầu vào.

Lỗi chuyển đổi kiểu

Nếu tìm thấy nguồn nhưng không thể chuyển đổi thành kiểu đích, thì trạng thái model sẽ được gắn cờ là không hợp lệ. Tham số hoặc property đích được đặt thành null hoặc giá trị mặc định, như đã lưu ý trong phần trên.

Trong controller API có attribute [ApiController] thuộc tính này, trạng thái model không hợp lệ sẽ dẫn đến phản hồi HTTP 400 tự động.

Trong trang Razor, hiển thị lại trang có thông báo lỗi:

public IActionResult OnPost()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    // ...

    return RedirectToPage("./Index");
}

Khi trang được hiển thị lại bằng đoạn code trên, thông tin nhập không hợp lệ sẽ không được hiển thị trong trường form. Điều này là do property model đã được đặt thành null hoặc giá trị mặc định. Đầu vào không hợp lệ sẽ xuất hiện trong một thông báo lỗi. Nếu bạn muốn hiển thị lại dữ liệu không hợp lệ trong trường form, hãy cân nhắc việc đặt property model thành một chuỗi và thực hiện chuyển đổi dữ liệu theo cách thủ công.

Bạn nên sử dụng chiến lược tương tự nếu không muốn lỗi chuyển đổi kiểu dẫn đến lỗi trạng thái model. Trong trường hợp đó, hãy biến property model thành một chuỗi.

Các kiểu đơn giản

Xem Model binding các kiểu đơn giản và phức tạp  để biết giải thích về các kiểu đơn giản và phức tạp.

Các kiểu đơn giản mà model binder có thể chuyển đổi chuỗi nguồn thành bao gồm:

Ràng buộc với IParsable<T>.TryParse

API IParsable<TSelf>.TryParse hỗ trợ các giá trị tham số action của controller liên kết:

public static bool TryParse (string? s, IFormatProvider? provider, out TSelf result);

Lớp DateRange sau thực thi IParsable<TSelf> để hỗ trợ liên kết phạm vi ngày:

public class DateRange : IParsable<DateRange>
{
    public DateOnly? From { get; init; }
    public DateOnly? To { get; init; }

    public static DateRange Parse(string value, IFormatProvider? provider)
    {
        if (!TryParse(value, provider, out var result))
        {
           throw new ArgumentException("Could not parse supplied value.", nameof(value));
        }

        return result;
    }

    public static bool TryParse(string? value,
                                IFormatProvider? provider, out DateRange dateRange)
    {
        var segments = value?.Split(',', StringSplitOptions.RemoveEmptyEntries 
                                       | StringSplitOptions.TrimEntries);

        if (segments?.Length == 2
            && DateOnly.TryParse(segments[0], provider, out var fromDate)
            && DateOnly.TryParse(segments[1], provider, out var toDate))
        {
            dateRange = new DateRange { From = fromDate, To = toDate };
            return true;
        }

        dateRange = new DateRange { From = default, To = default };
        return false;
    }
}

Trong đoạn code trên:

  • Chuyển đổi một chuỗi đại diện cho hai ngày thành một đối tượng DateRange
  • Model binder sử dụng phương thức IParsable<TSelf>.TryParse để liên kết DateRange.

Action của controller sau đây sử dụng lớp DateRange để liên kết một phạm vi ngày:

// GET /WeatherForecast/ByRange?range=7/24/2022,07/26/2022
public IActionResult ByRange([FromQuery] DateRange range)
{
    if (!ModelState.IsValid)
        return View("Error", ModelState.Values.SelectMany(v => v.Errors));

    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Where(wf => DateOnly.FromDateTime(wf.Date) >= range.From
                     && DateOnly.FromDateTime(wf.Date) <= range.To)
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d"),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int)(wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View("Index", weatherForecasts);
}

Lớp Locale sau thực thi IParsable<TSelf> để hỗ trợ liên kết với CultureInfo:

public class Locale : CultureInfo, IParsable<Locale>
{
    public Locale(string culture) : base(culture)
    {
    }

    public static Locale Parse(string value, IFormatProvider? provider)
    {
        if (!TryParse(value, provider, out var result))
        {
           throw new ArgumentException("Could not parse supplied value.", nameof(value));
        }

        return result;
    }

    public static bool TryParse([NotNullWhen(true)] string? value,
                                IFormatProvider? provider, out Locale locale)
    {
        if (value is null)
        {
            locale = new Locale(CurrentCulture.Name);
            return false;
        }
        
        try
        {
            locale = new Locale(value);
            return true;
        }
        catch (CultureNotFoundException)
        {
            locale = new Locale(CurrentCulture.Name);
            return false;
        }
    }
}

Action của controller sau đây sử dụng lớp Locale để liên kết một chuỗi CultureInfo:

// GET /en-GB/WeatherForecast
public IActionResult Index([FromRoute] Locale locale)
{
    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d", locale),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int)(wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View(weatherForecasts);
}

Action của controller sau đây sử dụng các lớp DateRange và Locale để liên kết một phạm vi ngày với CultureInfo:

// GET /af-ZA/WeatherForecast/RangeByLocale?range=2022-07-24,2022-07-29
public IActionResult RangeByLocale([FromRoute] Locale locale, [FromQuery] string range)
{
    if (!ModelState.IsValid)
        return View("Error", ModelState.Values.SelectMany(v => v.Errors));

    if (!DateRange.TryParse(range, locale, out DateRange rangeResult))
    {
        ModelState.TryAddModelError(nameof(range),
            $"Invalid date range: {range} for locale {locale.DisplayName}");

        return View("Error", ModelState.Values.SelectMany(v => v.Errors));
    }

    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Where(wf => DateOnly.FromDateTime(wf.Date) >= rangeResult.From
                     && DateOnly.FromDateTime(wf.Date) <= rangeResult.To)
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d", locale),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int) (wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View("Index", weatherForecasts);
}

Ứng dụng mẫu API trên GitHub hiển thị mẫu trên cho controller API.

Ràng buộc với TryParse

API  TryParse hỗ trợ các giá trị tham số action của controller liên kết:

public static bool TryParse(string value, T out result);
public static bool TryParse(string value, IFormatProvider provider, T out result);

IParsable<T>.TryParse là cách tiếp cận được đề xuất để liên kết tham số vì không giống như TryParse, nó không phụ thuộc vào sự phản chiếu.

Lớp DateRangeTP sau đây thực thi TryParse:

public class DateRangeTP
{
    public DateOnly? From { get; }
    public DateOnly? To { get; }

    public DateRangeTP(string from, string to)
    {
        if (string.IsNullOrEmpty(from))
            throw new ArgumentNullException(nameof(from));
        if (string.IsNullOrEmpty(to))
            throw new ArgumentNullException(nameof(to));

        From = DateOnly.Parse(from);
        To = DateOnly.Parse(to);
    }

    public static bool TryParse(string? value, out DateRangeTP? result)
    {
        var range = value?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
        if (range?.Length != 2)
        {
            result = default;
            return false;
        }

        result = new DateRangeTP(range[0], range[1]);
        return true;
    }
}

Action của controller sau đây sử dụng lớp DateRangeTP để liên kết một phạm vi ngày:

// GET /WeatherForecast/ByRangeTP?range=7/24/2022,07/26/2022
public IActionResult ByRangeTP([FromQuery] DateRangeTP range)
{
    if (!ModelState.IsValid)
        return View("Error", ModelState.Values.SelectMany(v => v.Errors));

    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Where(wf => DateOnly.FromDateTime(wf.Date) >= range.From
                     && DateOnly.FromDateTime(wf.Date) <= range.To)
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d"),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int)(wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View("Index", weatherForecasts);
}

Các kiểu phức tạp

Một kiểu phức tạp phải có một hàm tạo mặc định bublic và các property có thể ghi public để liên kết. Khi model binding xảy ra, lớp sẽ được khởi tạo bằng cách sử dụng hàm tạo mặc định public.

Đối với mỗi property của kiểu phức tạp, model binding sẽ xem qua các nguồn cho mẫu tên prefix.property_name. Nếu không tìm thấy gì, nó chỉ tìm property_name không có tiền tố. Quyết định sử dụng tiền tố không được đưa ra cho mỗi property. Ví dụ: với một truy vấn chứa ?Instructor.Id=100&Name=foo, được liên kết với phương thức OnGet(Instructor instructor), đối tượng kết quả thuộc kiểu Instructor chứa:

  • Id đặt thành 100.
  • Name đặt thành null. Model binding dự kiến Instructor.Name ​​vì Instructor.Id đã được sử dụng trong tham số truy vấn trước đó.

Để liên kết với một tham số, tiền tố là tên tham số. Để liên kết với một property public PageModel, tiền tố là tên property public. Một số attribute có property Prefix cho phép bạn ghi đè cách sử dụng mặc định của tham số hoặc tên property.

Ví dụ: giả sử kiểu phức tạp là lớp Instructor sau:

public class Instructor
{
    public int ID { get; set; }
    public string LastName { get; set; }
    public string FirstName { get; set; }
}

Tiền tố = tên tham số

Nếu model bị ràng buộc là một tham số có tên instructorToUpdate:

public IActionResult OnPost(int? id, Instructor instructorToUpdate)

Model binding bắt đầu bằng cách xem qua các nguồn để tìm khóa instructorToUpdate.ID. Nếu không tìm thấy, nó sẽ tìm kiếm ID không có tiền tố.

Tiền tố = tên property

Nếu model được liên kết là property có tên Instructor của controller hoặc lớp PageModel:

[BindProperty]
public Instructor Instructor { get; set; }

Model binding bắt đầu bằng cách xem qua các nguồn để tìm khóa Instructor.ID. Nếu không tìm thấy, nó sẽ tìm kiếm ID không có tiền tố.

Tiền tố tùy chỉnh

Nếu model bị ràng buộc có tham số được đặt tên instructorToUpdate và attribute Bind chỉ định Instructor làm tiền tố:

public IActionResult OnPost(
    int? id, [Bind(Prefix = "Instructor")] Instructor instructorToUpdate)

Model binding bắt đầu bằng cách xem qua các nguồn để tìm khóa Instructor.ID. Nếu không tìm thấy, nó sẽ tìm kiếm ID không có tiền tố.

Attribute cho các mục tiêu kiểu phức tạp

Một số attribute tích hợp có sẵn để kiểm soát model binding của các kiểu phức tạp:

Cảnh báo

Các attribute này ảnh hưởng đến model binding khi dữ liệu form được post là nguồn giá trị. Chúng không ảnh hưởng đến các trình định dạng đầu vào xử lý các nội dung yêu cầu JSON và XML được post. Các định dạng đầu vào sẽ được giải thích sau trong bài viết này.

Attribute [Bind]

Có thể được áp dụng cho một lớp hoặc một tham số phương thức. Chỉ định những property nào của model sẽ được đưa vào model binding. [Bind] không ảnh hưởng đến các bộ định dạng đầu vào.

Trong ví dụ sau, chỉ các property được chỉ định của model Instructor bị ràng buộc khi bất kỳ trình xử lý hoặc phương thức action nào được gọi:

[Bind("LastName,FirstMidName,HireDate")]
public class Instructor

Trong ví dụ sau, chỉ các property được chỉ định của model Instructor bị ràng buộc khi phương thức OnPost được gọi:

[HttpPost]
public IActionResult OnPost(
    [Bind("LastName,FirstMidName,HireDate")] Instructor instructor)

Attribute [Bind] có thể được sử dụng để bảo vệ khỏi việc đăng quá nhiều (Over-Postint) trong các tình huống create. Nó không hoạt động tốt trong các tình huống chỉnh sửa vì các property bị loại trừ được đặt thành null hoặc giá trị mặc định thay vì được giữ nguyên. Để bảo vệ khỏi việc đăng quá nhiều, nên sử dụng view model thay vì attribute [Bind]. Để biết thêm thông tin, hãy xem Lưu ý bảo mật về việc đăng quá nhiều.

Attribute [ModelBinder]

ModelBinderAttribution có thể được áp dụng cho các kiểu, property hoặc tham số. Nó cho phép chỉ định kiểu model binder được sử dụng để liên kết thể hiện hoặc kiểu cụ thể. Ví dụ:

[HttpPost]
public IActionResult OnPost(
    [ModelBinder(typeof(MyInstructorModelBinder))] Instructor instructor)

Attribute [ModelBinder] cũng có thể được sử dụng để thay đổi tên của property hoặc tham số khi nó bị ràng buộc bởi model:

public class Instructor
{
    [ModelBinder(Name = "instructor_id")]
    public string Id { get; set; }

    // ...
}

Attribute [BindRequired]

Làm cho model binding thêm lỗi trạng thái model nếu liên kết không thể xảy ra đối với property của model. Đây là một ví dụ:

public class InstructorBindRequired
{
    // ...

    [BindRequired]
    public DateTime HireDate { get; set; }
}

Xem thêm phần thảo luận về attribute [Required] trong Xác thực model.

Attribute [BindNever]

Có thể được áp dụng cho một property hoặc một kiểu. Ngăn chặn việc model binding thiết lập property của model. Khi được áp dụng cho một kiểu, hệ thống model binding sẽ loại trừ tất cả các property mà kiểu đó định nghĩa. Đây là một ví dụ:

public class InstructorBindNever
{
    [BindNever]
    public int Id { get; set; }

    // ...
}

Collection

Đối với các mục tiêu là tập hợp các kiểu đơn giản, model binding sẽ tìm kiếm các kết quả khớp với parameter_name hoặc property_name. Nếu không tìm thấy kết quả phù hợp, nó sẽ tìm một trong các định dạng được hỗ trợ mà không có tiền tố. Ví dụ:

  • Giả sử tham số bị ràng buộc là một mảng có tên selectedCourses:

    public IActionResult OnPost(int? id, int[] selectedCourses)
    
  • Dữ liệu chuỗi form hoặc truy vấn có thể ở một trong các định dạng sau:

    selectedCourses=1050&selectedCourses=2000 
    
    selectedCourses[0]=1050&selectedCourses[1]=2000
    
    [0]=1050&[1]=2000
    
    selectedCourses[a]=1050&selectedCourses[b]=2000&selectedCourses.index=a&selectedCourses.index=b
    
    [a]=1050&[b]=2000&index=a&index=b
    

    Tránh ràng buộc một tham số hoặc một property có tên index hoặc Index nếu nó liền kề với một giá trị của collection. Model binding cố gắng sử dụng index làm chỉ mục cho collection, điều này có thể dẫn đến liên kết không chính xác. Ví dụ: hãy xem xét action sau:

    public IActionResult Post(string index, List<Product> products)
    

    Trong code trên, tham số chuỗi truy vấn index liên kết với tham số phương thức index và cũng được sử dụng để liên kết collection sản phẩm. Đổi tên tham số index hoặc sử dụng attribute model binding để định cấu hình liên kết sẽ tránh được sự cố này:

    public IActionResult Post(string productIndex, List<Product> products)
    
  • Định dạng sau chỉ được hỗ trợ trong dữ liệu form:

    selectedCourses[]=1050&selectedCourses[]=2000
    
  • Đối với tất cả các định dạng ví dụ trên, model binding truyền một mảng gồm hai mục cho tham số selectedCourses:

    • selectedCourses[0]=1050
    • selectedCourses[1]=2000

    Các định dạng dữ liệu sử dụng số chỉ số dưới (... [0] ... [1] ...) phải đảm bảo được đánh số tuần tự bắt đầu từ 0. Nếu có bất kỳ khoảng trống nào trong việc đánh số chỉ số dưới, tất cả các mục sau khoảng trống sẽ bị bỏ qua. Ví dụ: nếu chỉ số dưới là 0 và 2 thay vì 0 và 1 thì mục thứ hai sẽ bị bỏ qua.

Dictionary

Đối với các mục tiêu Dictionary, model binding sẽ tìm kiếm các kết quả khớp với parameter_name hoặc property_name. Nếu không tìm thấy kết quả phù hợp, nó sẽ tìm một trong các định dạng được hỗ trợ mà không có tiền tố. Ví dụ:

  • Giả sử tham số đích là Dictionary<int, string> tên selectedCourses:

    public IActionResult OnPost(int? id, Dictionary<int, string> selectedCourses)
    
  • Form đã post hoặc dữ liệu chuỗi truy vấn có thể trông giống như một trong các ví dụ sau:

    selectedCourses[1050]=Chemistry&selectedCourses[2000]=Economics
    
    [1050]=Chemistry&selectedCourses[2000]=Economics
    
    selectedCourses[0].Key=1050&selectedCourses[0].Value=Chemistry&
    selectedCourses[1].Key=2000&selectedCourses[1].Value=Economics
    
    [0].Key=1050&[0].Value=Chemistry&[1].Key=2000&[1].Value=Economics
    
  • Đối với tất cả các định dạng ví dụ trên, model binding truyền một dictionary gồm hai mục cho tham số selectedCourses:

    • selectedCourses["1050"]="Chemistry"
    • selectedCourses["2000"]="Economics"

Các kiểu ràng buộc và bản ghi của hàm tạo

Model binding yêu cầu các kiểu phức tạp phải có hàm tạo không tham số. Các trình định dạng đầu vào System.Text.Json và Newtonsoft.Json đều hỗ trợ quá trình giải tuần tự hóa các lớp không có hàm tạo không tham số.

Các kiểu bản ghi là một cách tuyệt vời để trình bày ngắn gọn dữ liệu qua mạng. ASP.NET Core hỗ trợ model binding và xác thực các kiểu bản ghi bằng một hàm tạo duy nhất:

public record Person(
    [Required] string Name, [Range(0, 150)] int Age, [BindNever] int Id);

public class PersonController
{
    public IActionResult Index() => View();

    [HttpPost]
    public IActionResult Index(Person person)
    {
        // ...
    }
}

Person/Index.cshtml:

@model Person

Name: <input asp-for="Name" />
<br />
Age: <input asp-for="Age" />

Khi xác thực các kiểu bản ghi, thời gian chạy sẽ tìm kiếm siêu dữ liệu liên kết và xác thực cụ thể trên các tham số thay vì trên property.

Framework cho phép liên kết và xác thực các kiểu bản ghi:

public record Person([Required] string Name, [Range(0, 100)] int Age);

Để hoạt động, thì kiểu trên phải:

  • Là một kiểu record.
  • Có một hàm tạo public.
  • Chứa các tham số có property có cùng tên và kiểu. Tên không được khác nhau tùy theo trường hợp.

POCO không có hàm tạo không tham số

Các POCO không có hàm tạo không tham số sẽ không thể bị ràng buộc.

Đoạn code sau dẫn đến một ngoại lệ nói rằng kiểu đó phải có một hàm tạo không tham số:

public class Person(string Name)

public record Person([Required] string Name, [Range(0, 100)] int Age)
{
    public Person(string Name) : this (Name, 0);
}

Các loại bản ghi có hàm tạo được tạo thủ công

Các loại bản ghi có hàm tạo được tạo thủ công trông giống như hàm tạo chính hoạt động

public record Person
{
    public Person([Required] string Name, [Range(0, 100)] int Age)
        => (this.Name, this.Age) = (Name, Age);

    public string Name { get; set; }
    public int Age { get; set; }
}

Các loại bản ghi, siêu dữ liệu xác thực và ràng buộc

Đối với các loại bản ghi, siêu dữ liệu xác thực và ràng buộc trên các tham số được sử dụng. Mọi siêu dữ liệu về property đều bị bỏ qua.

public record Person (string Name, int Age)
{
   [BindProperty(Name = "SomeName")] // This does not get used
   [Required] // This does not get used
   public string Name { get; init; }
}

Xác thực và siêu dữ liệu

Xác thực sử dụng siêu dữ liệu trên tham số nhưng sử dụng property để đọc giá trị. Trong trường hợp thông thường với các hàm tạo chính, cả hai sẽ giống hệt nhau. Tuy nhiên, có nhiều cách để xử lý điều này:

public record Person([Required] string Name)
{
    private readonly string _name;

    // The following property is never null.
    // However this object could have been constructed as "new Person(null)".
    public string Name { get; init => _name = value ?? string.Empty; }
}

TryUpdateModel không cập nhật tham số trên kiểu bản ghi

public record Person(string Name)
{
    public int Age { get; set; }
}

var person = new Person("initial-name");
TryUpdateModel(person, ...);

Trong trường hợp này, MVC sẽ không cố gắng liên kết lại Name. Tuy nhiên, Age được phép cập nhật.

Hành vi toàn cầu hóa của dữ liệu route model binding và chuỗi truy vấn

Nhà cung cấp giá trị route ASP.NET Core và nhà cung cấp giá trị chuỗi truy vấn:

  • Hãy coi các giá trị là văn hóa bất biến.
  • Mong đợi rằng các URL không thay đổi về mặt văn hóa.

Ngược lại, các giá trị đến từ dữ liệu form sẽ trải qua quá trình chuyển đổi theo văn hóa. Đây là do thiết kế sao cho URL có thể chia sẻ được giữa các ngôn ngữ.

Để làm cho nhà cung cấp giá trị route ASP.NET Core và nhà cung cấp giá trị chuỗi truy vấn trải qua quá trình chuyển đổi phù hợp với văn hóa:

public class CultureQueryStringValueProviderFactory : IValueProviderFactory
{
    public Task CreateValueProviderAsync(ValueProviderFactoryContext context)
    {
        _ = context ?? throw new ArgumentNullException(nameof(context));

        var query = context.ActionContext.HttpContext.Request.Query;
        if (query?.Count > 0)
        {
            context.ValueProviders.Add(
                new QueryStringValueProvider(
                    BindingSource.Query,
                    query,
                    CultureInfo.CurrentCulture));
        }

        return Task.CompletedTask;
    }
}
builder.Services.AddControllers(options =>
{
    var index = options.ValueProviderFactories.IndexOf(
        options.ValueProviderFactories.OfType<QueryStringValueProviderFactory>()
            .Single());

    options.ValueProviderFactories[index] =
        new CultureQueryStringValueProviderFactory();
});

Các kiểu dữ liệu đặc biệt

Có một số kiểu dữ liệu đặc biệt mà model binding có thể xử lý.

IFormFile và IFormFileCollection

Một file đã tải lên (uploaded) có trong yêu cầu HTTP. Cũng được hỗ trợ là IEnumerable<IFormFile> cho nhiều tập tin.

CancellationToken

Các hành động có thể tùy ý liên kết một CancellationToken làm tham số. Điều này liên kết với Yêu cầu bị hủy bỏ báo hiệu khi kết nối cơ bản yêu cầu HTTP bị hủy bỏ. Các hành động có thể sử dụng tham số này để hủy các hoạt động không đồng bộ đang chạy dài được thực thi như một phần của action của controller.

FormCollection

Được sử dụng để truy xuất tất cả các giá trị từ dữ liệu form đã post.

Trình định dạng đầu vào

Dữ liệu trong phần nội dung yêu cầu có thể ở dạng JSON, XML hoặc một số định dạng khác. Để phân tích dữ liệu này, model binding sử dụng bộ định dạng đầu vào được định cấu hình để xử lý một kiểu nội dung cụ thể. Theo mặc định, ASP.NET Core bao gồm các trình định dạng đầu vào dựa trên JSON để xử lý dữ liệu JSON. Bạn có thể thêm các trình định dạng khác cho các kiểu nội dung khác.

ASP.NET Core chọn các trình định dạng đầu vào dựa trên attribute Consumes. Nếu không có attribute nào, nó sẽ sử dụng header Content-Type.

Để sử dụng các trình định dạng đầu vào XML tích hợp sẵn:

Tùy chỉnh model binding với các trình định dạng đầu vào

Trình định dạng đầu vào chịu trách nhiệm hoàn toàn trong việc đọc dữ liệu từ nội dung yêu cầu. Để tùy chỉnh quy trình này, hãy định cấu hình các API được trình định dạng đầu vào sử dụng. Phần này mô tả cách tùy chỉnh trình định dạng đầu vào dựa trên System.Text.Json để hiểu kiểu tùy chỉnh có tên ObjectId.

Hãy xem xét model sau có chứa property tùy chỉnh ObjectId hay không:

public class InstructorObjectId
{
    [Required]
    public ObjectId ObjectId { get; set; } = null!;
}

Để tùy chỉnh quy trình model binding khi sử dụng System.Text.Json, hãy tạo một lớp bắt thừa kế từ JsonConverter<T>:

internal class ObjectIdConverter : JsonConverter<ObjectId>
{
    public override ObjectId Read(
        ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        => new(JsonSerializer.Deserialize<int>(ref reader, options));

    public override void Write(
        Utf8JsonWriter writer, ObjectId value, JsonSerializerOptions options)
        => writer.WriteNumberValue(value.Id);
}

Để sử dụng trình chuyển đổi tùy chỉnh, hãy áp dụng attribute JsonConverterAttribution cho kiểu. Trong ví dụ sau, kiểu ObjectId được định cấu hình với ObjectIdConverter làm trình chuyển đổi tùy chỉnh:

[JsonConverter(typeof(ObjectIdConverter))]
public record ObjectId(int Id);

Để biết thêm thông tin, hãy xem Cách viết trình chuyển đổi tùy chỉnh.

Loại trừ các kiểu được chỉ định khỏi model binding

Hành vi của hệ thống xác thực và model binding được điều khiển bởi ModelMetadata. Bạn có thể tùy chỉnh ModelMetadata bằng cách thêm nhà cung cấp chi tiết vào MvcOptions.ModelMetadataDetailsProviders . Các nhà cung cấp chi tiết tích hợp có sẵn để vô hiệu hóa tính năng model binding hoặc model validation cho các kiểu được chỉ định.

Để tắt model binding trên tất cả các model thuộc kiểu được chỉ định, hãy thêm ExcludeBindingMetadataProvider trong Program.cs. Ví dụ: để tắt model binding trên tất cả các kiểu mẫu thuộc kiểu System.Version:

builder.Services.AddRazorPages()
    .AddMvcOptions(options =>
    {
        options.ModelMetadataDetailsProviders.Add(
            new ExcludeBindingMetadataProvider(typeof(Version)));
        options.ModelMetadataDetailsProviders.Add(
            new SuppressChildValidationMetadataProvider(typeof(Guid)));
    });

Để tắt xác thực trên các property của một kiểu được chỉ định, hãy thêm SuppressChildValidationMetadataProvider trong Program.cs. Ví dụ: để tắt xác thực trên các property kiểu System.Guid, ta có:

builder.Services.AddRazorPages()
    .AddMvcOptions(options =>
    {
        options.ModelMetadataDetailsProviders.Add(
            new ExcludeBindingMetadataProvider(typeof(Version)));
        options.ModelMetadataDetailsProviders.Add(
            new SuppressChildValidationMetadataProvider(typeof(Guid)));
    });

Model binder tùy chỉnh

Bạn có thể mở rộng model binding bằng cách viết một model binder tùy chỉnh và sử dụng attribute [ModelBinder] để chọn nó cho một mục tiêu nhất định. Tìm hiểu thêm về model binding tùy chỉnh.

Model binding thủ công

Model binding có thể được gọi thủ công bằng cách sử dụng phương thức TryUpdateModelAsync. Phương thức này được định nghĩa trên cả các lớp ControllerBase và PageModel. Tải chồng phương thức cho phép bạn chỉ định tiền tố và nhà cung cấp giá trị sẽ sử dụng. Phương thức trả về false nếu model binding không thành công. Đây là một ví dụ:

if (await TryUpdateModelAsync(
    newInstructor,
    "Instructor",
    x => x.Name, x => x.HireDate!))
{
    _instructorStore.Add(newInstructor);
    return RedirectToPage("./Index");
}

return Page();

TryUpdateModelAsync sử dụng nhà cung cấp giá trị để lấy dữ liệu từ nội dung form, chuỗi truy vấn và dữ liệu route. TryUpdateModelAsync thường là:

  • Được sử dụng với Razor Pages và ứng dụng MVC sử dụng controller và view để ngăn đăng quá mức.
  • Không được sử dụng với API web trừ khi được sử dụng từ dữ liệu form, chuỗi truy vấn và dữ liệu route. Các điểm cuối API Web sử dụng JSON sử dụng trình định dạng Đầu vào để giải tuần tự hóa nội dung yêu cầu thành một đối tượng.

Để biết thêm thông tin, hãy xem TryUpdateModelAsync.

Attribute [FromServices]

Tên của attribute này tuân theo mẫu attribute model binding chỉ định nguồn dữ liệu. Nhưng đó không phải là vấn đề ràng buộc dữ liệu từ nhà cung cấp giá trị. Nó nhận được một phiên bản của một kiểu từ vùng chứa Dependency Injection. Mục đích của nó là cung cấp một giải pháp thay thế cho việc đưa hàm tạo vào khi bạn chỉ cần một dịch vụ nếu một phương thức cụ thể được gọi.

Nếu một thể hiện của kiểu này không được đăng ký trong vùng chứa Dependency Injection thì ứng dụng sẽ đưa ra một ngoại lệ khi cố gắng liên kết tham số. Để đặt tham số là tùy chọn, hãy sử dụng một trong các phương pháp sau:

  • Làm cho tham số có thể rỗng (nullable).
  • Đặt giá trị mặc định cho tham số.

Đối với các tham số có thể rỗng, hãy đảm bảo rằng tham số đó không có null trước khi truy cập vào nó.

Tài nguyên bổ sung

Nguồn: learn.microsoft.com
» Tiếp: Triển khai ứng dụng web ASP.NET
Khóa học qua video:
Lập trình Python All Lập trình C# All SQL Server All Lập trình C All Java PHP HTML5-CSS3-JavaScript
Đăng ký Hội viên
Tất cả các video dành cho hội viên
Copied !!!