ASP.NET Core 中的自定义模型绑定
作者:Kirk Larkin
通过模型绑定,控制器操作可直接使用模型类型(作为方法参数传入)而不是 HTTP 请求。 由模型绑定器处理传入的请求数据和应用程序模型之间的映射。 开发人员可以通过实现自定义模型绑定器来扩展内置的模型绑定功能(尽管通常不需要编写自己的提供程序)。
默认模型绑定器限制
默认模型绑定器支持大多数常见的 .NET Core 数据类型,能够满足大部分开发人员的需求。 他们希望将基于文本的输入从请求直接绑定到模型类型。 绑定输入之前,可能需要对其进行转换。 例如,当拥有某个可以用来查找模型数据的键时。 基于该键,用户可以使用自定义模型绑定器来获取数据。
模型绑定简单和复杂类型
模型绑定为其操作对象的类型使用特定定义。 简单类型转换自使用 TypeConverter 或 TryParse
方法的单个字符串。 复杂类型转换自多个输入值。 框架基于是否存在 TypeConverter
或 TryParse
来确定差异。 建议为不需要外部资源或多个输入的 string
到 SomeType
转换创建类型转换器或使用 TryParse
。
有关模型绑定器可以从字符串转换的类型列表,请参阅简单类型。
创建自己的自定义模型绑定器之前,有必要查看现有模型绑定器的实现方式。 考虑使用 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 编码的字符串发布到之前的 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
类通过 Entity Framework Core 和 authorId
提取数据源中的实体来绑定 Author
参数:
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
属性应用到各个模型属性(例如视图模型上)或操作方法参数,以便为该类型或操作指定某一模型绑定器或模型名称。
实现 ModelBinderProvider
可以实现 IModelBinderProvider
,而不是应用属性。 这就是内置框架绑定器的实现方式。 指定绑定器所操作的类型时,指定它生成的参数的类型,而不是绑定器接受的输入。 以下绑定器提供程序适用于 AuthorEntityBinder
。 将其添加到 MVC 提供程序的集合中时,无需在 Author
或 Author
类型参数上使用 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;
}
}
}
注意:上述代码返回
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 Not Found)。 如果模型绑定失败,那么该操作方法本身的操作筛选器或逻辑会处理失败。
- 对于消除操作方法中的重复代码和跨领域问题最为有用。
- 通常不应用其将字符串转换为自定义类型,而应选择用 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 编码的字符串发布到之前的 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
类通过 Entity Framework Core 和 authorId
提取数据源中的实体来绑定 Author
参数:
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
属性应用到各个模型属性(例如视图模型上)或操作方法参数,以便为该类型或操作指定某一模型绑定器或模型名称。
实现 ModelBinderProvider
可以实现 IModelBinderProvider
,而不是应用属性。 这就是内置框架绑定器的实现方式。 指定绑定器所操作的类型时,指定它生成的参数的类型,而不是绑定器接受的输入。 以下绑定器提供程序适用于 AuthorEntityBinder
。 将其添加到 MVC 提供程序的集合中时,无需在 Author
或 Author
类型参数上使用 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;
}
}
}
注意:上述代码返回
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 Not Found)。 如果模型绑定失败,那么该操作方法本身的操作筛选器或逻辑会处理失败。
- 对于消除操作方法中的重复代码和跨领域问题最为有用。
- 通常不应用其将字符串转换为自定义类型,而应选择用 TypeConverter 来完成此操作。