共用方式為


ASP.NET Core 中的自定義模型系結

柯克·拉金

模型系結可讓控制器動作直接使用模型類型(以方法自變數的形式傳入),而不是 HTTP 要求。 傳入要求數據和應用程式模型之間的對應是由模型系結器處理。 開發人員可以透過實作自定義模型綁定器來擴展內建的模型綁定功能(不過通常情況下,您不需要創建自己的綁定提供程序)。

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

預設模型系結器限制

預設模型系結器支援大部分常見的 .NET Core 數據類型,而且應該符合大部分開發人員的需求。 它們預期會將要求中的文字型輸入直接系結至模型類型。 在系結輸入之前,您可能需要轉換輸入。 例如,當您有可用來查閱模型數據的索引鍵時。 您可以使用自定義模型系結器,根據鍵值擷取資料。

模型系結簡單和複雜類型

模型系結會針對其運作的類型使用特定定義。 簡單類型會使用單一字串搭配TypeConverter方法或是使用TryParse方法來進行轉換。 複合型別是從多個輸入值轉換而來。 架構會根據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 來擷取相關聯的實體。
  • 將相關聯的實體當做自變數傳遞至動作方法。

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

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,屬性會指定應該用來系結IModelBinder動作參數的 Author 型別。

下列AuthorEntityBinder類別會使用 Entity Framework Core 和 Author從數據源擷取實體,以系結 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;
    }
}

Note

上述 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 的提供者集合時,您不需要在 ModelBinderAuthor-typed 參數上使用 Author 屬性。

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

注意:上述程式代碼會返回 BinderTypeModelBinder 的結果。 BinderTypeModelBinder 作為模型系結器的工廠,並提供相依性注入(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 來擷取相關聯的實體。
  • 將相關聯的實體當做自變數傳遞至動作方法。

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

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,屬性會指定應該用來系結IModelBinder動作參數的 Author 型別。

下列AuthorEntityBinder類別會使用 Entity Framework Core 和 Author從數據源擷取實體,以系結 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;
    }
}

Note

上述 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 的提供者集合時,您不需要在 ModelBinderAuthor-typed 參數上使用 Author 屬性。

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

注意:上述程式代碼會返回 BinderTypeModelBinder 的結果。 BinderTypeModelBinder 作為模型系結器的工廠,並提供相依性注入(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 較佳的選項。