Bagikan melalui


Pengikatan Model Kustom di ASP.NET Core

Oleh Kirk Larkin

Pengikatan model memungkinkan tindakan pengontrol bekerja langsung dengan jenis model (diteruskan sebagai argumen metode), bukan permintaan HTTP. Pemetaan antara data permintaan masuk dan model aplikasi ditangani oleh pengikat model. Pengembang dapat memperluas fungsionalitas pengikatan model bawaan dengan menerapkan pengikat model kustom (meskipun biasanya, Anda tidak perlu menulis penyedia Anda sendiri).

Melihat atau mengunduh kode sampel (cara mengunduh)

Batasan binder model default

Pengikat model default mendukung sebagian besar jenis data .NET Core umum dan harus memenuhi kebutuhan sebagian besar pengembang. Mereka berharap untuk mengikat input berbasis teks dari permintaan langsung ke jenis model. Anda mungkin perlu mengubah input sebelum mengikatnya. Misalnya, ketika Anda memiliki kunci yang dapat digunakan untuk mencari data model. Anda dapat menggunakan pengikat model kustom untuk mengambil data berdasarkan kunci.

Pengikatan model jenis sederhana dan kompleks

Pengikatan model menggunakan definisi tertentu untuk jenis yang dioperasikannya. Jenis sederhana dikonversi dari satu string menggunakan TypeConverter atau TryParse metode . Jenis kompleks dikonversi dari beberapa nilai input. Kerangka kerja menentukan perbedaan berdasarkan keberadaan TypeConverter atau TryParse. Sebaiknya buat pengonversi jenis atau gunakan TryParse untuk string konversi ke SomeType yang tidak memerlukan sumber daya eksternal atau beberapa input.

Lihat Jenis sederhana untuk daftar jenis yang dapat dikonversi oleh pengikat model dari string.

Sebelum membuat binder model kustom Anda sendiri, ada baiknya meninjau bagaimana pengikat model yang ada diterapkan. ByteArrayModelBinder Pertimbangkan yang dapat digunakan untuk mengonversi string yang dikodekan base64 menjadi array byte. Array byte sering disimpan sebagai file atau bidang BLOB database.

Bekerja dengan ByteArrayModelBinder

String yang dikodekan Base64 dapat digunakan untuk mewakili data biner. Misalnya, gambar dapat dikodekan sebagai string. Sampel menyertakan gambar sebagai string yang dikodekan base64 dalam Base64String.txt.

ASP.NET Core MVC dapat mengambil string yang dikodekan base64 dan menggunakan ByteArrayModelBinder untuk mengonversinya menjadi array byte. Argumen ByteArrayModelBinderProvider peta byte[] ke ByteArrayModelBinder:

public IModelBinder GetBinder(ModelBinderProviderContext context)
{
    if (context == null)
    {
        throw new ArgumentNullException(nameof(context));
    }

    if (context.Metadata.ModelType == typeof(byte[]))
    {
        var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
        return new ByteArrayModelBinder(loggerFactory);
    }

    return null;
}

Saat membuat binder model kustom Anda sendiri, Anda dapat menerapkan jenis Anda sendiri IModelBinderProvider , atau menggunakan ModelBinderAttribute.

Contoh berikut menunjukkan cara menggunakan ByteArrayModelBinder untuk mengonversi string yang dikodekan base64 menjadi byte[] dan menyimpan hasilnya ke file:

[HttpPost]
public void Post([FromForm] byte[] file, string filename)
{
    // Don't trust the file name sent by the client. Use
    // Path.GetRandomFileName to generate a safe random
    // file name. _targetFilePath receives a value
    // from configuration (the appsettings.json file in
    // the sample app).
    var trustedFileName = Path.GetRandomFileName();
    var filePath = Path.Combine(_targetFilePath, trustedFileName);

    if (System.IO.File.Exists(filePath))
    {
        return;
    }

    System.IO.File.WriteAllBytes(filePath, file);
}

Jika Anda ingin melihat komentar kode yang diterjemahkan ke bahasa selain bahasa Inggris, beri tahu kami dalam masalah diskusi GitHub ini.

Anda dapat MEMPOSTING string yang dikodekan base64 ke metode api sebelumnya menggunakan alat seperti curl.

Selama pengikat dapat mengikat data permintaan ke properti atau argumen bernama dengan tepat, pengikatan model akan berhasil. Contoh berikut menunjukkan cara menggunakan ByteArrayModelBinder dengan model tampilan:

[HttpPost("Profile")]
public void SaveProfile([FromForm] ProfileViewModel model)
{
    // Don't trust the file name sent by the client. Use
    // Path.GetRandomFileName to generate a safe random
    // file name. _targetFilePath receives a value
    // from configuration (the appsettings.json file in
    // the sample app).
    var trustedFileName = Path.GetRandomFileName();
    var filePath = Path.Combine(_targetFilePath, trustedFileName);

    if (System.IO.File.Exists(filePath))
    {
        return;
    }

    System.IO.File.WriteAllBytes(filePath, model.File);
}

public class ProfileViewModel
{
    public byte[] File { get; set; }
    public string FileName { get; set; }
}

Sampel pengikat model kustom

Di bagian ini kita akan menerapkan pengikat model kustom yang:

  • Mengonversi data permintaan masuk menjadi argumen kunci yang ditik dengan kuat.
  • Menggunakan Entity Framework Core untuk mengambil entitas terkait.
  • Meneruskan entitas terkait sebagai argumen ke metode tindakan.

Sampel berikut menggunakan ModelBinder atribut pada Author model:

using CustomModelBindingSample.Binders;
using Microsoft.AspNetCore.Mvc;

namespace CustomModelBindingSample.Data
{
    [ModelBinder(BinderType = typeof(AuthorEntityBinder))]
    public class Author
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string GitHub { get; set; }
        public string Twitter { get; set; }
        public string BlogUrl { get; set; }
    }
}

Dalam kode sebelumnya, ModelBinder atribut menentukan jenis IModelBinder yang harus digunakan untuk mengikat Author parameter tindakan.

Kelas berikut AuthorEntityBinder mengikat Author parameter dengan mengambil entitas dari sumber data menggunakan Entity Framework Core dan authorId:

public class AuthorEntityBinder : IModelBinder
{
    private readonly AuthorContext _context;

    public AuthorEntityBinder(AuthorContext context)
    {
        _context = context;
    }

    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (bindingContext == null)
        {
            throw new ArgumentNullException(nameof(bindingContext));
        }

        var modelName = bindingContext.ModelName;

        // Try to fetch the value of the argument by name
        var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);

        if (valueProviderResult == ValueProviderResult.None)
        {
            return Task.CompletedTask;
        }

        bindingContext.ModelState.SetModelValue(modelName, valueProviderResult);

        var value = valueProviderResult.FirstValue;

        // Check if the argument value is null or empty
        if (string.IsNullOrEmpty(value))
        {
            return Task.CompletedTask;
        }

        if (!int.TryParse(value, out var id))
        {
            // Non-integer arguments result in model state errors
            bindingContext.ModelState.TryAddModelError(
                modelName, "Author Id must be an integer.");

            return Task.CompletedTask;
        }

        // Model will be null if not found, including for
        // out of range id values (0, -3, etc.)
        var model = _context.Authors.Find(id);
        bindingContext.Result = ModelBindingResult.Success(model);
        return Task.CompletedTask;
    }
}

Catatan

Kelas sebelumnya dimaksudkan untuk mengilustrasikan pengilustrasikan pengikat AuthorEntityBinder model kustom. Kelas tidak dimaksudkan untuk mengilustrasikan praktik terbaik untuk skenario pencarian. Untuk pencarian, ikat authorId dan kueri database dalam metode tindakan. Pendekatan ini memisahkan kegagalan pengikatan model dari NotFound kasus.

Kode berikut menunjukkan cara menggunakan AuthorEntityBinder dalam metode tindakan:

[HttpGet("get/{author}")]
public IActionResult Get(Author author)
{
    if (author == null)
    {
        return NotFound();
    }

    return Ok(author);
}

Atribut ModelBinder dapat digunakan untuk menerapkan AuthorEntityBinder parameter ke yang tidak menggunakan konvensi default:

[HttpGet("{id}")]
public IActionResult GetById([ModelBinder(Name = "id")] Author author)
{
    if (author == null)
    {
        return NotFound();
    }

    return Ok(author);
}

Dalam contoh ini, karena nama argumen bukan default authorId, itu ditentukan pada parameter menggunakan ModelBinder atribut . Pengontrol dan metode tindakan disederhanakan dibandingkan dengan mencari entitas dalam metode tindakan. Logika untuk mengambil penulis menggunakan Entity Framework Core dipindahkan ke pengikat model. Ini bisa menjadi penyederhanaan yang cukup besar ketika Anda memiliki beberapa metode yang mengikat Author model.

Anda dapat menerapkan ModelBinder atribut ke properti model individual (seperti pada viewmodel) atau parameter metode tindakan untuk menentukan pengikat model atau nama model tertentu hanya untuk jenis atau tindakan tersebut.

Menerapkan ModelBinderProvider

Alih-alih menerapkan atribut, Anda dapat menerapkan IModelBinderProvider. Ini adalah bagaimana pengikat kerangka kerja bawaan diimplementasikan. Saat Anda menentukan jenis binder yang dioperasikan, Anda menentukan jenis argumen yang dihasilkannya, bukan input yang diterima binder Anda. Penyedia pengikat berikut bekerja dengan AuthorEntityBinder. Saat ditambahkan ke kumpulan penyedia MVC, Anda tidak perlu menggunakan ModelBinder atribut pada Author parameter atau Author-typed.

using CustomModelBindingSample.Data;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using System;

namespace CustomModelBindingSample.Binders
{
    public class AuthorEntityBinderProvider : IModelBinderProvider
    {
        public IModelBinder GetBinder(ModelBinderProviderContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            if (context.Metadata.ModelType == typeof(Author))
            {
                return new BinderTypeModelBinder(typeof(AuthorEntityBinder));
            }

            return null;
        }
    }
}

Catatan: Kode sebelumnya mengembalikan BinderTypeModelBinder. BinderTypeModelBinder bertindak sebagai pabrik untuk pengikat model dan menyediakan injeksi dependensi (DI). AuthorEntityBinder mengharuskan DI untuk mengakses EF Core. Gunakan BinderTypeModelBinder jika pengikat model Anda memerlukan layanan dari DI.

Untuk menggunakan penyedia pengikat model kustom, tambahkan di ConfigureServices:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<AuthorContext>(options => options.UseInMemoryDatabase("Authors"));

    services.AddControllers(options =>
    {
        options.ModelBinderProviders.Insert(0, new AuthorEntityBinderProvider());
    });
}

Saat mengevaluasi pengikat model, pengumpulan penyedia diperiksa secara berurutan. Penyedia pertama yang mengembalikan pengikat yang cocok dengan model input digunakan. Menambahkan penyedia Anda ke akhir koleksi dapat mengakibatkan pengikat model bawaan dipanggil sebelum pengikat kustom Anda memiliki kesempatan. Dalam contoh ini, penyedia kustom ditambahkan ke awal koleksi untuk memastikannya selalu digunakan untuk Author argumen tindakan.

Pengikatan model polimorfik

Pengikatan ke berbagai model jenis turunan dikenal sebagai pengikatan model polimorfik. Pengikatan model kustom polimorfik diperlukan ketika nilai permintaan harus terikat ke jenis model turunan tertentu. Pengikatan model polimorfik:

  • Tidak khas untuk REST API yang dirancang untuk beroperasi dengan semua bahasa.
  • Menyulitkan alasan tentang model terikat.

Namun, jika aplikasi memerlukan pengikatan model polimorfik, implementasi mungkin terlihat seperti kode berikut:

public abstract class Device
{
    public string Kind { get; set; }
}

public class Laptop : Device
{
    public string CPUIndex { get; set; }
}

public class SmartPhone : Device
{
    public string ScreenSize { get; set; }
}

public class DeviceModelBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context.Metadata.ModelType != typeof(Device))
        {
            return null;
        }

        var subclasses = new[] { typeof(Laptop), typeof(SmartPhone), };

        var binders = new Dictionary<Type, (ModelMetadata, IModelBinder)>();
        foreach (var type in subclasses)
        {
            var modelMetadata = context.MetadataProvider.GetMetadataForType(type);
            binders[type] = (modelMetadata, context.CreateBinder(modelMetadata));
        }

        return new DeviceModelBinder(binders);
    }
}

public class DeviceModelBinder : IModelBinder
{
    private Dictionary<Type, (ModelMetadata, IModelBinder)> binders;

    public DeviceModelBinder(Dictionary<Type, (ModelMetadata, IModelBinder)> binders)
    {
        this.binders = binders;
    }

    public async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var modelKindName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, nameof(Device.Kind));
        var modelTypeValue = bindingContext.ValueProvider.GetValue(modelKindName).FirstValue;

        IModelBinder modelBinder;
        ModelMetadata modelMetadata;
        if (modelTypeValue == "Laptop")
        {
            (modelMetadata, modelBinder) = binders[typeof(Laptop)];
        }
        else if (modelTypeValue == "SmartPhone")
        {
            (modelMetadata, modelBinder) = binders[typeof(SmartPhone)];
        }
        else
        {
            bindingContext.Result = ModelBindingResult.Failed();
            return;
        }

        var newBindingContext = DefaultModelBindingContext.CreateBindingContext(
            bindingContext.ActionContext,
            bindingContext.ValueProvider,
            modelMetadata,
            bindingInfo: null,
            bindingContext.ModelName);

        await modelBinder.BindModelAsync(newBindingContext);
        bindingContext.Result = newBindingContext.Result;

        if (newBindingContext.Result.IsModelSet)
        {
            // Setting the ValidationState ensures properties on derived types are correctly 
            bindingContext.ValidationState[newBindingContext.Result.Model] = new ValidationStateEntry
            {
                Metadata = modelMetadata,
            };
        }
    }
}

Rekomendasi dan praktik terbaik

Pengikat model kustom:

  • Tidak boleh mencoba mengatur kode status atau mengembalikan hasil (misalnya, 404 Tidak Ditemukan). Jika pengikatan model gagal, filter tindakan atau logika dalam metode tindakan itu sendiri harus menangani kegagalan.
  • Paling berguna untuk menghilangkan kode berulang dan masalah lintas pemotongan dari metode tindakan.
  • Biasanya tidak boleh digunakan untuk mengonversi string menjadi jenis kustom, TypeConverter biasanya merupakan opsi yang lebih baik.

Oleh Steve Smith

Pengikatan model memungkinkan tindakan pengontrol bekerja langsung dengan jenis model (diteruskan sebagai argumen metode), bukan permintaan HTTP. Pemetaan antara data permintaan masuk dan model aplikasi ditangani oleh pengikat model. Pengembang dapat memperluas fungsionalitas pengikatan model bawaan dengan menerapkan pengikat model kustom (meskipun biasanya, Anda tidak perlu menulis penyedia Anda sendiri).

Melihat atau mengunduh kode sampel (cara mengunduh)

Batasan binder model default

Pengikat model default mendukung sebagian besar jenis data .NET Core umum dan harus memenuhi kebutuhan sebagian besar pengembang. Mereka berharap untuk mengikat input berbasis teks dari permintaan langsung ke jenis model. Anda mungkin perlu mengubah input sebelum mengikatnya. Misalnya, ketika Anda memiliki kunci yang dapat digunakan untuk mencari data model. Anda dapat menggunakan pengikat model kustom untuk mengambil data berdasarkan kunci.

Tinjauan pengikatan model

Pengikatan model menggunakan definisi tertentu untuk jenis yang dioperasikannya. Jenis sederhana dikonversi dari satu string dalam input. Jenis kompleks dikonversi dari beberapa nilai input. Kerangka kerja menentukan perbedaan berdasarkan keberadaan TypeConverter. Sebaiknya buat pengonversi jenis jika Anda memiliki pemetaan sederhana string yang>SomeType tidak memerlukan sumber daya eksternal.

Sebelum membuat binder model kustom Anda sendiri, ada baiknya meninjau bagaimana pengikat model yang ada diterapkan. ByteArrayModelBinder Pertimbangkan yang dapat digunakan untuk mengonversi string yang dikodekan base64 menjadi array byte. Array byte sering disimpan sebagai file atau bidang BLOB database.

Bekerja dengan ByteArrayModelBinder

String yang dikodekan Base64 dapat digunakan untuk mewakili data biner. Misalnya, gambar dapat dikodekan sebagai string. Sampel menyertakan gambar sebagai string yang dikodekan base64 dalam Base64String.txt.

ASP.NET Core MVC dapat mengambil string yang dikodekan base64 dan menggunakan ByteArrayModelBinder untuk mengonversinya menjadi array byte. Argumen ByteArrayModelBinderProvider peta byte[] ke ByteArrayModelBinder:

public IModelBinder GetBinder(ModelBinderProviderContext context)
{
    if (context == null)
    {
        throw new ArgumentNullException(nameof(context));
    }

    if (context.Metadata.ModelType == typeof(byte[]))
    {
        return new ByteArrayModelBinder();
    }

    return null;
}

Saat membuat binder model kustom Anda sendiri, Anda dapat menerapkan jenis Anda sendiri IModelBinderProvider , atau menggunakan ModelBinderAttribute.

Contoh berikut menunjukkan cara menggunakan ByteArrayModelBinder untuk mengonversi string yang dikodekan base64 menjadi byte[] dan menyimpan hasilnya ke file:

[HttpPost]
public void Post([FromForm] byte[] file, string filename)
{
    // Don't trust the file name sent by the client. Use
    // Path.GetRandomFileName to generate a safe random
    // file name. _targetFilePath receives a value
    // from configuration (the appsettings.json file in
    // the sample app).
    var trustedFileName = Path.GetRandomFileName();
    var filePath = Path.Combine(_targetFilePath, trustedFileName);

    if (System.IO.File.Exists(filePath))
    {
        return;
    }

    System.IO.File.WriteAllBytes(filePath, file);
}

Anda dapat MEMPOSTING string yang dikodekan base64 ke metode api sebelumnya menggunakan alat seperti curl.

Selama pengikat dapat mengikat data permintaan ke properti atau argumen bernama dengan tepat, pengikatan model akan berhasil. Contoh berikut menunjukkan cara menggunakan ByteArrayModelBinder dengan model tampilan:

[HttpPost("Profile")]
public void SaveProfile([FromForm] ProfileViewModel model)
{
    // Don't trust the file name sent by the client. Use
    // Path.GetRandomFileName to generate a safe random
    // file name. _targetFilePath receives a value
    // from configuration (the appsettings.json file in
    // the sample app).
    var trustedFileName = Path.GetRandomFileName();
    var filePath = Path.Combine(_targetFilePath, trustedFileName);

    if (System.IO.File.Exists(filePath))
    {
        return;
    }

    System.IO.File.WriteAllBytes(filePath, model.File);
}

public class ProfileViewModel
{
    public byte[] File { get; set; }
    public string FileName { get; set; }
}

Sampel pengikat model kustom

Di bagian ini kita akan menerapkan pengikat model kustom yang:

  • Mengonversi data permintaan masuk menjadi argumen kunci yang ditik dengan kuat.
  • Menggunakan Entity Framework Core untuk mengambil entitas terkait.
  • Meneruskan entitas terkait sebagai argumen ke metode tindakan.

Sampel berikut menggunakan ModelBinder atribut pada Author model:

using CustomModelBindingSample.Binders;
using Microsoft.AspNetCore.Mvc;

namespace CustomModelBindingSample.Data
{
    [ModelBinder(BinderType = typeof(AuthorEntityBinder))]
    public class Author
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public string GitHub { get; set; }
        public string Twitter { get; set; }
        public string BlogUrl { get; set; }
    }
}

Dalam kode sebelumnya, ModelBinder atribut menentukan jenis IModelBinder yang harus digunakan untuk mengikat Author parameter tindakan.

Kelas berikut AuthorEntityBinder mengikat Author parameter dengan mengambil entitas dari sumber data menggunakan Entity Framework Core dan authorId:

public class AuthorEntityBinder : IModelBinder
{
    private readonly AppDbContext _db;

    public AuthorEntityBinder(AppDbContext db)
    {
        _db = db;
    }

    public Task BindModelAsync(ModelBindingContext bindingContext)
    {
        if (bindingContext == null)
        {
            throw new ArgumentNullException(nameof(bindingContext));
        }

        var modelName = bindingContext.ModelName;

        // Try to fetch the value of the argument by name
        var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);

        if (valueProviderResult == ValueProviderResult.None)
        {
            return Task.CompletedTask;
        }

        bindingContext.ModelState.SetModelValue(modelName, valueProviderResult);

        var value = valueProviderResult.FirstValue;

        // Check if the argument value is null or empty
        if (string.IsNullOrEmpty(value))
        {
            return Task.CompletedTask;
        }

        if (!int.TryParse(value, out var id))
        {
            // Non-integer arguments result in model state errors
            bindingContext.ModelState.TryAddModelError(
                modelName, "Author Id must be an integer.");

            return Task.CompletedTask;
        }

        // Model will be null if not found, including for 
        // out of range id values (0, -3, etc.)
        var model = _db.Authors.Find(id);
        bindingContext.Result = ModelBindingResult.Success(model);
        return Task.CompletedTask;
    }
}

Catatan

Kelas sebelumnya dimaksudkan untuk mengilustrasikan pengilustrasikan pengikat AuthorEntityBinder model kustom. Kelas tidak dimaksudkan untuk mengilustrasikan praktik terbaik untuk skenario pencarian. Untuk pencarian, ikat authorId dan kueri database dalam metode tindakan. Pendekatan ini memisahkan kegagalan pengikatan model dari NotFound kasus.

Kode berikut menunjukkan cara menggunakan AuthorEntityBinder dalam metode tindakan:

[HttpGet("get/{author}")]
public IActionResult Get(Author author)
{
    if (author == null)
    {
        return NotFound();
    }
    
    return Ok(author);
}

Atribut ModelBinder dapat digunakan untuk menerapkan AuthorEntityBinder parameter ke yang tidak menggunakan konvensi default:

[HttpGet("{id}")]
public IActionResult GetById([ModelBinder(Name = "id")] Author author)
{
    if (author == null)
    {
        return NotFound();
    }

    return Ok(author);
}

Dalam contoh ini, karena nama argumen bukan default authorId, itu ditentukan pada parameter menggunakan ModelBinder atribut . Pengontrol dan metode tindakan disederhanakan dibandingkan dengan mencari entitas dalam metode tindakan. Logika untuk mengambil penulis menggunakan Entity Framework Core dipindahkan ke pengikat model. Ini bisa menjadi penyederhanaan yang cukup besar ketika Anda memiliki beberapa metode yang mengikat Author model.

Anda dapat menerapkan ModelBinder atribut ke properti model individual (seperti pada viewmodel) atau parameter metode tindakan untuk menentukan pengikat model atau nama model tertentu hanya untuk jenis atau tindakan tersebut.

Menerapkan ModelBinderProvider

Alih-alih menerapkan atribut, Anda dapat menerapkan IModelBinderProvider. Ini adalah bagaimana pengikat kerangka kerja bawaan diimplementasikan. Saat Anda menentukan jenis binder yang dioperasikan, Anda menentukan jenis argumen yang dihasilkannya, bukan input yang diterima binder Anda. Penyedia pengikat berikut bekerja dengan AuthorEntityBinder. Saat ditambahkan ke kumpulan penyedia MVC, Anda tidak perlu menggunakan ModelBinder atribut pada Author parameter atau Author-typed.

using CustomModelBindingSample.Data;
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
using System;

namespace CustomModelBindingSample.Binders
{
    public class AuthorEntityBinderProvider : IModelBinderProvider
    {
        public IModelBinder GetBinder(ModelBinderProviderContext context)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            if (context.Metadata.ModelType == typeof(Author))
            {
                return new BinderTypeModelBinder(typeof(AuthorEntityBinder));
            }

            return null;
        }
    }
}

Catatan: Kode sebelumnya mengembalikan BinderTypeModelBinder. BinderTypeModelBinder bertindak sebagai pabrik untuk pengikat model dan menyediakan injeksi dependensi (DI). AuthorEntityBinder mengharuskan DI untuk mengakses EF Core. Gunakan BinderTypeModelBinder jika pengikat model Anda memerlukan layanan dari DI.

Untuk menggunakan penyedia pengikat model kustom, tambahkan di ConfigureServices:

public void ConfigureServices(IServiceCollection services)
{
    services.AddDbContext<AppDbContext>(options => options.UseInMemoryDatabase("App"));

    services.AddMvc(options =>
        {
            // add custom binder to beginning of collection
            options.ModelBinderProviders.Insert(0, new AuthorEntityBinderProvider());
        })
        .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
}

Saat mengevaluasi pengikat model, pengumpulan penyedia diperiksa secara berurutan. Penyedia pertama yang mengembalikan pengikat digunakan. Menambahkan penyedia Anda ke akhir koleksi dapat mengakibatkan pengikat model bawaan dipanggil sebelum pengikat kustom Anda memiliki kesempatan. Dalam contoh ini, penyedia kustom ditambahkan ke awal koleksi untuk memastikannya digunakan untuk Author argumen tindakan.

Pengikatan model polimorfik

Pengikatan ke berbagai model jenis turunan dikenal sebagai pengikatan model polimorfik. Pengikatan model kustom polimorfik diperlukan ketika nilai permintaan harus terikat ke jenis model turunan tertentu. Pengikatan model polimorfik:

  • Tidak khas untuk REST API yang dirancang untuk beroperasi dengan semua bahasa.
  • Menyulitkan alasan tentang model terikat.

Namun, jika aplikasi memerlukan pengikatan model polimorfik, implementasi mungkin terlihat seperti kode berikut:

public abstract class Device
{
    public string Kind { get; set; }
}

public class Laptop : Device
{
    public string CPUIndex { get; set; }
}

public class SmartPhone : Device
{
    public string ScreenSize { get; set; }
}

public class DeviceModelBinderProvider : IModelBinderProvider
{
    public IModelBinder GetBinder(ModelBinderProviderContext context)
    {
        if (context.Metadata.ModelType != typeof(Device))
        {
            return null;
        }

        var subclasses = new[] { typeof(Laptop), typeof(SmartPhone), };

        var binders = new Dictionary<Type, (ModelMetadata, IModelBinder)>();
        foreach (var type in subclasses)
        {
            var modelMetadata = context.MetadataProvider.GetMetadataForType(type);
            binders[type] = (modelMetadata, context.CreateBinder(modelMetadata));
        }

        return new DeviceModelBinder(binders);
    }
}

public class DeviceModelBinder : IModelBinder
{
    private Dictionary<Type, (ModelMetadata, IModelBinder)> binders;

    public DeviceModelBinder(Dictionary<Type, (ModelMetadata, IModelBinder)> binders)
    {
        this.binders = binders;
    }

    public async Task BindModelAsync(ModelBindingContext bindingContext)
    {
        var modelKindName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, nameof(Device.Kind));
        var modelTypeValue = bindingContext.ValueProvider.GetValue(modelKindName).FirstValue;

        IModelBinder modelBinder;
        ModelMetadata modelMetadata;
        if (modelTypeValue == "Laptop")
        {
            (modelMetadata, modelBinder) = binders[typeof(Laptop)];
        }
        else if (modelTypeValue == "SmartPhone")
        {
            (modelMetadata, modelBinder) = binders[typeof(SmartPhone)];
        }
        else
        {
            bindingContext.Result = ModelBindingResult.Failed();
            return;
        }

        var newBindingContext = DefaultModelBindingContext.CreateBindingContext(
            bindingContext.ActionContext,
            bindingContext.ValueProvider,
            modelMetadata,
            bindingInfo: null,
            bindingContext.ModelName);

        await modelBinder.BindModelAsync(newBindingContext);
        bindingContext.Result = newBindingContext.Result;

        if (newBindingContext.Result.IsModelSet)
        {
            // Setting the ValidationState ensures properties on derived types are correctly 
            bindingContext.ValidationState[newBindingContext.Result.Model] = new ValidationStateEntry
            {
                Metadata = modelMetadata,
            };
        }
    }
}

Rekomendasi dan praktik terbaik

Pengikat model kustom:

  • Tidak boleh mencoba mengatur kode status atau mengembalikan hasil (misalnya, 404 Tidak Ditemukan). Jika pengikatan model gagal, filter tindakan atau logika dalam metode tindakan itu sendiri harus menangani kegagalan.
  • Paling berguna untuk menghilangkan kode berulang dan masalah lintas pemotongan dari metode tindakan.
  • Biasanya tidak boleh digunakan untuk mengonversi string menjadi jenis kustom, TypeConverter biasanya merupakan opsi yang lebih baik.