ASP.NET Core 中的自定义模型绑定Custom Model Binding in ASP.NET Core

作者:Steve SmithKirk LarkinBy Steve Smith and Kirk Larkin

通过模型绑定,控制器操作可直接使用模型类型(作为方法参数传入)而不是 HTTP 请求。Model binding allows controller actions to work directly with model types (passed in as method arguments), rather than HTTP requests. 由模型绑定器处理传入的请求数据和应用程序模型之间的映射。Mapping between incoming request data and application models is handled by model binders. 开发人员可以通过实现自定义模型绑定器来扩展内置的模型绑定功能(尽管通常不需要编写自己的提供程序)。Developers can extend the built-in model binding functionality by implementing custom model binders (though typically, you don't need to write your own provider).

查看或下载示例代码如何下载View or download sample code (how to download)

默认模型绑定器限制Default model binder limitations

默认模型绑定器支持大多数常见的 .NET Core 数据类型,能够满足大部分开发人员的需求。The default model binders support most of the common .NET Core data types and should meet most developers' needs. 他们希望将基于文本的输入从请求直接绑定到模型类型。They expect to bind text-based input from the request directly to model types. 绑定输入之前,可能需要对其进行转换。You might need to transform the input prior to binding it. 例如,当拥有某个可以用来查找模型数据的键时。For example, when you have a key that can be used to look up model data. 基于该键,用户可以使用自定义模型绑定器来获取数据。You can use a custom model binder to fetch data based on the key.

模型绑定查看Model binding review

模型绑定为其操作对象的类型使用特定定义。Model binding uses specific definitions for the types it operates on. 简单类型** 转换自输入中的单个字符串。A simple type is converted from a single string in the input. 复杂类型** 转换自多个输入值。A complex type is converted from multiple input values. 框架基于是否存在 TypeConverter 来确定差异。The framework determines the difference based on the existence of a TypeConverter. 如果简单 string -> SomeType 映射不需要外部资源,建议创建类型转换器。We recommended you create a type converter if you have a simple string -> SomeType mapping that doesn't require external resources.

创建自己的自定义模型绑定器之前,有必要查看现有模型绑定器的实现方式。Before creating your own custom model binder, it's worth reviewing how existing model binders are implemented. 考虑使用 ByteArrayModelBinder,它可将 base64 编码的字符串转换为字节数组。Consider the ByteArrayModelBinder which can be used to convert base64-encoded strings into byte arrays. 字节数组通常存储为文件或数据库 BLOB 字段。The byte arrays are often stored as files or database BLOB fields.

使用 ByteArrayModelBinderWorking with the ByteArrayModelBinder

Base64 编码的字符串可用来表示二进制数据。Base64-encoded strings can be used to represent binary data. 例如,可将图像编码为一个字符串。For example, an image can be encoded as a string. 示例包括作为使用 Base64String.txt 的 base64 编码字符串的图像。The sample includes an image as a base64-encoded string in Base64String.txt.

ASP.NET Core MVC 可以采用 base64 编码的字符串,并使用 ByteArrayModelBinder 将其转换为字节数组。ASP.NET Core MVC can take a base64-encoded string and use a ByteArrayModelBinder to convert it into a byte array. ByteArrayModelBinderProviderbyte[] 参数映射到 ByteArrayModelBinderThe ByteArrayModelBinderProvider maps byte[] arguments to 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 类型,或使用 ModelBinderAttributeWhen creating your own custom model binder, you can implement your own IModelBinderProvider type, or use the ModelBinderAttribute.

以下示例显示如何使用 ByteArrayModelBinder 将 base64 编码的字符串转换为 byte[],并将结果保存到文件中:The following example shows how to use ByteArrayModelBinder to convert a base64-encoded string to a byte[] and save the result to a 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);
}

若要查看翻译为非英语语言的代码注释,请在 此 GitHub 讨论问题中告诉我们。If you would like to see code comments translated to languages other than English, let us know in this GitHub discussion issue.

可以使用 Postman 等工具将 base64 编码的字符串发布到此 api 方法:You can POST a base64-encoded string to this api method using a tool like Postman:

postmanpostman

只要绑定器可以将请求数据绑定到相应命名的属性或参数,模型绑定就会成功。As long as the binder can bind request data to appropriately named properties or arguments, model binding will succeed. 以下示例演示如何将 ByteArrayModelBinder 与 视图模型结合使用:The following example shows how to use ByteArrayModelBinder with a view model:

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

自定义模型绑定器示例Custom model binder sample

在本部分中,我们将实现具有以下功能的自定义模型绑定器:In this section we'll implement a custom model binder that:

  • 将传入的请求数据转换为强类型键参数。Converts incoming request data into strongly typed key arguments.
  • 使用 Entity Framework Core 来提取关联的实体。Uses Entity Framework Core to fetch the associated entity.
  • 将关联的实体作为自变量传递给操作方法。Passes the associated entity as an argument to the action method.

以下示例在 Author 模型上使用 ModelBinder 属性:The following sample uses the ModelBinder attribute on the 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; }
    }
}

在前面的代码中,ModelBinder 属性指定应当用于绑定 Author 操作参数的 IModelBinder 的类型。In the preceding code, the ModelBinder attribute specifies the type of IModelBinder that should be used to bind Author action parameters.

以下 AuthorEntityBinder 类通过 Entity Framework Core 和 authorId 提取数据源中的实体来绑定 Author 参数:The following AuthorEntityBinder class binds an Author parameter by fetching the entity from a data source using Entity Framework Core and an 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 类旨在说明自定义模型绑定器。The preceding AuthorEntityBinder class is intended to illustrate a custom model binder. 该类不是意在说明查找方案的最佳做法。The class isn't intended to illustrate best practices for a lookup scenario. 对于查找,请绑定 authorId 并在操作方法中查询数据库。For lookup, bind the authorId and query the database in an action method. 此方法将模型绑定故障与 NotFound 案例分开。This approach separates model binding failures from NotFound cases.

以下代码显示如何在操作方法中使用 AuthorEntityBinderThe following code shows how to use the AuthorEntityBinder in an action method:

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

    return Ok(author);
}

可使用 ModelBinder 属性将 AuthorEntityBinder 应用于不使用默认约定的参数:The ModelBinder attribute can be used to apply the AuthorEntityBinder to parameters that don't use default conventions:

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

    return Ok(author);
}

在此示例中,由于参数的名称不是默认的 authorId,因此,使用 ModelBinder 属性在参数上指定该名称。In this example, since the name of the argument isn't the default authorId, it's specified on the parameter using the ModelBinder attribute. 比起在操作方法中查找实体,控制器和操作方法都得到了简化。Both the controller and action method are simplified compared to looking up the entity in the action method. 使用 Entity Framework Core 获取创建者的逻辑会移动到模型绑定器。The logic to fetch the author using Entity Framework Core is moved to the model binder. 如果有多种方法绑定到 Author 模型,就能得到很大程度的简化。This can be a considerable simplification when you have several methods that bind to the Author model.

可以将 ModelBinder 属性应用到各个模型属性(例如视图模型上)或操作方法参数,以便为该类型或操作指定某一模型绑定器或模型名称。You can apply the ModelBinder attribute to individual model properties (such as on a viewmodel) or to action method parameters to specify a certain model binder or model name for just that type or action.

实现 ModelBinderProviderImplementing a ModelBinderProvider

可以实现 IModelBinderProvider,而不是应用属性。Instead of applying an attribute, you can implement IModelBinderProvider. 这就是内置框架绑定器的实现方式。This is how the built-in framework binders are implemented. 指定绑定器所操作的类型时,指定它生成的参数的类型,而不是**** 绑定器接受的输入。When you specify the type your binder operates on, you specify the type of argument it produces, not the input your binder accepts. 以下绑定器提供程序适用于 AuthorEntityBinderThe following binder provider works with the AuthorEntityBinder. 将其添加到 MVC 提供程序的集合中时,无需在 AuthorAuthor 类型参数上使用 ModelBinder 属性。When it's added to MVC's collection of providers, you don't need to use the ModelBinder attribute on Author or Author-typed parameters.

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

注意:上述代码返回 BinderTypeModelBinderNote: The preceding code returns a BinderTypeModelBinder. BinderTypeModelBinder 充当模型绑定器中心,并提供依赖关系注入 (DI)。BinderTypeModelBinder acts as a factory for model binders and provides dependency injection (DI). AuthorEntityBinder 需要 DI 来访问 EF Core。The AuthorEntityBinder requires DI to access EF Core. 如果模型绑定器需要 DI 中的服务,请使用 BinderTypeModelBinderUse BinderTypeModelBinder if your model binder requires services from DI.

若要使用自定义模型绑定器提供程序,请将其添加到 ConfigureServices 中:To use a custom model binder provider, add it in ConfigureServices:

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

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

评估模型绑定器时,按顺序检查提供程序的集合。When evaluating model binders, the collection of providers is examined in order. 使用返回绑定器的第一个提供程序。The first provider that returns a binder is used. 向集合的末尾添加提供程序,可能会导致在调用自定义绑定器之前调用内置模型绑定器。Adding your provider to the end of the collection may result in a built-in model binder being called before your custom binder has a chance. 在此示例中,向集合的开头添加自定义提供程序,确保它用于 Author 操作参数。In this example, the custom provider is added to the beginning of the collection to ensure it's used for Author action arguments.

多态模型绑定Polymorphic model binding

绑定到不同的派生类型模型称为多态模型绑定。Binding to different models of derived types is known as polymorphic model binding. 如果请求值必须绑定到特定的派生模型类型,则需要多态自定义模型绑定。Polymorphic custom model binding is required when the request value must be bound to the specific derived model type. 多态模型绑定:Polymorphic model binding:

  • 对于旨在与所有语言进行互操作的 REST API 并不常见。Isn't typical for a REST API that's designed to interoperate with all languages.
  • 使绑定模型难以推理。Makes it difficult to reason about the bound models.

但是,如果应用需要多态模型绑定,则实现可能类似于以下代码:However, if an app requires polymorphic model binding, an implementation might look like the following code:

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] = new ValidationStateEntry
            {
                Metadata = modelMetadata,
            };
        }
    }
}

建议和最佳做法Recommendations and best practices

自定义模型绑定:Custom model binders:

  • 不应尝试设置状态代码或返回结果(例如 404 Not Found)。Shouldn't attempt to set status codes or return results (for example, 404 Not Found). 如果模型绑定失败,那么该操作方法本身的操作筛选器或逻辑会处理失败。If model binding fails, an action filter or logic within the action method itself should handle the failure.
  • 对于消除操作方法中的重复代码和跨领域问题最为有用。Are most useful for eliminating repetitive code and cross-cutting concerns from action methods.
  • 通常不应用其将字符串转换为自定义类型,而应选择用 TypeConverter 来完成此操作。Typically shouldn't be used to convert a string into a custom type, a TypeConverter is usually a better option.

作者:Steve SmithBy Steve Smith

通过模型绑定,控制器操作可直接使用模型类型(作为方法参数传入)而不是 HTTP 请求。Model binding allows controller actions to work directly with model types (passed in as method arguments), rather than HTTP requests. 由模型绑定器处理传入的请求数据和应用程序模型之间的映射。Mapping between incoming request data and application models is handled by model binders. 开发人员可以通过实现自定义模型绑定器来扩展内置的模型绑定功能(尽管通常不需要编写自己的提供程序)。Developers can extend the built-in model binding functionality by implementing custom model binders (though typically, you don't need to write your own provider).

查看或下载示例代码如何下载View or download sample code (how to download)

默认模型绑定器限制Default model binder limitations

默认模型绑定器支持大多数常见的 .NET Core 数据类型,能够满足大部分开发人员的需求。The default model binders support most of the common .NET Core data types and should meet most developers' needs. 他们希望将基于文本的输入从请求直接绑定到模型类型。They expect to bind text-based input from the request directly to model types. 绑定输入之前,可能需要对其进行转换。You might need to transform the input prior to binding it. 例如,当拥有某个可以用来查找模型数据的键时。For example, when you have a key that can be used to look up model data. 基于该键,用户可以使用自定义模型绑定器来获取数据。You can use a custom model binder to fetch data based on the key.

模型绑定查看Model binding review

模型绑定为其操作对象的类型使用特定定义。Model binding uses specific definitions for the types it operates on. 简单类型** 转换自输入中的单个字符串。A simple type is converted from a single string in the input. 复杂类型** 转换自多个输入值。A complex type is converted from multiple input values. 框架基于是否存在 TypeConverter 来确定差异。The framework determines the difference based on the existence of a TypeConverter. 如果简单 string -> SomeType 映射不需要外部资源,建议创建类型转换器。We recommended you create a type converter if you have a simple string -> SomeType mapping that doesn't require external resources.

创建自己的自定义模型绑定器之前,有必要查看现有模型绑定器的实现方式。Before creating your own custom model binder, it's worth reviewing how existing model binders are implemented. 考虑使用 ByteArrayModelBinder,它可将 base64 编码的字符串转换为字节数组。Consider the ByteArrayModelBinder which can be used to convert base64-encoded strings into byte arrays. 字节数组通常存储为文件或数据库 BLOB 字段。The byte arrays are often stored as files or database BLOB fields.

使用 ByteArrayModelBinderWorking with the ByteArrayModelBinder

Base64 编码的字符串可用来表示二进制数据。Base64-encoded strings can be used to represent binary data. 例如,可将图像编码为一个字符串。For example, an image can be encoded as a string. 示例包括作为使用 Base64String.txt 的 base64 编码字符串的图像。The sample includes an image as a base64-encoded string in Base64String.txt.

ASP.NET Core MVC 可以采用 base64 编码的字符串,并使用 ByteArrayModelBinder 将其转换为字节数组。ASP.NET Core MVC can take a base64-encoded string and use a ByteArrayModelBinder to convert it into a byte array. ByteArrayModelBinderProviderbyte[] 参数映射到 ByteArrayModelBinderThe ByteArrayModelBinderProvider maps byte[] arguments to 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 类型,或使用 ModelBinderAttributeWhen creating your own custom model binder, you can implement your own IModelBinderProvider type, or use the ModelBinderAttribute.

以下示例显示如何使用 ByteArrayModelBinder 将 base64 编码的字符串转换为 byte[],并将结果保存到文件中:The following example shows how to use ByteArrayModelBinder to convert a base64-encoded string to a byte[] and save the result to a 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);
}

可以使用 Postman 等工具将 base64 编码的字符串发布到此 api 方法:You can POST a base64-encoded string to this api method using a tool like Postman:

postmanpostman

只要绑定器可以将请求数据绑定到相应命名的属性或参数,模型绑定就会成功。As long as the binder can bind request data to appropriately named properties or arguments, model binding will succeed. 以下示例演示如何将 ByteArrayModelBinder 与 视图模型结合使用:The following example shows how to use ByteArrayModelBinder with a view model:

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

自定义模型绑定器示例Custom model binder sample

在本部分中,我们将实现具有以下功能的自定义模型绑定器:In this section we'll implement a custom model binder that:

  • 将传入的请求数据转换为强类型键参数。Converts incoming request data into strongly typed key arguments.
  • 使用 Entity Framework Core 来提取关联的实体。Uses Entity Framework Core to fetch the associated entity.
  • 将关联的实体作为自变量传递给操作方法。Passes the associated entity as an argument to the action method.

以下示例在 Author 模型上使用 ModelBinder 属性:The following sample uses the ModelBinder attribute on the 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; }
    }
}

在前面的代码中,ModelBinder 属性指定应当用于绑定 Author 操作参数的 IModelBinder 的类型。In the preceding code, the ModelBinder attribute specifies the type of IModelBinder that should be used to bind Author action parameters.

以下 AuthorEntityBinder 类通过 Entity Framework Core 和 authorId 提取数据源中的实体来绑定 Author 参数:The following AuthorEntityBinder class binds an Author parameter by fetching the entity from a data source using Entity Framework Core and an 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 类旨在说明自定义模型绑定器。The preceding AuthorEntityBinder class is intended to illustrate a custom model binder. 该类不是意在说明查找方案的最佳做法。The class isn't intended to illustrate best practices for a lookup scenario. 对于查找,请绑定 authorId 并在操作方法中查询数据库。For lookup, bind the authorId and query the database in an action method. 此方法将模型绑定故障与 NotFound 案例分开。This approach separates model binding failures from NotFound cases.

以下代码显示如何在操作方法中使用 AuthorEntityBinderThe following code shows how to use the AuthorEntityBinder in an action method:

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

可使用 ModelBinder 属性将 AuthorEntityBinder 应用于不使用默认约定的参数:The ModelBinder attribute can be used to apply the AuthorEntityBinder to parameters that don't use default conventions:

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

    return Ok(author);
}

在此示例中,由于参数的名称不是默认的 authorId,因此,使用 ModelBinder 属性在参数上指定该名称。In this example, since the name of the argument isn't the default authorId, it's specified on the parameter using the ModelBinder attribute. 比起在操作方法中查找实体,控制器和操作方法都得到了简化。Both the controller and action method are simplified compared to looking up the entity in the action method. 使用 Entity Framework Core 获取创建者的逻辑会移动到模型绑定器。The logic to fetch the author using Entity Framework Core is moved to the model binder. 如果有多种方法绑定到 Author 模型,就能得到很大程度的简化。This can be a considerable simplification when you have several methods that bind to the Author model.

可以将 ModelBinder 属性应用到各个模型属性(例如视图模型上)或操作方法参数,以便为该类型或操作指定某一模型绑定器或模型名称。You can apply the ModelBinder attribute to individual model properties (such as on a viewmodel) or to action method parameters to specify a certain model binder or model name for just that type or action.

实现 ModelBinderProviderImplementing a ModelBinderProvider

可以实现 IModelBinderProvider,而不是应用属性。Instead of applying an attribute, you can implement IModelBinderProvider. 这就是内置框架绑定器的实现方式。This is how the built-in framework binders are implemented. 指定绑定器所操作的类型时,指定它生成的参数的类型,而不是**** 绑定器接受的输入。When you specify the type your binder operates on, you specify the type of argument it produces, not the input your binder accepts. 以下绑定器提供程序适用于 AuthorEntityBinderThe following binder provider works with the AuthorEntityBinder. 将其添加到 MVC 提供程序的集合中时,无需在 AuthorAuthor 类型参数上使用 ModelBinder 属性。When it's added to MVC's collection of providers, you don't need to use the ModelBinder attribute on Author or Author-typed parameters.

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

注意:上述代码返回 BinderTypeModelBinderNote: The preceding code returns a BinderTypeModelBinder. BinderTypeModelBinder 充当模型绑定器中心,并提供依赖关系注入 (DI)。BinderTypeModelBinder acts as a factory for model binders and provides dependency injection (DI). AuthorEntityBinder 需要 DI 来访问 EF Core。The AuthorEntityBinder requires DI to access EF Core. 如果模型绑定器需要 DI 中的服务,请使用 BinderTypeModelBinderUse BinderTypeModelBinder if your model binder requires services from DI.

若要使用自定义模型绑定器提供程序,请将其添加到 ConfigureServices 中:To use a custom model binder provider, add it in 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);
}

评估模型绑定器时,按顺序检查提供程序的集合。When evaluating model binders, the collection of providers is examined in order. 使用返回绑定器的第一个提供程序。The first provider that returns a binder is used. 向集合的末尾添加提供程序,可能会导致在调用自定义绑定器之前调用内置模型绑定器。Adding your provider to the end of the collection may result in a built-in model binder being called before your custom binder has a chance. 在此示例中,向集合的开头添加自定义提供程序,确保它用于 Author 操作参数。In this example, the custom provider is added to the beginning of the collection to ensure it's used for Author action arguments.

多态模型绑定Polymorphic model binding

绑定到不同的派生类型模型称为多态模型绑定。Binding to different models of derived types is known as polymorphic model binding. 如果请求值必须绑定到特定的派生模型类型,则需要多态自定义模型绑定。Polymorphic custom model binding is required when the request value must be bound to the specific derived model type. 多态模型绑定:Polymorphic model binding:

  • 对于旨在与所有语言进行互操作的 REST API 并不常见。Isn't typical for a REST API that's designed to interoperate with all languages.
  • 使绑定模型难以推理。Makes it difficult to reason about the bound models.

但是,如果应用需要多态模型绑定,则实现可能类似于以下代码:However, if an app requires polymorphic model binding, an implementation might look like the following code:

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] = new ValidationStateEntry
            {
                Metadata = modelMetadata,
            };
        }
    }
}

建议和最佳做法Recommendations and best practices

自定义模型绑定:Custom model binders:

  • 不应尝试设置状态代码或返回结果(例如 404 Not Found)。Shouldn't attempt to set status codes or return results (for example, 404 Not Found). 如果模型绑定失败,那么该操作方法本身的操作筛选器或逻辑会处理失败。If model binding fails, an action filter or logic within the action method itself should handle the failure.
  • 对于消除操作方法中的重复代码和跨领域问题最为有用。Are most useful for eliminating repetitive code and cross-cutting concerns from action methods.
  • 通常不应用其将字符串转换为自定义类型,而应选择用 TypeConverter 来完成此操作。Typically shouldn't be used to convert a string into a custom type, a TypeConverter is usually a better option.