Model binding personalizado no ASP.NET CoreCustom Model Binding in ASP.NET Core

Por Steve SmithBy 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.Model binding allows controller actions to work directly with model types (passed in as method arguments), rather than HTTP requests. O mapeamento entre os dados de solicitação de entrada e os modelos de aplicativo é manipulado por associadores de modelos.Mapping between incoming request data and application models is handled by model binders. 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).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).

Exibir ou baixar a amostra do GitHubView or download sample from GitHub

Limitações dos associadores de modelos padrãoDefault model binder limitations

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.The default model binders support most of the common .NET Core data types and should meet most developers' needs. Eles esperam associar a entrada baseada em texto da solicitação diretamente a tipos de modelo.They expect to bind text-based input from the request directly to model types. Talvez seja necessário transformar a entrada antes de associá-la.You might need to transform the input prior to binding it. Por exemplo, quando você tem uma chave que pode ser usada para pesquisar dados de modelo.For example, when you have a key that can be used to look up model data. Use um associador de modelos personalizado para buscar dados com base na chave.You can use a custom model binder to fetch data based on the key.

Análise do model bindingModel binding review

O model binding usa definições específicas para os tipos nos quais opera.Model binding uses specific definitions for the types it operates on. Um tipo simples é convertido de uma única cadeia de caracteres na entrada.A simple type is converted from a single string in the input. Um tipo complexo é convertido de vários valores de entrada.A complex type is converted from multiple input values. A estrutura determina a diferença de acordo com a existência de um TypeConverter.The framework determines the difference based on the existence of a TypeConverter. Recomendamos que você crie um conversor de tipo se tiver um mapeamento string -> SomeType simples que não exige recursos externos.We recommended you create a type converter if you have a simple string -> SomeType mapping that doesn't require external resources.

Antes de criar seu próprio associador de modelos personalizado, vale a pena analisar como os associadores de modelos existentes são implementados.Before creating your own custom model binder, it's worth reviewing how existing model binders are implemented. Considere o ByteArrayModelBinder, que pode ser usado para converter cadeias de caracteres codificadas em Base64 em matrizes de bytes.Consider the ByteArrayModelBinder which can be used to convert base64-encoded strings into byte arrays. As matrizes de bytes costumam ser armazenadas como arquivos ou campos BLOB do banco de dados.The byte arrays are often stored as files or database BLOB fields.

Trabalhando com o ByteArrayModelBinderWorking with the ByteArrayModelBinder

Cadeias de caracteres codificadas em Base64 podem ser usadas para representar dados binários.Base64-encoded strings can be used to represent binary data. Por exemplo, a imagem a seguir pode ser codificada como uma cadeia de caracteres.For example, the following image can be encoded as a string.

dotnet botdotnet bot

Uma pequena parte da cadeia de caracteres codificada é mostrada na seguinte imagem:A small portion of the encoded string is shown in the following image:

dotnet bot codificadodotnet bot encoded

Siga as instruções do LEIAME da amostra para converter a cadeia de caracteres codificada em Base64 em um arquivo.Follow the instructions in the sample's README to convert the base64-encoded string into a file.

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.ASP.NET Core MVC can take a base64-encoded string and use a ByteArrayModelBinder to convert it into a byte array. O ByteArrayModelBinderProvider que implementa IModelBinderProvider mapeia argumentos byte[] para ByteArrayModelBinder:The ByteArrayModelBinderProvider which implements IModelBinderProvider 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;
}

Ao criar seu próprio associador de modelos personalizado, você pode implementar seu próprio tipo IModelBinderProvider ou usar o ModelBinderAttribute.When creating your own custom model binder, you can implement your own IModelBinderProvider type, or use the 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:The following example shows how to use ByteArrayModelBinder to convert a base64-encoded string to a byte[] and save the result to a file:

// POST: api/image
[HttpPost]
public void Post(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 o Postman:You can POST a base64-encoded string to this api method using a tool like Postman:

postmanpostman

Desde que o associador possa associar dados de solicitação a propriedades ou argumentos nomeados de forma adequada, o model binding terá êxito.As long as the binder can bind request data to appropriately named properties or arguments, model binding will succeed. O seguinte exemplo mostra como usar ByteArrayModelBinder com um modelo de exibição:The following example shows how to use ByteArrayModelBinder with a view model:

[HttpPost("Profile")]
public void SaveProfile(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 personalizadoCustom model binder sample

Nesta seção, implementaremos um associador de modelos personalizado que:In this section we'll implement a custom model binder that:

  • Converte dados de solicitação de entrada em argumentos de chave fortemente tipados.Converts incoming request data into strongly typed key arguments.
  • Usa o Entity Framework Core para buscar a entidade associada.Uses Entity Framework Core to fetch the associated entity.
  • Passa a entidade associada como um argumento para o método de ação.Passes the associated entity as an argument to the action method.

A seguinte amostra usa o atributo ModelBinder no modelo Author:The following sample uses the ModelBinder attribute on the Author model:

using CustomModelBindingSample.Binders;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

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.In the preceding code, the ModelBinder attribute specifies the type of IModelBinder that should be used to bind Author action parameters.

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

        int id = 0;
        if (!int.TryParse(value, out 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.The preceding AuthorEntityBinder class is intended to illustrate a custom model binder. A classe não é destinada a ilustrar as melhores práticas para um cenário de pesquisa.The class isn't intended to illustrate best practices for a lookup scenario. Para pesquisa, associe o authorId e consulte o banco de dados em um método de ação.For lookup, bind the authorId and query the database in an action method. Essa abordagem separa falhas de model binding de casos de NotFound.This approach separates model binding failures from NotFound cases.

O seguinte código mostra como usar o AuthorEntityBinder em um método de ação:The following code shows how to use the AuthorEntityBinder in an action method:

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

O atributo ModelBinder pode ser usado para aplicar o AuthorEntityBinder aos parâmetros que não usam convenções padrão: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();
    }
    if (!ModelState.IsValid)
    {
        return BadRequest(ModelState);
    }
    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.In this example, since the name of the argument isn't the default authorId, it's specified on the parameter using the ModelBinder attribute. Observe que o controlador e o método de ação são simplificados, comparado à pesquisa da entidade no método de ação.Note that both the controller and action method are simplified compared to looking up the entity in the action method. A lógica para buscar o autor usando o Entity Framework Core é movida para o associador de modelos.The logic to fetch the author using Entity Framework Core is moved to the model binder. Isso pode ser uma simplificação considerável quando há vários métodos associados ao modelo Author.This can be a considerable simplification when you have several methods that bind to the Author model.

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.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.

Implementando um ModelBinderProviderImplementing a ModelBinderProvider

Em vez de aplicar um atributo, você pode implementar IModelBinderProvider.Instead of applying an attribute, you can implement IModelBinderProvider. É assim que os associadores de estrutura interna são implementados.This is how the built-in framework binders are implemented. 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.When you specify the type your binder operates on, you specify the type of argument it produces, not the input your binder accepts. O provedor de associador a seguir funciona com o AuthorEntityBinder.The following binder provider works with the 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.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;
        }
    }
}

Observação: o código anterior retorna um BinderTypeModelBinder.Note: The preceding code returns a BinderTypeModelBinder. O BinderTypeModelBinder atua como um alocador para associadores de modelos e fornece a DI (injeção de dependência).BinderTypeModelBinder acts as a factory for model binders and provides dependency injection (DI). O AuthorEntityBinder exige que a DI acesse o EF Core.The AuthorEntityBinder requires DI to access EF Core. Use BinderTypeModelBinder se o associador de modelos exigir serviços da DI.Use BinderTypeModelBinder if your model binder requires services from DI.

Para usar um provedor de associador de modelos personalizado, adicione-o a ConfigureServices:To use a custom model binder provider, add it in ConfigureServices:

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

    services.AddMvc(options =>
    {
        // add custom binder to beginning of collection
        options.ModelBinderProviders.Insert(0, new AuthorEntityBinderProvider());
    });
}

Ao avaliar associadores de modelos, a coleção de provedores é examinada na ordem.When evaluating model binders, the collection of providers is examined in order. O primeiro provedor que retorna um associador é usado.The first provider that returns a binder is used.

A imagem a seguir mostra os associadores de modelos padrão do depurador.The following image shows the default model binders from the debugger.

associadores de modelo padrãodefault model binders

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.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. Neste exemplo, o provedor personalizado é adicionado ao início da coleção para garantir que ele é usado para argumentos de ação Author.In this example, the custom provider is added to the beginning of the collection to ensure it's used for Author action arguments.

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

    services.AddMvc(options =>
    {
        // add custom binder to beginning of collection
        options.ModelBinderProviders.Insert(0, new AuthorEntityBinderProvider());
    });
}

Recomendações e melhores práticasRecommendations and best practices

Associadores de modelos personalizados:Custom model binders:

  • Não devem tentar definir códigos de status ou retornar resultados (por exemplo, 404 Não Encontrado).Shouldn't attempt to set status codes or return results (for example, 404 Not Found). 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.If model binding fails, an action filter or logic within the action method itself should handle the failure.
  • São muito úteis para eliminar código repetitivo e interesses paralelos de métodos de ação.Are most useful for eliminating repetitive code and cross-cutting concerns from action methods.
  • Normalmente, não devem ser usados para converter uma cadeia de caracteres em um tipo personalizado; um TypeConverter geralmente é uma opção melhor.Typically shouldn't be used to convert a string into a custom type, a TypeConverter is usually a better option.