Partilhar via


Vinculação de modelo personalizado no ASP.NET Core

Por Kirk Larkin

A vinculação de modelo permite que as ações do controlador trabalhem diretamente com tipos de modelo (passados como argumentos de método), em vez de solicitações HTTP. O mapeamento entre dados de solicitação de entrada e modelos de aplicativo é tratado por associadores de modelo. Os desenvolvedores podem estender a funcionalidade de vinculação de modelo interna implementando fichários de modelo personalizados (embora, normalmente, você não precise escrever seu próprio provedor).

Visualizar ou descarregar amostra de código (como descarregar)

Limitações do fichário de modelo padrão

Os fichários de modelo padrão suportam a maioria dos tipos de dados comuns do .NET Core e devem atender às necessidades da maioria dos desenvolvedores. Eles esperam vincular a entrada baseada em texto da solicitação diretamente aos tipos de modelo. Talvez seja necessário transformar a entrada antes de vinculá-la. Por exemplo, quando você tem uma chave que pode ser usada para pesquisar dados do modelo. Você pode usar um fichário de modelo personalizado para buscar dados com base na chave.

Tipos simples e complexos de vinculação de modelo

A vinculação de modelo usa definições específicas para os tipos em que opera. Um tipo simples é convertido a partir de uma única cadeia de caracteres usando TypeConverter ou um TryParse método. Um tipo complexo é convertido a partir de vários valores de entrada. O quadro determina a diferença com base na existência de um TypeConverter ou TryParse. Recomendamos criar um conversor de tipo ou usar TryParse para uma conversão string para SomeType que não exija recursos externos ou múltiplas entradas.

Consulte Tipos Simples para obter uma lista de tipos que o associador de modelos pode converter de strings.

Antes de criar seu próprio fichário de modelo personalizado, vale a pena revisar como os fichários de modelo 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 geralmente são armazenadas como arquivos ou campos BLOB de 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. O exemplo inclui uma imagem como uma cadeia de caracteres codificada em base64 no Base64String.txt.

ASP.NET Core MVC pode pegar uma cadeia de caracteres codificada em base64 e usar a ByteArrayModelBinder para convertê-la em uma matriz de bytes. Os ByteArrayModelBinderProvider mapeiam os 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 fichário de modelo personalizado, você pode implementar seu próprio IModelBinderProvider tipo ou usar o ModelBinderAttribute.

O exemplo a seguir 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 quiser ver os comentários de código traduzidos para outros idiomas além do inglês, avise-nos nesta discussão do GitHub .

Você pode POSTAR uma cadeia de caracteres codificada em base64 para o método api anterior usando uma ferramenta como curl.

Contanto que o associador possa vincular dados da solicitação a propriedades ou argumentos corretamente nomeados, a vinculação de modelo será bem-sucedida. O exemplo a seguir 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; }
}

Exemplo de vinculador de modelo personalizado

Nesta seção, implementaremos um fichário de modelo 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 argumento para o método de ação.

O exemplo seguinte utiliza 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 vincular parâmetros de ação Author.

A seguinte classe AuthorEntityBinder associa um Author parâmetro ao buscar a entidade de uma fonte de dados com 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;
    }
}

Note

A classe anterior AuthorEntityBinder destina-se a ilustrar um fichário de modelo personalizado. A classe não se destina a ilustrar as práticas recomendadas para um cenário de pesquisa. Para pesquisa, associe o authorId e consulte o banco de dados num método de ação. Essa abordagem separa falhas de vinculação de modelo de NotFound casos.

O código a seguir mostra como usar o método AuthorEntityBinder num método de ação:

[HttpGet("get/{author}")]
public IActionResult Get(Author author)
{
    if (author == null)
    {
        return NotFound();
    }

    return Ok(author);
}

O ModelBinder atributo pode ser usado para aplicar o AuthorEntityBinder a 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 padrão authorId, ele é especificado no parâmetro usando o ModelBinder atributo. Tanto o controlador quanto o método de ação são simplificados em comparação com a pesquisa da entidade no método de ação. A lógica para buscar o autor usando o Entity Framework Core é movida para o vinculador de modelos. Isso pode ser uma simplificação considerável quando você tem vários métodos que se ligam ao Author modelo.

Você pode aplicar 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 determinado ligador de modelo ou nome de modelo para esse tipo ou ação.

Implementação de um ModelBinderProvider

Em vez de aplicar um atributo, pode implementar IModelBinderProvider. É assim que os vinculadores de estrutura internos são implementados. Ao especificar o tipo no qual o controlador opera, você está a definir o tipo de argumento que ele produz, não a entrada que o controlador aceita. O seguinte provedor de fichário funciona com o AuthorEntityBinder. Quando isto é adicionado à coleção de fornecedores do MVC, não precisa usar o atributo ModelBinder nos parâmetros Author ou do 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;
        }
    }
}

Nota: O código anterior retorna um BinderTypeModelBinder. BinderTypeModelBinder atua como uma fábrica para acopladores de modelos e fornece injeção de dependência (DI). O AuthorEntityBinder DI requer acesso .EF Core Use BinderTypeModelBinder se o seu fichário de modelo requer serviços da DI.

Para usar um provedor de vinculador de modelo personalizado, adicione-o em ConfigureServices:

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

    services.AddControllers(options =>
    {
        options.ModelBinderProviders.Insert(0, new AuthorEntityBinderProvider());
    });
}

Ao avaliar fichários de modelo, a coleção de provedores é examinada em ordem. O primeiro provedor que retorna um fichário que corresponde ao modelo de entrada é usado. Adicionar o seu fornecedor ao final da coleção pode, assim, resultar em um agregador de modelos incorporado ser chamado antes que o seu agregador personalizado tenha a oportunidade. Neste exemplo, o provedor personalizado é adicionado ao início da coleção para garantir que ele seja sempre usado para Author argumentos de ação.

Ligação de modelo polimórfico

A ligação a diferentes modelos de tipos derivados é conhecida como ligação de modelo polimórfico. A vinculação de modelo personalizado polimórfico é necessária quando o valor da solicitação deve ser vinculado ao tipo de modelo derivado específico. Ligação de modelo polimórfico:

  • Não é típico de uma REST API projetada para interoperar com todos os idiomas.
  • Torna o raciocínio sobre os modelos vinculados difícil.

No entanto, se um aplicativo exigir associação de modelo polimórfico, uma implementação poderá se parecer com o 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 deve tentar definir códigos de status ou retornar resultados (por exemplo, 404 Não encontrado). Se a vinculação de modelo falhar, um filtro de ação ou lógica dentro do próprio método de ação deverá lidar com a falha.
  • São mais úteis para eliminar códigos repetitivos e preocupações transversais dos métodos de ação.
  • Normalmente, não deve ser usado para converter uma cadeia de caracteres em um tipo personalizado, geralmente é TypeConverter uma opção melhor.

Por Steve Smith

A vinculação de modelo permite que as ações do controlador trabalhem diretamente com tipos de modelo (passados como argumentos de método), em vez de solicitações HTTP. O mapeamento entre dados de solicitação de entrada e modelos de aplicativo é tratado por associadores de modelo. Os desenvolvedores podem estender a funcionalidade de vinculação de modelo interna implementando fichários de modelo personalizados (embora, normalmente, você não precise escrever seu próprio provedor).

Visualizar ou descarregar amostra de código (como descarregar)

Limitações do fichário de modelo padrão

Os fichários de modelo padrão suportam a maioria dos tipos de dados comuns do .NET Core e devem atender às necessidades da maioria dos desenvolvedores. Eles esperam vincular a entrada baseada em texto da solicitação diretamente aos tipos de modelo. Talvez seja necessário transformar a entrada antes de vinculá-la. Por exemplo, quando você tem uma chave que pode ser usada para pesquisar dados do modelo. Você pode usar um fichário de modelo personalizado para buscar dados com base na chave.

Revisão da vinculação do modelo

A vinculação de modelo usa definições específicas para os tipos em que opera. Um tipo simples é convertido a partir de uma única cadeia de caracteres na entrada. Um tipo complexo é convertido a partir de vários valores de entrada. O quadro determina a diferença com base na existência de um TypeConverter. Recomendamos que se crie um conversor de tipos se tiver um mapeamento simples string ->SomeType que não requeira recursos externos.

Antes de criar seu próprio fichário de modelo personalizado, vale a pena revisar como os fichários de modelo 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 geralmente são armazenadas como arquivos ou campos BLOB de 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. O exemplo inclui uma imagem como uma cadeia de caracteres codificada em base64 no Base64String.txt.

ASP.NET Core MVC pode pegar uma cadeia de caracteres codificada em base64 e usar a ByteArrayModelBinder para convertê-la em uma matriz de bytes. Os ByteArrayModelBinderProvider mapeiam os 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 fichário de modelo personalizado, você pode implementar seu próprio IModelBinderProvider tipo ou usar o ModelBinderAttribute.

O exemplo a seguir 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);
}

Você pode POSTAR uma cadeia de caracteres codificada em base64 para o método api anterior usando uma ferramenta como curl.

Contanto que o associador possa vincular dados da solicitação a propriedades ou argumentos corretamente nomeados, a vinculação de modelo será bem-sucedida. O exemplo a seguir 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; }
}

Exemplo de vinculador de modelo personalizado

Nesta seção, implementaremos um fichário de modelo 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 argumento para o método de ação.

O exemplo seguinte utiliza 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 vincular parâmetros de ação Author.

A seguinte classe AuthorEntityBinder associa um Author parâmetro ao buscar a entidade de uma fonte de dados com 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;
    }
}

Note

A classe anterior AuthorEntityBinder destina-se a ilustrar um fichário de modelo personalizado. A classe não se destina a ilustrar as práticas recomendadas para um cenário de pesquisa. Para pesquisa, associe o authorId e consulte o banco de dados num método de ação. Essa abordagem separa falhas de vinculação de modelo de NotFound casos.

O código a seguir mostra como usar o método AuthorEntityBinder num método de ação:

[HttpGet("get/{author}")]
public IActionResult Get(Author author)
{
    if (author == null)
    {
        return NotFound();
    }
    
    return Ok(author);
}

O ModelBinder atributo pode ser usado para aplicar o AuthorEntityBinder a 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 padrão authorId, ele é especificado no parâmetro usando o ModelBinder atributo. Tanto o controlador quanto o método de ação são simplificados em comparação com a pesquisa da entidade no método de ação. A lógica para buscar o autor usando o Entity Framework Core é movida para o vinculador de modelos. Isso pode ser uma simplificação considerável quando você tem vários métodos que se ligam ao Author modelo.

Você pode aplicar 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 determinado ligador de modelo ou nome de modelo para esse tipo ou ação.

Implementação de um ModelBinderProvider

Em vez de aplicar um atributo, pode implementar IModelBinderProvider. É assim que os vinculadores de estrutura internos são implementados. Ao especificar o tipo no qual o controlador opera, você está a definir o tipo de argumento que ele produz, não a entrada que o controlador aceita. O seguinte provedor de fichário funciona com o AuthorEntityBinder. Quando isto é adicionado à coleção de fornecedores do MVC, não precisa usar o atributo ModelBinder nos parâmetros Author ou do 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;
        }
    }
}

Nota: O código anterior retorna um BinderTypeModelBinder. BinderTypeModelBinder atua como uma fábrica para acopladores de modelos e fornece injeção de dependência (DI). O AuthorEntityBinder DI requer acesso .EF Core Use BinderTypeModelBinder se o seu fichário de modelo requer serviços da DI.

Para usar um provedor de vinculador de modelo personalizado, adicione-o em 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 fichários de modelo, a coleção de provedores é examinada em ordem. O primeiro provedor que retorna um fichário é usado. Adicionar o seu provedor ao final da coleção pode resultar em um associador de modelo interno ser chamado antes que o seu associador personalizado tenha uma oportunidade. Neste exemplo, o provedor personalizado é adicionado ao início da coleção para que seja usado para os argumentos de ação Author.

Ligação de modelo polimórfico

A ligação a diferentes modelos de tipos derivados é conhecida como ligação de modelo polimórfico. A vinculação de modelo personalizado polimórfico é necessária quando o valor da solicitação deve ser vinculado ao tipo de modelo derivado específico. Ligação de modelo polimórfico:

  • Não é típico de uma REST API projetada para interoperar com todos os idiomas.
  • Torna o raciocínio sobre os modelos vinculados difícil.

No entanto, se um aplicativo exigir associação de modelo polimórfico, uma implementação poderá se parecer com o 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 deve tentar definir códigos de status ou retornar resultados (por exemplo, 404 Não encontrado). Se a vinculação de modelo falhar, um filtro de ação ou lógica dentro do próprio método de ação deverá lidar com a falha.
  • São mais úteis para eliminar códigos repetitivos e preocupações transversais dos métodos de ação.
  • Normalmente, não deve ser usado para converter uma cadeia de caracteres em um tipo personalizado, geralmente é TypeConverter uma opção melhor.