ASP.NET Core 中的自訂模型繫結

作者:Kirk Larkin

模型繫結可直接透過模型類型 (傳入作為方法引數) 來執行控制器動作,而不用透過 HTTP 要求。 內送要求資料與應用程式模型之間的對應是由模型繫結器來處理。 開發人員可以透過實作自訂模型繫結器,來擴充內建模型繫結功能 (不過一般而言,您並不需要撰寫自己的提供者)。

檢視或下載範例程式碼 \(英文\) (如何下載)

預設模型繫結器限制

預設模型繫結器支援大多數的常見 .NET Core 資料類型,因此應該符合大部分開發人員的需求。 這些開發人員希望能夠將要求中以文字為主的輸入直接繫結至模型類型。 輸入可能需要進行轉換才能繫結。 例如,當您有可用來查詢模型資料的索引鍵時, 您可以使用自訂模型繫結器,根據索引鍵來擷取資料。

模型繫結簡單和複雜類型

模型繫結使用特定定義來描述其作業類型。 簡單的類型 是使用 TypeConverterTryParse 方法從單一字串轉換而來。 「複雜類型」是指從多個輸入值進行轉換。 架構會根據是否有 TypeConverterTryParse 來判斷差異。 建議您建立類型轉換器,或使用 TryParse 進行 stringSomeType 的轉換,而不需要外部資源或多個輸入。

如需模型繫結器可以從字串轉換的類型清單,請參閱簡單型別

在您建立自己的自訂模型繫結器之前,建議您先檢閱現有模型繫結器的實作方式。 請考慮使用 ByteArrayModelBinder,將 base64 編碼字串轉換成位元組陣列。 位元組陣列通常會儲存為檔案或資料庫 BLOB 欄位。

使用 ByteArrayModelBinder

Base64 編碼字串可用來代表二進位資料。 例如,影像可編碼為字串。 此範例包含 Base64String.txt 中 base64 編碼字串的影像。

ASP.NET Core MVC 接受 Base64 編碼字串,並使用 ByteArrayModelBinder 將其轉換成位元組陣列。 ByteArrayModelBinderProvider 會將 byte[] 引數對應至 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;
}

當您建立自己的自訂模型繫結器時,您可以實作自己的 IModelBinderProvider 類型,或使用 ModelBinderAttribute

下列範例示範如何使用 ByteArrayModelBinder,將 Base64 編碼字串轉換成 byte[],並將結果儲存至檔案:

[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);
}

如果您想要查看翻譯為英文以外語言的程式碼註解,請在此 GitHub 討論問題中告訴我們。

您可以使用像 curl 這樣的工具將 Base64 編碼的字串 POST 到前面的 api 方法。

只要繫結器可以將要求資料繫結至適當命名的屬性或引數,模型繫結就會成功。 下列範例示範如何使用具有檢視模型的 ByteArrayModelBinder

[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; }
}

自訂模型繫結器範例

在本節中,我們會實作自訂模型繫結:

  • 將內送要求資料轉換成強型別索引鍵引數。
  • 使用 Entity Framework Core 來擷取相關聯的實體。
  • 將相關聯的實體當作引數傳遞至動作方法。

下列範例會在 Author 模型上使用 ModelBinder 屬性:

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; }
    }
}

在上述程式碼中,ModelBinder 屬性指定應該用來繫結 Author 動作參數的 IModelBinder 類型。

下列 AuthorEntityBinder 類別可繫結 Author 參數,做法是使用 Entity Framework Core 和 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;
    }
}

注意

上述 AuthorEntityBinder 類別會說明自訂模型繫結器。 此類別不會說明查閱情節的最佳做法。 若要進行查閱,請繫結 authorId,並在動作方法中查詢資料庫。 此方法會將模型繫結失敗從 NotFound 案例中分離。

下列程式碼示範如何在動作方法中使用 AuthorEntityBinder

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

    return Ok(author);
}

您可以使用 ModelBinder 屬性,將 AuthorEntityBinder 套用至未使用預設慣例的參數:

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

    return Ok(author);
}

在此範例中,由於引數名稱不是預設的 authorId,因此會使用 ModelBinder 屬性在參數上指定。 控制器和動作方法相較於查詢動作方法中的實體,都更為簡化。 使用 Entity Framework Core 擷取作者的邏輯已移至模型繫結器。 當您有數個繫結至 Author 模型的方法時,這樣做會明顯簡化許多。

您可以將 ModelBinder 屬性套用至個別模型屬性 (例如在 ViewModel 上) 或動作方法參數,只指定該類型或動作的特定模型繫結器或模型名稱。

實作 ModelBinderProvider

除了套用屬性,您還可以實作 IModelBinderProvider。 這是內建架構繫結器的實作方式。 當您指定繫結器的作業類型時,您會指定其所產生的引數類型,而不是繫結器接受的輸入。 下列繫結器提供者可搭配 AuthorEntityBinder 使用。 當它新增至提供者的 MVC 集合時,您不需要在 AuthorAuthor 型別參數上使用 ModelBinder 屬性。

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;
        }
    }
}

注意:上述程式碼會傳回 BinderTypeModelBinderBinderTypeModelBinder 會作為模型繫結器的 Factory,並提供相依性插入 (DI)。 AuthorEntityBinder 需要 DI 才能存取 EF Core。 如果您的模型繫結器需要來自 DI 的服務,請使用 BinderTypeModelBinder

若要使用自訂模型繫結器提供者,將它新增 ConfigureServices

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

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

評估模型繫結器時,會依序查看提供者集合, 會使用傳回符合輸入模型之繫結器的第一個提供者。 將您的提供者新增至集合結尾可能會導致在有機會呼叫自訂繫結器之前,先呼叫內建模型繫結器。 在此範例中,會將自訂提供者新增至集合開頭,以確保其一律用於 Author 動作引數。

多型模型繫結

繫結至衍生型別的不同模型稱為多型模型繫結。 當要求值必須繫結至特定的衍生模型類型時,需要多型自訂模型繫結。 多型模型繫結:

  • 對於設計來與所有語言交互操作的 REST API 來說,並不一般。
  • 讓繫結模型變得難以推理。

不過,如果應用程式需要多型模型繫結,實作看起來可能會像下列程式碼:

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,
            };
        }
    }
}

建議與最佳做法

自訂模型繫結器:

  • 不應該嘗試設定狀態碼或傳回結果 (例如 404 找不到)。 如果模型繫結失敗,動作篩選或動作方法本身內的邏輯應該會處理失敗。
  • 最適合用來排除動作方法中的重複程式碼和交叉關注。
  • 一般而言,不應該用來將字串轉換成自訂類型,TypeConverter 通常是較好的選擇。

作者:Steve Smith

模型繫結可直接透過模型類型 (傳入作為方法引數) 來執行控制器動作,而不用透過 HTTP 要求。 內送要求資料與應用程式模型之間的對應是由模型繫結器來處理。 開發人員可以透過實作自訂模型繫結器,來擴充內建模型繫結功能 (不過一般而言,您並不需要撰寫自己的提供者)。

檢視或下載範例程式碼 \(英文\) (如何下載)

預設模型繫結器限制

預設模型繫結器支援大多數的常見 .NET Core 資料類型,因此應該符合大部分開發人員的需求。 這些開發人員希望能夠將要求中以文字為主的輸入直接繫結至模型類型。 輸入可能需要進行轉換才能繫結。 例如,當您有可用來查詢模型資料的索引鍵時, 您可以使用自訂模型繫結器,根據索引鍵來擷取資料。

模型繫結檢閱

模型繫結使用特定定義來描述其作業類型。 「簡單型別」是指從輸入中的單一字串進行轉換。 「複雜類型」是指從多個輸入值進行轉換。 架構會根據是否有 TypeConverter 來判斷是否為不同類型。 如果您有不需要外部資源的簡單 string ->SomeType 對應,建議您建立型別轉換器。

在您建立自己的自訂模型繫結器之前,建議您先檢閱現有模型繫結器的實作方式。 請考慮使用 ByteArrayModelBinder,將 base64 編碼字串轉換成位元組陣列。 位元組陣列通常會儲存為檔案或資料庫 BLOB 欄位。

使用 ByteArrayModelBinder

Base64 編碼字串可用來代表二進位資料。 例如,影像可編碼為字串。 此範例包含 Base64String.txt 中 base64 編碼字串的影像。

ASP.NET Core MVC 接受 Base64 編碼字串,並使用 ByteArrayModelBinder 將其轉換成位元組陣列。 ByteArrayModelBinderProvider 會將 byte[] 引數對應至 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;
}

當您建立自己的自訂模型繫結器時,您可以實作自己的 IModelBinderProvider 類型,或使用 ModelBinderAttribute

下列範例示範如何使用 ByteArrayModelBinder,將 Base64 編碼字串轉換成 byte[],並將結果儲存至檔案:

[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);
}

您可以使用像 curl 這樣的工具將 Base64 編碼的字串 POST 到前面的 api 方法。

只要繫結器可以將要求資料繫結至適當命名的屬性或引數,模型繫結就會成功。 下列範例示範如何使用具有檢視模型的 ByteArrayModelBinder

[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; }
}

自訂模型繫結器範例

在本節中,我們會實作自訂模型繫結:

  • 將內送要求資料轉換成強型別索引鍵引數。
  • 使用 Entity Framework Core 來擷取相關聯的實體。
  • 將相關聯的實體當作引數傳遞至動作方法。

下列範例會在 Author 模型上使用 ModelBinder 屬性:

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; }
    }
}

在上述程式碼中,ModelBinder 屬性指定應該用來繫結 Author 動作參數的 IModelBinder 類型。

下列 AuthorEntityBinder 類別可繫結 Author 參數,做法是使用 Entity Framework Core 和 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;
    }
}

注意

上述 AuthorEntityBinder 類別會說明自訂模型繫結器。 此類別不會說明查閱情節的最佳做法。 若要進行查閱,請繫結 authorId,並在動作方法中查詢資料庫。 此方法會將模型繫結失敗從 NotFound 案例中分離。

下列程式碼示範如何在動作方法中使用 AuthorEntityBinder

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

您可以使用 ModelBinder 屬性,將 AuthorEntityBinder 套用至未使用預設慣例的參數:

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

    return Ok(author);
}

在此範例中,由於引數名稱不是預設的 authorId,因此會使用 ModelBinder 屬性在參數上指定。 控制器和動作方法相較於查詢動作方法中的實體,都更為簡化。 使用 Entity Framework Core 擷取作者的邏輯已移至模型繫結器。 當您有數個繫結至 Author 模型的方法時,這樣做會明顯簡化許多。

您可以將 ModelBinder 屬性套用至個別模型屬性 (例如在 ViewModel 上) 或動作方法參數,只指定該類型或動作的特定模型繫結器或模型名稱。

實作 ModelBinderProvider

除了套用屬性,您還可以實作 IModelBinderProvider。 這是內建架構繫結器的實作方式。 當您指定繫結器的作業類型時,您會指定其所產生的引數類型,而不是繫結器接受的輸入。 下列繫結器提供者可搭配 AuthorEntityBinder 使用。 當它新增至提供者的 MVC 集合時,您不需要在 AuthorAuthor 型別參數上使用 ModelBinder 屬性。

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;
        }
    }
}

注意:上述程式碼會傳回 BinderTypeModelBinderBinderTypeModelBinder 會作為模型繫結器的 Factory,並提供相依性插入 (DI)。 AuthorEntityBinder 需要 DI 才能存取 EF Core。 如果您的模型繫結器需要來自 DI 的服務,請使用 BinderTypeModelBinder

若要使用自訂模型繫結器提供者,將它新增 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);
}

評估模型繫結器時,會依序查看提供者集合, 並使用第一個傳回繫結器的提供者。 將您的提供者新增集合結尾可能會導致在有機會呼叫自訂繫結器之前,即呼叫內建模型繫結器。 在此範例中,會將自訂提供者新增集合開頭,以確保其用於 Author 動作引數。

多型模型繫結

繫結至衍生型別的不同模型稱為多型模型繫結。 當要求值必須繫結至特定的衍生模型類型時,需要多型自訂模型繫結。 多型模型繫結:

  • 對於設計來與所有語言交互操作的 REST API 來說,並不一般。
  • 讓繫結模型變得難以推理。

不過,如果應用程式需要多型模型繫結,實作看起來可能會像下列程式碼:

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,
            };
        }
    }
}

建議與最佳做法

自訂模型繫結器:

  • 不應該嘗試設定狀態碼或傳回結果 (例如 404 找不到)。 如果模型繫結失敗,動作篩選或動作方法本身內的邏輯應該會處理失敗。
  • 最適合用來排除動作方法中的重複程式碼和交叉關注。
  • 一般而言,不應該用來將字串轉換成自訂類型,TypeConverter 通常是較好的選擇。