Model binding personalizado no ASP.NET Core
Por Kirk Larkin
O model binding permite que as ações do controlador funcionem diretamente com tipos de modelo (passados como argumentos de método), em vez de solicitações HTTP. O mapeamento entre os dados de solicitação de entrada e os modelos de aplicativo é manipulado por associadores de modelos. Os desenvolvedores podem estender a funcionalidade de model binding interna implementando associadores de modelos personalizados (embora, normalmente, você não precise escrever seu próprio provedor).
Exibir ou baixar código de exemplo (como baixar)
Limitações dos associadores de modelos padrão
Os associadores de modelos padrão dão suporte à maioria dos tipos de dados comuns do .NET Core e devem atender à maior parte das necessidades dos desenvolvedores. Eles esperam associar a entrada baseada em texto da solicitação diretamente a tipos de modelo. Talvez seja necessário transformar a entrada antes de associá-la. Por exemplo, quando você tem uma chave que pode ser usada para pesquisar dados de modelo. Use um associador de modelos personalizado para buscar dados com base na chave.
Model binding de tipos simples e complexos
O model binding usa definições específicas para os tipos nos quais opera. Um tipo simples é convertido de uma única cadeia de caracteres usando TypeConverter ou um método TryParse
. Um tipo complexo é convertido de vários valores de entrada. A estrutura determina a diferença de acordo com a existência de um TypeConverter
ou TryParse
. É recomendável criar um conversor de tipo ou usar TryParse
para uma conversão de string
em SomeType
que não exija recursos externos ou várias entradas.
Consulte Tipos simples para obter uma lista de tipos que o associador de modelos pode converter de cadeias de caracteres.
Antes de criar seu próprio associador de modelos personalizado, vale a pena analisar como os associadores de modelos existentes são implementados. Considere o ByteArrayModelBinder, que pode ser usado para converter cadeias de caracteres codificadas em base64 em matrizes de bytes. As matrizes de bytes costumam ser armazenadas como arquivos ou campos BLOB do banco de dados.
Trabalhando com o ByteArrayModelBinder
Cadeias de caracteres codificadas em Base64 podem ser usadas para representar dados binários. Por exemplo, uma imagem pode ser codificada como uma cadeia de caracteres. A amostra inclui uma imagem como uma cadeia de caracteres codificada em base64 no Base64String.txt.
O ASP.NET Core MVC pode usar uma cadeia de caracteres codificada em Base64 e usar um ByteArrayModelBinder
para convertê-la em uma matriz de bytes. O ByteArrayModelBinderProvider mapeia argumentos byte[]
para 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;
}
Ao criar seu próprio associador de modelos personalizado, você pode implementar seu próprio tipo IModelBinderProvider
ou usar o ModelBinderAttribute.
O seguinte exemplo mostra como usar ByteArrayModelBinder
para converter uma cadeia de caracteres codificada em Base64 em um byte[]
e salvar o resultado em um arquivo:
[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);
}
Se você quiser ver os comentários de código traduzidos para idiomas diferentes do inglês, informe-nos neste problema de discussão do GitHub.
Execute POST em uma cadeia de caracteres codificada em Base64 para esse método de API usando uma ferramenta como curl.
Desde que o associador possa associar dados de solicitação a propriedades ou argumentos nomeados de forma adequada, o model binding terá êxito. O seguinte exemplo mostra como usar ByteArrayModelBinder
com um modelo de exibição:
[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; }
}
Amostra de associador de modelos personalizado
Nesta seção, implementaremos um associador de modelos personalizado que:
- Converte dados de solicitação de entrada em argumentos de chave fortemente tipados.
- Usa o Entity Framework Core para buscar a entidade associada.
- Passa a entidade associada como um argumento para o método de ação.
A seguinte amostra usa o atributo ModelBinder
no modelo 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; }
}
}
No código anterior, o atributo ModelBinder
especifica o tipo de IModelBinder
que deve ser usado para associar parâmetros de ação Author
.
A classe AuthorEntityBinder
a seguir associa um parâmetro Author
efetuando fetch da entidade de uma fonte de dados usando o Entity Framework Core e um 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;
}
}
Observação
A classe AuthorEntityBinder
precedente é destinada a ilustrar um associador de modelos personalizado. A classe não é destinada a ilustrar as melhores práticas para um cenário de pesquisa. Para pesquisa, associe o authorId
e consulte o banco de dados em um método de ação. Essa abordagem separa falhas de model binding de casos de NotFound
.
O seguinte código mostra como usar o AuthorEntityBinder
em um método de ação:
[HttpGet("get/{author}")]
public IActionResult Get(Author author)
{
if (author == null)
{
return NotFound();
}
return Ok(author);
}
O atributo ModelBinder
pode ser usado para aplicar o AuthorEntityBinder
aos parâmetros que não usam convenções padrão:
[HttpGet("{id}")]
public IActionResult GetById([ModelBinder(Name = "id")] Author author)
{
if (author == null)
{
return NotFound();
}
return Ok(author);
}
Neste exemplo, como o nome do argumento não é o authorId
padrão, ele é especificado no parâmetro com o atributo ModelBinder
. Observe que o controlador e o método de ação são simplificados, comparado à pesquisa da entidade no método de ação. A lógica para buscar o autor usando o Entity Framework Core é movida para o associador de modelos. Isso pode ser uma simplificação considerável quando há vários métodos associados ao modelo Author
.
Aplique o atributo ModelBinder
a propriedades de modelo individuais (como em um viewmodel) ou a parâmetros de método de ação para especificar um associador de modelos ou nome de modelo específico para apenas esse tipo ou essa ação.
Implementando um ModelBinderProvider
Em vez de aplicar um atributo, você pode implementar IModelBinderProvider
. É assim que os associadores de estrutura interna são implementados. Quando você especifica o tipo no qual o associador opera, você especifica o tipo de argumento que ele produz, não a entrada aceita pelo associador. O provedor de associador a seguir funciona com o AuthorEntityBinder
. Quando ele for adicionado à coleção do MVC de provedores, não será necessário usar o atributo ModelBinder
nos parâmetros Author
ou de tipo 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;
}
}
}
Observação: o código anterior retorna um
BinderTypeModelBinder
. OBinderTypeModelBinder
atua como um alocador para associadores de modelos e fornece a DI (injeção de dependência). OAuthorEntityBinder
exige que a DI acesse o EF Core. UseBinderTypeModelBinder
se o associador de modelos exigir serviços da DI.
Para usar um provedor de associador de modelos personalizado, adicione-o a ConfigureServices
:
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<AuthorContext>(options => options.UseInMemoryDatabase("Authors"));
services.AddControllers(options =>
{
options.ModelBinderProviders.Insert(0, new AuthorEntityBinderProvider());
});
}
Ao avaliar associadores de modelos, a coleção de provedores é examinada na ordem. O primeiro provedor que retorna um associador que corresponde ao modelo de entrada será usado. A adição do provedor ao final da coleção pode resultar na chamada a um associador de modelos interno antes que o associador personalizado tenha uma oportunidade. Neste exemplo, o provedor personalizado é adicionado ao início da coleção para garantir que ele seja sempre usado para argumentos de ação Author
.
Model binding polimórfico
A associação a diferentes modelos de tipos derivados é conhecida como model binding polimórfico. O model binding personalizado polimórfico é necessário quando o valor da solicitação deve ser associado ao tipo de modelo derivado específico. Model binding polimórfico:
- Não é típico de uma API REST projetada para interoperar com todos os idiomas.
- Dificulta raciocinar sobre os modelos associados.
No entanto, se um aplicativo exigir model binding polimórfico, uma implementação poderá ser semelhante ao seguinte código:
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,
};
}
}
}
Recomendações e melhores práticas
Associadores de modelos personalizados:
- Não devem tentar definir códigos de status ou retornar resultados (por exemplo, 404 Não Encontrado). Se o model binding falhar, um filtro de ação ou uma lógica no próprio método de ação deverá resolver a falha.
- São muito úteis para eliminar código repetitivo e interesses paralelos de métodos de ação.
- Normalmente, não devem ser usados para converter uma cadeia de caracteres em um tipo personalizado; um TypeConverter geralmente é uma opção melhor.
Por Steve Smith
O model binding permite que as ações do controlador funcionem diretamente com tipos de modelo (passados como argumentos de método), em vez de solicitações HTTP. O mapeamento entre os dados de solicitação de entrada e os modelos de aplicativo é manipulado por associadores de modelos. Os desenvolvedores podem estender a funcionalidade de model binding interna implementando associadores de modelos personalizados (embora, normalmente, você não precise escrever seu próprio provedor).
Exibir ou baixar código de exemplo (como baixar)
Limitações dos associadores de modelos padrão
Os associadores de modelos padrão dão suporte à maioria dos tipos de dados comuns do .NET Core e devem atender à maior parte das necessidades dos desenvolvedores. Eles esperam associar a entrada baseada em texto da solicitação diretamente a tipos de modelo. Talvez seja necessário transformar a entrada antes de associá-la. Por exemplo, quando você tem uma chave que pode ser usada para pesquisar dados de modelo. Use um associador de modelos personalizado para buscar dados com base na chave.
Análise do model binding
O model binding usa definições específicas para os tipos nos quais opera. Um tipo simples é convertido de uma única cadeia de caracteres na entrada. Um tipo complexo é convertido de vários valores de entrada. A estrutura determina a diferença de acordo com a existência de um TypeConverter
. Recomendamos que você crie um conversor de tipo se tiver um mapeamento string
->SomeType
simples que não exige recursos externos.
Antes de criar seu próprio associador de modelos personalizado, vale a pena analisar como os associadores de modelos existentes são implementados. Considere o ByteArrayModelBinder, que pode ser usado para converter cadeias de caracteres codificadas em base64 em matrizes de bytes. As matrizes de bytes costumam ser armazenadas como arquivos ou campos BLOB do banco de dados.
Trabalhando com o ByteArrayModelBinder
Cadeias de caracteres codificadas em Base64 podem ser usadas para representar dados binários. Por exemplo, uma imagem pode ser codificada como uma cadeia de caracteres. A amostra inclui uma imagem como uma cadeia de caracteres codificada em base64 no Base64String.txt.
O ASP.NET Core MVC pode usar uma cadeia de caracteres codificada em Base64 e usar um ByteArrayModelBinder
para convertê-la em uma matriz de bytes. O ByteArrayModelBinderProvider mapeia argumentos byte[]
para 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;
}
Ao criar seu próprio associador de modelos personalizado, você pode implementar seu próprio tipo IModelBinderProvider
ou usar o ModelBinderAttribute.
O seguinte exemplo mostra como usar ByteArrayModelBinder
para converter uma cadeia de caracteres codificada em Base64 em um byte[]
e salvar o resultado em um arquivo:
[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);
}
Execute POST em uma cadeia de caracteres codificada em Base64 para esse método de API usando uma ferramenta como curl.
Desde que o associador possa associar dados de solicitação a propriedades ou argumentos nomeados de forma adequada, o model binding terá êxito. O seguinte exemplo mostra como usar ByteArrayModelBinder
com um modelo de exibição:
[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; }
}
Amostra de associador de modelos personalizado
Nesta seção, implementaremos um associador de modelos personalizado que:
- Converte dados de solicitação de entrada em argumentos de chave fortemente tipados.
- Usa o Entity Framework Core para buscar a entidade associada.
- Passa a entidade associada como um argumento para o método de ação.
A seguinte amostra usa o atributo ModelBinder
no modelo 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; }
}
}
No código anterior, o atributo ModelBinder
especifica o tipo de IModelBinder
que deve ser usado para associar parâmetros de ação Author
.
A classe AuthorEntityBinder
a seguir associa um parâmetro Author
efetuando fetch da entidade de uma fonte de dados usando o Entity Framework Core e um 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;
}
}
Observação
A classe AuthorEntityBinder
precedente é destinada a ilustrar um associador de modelos personalizado. A classe não é destinada a ilustrar as melhores práticas para um cenário de pesquisa. Para pesquisa, associe o authorId
e consulte o banco de dados em um método de ação. Essa abordagem separa falhas de model binding de casos de NotFound
.
O seguinte código mostra como usar o AuthorEntityBinder
em um método de ação:
[HttpGet("get/{author}")]
public IActionResult Get(Author author)
{
if (author == null)
{
return NotFound();
}
return Ok(author);
}
O atributo ModelBinder
pode ser usado para aplicar o AuthorEntityBinder
aos parâmetros que não usam convenções padrão:
[HttpGet("{id}")]
public IActionResult GetById([ModelBinder(Name = "id")] Author author)
{
if (author == null)
{
return NotFound();
}
return Ok(author);
}
Neste exemplo, como o nome do argumento não é o authorId
padrão, ele é especificado no parâmetro com o atributo ModelBinder
. Observe que o controlador e o método de ação são simplificados, comparado à pesquisa da entidade no método de ação. A lógica para buscar o autor usando o Entity Framework Core é movida para o associador de modelos. Isso pode ser uma simplificação considerável quando há vários métodos associados ao modelo Author
.
Aplique o atributo ModelBinder
a propriedades de modelo individuais (como em um viewmodel) ou a parâmetros de método de ação para especificar um associador de modelos ou nome de modelo específico para apenas esse tipo ou essa ação.
Implementando um ModelBinderProvider
Em vez de aplicar um atributo, você pode implementar IModelBinderProvider
. É assim que os associadores de estrutura interna são implementados. Quando você especifica o tipo no qual o associador opera, você especifica o tipo de argumento que ele produz, não a entrada aceita pelo associador. O provedor de associador a seguir funciona com o AuthorEntityBinder
. Quando ele for adicionado à coleção do MVC de provedores, não será necessário usar o atributo ModelBinder
nos parâmetros Author
ou de tipo 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;
}
}
}
Observação: o código anterior retorna um
BinderTypeModelBinder
. OBinderTypeModelBinder
atua como um alocador para associadores de modelos e fornece a DI (injeção de dependência). OAuthorEntityBinder
exige que a DI acesse o EF Core. UseBinderTypeModelBinder
se o associador de modelos exigir serviços da DI.
Para usar um provedor de associador de modelos personalizado, adicione-o a 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);
}
Ao avaliar associadores de modelos, a coleção de provedores é examinada na ordem. O primeiro provedor que retorna um associador é usado. A adição do provedor ao final da coleção pode resultar na chamada a um associador de modelos interno antes que o associador personalizado tenha uma oportunidade. Neste exemplo, o provedor personalizado é adicionado ao início da coleção para garantir que ele é usado para argumentos de ação Author
.
Model binding polimórfico
A associação a diferentes modelos de tipos derivados é conhecida como model binding polimórfico. O model binding personalizado polimórfico é necessário quando o valor da solicitação deve ser associado ao tipo de modelo derivado específico. Model binding polimórfico:
- Não é típico de uma API REST projetada para interoperar com todos os idiomas.
- Dificulta raciocinar sobre os modelos associados.
No entanto, se um aplicativo exigir model binding polimórfico, uma implementação poderá ser semelhante ao seguinte código:
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,
};
}
}
}
Recomendações e melhores práticas
Associadores de modelos personalizados:
- Não devem tentar definir códigos de status ou retornar resultados (por exemplo, 404 Não Encontrado). Se o model binding falhar, um filtro de ação ou uma lógica no próprio método de ação deverá resolver a falha.
- São muito úteis para eliminar código repetitivo e interesses paralelos de métodos de ação.
- Normalmente, não devem ser usados para converter uma cadeia de caracteres em um tipo personalizado; um TypeConverter geralmente é uma opção melhor.