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

本文内容

作者:Steve SmithKirk Larkin

通过模型绑定,控制器操作可直接使用模型类型(作为方法参数传入)而不是 HTTP 请求。由模型绑定器处理传入的请求数据和应用程序模型之间的映射。开发人员可以通过实现自定义模型绑定器来扩展内置的模型绑定功能(尽管通常不需要编写自己的提供程序)。

查看或下载示例代码如何下载

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

默认模型绑定器支持大多数常见的 .NET Core 数据类型,能够满足大部分开发人员的需求。他们希望将基于文本的输入从请求直接绑定到模型类型。绑定输入之前,可能需要对其进行转换。例如,当拥有某个可以用来查找模型数据的键时。基于该键,用户可以使用自定义模型绑定器来获取数据。

模型绑定查看Model binding review

模型绑定为其操作对象的类型使用特定定义。简单类型转换自输入中的单个字符串。复杂类型转换自多个输入值。框架基于是否存在 TypeConverter 来确定差异。如果简单 string -> SomeType 映射不需要外部资源,建议创建类型转换器。

创建自己的自定义模型绑定器之前,有必要查看现有模型绑定器的实现方式。考虑使用 ByteArrayModelBinder,它可将 base64 编码的字符串转换为字节数组。字节数组通常存储为文件或数据库 BLOB 字段。

使用 ByteArrayModelBinderWorking with the ByteArrayModelBinder

Base64 编码的字符串可用来表示二进制数据。例如,可将图像编码为一个字符串。示例包括作为使用 Base64String.txt 的 base64 编码字符串的图像。

ASP.NET Core MVC 可以采用 base64 编码的字符串,并使用 ByteArrayModelBinder 将其转换为字节数组。ByteArrayModelBinderProviderbyte[] 参数映射到 ByteArrayModelBinder

  1. public IModelBinder GetBinder(ModelBinderProviderContext context)
  2. {
  3. if (context == null)
  4. {
  5. throw new ArgumentNullException(nameof(context));
  6. }
  7. if (context.Metadata.ModelType == typeof(byte[]))
  8. {
  9. var loggerFactory = context.Services.GetRequiredService<ILoggerFactory>();
  10. return new ByteArrayModelBinder(loggerFactory);
  11. }
  12. return null;
  13. }

创建自己的自定义模型绑定器时,可实现自己的 IModelBinderProvider 类型,或使用 ModelBinderAttribute

以下示例显示如何使用 ByteArrayModelBinder 将 base64 编码的字符串转换为 byte[],并将结果保存到文件中:

  1. [HttpPost]
  2. public void Post([FromForm] byte[] file, string filename)
  3. {
  4. // Don't trust the file name sent by the client. Use
  5. // Path.GetRandomFileName to generate a safe random
  6. // file name. _targetFilePath receives a value
  7. // from configuration (the appsettings.json file in
  8. // the sample app).
  9. var trustedFileName = Path.GetRandomFileName();
  10. var filePath = Path.Combine(_targetFilePath, trustedFileName);
  11. if (System.IO.File.Exists(filePath))
  12. {
  13. return;
  14. }
  15. System.IO.File.WriteAllBytes(filePath, file);
  16. }

若要查看翻译为非英语语言的代码注释,请在 此 GitHub 讨论问题中告诉我们。

可以使用 Postman 等工具将 base64 编码的字符串发布到此 api 方法:

Postman

只要绑定器可以将请求数据绑定到相应命名的属性或参数,模型绑定就会成功。以下示例演示如何将 ByteArrayModelBinder 与 视图模型结合使用:

  1. [HttpPost("Profile")]
  2. public void SaveProfile([FromForm] ProfileViewModel model)
  3. {
  4. // Don't trust the file name sent by the client. Use
  5. // Path.GetRandomFileName to generate a safe random
  6. // file name. _targetFilePath receives a value
  7. // from configuration (the appsettings.json file in
  8. // the sample app).
  9. var trustedFileName = Path.GetRandomFileName();
  10. var filePath = Path.Combine(_targetFilePath, trustedFileName);
  11. if (System.IO.File.Exists(filePath))
  12. {
  13. return;
  14. }
  15. System.IO.File.WriteAllBytes(filePath, model.File);
  16. }
  17. public class ProfileViewModel
  18. {
  19. public byte[] File { get; set; }
  20. public string FileName { get; set; }
  21. }

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

在本部分中,我们将实现具有以下功能的自定义模型绑定器:

  • 将传入的请求数据转换为强类型键参数。
  • 使用 Entity Framework Core 来提取关联的实体。
  • 将关联的实体作为自变量传递给操作方法。

以下示例在 ModelBinder 模型上使用 Author 属性:

  1. using CustomModelBindingSample.Binders;
  2. using Microsoft.AspNetCore.Mvc;
  3. namespace CustomModelBindingSample.Data
  4. {
  5. [ModelBinder(BinderType = typeof(AuthorEntityBinder))]
  6. public class Author
  7. {
  8. public int Id { get; set; }
  9. public string Name { get; set; }
  10. public string GitHub { get; set; }
  11. public string Twitter { get; set; }
  12. public string BlogUrl { get; set; }
  13. }
  14. }

在前面的代码中,ModelBinder 属性指定应当用于绑定 IModelBinder 操作参数的 Author 的类型。

以下 AuthorEntityBinder 类通过 Entity Framework Core 和 Author 提取数据源中的实体来绑定 authorId 参数:

  1. public class AuthorEntityBinder : IModelBinder
  2. {
  3. private readonly AuthorContext _context;
  4. public AuthorEntityBinder(AuthorContext context)
  5. {
  6. _context = context;
  7. }
  8. public Task BindModelAsync(ModelBindingContext bindingContext)
  9. {
  10. if (bindingContext == null)
  11. {
  12. throw new ArgumentNullException(nameof(bindingContext));
  13. }
  14. var modelName = bindingContext.ModelName;
  15. // Try to fetch the value of the argument by name
  16. var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
  17. if (valueProviderResult == ValueProviderResult.None)
  18. {
  19. return Task.CompletedTask;
  20. }
  21. bindingContext.ModelState.SetModelValue(modelName, valueProviderResult);
  22. var value = valueProviderResult.FirstValue;
  23. // Check if the argument value is null or empty
  24. if (string.IsNullOrEmpty(value))
  25. {
  26. return Task.CompletedTask;
  27. }
  28. if (!int.TryParse(value, out var id))
  29. {
  30. // Non-integer arguments result in model state errors
  31. bindingContext.ModelState.TryAddModelError(
  32. modelName, "Author Id must be an integer.");
  33. return Task.CompletedTask;
  34. }
  35. // Model will be null if not found, including for
  36. // out of range id values (0, -3, etc.)
  37. var model = _context.Authors.Find(id);
  38. bindingContext.Result = ModelBindingResult.Success(model);
  39. return Task.CompletedTask;
  40. }
  41. }

备注

前面的 AuthorEntityBinder 类旨在说明自定义模型绑定器。该类不是意在说明查找方案的最佳做法。对于查找,请绑定 authorId 并在操作方法中查询数据库。此方法将模型绑定故障与 NotFound 案例分开。

以下代码显示如何在操作方法中使用 AuthorEntityBinder

  1. [HttpGet("get/{authorId}")]
  2. public IActionResult Get(Author author)
  3. {
  4. if (author == null)
  5. {
  6. return NotFound();
  7. }
  8. return Ok(author);
  9. }

可使用 ModelBinder 属性将 AuthorEntityBinder 应用于不使用默认约定的参数:

  1. [HttpGet("{id}")]
  2. public IActionResult GetById([ModelBinder(Name = "id")] Author author)
  3. {
  4. if (author == null)
  5. {
  6. return NotFound();
  7. }
  8. return Ok(author);
  9. }

在此示例中,由于参数的名称不是默认的 authorId,因此,使用 ModelBinder 属性在参数上指定该名称。比起在操作方法中查找实体,控制器和操作方法都得到了简化。使用 Entity Framework Core 获取创建者的逻辑会移动到模型绑定器。如果有多种方法绑定到 Author 模型,就能得到很大程度的简化。

可以将 ModelBinder 属性应用到各个模型属性(例如视图模型上)或操作方法参数,以便为该类型或操作指定某一模型绑定器或模型名称。

实现 ModelBinderProviderImplementing a ModelBinderProvider

可以实现 IModelBinderProvider,而不是应用属性。这就是内置框架绑定器的实现方式。指定绑定器所操作的类型时,指定它生成的参数的类型,而不是绑定器接受的输入。以下绑定器提供程序适用于 AuthorEntityBinder将其添加到 MVC 提供程序的集合中时,无需在 ModelBinderAuthor 类型参数上使用 Author 属性。

  1. using CustomModelBindingSample.Data;
  2. using Microsoft.AspNetCore.Mvc.ModelBinding;
  3. using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
  4. using System;
  5. namespace CustomModelBindingSample.Binders
  6. {
  7. public class AuthorEntityBinderProvider : IModelBinderProvider
  8. {
  9. public IModelBinder GetBinder(ModelBinderProviderContext context)
  10. {
  11. if (context == null)
  12. {
  13. throw new ArgumentNullException(nameof(context));
  14. }
  15. if (context.Metadata.ModelType == typeof(Author))
  16. {
  17. return new BinderTypeModelBinder(typeof(AuthorEntityBinder));
  18. }
  19. return null;
  20. }
  21. }
  22. }

注意:上述代码返回 BinderTypeModelBinderBinderTypeModelBinder 充当模型绑定器中心,并提供依赖关系注入 (DI)。 AuthorEntityBinder 需要 DI 来访问 EF Core。 如果模型绑定器需要 DI 中的服务,请使用 BinderTypeModelBinder


若要使用自定义模型绑定器提供程序,请将其添加到 ConfigureServices 中:

  1. public void ConfigureServices(IServiceCollection services)
  2. {
  3. services.AddDbContext<AuthorContext>(options => options.UseInMemoryDatabase("Authors"));
  4. services.AddControllers(options =>
  5. {
  6. options.ModelBinderProviders.Insert(0, new AuthorEntityBinderProvider());
  7. });
  8. }

评估模型绑定器时,按顺序检查提供程序的集合。使用返回绑定器的第一个提供程序。向集合的末尾添加提供程序,可能会导致在调用自定义绑定器之前调用内置模型绑定器。在此示例中,向集合的开头添加自定义提供程序,确保它用于 Author 操作参数。

多态模型绑定Polymorphic model binding

绑定到不同的派生类型模型称为多态模型绑定。如果请求值必须绑定到特定的派生模型类型,则需要多态自定义模型绑定。多态模型绑定:

  • 对于旨在与所有语言进行互操作的 REST API 并不常见。
  • 使绑定模型难以推理。

但是,如果应用需要多态模型绑定,则实现可能类似于以下代码:

  1. public abstract class Device
  2. {
  3. public string Kind { get; set; }
  4. }
  5. public class Laptop : Device
  6. {
  7. public string CPUIndex { get; set; }
  8. }
  9. public class SmartPhone : Device
  10. {
  11. public string ScreenSize { get; set; }
  12. }
  13. public class DeviceModelBinderProvider : IModelBinderProvider
  14. {
  15. public IModelBinder GetBinder(ModelBinderProviderContext context)
  16. {
  17. if (context.Metadata.ModelType != typeof(Device))
  18. {
  19. return null;
  20. }
  21. var subclasses = new[] { typeof(Laptop), typeof(SmartPhone), };
  22. var binders = new Dictionary<Type, (ModelMetadata, IModelBinder)>();
  23. foreach (var type in subclasses)
  24. {
  25. var modelMetadata = context.MetadataProvider.GetMetadataForType(type);
  26. binders[type] = (modelMetadata, context.CreateBinder(modelMetadata));
  27. }
  28. return new DeviceModelBinder(binders);
  29. }
  30. }
  31. public class DeviceModelBinder : IModelBinder
  32. {
  33. private Dictionary<Type, (ModelMetadata, IModelBinder)> binders;
  34. public DeviceModelBinder(Dictionary<Type, (ModelMetadata, IModelBinder)> binders)
  35. {
  36. this.binders = binders;
  37. }
  38. public async Task BindModelAsync(ModelBindingContext bindingContext)
  39. {
  40. var modelKindName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, nameof(Device.Kind));
  41. var modelTypeValue = bindingContext.ValueProvider.GetValue(modelKindName).FirstValue;
  42. IModelBinder modelBinder;
  43. ModelMetadata modelMetadata;
  44. if (modelTypeValue == "Laptop")
  45. {
  46. (modelMetadata, modelBinder) = binders[typeof(Laptop)];
  47. }
  48. else if (modelTypeValue == "SmartPhone")
  49. {
  50. (modelMetadata, modelBinder) = binders[typeof(SmartPhone)];
  51. }
  52. else
  53. {
  54. bindingContext.Result = ModelBindingResult.Failed();
  55. return;
  56. }
  57. var newBindingContext = DefaultModelBindingContext.CreateBindingContext(
  58. bindingContext.ActionContext,
  59. bindingContext.ValueProvider,
  60. modelMetadata,
  61. bindingInfo: null,
  62. bindingContext.ModelName);
  63. await modelBinder.BindModelAsync(newBindingContext);
  64. bindingContext.Result = newBindingContext.Result;
  65. if (newBindingContext.Result.IsModelSet)
  66. {
  67. // Setting the ValidationState ensures properties on derived types are correctly
  68. bindingContext.ValidationState[newBindingContext.Result] = new ValidationStateEntry
  69. {
  70. Metadata = modelMetadata,
  71. };
  72. }
  73. }
  74. }

建议和最佳做法Recommendations and best practices

自定义模型绑定:

  • 不应尝试设置状态代码或返回结果(例如 404 Not Found)。如果模型绑定失败,那么该操作方法本身的操作筛选器或逻辑会处理失败。
  • 对于消除操作方法中的重复代码和跨领域问题最为有用。
  • 通常不应用其将字符串转换为自定义类型,而应选择用 TypeConverter 来完成此操作。

作者:Steve Smith

通过模型绑定,控制器操作可直接使用模型类型(作为方法参数传入)而不是 HTTP 请求。由模型绑定器处理传入的请求数据和应用程序模型之间的映射。开发人员可以通过实现自定义模型绑定器来扩展内置的模型绑定功能(尽管通常不需要编写自己的提供程序)。

查看或下载示例代码如何下载

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

默认模型绑定器支持大多数常见的 .NET Core 数据类型,能够满足大部分开发人员的需求。他们希望将基于文本的输入从请求直接绑定到模型类型。绑定输入之前,可能需要对其进行转换。例如,当拥有某个可以用来查找模型数据的键时。基于该键,用户可以使用自定义模型绑定器来获取数据。

模型绑定查看Model binding review

模型绑定为其操作对象的类型使用特定定义。简单类型转换自输入中的单个字符串。复杂类型转换自多个输入值。框架基于是否存在 TypeConverter 来确定差异。如果简单 string -> SomeType 映射不需要外部资源,建议创建类型转换器。

创建自己的自定义模型绑定器之前,有必要查看现有模型绑定器的实现方式。考虑使用 ByteArrayModelBinder,它可将 base64 编码的字符串转换为字节数组。字节数组通常存储为文件或数据库 BLOB 字段。

使用 ByteArrayModelBinderWorking with the ByteArrayModelBinder

Base64 编码的字符串可用来表示二进制数据。例如,可将图像编码为一个字符串。示例包括作为使用 Base64String.txt 的 base64 编码字符串的图像。

ASP.NET Core MVC 可以采用 base64 编码的字符串,并使用 ByteArrayModelBinder 将其转换为字节数组。ByteArrayModelBinderProviderbyte[] 参数映射到 ByteArrayModelBinder

  1. public IModelBinder GetBinder(ModelBinderProviderContext context)
  2. {
  3. if (context == null)
  4. {
  5. throw new ArgumentNullException(nameof(context));
  6. }
  7. if (context.Metadata.ModelType == typeof(byte[]))
  8. {
  9. return new ByteArrayModelBinder();
  10. }
  11. return null;
  12. }

创建自己的自定义模型绑定器时,可实现自己的 IModelBinderProvider 类型,或使用 ModelBinderAttribute

以下示例显示如何使用 ByteArrayModelBinder 将 base64 编码的字符串转换为 byte[],并将结果保存到文件中:

  1. [HttpPost]
  2. public void Post([FromForm] byte[] file, string filename)
  3. {
  4. // Don't trust the file name sent by the client. Use
  5. // Path.GetRandomFileName to generate a safe random
  6. // file name. _targetFilePath receives a value
  7. // from configuration (the appsettings.json file in
  8. // the sample app).
  9. var trustedFileName = Path.GetRandomFileName();
  10. var filePath = Path.Combine(_targetFilePath, trustedFileName);
  11. if (System.IO.File.Exists(filePath))
  12. {
  13. return;
  14. }
  15. System.IO.File.WriteAllBytes(filePath, file);
  16. }

可以使用 Postman 等工具将 base64 编码的字符串发布到此 api 方法:

Postman

只要绑定器可以将请求数据绑定到相应命名的属性或参数,模型绑定就会成功。以下示例演示如何将 ByteArrayModelBinder 与 视图模型结合使用:

  1. [HttpPost("Profile")]
  2. public void SaveProfile([FromForm] ProfileViewModel model)
  3. {
  4. // Don't trust the file name sent by the client. Use
  5. // Path.GetRandomFileName to generate a safe random
  6. // file name. _targetFilePath receives a value
  7. // from configuration (the appsettings.json file in
  8. // the sample app).
  9. var trustedFileName = Path.GetRandomFileName();
  10. var filePath = Path.Combine(_targetFilePath, trustedFileName);
  11. if (System.IO.File.Exists(filePath))
  12. {
  13. return;
  14. }
  15. System.IO.File.WriteAllBytes(filePath, model.File);
  16. }
  17. public class ProfileViewModel
  18. {
  19. public byte[] File { get; set; }
  20. public string FileName { get; set; }
  21. }

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

在本部分中,我们将实现具有以下功能的自定义模型绑定器:

  • 将传入的请求数据转换为强类型键参数。
  • 使用 Entity Framework Core 来提取关联的实体。
  • 将关联的实体作为自变量传递给操作方法。

以下示例在 ModelBinder 模型上使用 Author 属性:

  1. using CustomModelBindingSample.Binders;
  2. using Microsoft.AspNetCore.Mvc;
  3. namespace CustomModelBindingSample.Data
  4. {
  5. [ModelBinder(BinderType = typeof(AuthorEntityBinder))]
  6. public class Author
  7. {
  8. public int Id { get; set; }
  9. public string Name { get; set; }
  10. public string GitHub { get; set; }
  11. public string Twitter { get; set; }
  12. public string BlogUrl { get; set; }
  13. }
  14. }

在前面的代码中,ModelBinder 属性指定应当用于绑定 IModelBinder 操作参数的 Author 的类型。

以下 AuthorEntityBinder 类通过 Entity Framework Core 和 Author 提取数据源中的实体来绑定 authorId 参数:

  1. public class AuthorEntityBinder : IModelBinder
  2. {
  3. private readonly AppDbContext _db;
  4. public AuthorEntityBinder(AppDbContext db)
  5. {
  6. _db = db;
  7. }
  8. public Task BindModelAsync(ModelBindingContext bindingContext)
  9. {
  10. if (bindingContext == null)
  11. {
  12. throw new ArgumentNullException(nameof(bindingContext));
  13. }
  14. var modelName = bindingContext.ModelName;
  15. // Try to fetch the value of the argument by name
  16. var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
  17. if (valueProviderResult == ValueProviderResult.None)
  18. {
  19. return Task.CompletedTask;
  20. }
  21. bindingContext.ModelState.SetModelValue(modelName, valueProviderResult);
  22. var value = valueProviderResult.FirstValue;
  23. // Check if the argument value is null or empty
  24. if (string.IsNullOrEmpty(value))
  25. {
  26. return Task.CompletedTask;
  27. }
  28. if (!int.TryParse(value, out var id))
  29. {
  30. // Non-integer arguments result in model state errors
  31. bindingContext.ModelState.TryAddModelError(
  32. modelName, "Author Id must be an integer.");
  33. return Task.CompletedTask;
  34. }
  35. // Model will be null if not found, including for
  36. // out of range id values (0, -3, etc.)
  37. var model = _db.Authors.Find(id);
  38. bindingContext.Result = ModelBindingResult.Success(model);
  39. return Task.CompletedTask;
  40. }
  41. }

备注

前面的 AuthorEntityBinder 类旨在说明自定义模型绑定器。该类不是意在说明查找方案的最佳做法。对于查找,请绑定 authorId 并在操作方法中查询数据库。此方法将模型绑定故障与 NotFound 案例分开。

以下代码显示如何在操作方法中使用 AuthorEntityBinder

  1. [HttpGet("get/{authorId}")]
  2. public IActionResult Get(Author author)
  3. {
  4. if (author == null)
  5. {
  6. return NotFound();
  7. }
  8. return Ok(author);
  9. }

可使用 ModelBinder 属性将 AuthorEntityBinder 应用于不使用默认约定的参数:

  1. [HttpGet("{id}")]
  2. public IActionResult GetById([ModelBinder(Name = "id")] Author author)
  3. {
  4. if (author == null)
  5. {
  6. return NotFound();
  7. }
  8. return Ok(author);
  9. }

在此示例中,由于参数的名称不是默认的 authorId,因此,使用 ModelBinder 属性在参数上指定该名称。比起在操作方法中查找实体,控制器和操作方法都得到了简化。使用 Entity Framework Core 获取创建者的逻辑会移动到模型绑定器。如果有多种方法绑定到 Author 模型,就能得到很大程度的简化。

可以将 ModelBinder 属性应用到各个模型属性(例如视图模型上)或操作方法参数,以便为该类型或操作指定某一模型绑定器或模型名称。

实现 ModelBinderProviderImplementing a ModelBinderProvider

可以实现 IModelBinderProvider,而不是应用属性。这就是内置框架绑定器的实现方式。指定绑定器所操作的类型时,指定它生成的参数的类型,而不是绑定器接受的输入。以下绑定器提供程序适用于 AuthorEntityBinder将其添加到 MVC 提供程序的集合中时,无需在 ModelBinderAuthor 类型参数上使用 Author 属性。

  1. using CustomModelBindingSample.Data;
  2. using Microsoft.AspNetCore.Mvc.ModelBinding;
  3. using Microsoft.AspNetCore.Mvc.ModelBinding.Binders;
  4. using System;
  5. namespace CustomModelBindingSample.Binders
  6. {
  7. public class AuthorEntityBinderProvider : IModelBinderProvider
  8. {
  9. public IModelBinder GetBinder(ModelBinderProviderContext context)
  10. {
  11. if (context == null)
  12. {
  13. throw new ArgumentNullException(nameof(context));
  14. }
  15. if (context.Metadata.ModelType == typeof(Author))
  16. {
  17. return new BinderTypeModelBinder(typeof(AuthorEntityBinder));
  18. }
  19. return null;
  20. }
  21. }
  22. }

注意:上述代码返回 BinderTypeModelBinderBinderTypeModelBinder 充当模型绑定器中心,并提供依赖关系注入 (DI)。 AuthorEntityBinder 需要 DI 来访问 EF Core。 如果模型绑定器需要 DI 中的服务,请使用 BinderTypeModelBinder


若要使用自定义模型绑定器提供程序,请将其添加到 ConfigureServices 中:

  1. public void ConfigureServices(IServiceCollection services)
  2. {
  3. services.AddDbContext<AppDbContext>(options => options.UseInMemoryDatabase("App"));
  4. services.AddMvc(options =>
  5. {
  6. // add custom binder to beginning of collection
  7. options.ModelBinderProviders.Insert(0, new AuthorEntityBinderProvider());
  8. })
  9. .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
  10. }

评估模型绑定器时,按顺序检查提供程序的集合。使用返回绑定器的第一个提供程序。向集合的末尾添加提供程序,可能会导致在调用自定义绑定器之前调用内置模型绑定器。在此示例中,向集合的开头添加自定义提供程序,确保它用于 Author 操作参数。

多态模型绑定Polymorphic model binding

绑定到不同的派生类型模型称为多态模型绑定。如果请求值必须绑定到特定的派生模型类型,则需要多态自定义模型绑定。多态模型绑定:

  • 对于旨在与所有语言进行互操作的 REST API 并不常见。
  • 使绑定模型难以推理。

但是,如果应用需要多态模型绑定,则实现可能类似于以下代码:

  1. public abstract class Device
  2. {
  3. public string Kind { get; set; }
  4. }
  5. public class Laptop : Device
  6. {
  7. public string CPUIndex { get; set; }
  8. }
  9. public class SmartPhone : Device
  10. {
  11. public string ScreenSize { get; set; }
  12. }
  13. public class DeviceModelBinderProvider : IModelBinderProvider
  14. {
  15. public IModelBinder GetBinder(ModelBinderProviderContext context)
  16. {
  17. if (context.Metadata.ModelType != typeof(Device))
  18. {
  19. return null;
  20. }
  21. var subclasses = new[] { typeof(Laptop), typeof(SmartPhone), };
  22. var binders = new Dictionary<Type, (ModelMetadata, IModelBinder)>();
  23. foreach (var type in subclasses)
  24. {
  25. var modelMetadata = context.MetadataProvider.GetMetadataForType(type);
  26. binders[type] = (modelMetadata, context.CreateBinder(modelMetadata));
  27. }
  28. return new DeviceModelBinder(binders);
  29. }
  30. }
  31. public class DeviceModelBinder : IModelBinder
  32. {
  33. private Dictionary<Type, (ModelMetadata, IModelBinder)> binders;
  34. public DeviceModelBinder(Dictionary<Type, (ModelMetadata, IModelBinder)> binders)
  35. {
  36. this.binders = binders;
  37. }
  38. public async Task BindModelAsync(ModelBindingContext bindingContext)
  39. {
  40. var modelKindName = ModelNames.CreatePropertyModelName(bindingContext.ModelName, nameof(Device.Kind));
  41. var modelTypeValue = bindingContext.ValueProvider.GetValue(modelKindName).FirstValue;
  42. IModelBinder modelBinder;
  43. ModelMetadata modelMetadata;
  44. if (modelTypeValue == "Laptop")
  45. {
  46. (modelMetadata, modelBinder) = binders[typeof(Laptop)];
  47. }
  48. else if (modelTypeValue == "SmartPhone")
  49. {
  50. (modelMetadata, modelBinder) = binders[typeof(SmartPhone)];
  51. }
  52. else
  53. {
  54. bindingContext.Result = ModelBindingResult.Failed();
  55. return;
  56. }
  57. var newBindingContext = DefaultModelBindingContext.CreateBindingContext(
  58. bindingContext.ActionContext,
  59. bindingContext.ValueProvider,
  60. modelMetadata,
  61. bindingInfo: null,
  62. bindingContext.ModelName);
  63. await modelBinder.BindModelAsync(newBindingContext);
  64. bindingContext.Result = newBindingContext.Result;
  65. if (newBindingContext.Result.IsModelSet)
  66. {
  67. // Setting the ValidationState ensures properties on derived types are correctly
  68. bindingContext.ValidationState[newBindingContext.Result] = new ValidationStateEntry
  69. {
  70. Metadata = modelMetadata,
  71. };
  72. }
  73. }
  74. }

建议和最佳做法Recommendations and best practices

自定义模型绑定:

  • 不应尝试设置状态代码或返回结果(例如 404 Not Found)。如果模型绑定失败,那么该操作方法本身的操作筛选器或逻辑会处理失败。
  • 对于消除操作方法中的重复代码和跨领域问题最为有用。
  • 通常不应用其将字符串转换为自定义类型,而应选择用 TypeConverter 来完成此操作。