由 柯克·拉金
通过模型绑定,允许控制器动作直接处理作为方法参数传入的模型类型,而不是 HTTP 请求。 传入请求数据和应用程序模型之间的映射由模型绑定器处理。 开发人员可以通过实现自定义模型绑定器来扩展内置模型绑定功能(尽管通常不需要编写自己的提供程序)。
默认模型绑定器限制
默认模型绑定器支持大多数常见的 .NET Core 数据类型,并且应满足大多数开发人员的需求。 他们希望将基于文本的输入直接从请求绑定到模型类型。 在绑定输入之前,可能需要对其进行转换。 例如,当你有一个可用于查找模型数据的键时。 可以使用自定义模型绑定器基于密钥提取数据。
模型绑定简单和复杂类型
模型绑定为其操作对象的类型使用特定定义。 使用或TypeConverter方法从单个字符串转换为TryParse。
复杂类型是由多个输入值转换而来的。 框架基于是否存在 TypeConverter 或 TryParse 来确定差异。 建议为不需要外部资源或多个输入的 TryParse 到 string 转换创建类型转换器或使用 SomeType。
有关模型绑定器可从字符串转换的类型列表,请参阅 简单类型 。
在创建自定义模型绑定器之前,值得一看如何实现现有模型绑定器。 ByteArrayModelBinder请考虑一种可用于将 base64 编码的字符串转换为字节数组的方法。 字节数组通常存储为文件或数据库 BLOB 字段。
使用 ByteArrayModelBinder
Base64 编码的字符串可用于表示二进制数据。 例如,可以将图像编码为字符串。 该示例在 Base64String.txt 中包含以 base64 编码的图像字符串。
ASP.NET Core MVC 可以采用 base64 编码的字符串,并使用 a 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);
}
可以使用 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 提取关联的实体。
- 将关联的实体作为参数传递给作方法。
以下示例使用 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类通过使用Author路由值,通过 Entity Framework Core 从数据源获取实体来绑定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;
}
}
Note
前面的 AuthorEntityBinder 类旨在说明自定义模型联编程序。 该类不是为演示查找用例的最佳实践而设。 若要查找,请直接绑定 author 参数,并在作方法中查询数据库。 此方法将模型绑定失败与 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);
}
在此示例中,由于路由参数名称(id)与操作方法参数名称(author)不匹配,因此使用ModelBinder属性上的Name属性来指定要绑定的路由值。 与在操作方法中查找实体相比,控制器和操作方法都更加简化。 使用 Entity Framework Core 提取作者的逻辑已移至模型绑定器。 当有多个绑定到 Author 模型的方法时,这可以大幅简化。
可以将属性 ModelBinder 应用于单个模型属性(例如在 ViewModel 上)或操作方法参数,以便为该类型或操作指定特定的模型绑定器或模型名称。
实现 ModelBinderProvider
你可以实现 IModelBinderProvider,而不是应用一个属性。 这就是实现内置框架绑定器的方式。 当您指定绑定器操作的类型时,您是在指定它生成的参数类型,而不是 绑定器接受的输入。 以下绑定器提供者适用于AuthorEntityBinder。 当它添加到 MVC 的提供程序集合中时,您不需要在ModelBinder或Author类型的参数上使用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;
}
}
}
Note
上述代码返回一个 BinderTypeModelBinder。
BinderTypeModelBinder 充当模型绑定器的工厂,并提供依赖项注入(DI)。 DI 需要 AuthorEntityBinder 才能访问 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动作的参数。
多态模型绑定
绑定到不同派生类型的模型称为多态模型绑定。 当请求值必须绑定到特定的派生模型类型时,需要多态自定义模型绑定。 多态模型绑定:
- 对于设计用于与所有语言合作的 API 来说,这并不常见 REST 。
- 使绑定模型难以推理。
但是,如果应用需要多态模型绑定,则实现可能类似于以下代码:
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 编码的字符串,并使用 a 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路由值从数据源提取实体来绑定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;
}
}
Note
前面的 AuthorEntityBinder 类旨在说明自定义模型联编程序。 该类不是为演示查找用例的最佳实践而设。 若要查找,请直接绑定 author 参数,并在作方法中查询数据库。 此方法将模型绑定失败与 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);
}
在此示例中,由于路由参数名称(id)与操作方法参数名称(author)不匹配,因此使用ModelBinder属性上的Name属性来指定要绑定的路由值。 与在操作方法中查找实体相比,控制器和操作方法都更加简化。 使用 Entity Framework Core 提取作者的逻辑已移至模型绑定器。 当有多个绑定到 Author 模型的方法时,这可以大幅简化。
可以将属性 ModelBinder 应用于单个模型属性(例如在 ViewModel 上)或操作方法参数,以便为该类型或操作指定特定的模型绑定器或模型名称。
实现 ModelBinderProvider
你可以实现 IModelBinderProvider,而不是应用一个属性。 这就是实现内置框架绑定器的方式。 当您指定绑定器操作的类型时,您是在指定它生成的参数类型,而不是 绑定器接受的输入。 以下绑定器提供者适用于AuthorEntityBinder。 当它添加到 MVC 的提供程序集合中时,您不需要在ModelBinder或Author类型的参数上使用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;
}
}
}
Note
上述代码返回一个 BinderTypeModelBinder。
BinderTypeModelBinder 充当模型绑定器的工厂,并提供依赖项注入(DI)。 DI 需要 AuthorEntityBinder 才能访问 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 操作参数。
多态模型绑定
绑定到不同派生类型的模型称为多态模型绑定。 当请求值必须绑定到特定的派生模型类型时,需要多态自定义模型绑定。 多态模型绑定:
- 对于设计用于与所有语言合作的 API 来说,这并不常见 REST 。
- 使绑定模型难以推理。
但是,如果应用需要多态模型绑定,则实现可能类似于以下代码:
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 通常是更好的选项。