Model binding no ASP.NET Core

Observação

Esta não é a versão mais recente deste artigo. Para informações sobre a versão vigente, confira a Versão do .NET 8 deste artigo.

Importante

Essas informações relacionam-se ao produto de pré-lançamento, que poderá ser substancialmente modificado antes do lançamento comercial. A Microsoft não oferece nenhuma garantia, explícita ou implícita, quanto às informações fornecidas aqui.

Para informações sobre a versão vigente, confira a Versão do .NET 8 deste artigo.

Este artigo explica o que é model binding, como ele funciona e como personalizar seu comportamento.

O que é o model binding

Controladores e páginas Razor funcionam com os dados provenientes de solicitações HTTP. Por exemplo, dados de rota podem fornecer uma chave de registro e campos de formulário postados podem fornecer valores para as propriedades do modelo. Escrever código para recuperar cada um desses valores e convertê-los de cadeias de caracteres em tipos .NET seria uma tarefa entediante e propensa a erro. O model binding automatiza esse processo. O sistema de model binding:

  • Recupera dados de várias fontes, como dados de rota, campos de formulário e cadeias de caracteres de consulta.
  • Fornece os dados a controladores e páginas Razor em parâmetros de método e propriedades públicas.
  • Converte dados de cadeia de caracteres em tipos .NET.
  • Atualiza as propriedades de tipos complexos.

Exemplo

Suponha que você tenha o seguinte método de ação:

[HttpGet("{id}")]
public ActionResult<Pet> GetById(int id, bool dogsOnly)

E o aplicativo receba uma solicitação com esta URL:

https://contoso.com/api/pets/2?DogsOnly=true

O model binding passa pelas etapas a seguir depois que o sistema de roteamento seleciona o método de ação:

  • Localiza o primeiro parâmetro de GetById, um número inteiro denominado id.
  • Examina as fontes disponíveis na solicitação HTTP e localiza id = "2" em dados de rota.
  • Converte a cadeia de caracteres "2" em inteiro 2.
  • Localiza o próximo parâmetro de GetById, um booliano chamado dogsOnly.
  • Examina as fontes e localiza "DogsOnly=true" na cadeia de consulta. A correspondência de nomes não diferencia maiúsculas de minúsculas.
  • Converte a cadeia de caracteres "true" no booliano true.

A estrutura então chama o método GetById, passando 2 para o parâmetro id e true para o parâmetro dogsOnly.

No exemplo anterior, os destinos do model binding são parâmetros de método que são tipos simples. Destinos também podem ser as propriedades de um tipo complexo. Depois de cada propriedade ser associada com êxito, a validação do modelo ocorre para essa propriedade. O registro de quais dados estão associados ao modelo, além de quaisquer erros de validação ou de associação, é armazenado em ControllerBase.ModelState ou PageModel.ModelState. Para descobrir se esse processo foi bem-sucedido, o aplicativo verifica o sinalizador ModelState.IsValid.

Destinos

O model binding tenta encontrar valores para os seguintes tipos de destinos:

  • Parâmetros do método de ação do controlador para o qual uma solicitação é roteada.
  • Os parâmetros do método do manipulador de páginas Razor para o qual uma solicitação é roteada.
  • Propriedades públicas de um controlador ou classe PageModel, se especificadas por atributos.

Atributo [BindProperty]

Pode ser aplicado a uma propriedade pública de um controlador ou classe PageModel para fazer o model binding ter essa propriedade como destino:

public class EditModel : PageModel
{
    [BindProperty]
    public Instructor? Instructor { get; set; }

    // ...
}

Atributo [BindProperties]

Pode ser aplicado a um controlador ou classe PageModel para informar o model binding para ter todas as propriedades públicas da classe como destino:

[BindProperties]
public class CreateModel : PageModel
{
    public Instructor? Instructor { get; set; }

    // ...
}

Model binding para solicitações HTTP GET

Por padrão, as propriedades não são vinculadas para solicitações HTTP GET. Normalmente, tudo o que você precisa para uma solicitação GET é um parâmetro de ID de registro. A ID do registro é usada para pesquisar o item no banco de dados. Portanto, não é necessário associar uma propriedade que contém uma instância do modelo. Em cenários em que você deseja associar propriedades a dados de solicitações GET, defina a propriedade SupportsGet como true:

[BindProperty(Name = "ai_user", SupportsGet = true)]
public string? ApplicationInsightsCookie { get; set; }

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.

Origens

Por padrão, o model binding obtém dados na forma de pares chave-valor das seguintes fontes em uma solicitação HTTP:

  1. Campos de formulário
  2. O corpo da solicitação (para controladores que têm o atributo [ApiController]).
  3. Rotear dados
  4. Parâmetros de cadeia de caracteres de consulta
  5. Arquivos carregados

Para cada propriedade ou parâmetro de destino, as fontes são verificadas na ordem indicada na lista anterior. Há algumas exceções:

  • Os valores de cadeia de caracteres de consulta e dados de rota são usados apenas para tipos simples.
  • Arquivos carregados são associados apenas a tipos de destino que implementam IFormFile ou IEnumerable<IFormFile>.

Se a origem padrão não estiver correta, use um dos seguintes atributos para especificar a origem:

  • [FromQuery] – obtém valores da cadeia de caracteres de consulta.
  • [FromRoute] – obtém valores dos dados de rota.
  • [FromForm] – obtém valores de campos de formulário postados.
  • [FromBody] – obtém valores do corpo da solicitação.
  • [FromHeader] – obtém valores de cabeçalhos HTTP.

Esses atributos:

  • São adicionados às propriedades de modelo individualmente, e não à classe de modelo, como no exemplo a seguir:

    public class Instructor
    {
        public int Id { get; set; }
    
        [FromQuery(Name = "Note")]
        public string? NoteFromQueryString { get; set; }
    
        // ...
    }
    
  • Opcionalmente, aceite um valor de nome de modelo no construtor. Essa opção é fornecida caso o nome da propriedade não corresponda ao valor na solicitação. Por exemplo, o valor na solicitação pode ser um cabeçalho com um hífen em seu nome, como no exemplo a seguir:

    public void OnGet([FromHeader(Name = "Accept-Language")] string language)
    

Atributo [FromBody]

Aplique o atributo [FromBody] a um parâmetro para preencher suas propriedades do corpo de uma solicitação HTTP. O runtime do ASP.NET Core delega a responsabilidade de ler o corpo a um formatador de entrada. O formatadores de entrada são explicados posteriormente neste artigo.

Quando [FromBody] é aplicado a um parâmetro de tipo complexo, todos os atributos de origem de associação aplicados às suas propriedades são ignorados. Por exemplo, a ação Create a seguir especifica que seu parâmetro pet é preenchido do corpo:

public ActionResult<Pet> Create([FromBody] Pet pet)

A classe Pet especifica que sua propriedade Breed é preenchida a partir de um parâmetro de cadeia de caracteres de consulta:

public class Pet
{
    public string Name { get; set; } = null!;

    [FromQuery] // Attribute is ignored.
    public string Breed { get; set; } = null!;
}

No exemplo anterior:

  • O atributo [FromQuery] é ignorado.
  • A propriedade Breed não é preenchida de um parâmetro de cadeia de caracteres de consulta.

Os formatadores de entrada leem apenas o corpo e não entendem os atributos de origem da associação. Se um valor adequado for encontrado no corpo, esse valor será usado para preencher a propriedade Breed.

Não aplique [FromBody] a mais de um parâmetro por método de ação. Depois que o fluxo da solicitação é lido por um formatador de entrada, ele não fica mais disponível para ser lido novamente para associar outros parâmetros [FromBody].

Fontes adicionais

Os dados de origem são fornecidos ao sistema de model binding pelos provedores de valor. Você pode escrever e registrar provedores de valor personalizados que obtêm dados para model binding de outras fontes. Por exemplo, talvez você queira dados de cookies ou estado de sessão. Para obter dados de uma nova fonte:

  • Crie uma classe que implementa IValueProvider.
  • Crie uma classe que implementa IValueProviderFactory.
  • Registre a classe de alocador em Program.cs.

A amostra inclui um provedor de valor e um exemplo de alocador que obtém valores de cookies. Registre fábricas de provedores de valor personalizado em Program.cs:

builder.Services.AddControllers(options =>
{
    options.ValueProviderFactories.Add(new CookieValueProviderFactory());
});

O código anterior coloca o provedor de valor personalizado depois de todos os provedores de valor internos. Para torná-lo o primeiro na lista, chame Insert(0, new CookieValueProviderFactory()), em vez de Add.

Nenhuma fonte para uma propriedade de modelo

Por padrão, um erro de estado de modelo não será criado se nenhum valor for encontrado para uma propriedade de modelo. A propriedade é definida como null ou um valor padrão:

  • Tipos simples anuláveis são definidos como null.
  • Tipos de valor não anuláveis são definidos como default(T). Por exemplo, um parâmetro int id é definido como 0.
  • Para tipos complexos, o model binding cria uma instância usando o construtor padrão sem definir propriedades.
  • As matrizes são definidas como Array.Empty<T>(), exceto que matrizes byte[] são definidas como null.

Se o estado do modelo precisar ser invalidado quando nada for encontrado em campos de formulário para uma propriedade de modelo, use o atributo [BindRequired].

Observe que este comportamento [BindRequired] se aplica ao model binding de dados de formulário postados, não a dados JSON ou XML em um corpo da solicitação. Dados do corpo da solicitação são tratados pelos formatadores de entrada.

Erros de conversão de tipos

Se uma fonte for encontrada, mas não puder ser convertida no tipo de destino, o estado do modelo será sinalizado como inválido. O parâmetro ou a propriedade de destino é definido como nulo ou um valor padrão, conforme observado na seção anterior.

Em um controlador de API que tem o atributo [ApiController], um estado de modelo inválido resulta em uma resposta HTTP 400 automática.

Em uma página Razor, exiba novamente a página com uma mensagem de erro:

public IActionResult OnPost()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    // ...

    return RedirectToPage("./Index");
}

Quando a página é exibida novamente pelo código anterior, a entrada inválida não é mostrada no campo de formulário. Isso ocorre porque a propriedade do modelo foi definida como nulo ou um valor padrão. A entrada inválida aparece em uma mensagem de erro. Se você quiser exibir novamente os dados inválidos no campo de formulário, considere tornar a propriedade do modelo uma cadeia de caracteres e fazer a conversão de dados manualmente.

A mesma estratégia será recomendada se você não desejar que erros de conversão de tipo resultem em erros de estado de modelo. Nesse caso, torne a propriedade de modelo uma cadeia de caracteres.

Tipos simples

Confira Model binding de tipos simples e complexos para obter uma explicação sobre tipos simples e complexos.

Os tipos simples em que o associador de modelos pode converter cadeias de caracteres de origem incluem os seguintes:

Associar a IParsable<T>.TryParse

A API IParsable<TSelf>.TryParse dá suporte a valores de parâmetro de ação do controlador de associação:

public static bool TryParse (string? s, IFormatProvider? provider, out TSelf result);

A seguinte classe DateRange implementa IParsable<TSelf> para dar suporte à associação de um intervalo de datas:

public class DateRange : IParsable<DateRange>
{
    public DateOnly? From { get; init; }
    public DateOnly? To { get; init; }

    public static DateRange Parse(string value, IFormatProvider? provider)
    {
        if (!TryParse(value, provider, out var result))
        {
           throw new ArgumentException("Could not parse supplied value.", nameof(value));
        }

        return result;
    }

    public static bool TryParse(string? value,
                                IFormatProvider? provider, out DateRange dateRange)
    {
        var segments = value?.Split(',', StringSplitOptions.RemoveEmptyEntries 
                                       | StringSplitOptions.TrimEntries);

        if (segments?.Length == 2
            && DateOnly.TryParse(segments[0], provider, out var fromDate)
            && DateOnly.TryParse(segments[1], provider, out var toDate))
        {
            dateRange = new DateRange { From = fromDate, To = toDate };
            return true;
        }

        dateRange = new DateRange { From = default, To = default };
        return false;
    }
}

O código anterior:

  • Converte uma cadeia de caracteres que representa duas datas em um objeto DateRange
  • O associador de modelo usa o método IParsable<TSelf>.TryParse para associar o DateRange.

A ação do controlador a seguir usa a classe DateRange para associar um intervalo de datas:

// GET /WeatherForecast/ByRange?range=7/24/2022,07/26/2022
public IActionResult ByRange([FromQuery] DateRange range)
{
    if (!ModelState.IsValid)
        return View("Error", ModelState.Values.SelectMany(v => v.Errors));

    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Where(wf => DateOnly.FromDateTime(wf.Date) >= range.From
                     && DateOnly.FromDateTime(wf.Date) <= range.To)
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d"),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int)(wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View("Index", weatherForecasts);
}

A classe Locale a seguir implementa IParsable<TSelf> para dar suporte à associação a CultureInfo:

public class Locale : CultureInfo, IParsable<Locale>
{
    public Locale(string culture) : base(culture)
    {
    }

    public static Locale Parse(string value, IFormatProvider? provider)
    {
        if (!TryParse(value, provider, out var result))
        {
           throw new ArgumentException("Could not parse supplied value.", nameof(value));
        }

        return result;
    }

    public static bool TryParse([NotNullWhen(true)] string? value,
                                IFormatProvider? provider, out Locale locale)
    {
        if (value is null)
        {
            locale = new Locale(CurrentCulture.Name);
            return false;
        }
        
        try
        {
            locale = new Locale(value);
            return true;
        }
        catch (CultureNotFoundException)
        {
            locale = new Locale(CurrentCulture.Name);
            return false;
        }
    }
}

A ação do controlador a seguir usa a classe Locale para associar uma cadeia de caracteres CultureInfo:

// GET /en-GB/WeatherForecast
public IActionResult Index([FromRoute] Locale locale)
{
    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d", locale),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int)(wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View(weatherForecasts);
}

A ação do controlador a seguir usa as classes DateRange e Locale para associar um intervalo de datas a CultureInfo:

// GET /af-ZA/WeatherForecast/RangeByLocale?range=2022-07-24,2022-07-29
public IActionResult RangeByLocale([FromRoute] Locale locale, [FromQuery] string range)
{
    if (!ModelState.IsValid)
        return View("Error", ModelState.Values.SelectMany(v => v.Errors));

    if (!DateRange.TryParse(range, locale, out DateRange rangeResult))
    {
        ModelState.TryAddModelError(nameof(range),
            $"Invalid date range: {range} for locale {locale.DisplayName}");

        return View("Error", ModelState.Values.SelectMany(v => v.Errors));
    }

    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Where(wf => DateOnly.FromDateTime(wf.Date) >= rangeResult.From
                     && DateOnly.FromDateTime(wf.Date) <= rangeResult.To)
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d", locale),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int) (wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View("Index", weatherForecasts);
}

O aplicativo de exemplo de API no GitHub mostra o exemplo anterior para um controlador de API.

Associar a TryParse

A API TryParse dá suporte a valores de parâmetro de ação do controlador de associação:

public static bool TryParse(string value, T out result);
public static bool TryParse(string value, IFormatProvider provider, T out result);

IParsable<T>.TryParse é a abordagem recomendada para associação de parâmetros porque, ao contrário de TryParse, ela não depende da reflexão.

A classe TryParse a seguir implementa DateRangeTP:

public class DateRangeTP
{
    public DateOnly? From { get; }
    public DateOnly? To { get; }

    public DateRangeTP(string from, string to)
    {
        if (string.IsNullOrEmpty(from))
            throw new ArgumentNullException(nameof(from));
        if (string.IsNullOrEmpty(to))
            throw new ArgumentNullException(nameof(to));

        From = DateOnly.Parse(from);
        To = DateOnly.Parse(to);
    }

    public static bool TryParse(string? value, out DateRangeTP? result)
    {
        var range = value?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
        if (range?.Length != 2)
        {
            result = default;
            return false;
        }

        result = new DateRangeTP(range[0], range[1]);
        return true;
    }
}

A ação do controlador a seguir usa a classe DateRangeTP para associar um intervalo de datas:

// GET /WeatherForecast/ByRangeTP?range=7/24/2022,07/26/2022
public IActionResult ByRangeTP([FromQuery] DateRangeTP range)
{
    if (!ModelState.IsValid)
        return View("Error", ModelState.Values.SelectMany(v => v.Errors));

    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Where(wf => DateOnly.FromDateTime(wf.Date) >= range.From
                     && DateOnly.FromDateTime(wf.Date) <= range.To)
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d"),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int)(wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View("Index", weatherForecasts);
}

Tipos complexos

Um tipo complexo deve ter um construtor padrão público e propriedades públicas graváveis para associar. Quando ocorre model binding, a classe é instanciada usando o construtor padrão público.

Para cada propriedade do tipo complexo, o model binding examina as fontes em busca do nome padrãoprefix.property_name. Se nada for encontrado, ela procurará apenas property_name sem o prefixo. A decisão de usar o prefixo não é tomada por propriedade. Por exemplo, com uma consulta que contém ?Instructor.Id=100&Name=foo, associada ao método OnGet(Instructor instructor), o objeto resultante do tipo Instructor contém:

  • Id definido como 100.
  • Name definido como null. A associação de modelo espera Instructor.Name porque Instructor.Id foi usado no parâmetro de consulta anterior.

Para associação a um parâmetro, o prefixo é o nome do parâmetro. Para associação a uma propriedade pública PageModel, o prefixo é o nome da propriedade pública. Alguns atributos têm uma propriedade Prefix que permite substituir o uso padrão do nome da propriedade ou do parâmetro.

Por exemplo, suponha que o tipo complexo seja a seguinte classe Instructor:

public class Instructor
{
    public int ID { get; set; }
    public string LastName { get; set; }
    public string FirstName { get; set; }
}

Prefixo = nome do parâmetro

Se o modelo a ser associado for um parâmetro chamado instructorToUpdate:

public IActionResult OnPost(int? id, Instructor instructorToUpdate)

O model binding começa examinando as fontes para a chave instructorToUpdate.ID. Se ela não for encontrada, procurará ID sem um prefixo.

Prefixo = nome da propriedade

Se o modelo a ser associado for uma propriedade chamada Instructor do controlador ou da classe PageModel:

[BindProperty]
public Instructor Instructor { get; set; }

O model binding começa examinando as fontes para a chave Instructor.ID. Se ela não for encontrada, procurará ID sem um prefixo.

Prefixo personalizado

Se o modelo a ser associado for um parâmetro denominado instructorToUpdate e um atributo Bind especificar Instructor como o prefixo:

public IActionResult OnPost(
    int? id, [Bind(Prefix = "Instructor")] Instructor instructorToUpdate)

O model binding começa examinando as fontes para a chave Instructor.ID. Se ela não for encontrada, procurará ID sem um prefixo.

Atributos para destinos de tipo complexo

Vários atributos internos estão disponíveis para controlar o model binding de tipos complexos:

Aviso

Esses atributos afetam o model binding quando os dados de formulário postados são a origem dos valores. Eles não afetam os formatadores de entrada, que processam corpos de solicitação XML e JSON postados. O formatadores de entrada são explicados posteriormente neste artigo.

Atributo [Bind]

Pode ser aplicado a uma classe ou a um parâmetro de método. Especifica quais propriedades de um modelo devem ser incluídas no model binding. [Bind]não afeta formatadores de entrada.

No exemplo a seguir, somente as propriedades especificadas do modelo Instructor são associadas quando qualquer método de ação ou o manipulador é chamado:

[Bind("LastName,FirstMidName,HireDate")]
public class Instructor

No exemplo a seguir, somente as propriedades especificadas do modelo Instructor são associadas quando o método OnPost é chamado:

[HttpPost]
public IActionResult OnPost(
    [Bind("LastName,FirstMidName,HireDate")] Instructor instructor)

O atributo [Bind] pode ser usado para proteção contra o excesso de postagem cenários de criar. Ele não funciona bem em cenários de edição, pois as propriedades excluídas são definidas como nulas ou um valor padrão, em vez de serem deixadas inalteradas. Para defesa contra o excesso de postagem, são recomendados modelos de exibição, em vez do atributo [Bind]. Para obter mais informações, veja Observação de segurança sobre o excesso de postagem.

Atributo [ModelBinder]

ModelBinderAttribute pode ser aplicado a tipos, propriedades ou parâmetros. Ele permite especificar o tipo de associador de modelo usado para associar a instância ou o tipo específico. Por exemplo:

[HttpPost]
public IActionResult OnPost(
    [ModelBinder<MyInstructorModelBinder>] Instructor instructor)

O atributo [ModelBinder] também pode ser usado para alterar o nome de uma propriedade ou parâmetro quando está sendo associado ao modelo:

public class Instructor
{
    [ModelBinder(Name = "instructor_id")]
    public string Id { get; set; }

    // ...
}

Atributo [BindRequired]

Faz com que o model binding adicione um erro de estado de modelo se a associação não puder ocorrer para a propriedade de um modelo. Veja um exemplo:

public class InstructorBindRequired
{
    // ...

    [BindRequired]
    public DateTime HireDate { get; set; }
}

Veja também a discussão sobre o atributo [Required] em Validação do modelo.

Atributo [BindNever]

Pode ser aplicado a uma propriedade ou um tipo. Impede que o model binding configure a propriedade de um modelo. Quando aplicado a um tipo, o sistema de associação de modelo exclui todas as propriedades definidas pelo tipo. Veja um exemplo:

public class InstructorBindNever
{
    [BindNever]
    public int Id { get; set; }

    // ...
}

Coleções

Para destinos que são coleções de tipos simples, o model binding procura correspondências para parameter_name ou property_name. Se nenhuma correspondência for encontrada, procurará um dos formatos compatíveis sem o prefixo. Por exemplo:

  • Suponha que o parâmetro a ser associado seja uma matriz chamada selectedCourses:

    public IActionResult OnPost(int? id, int[] selectedCourses)
    
  • Dados de cadeia de caracteres de consulta ou formulário podem estar em um dos seguintes formatos:

    selectedCourses=1050&selectedCourses=2000 
    
    selectedCourses[0]=1050&selectedCourses[1]=2000
    
    [0]=1050&[1]=2000
    
    selectedCourses[a]=1050&selectedCourses[b]=2000&selectedCourses.index=a&selectedCourses.index=b
    
    [a]=1050&[b]=2000&index=a&index=b
    

    Evite associar um parâmetro ou uma propriedade chamada index ou Index se estes forem adjacentes a um valor de coleção. A associação de modelo tenta usar index como o índice para a coleção, o que pode resultar em associação incorreta. Por exemplo, considere a seguinte ação.

    public IActionResult Post(string index, List<Product> products)
    

    No código anterior, o parâmetro de cadeia de caracteres de consulta index associa-se ao parâmetro de método index e também é usado para associar a coleção de produtos. Renomear o parâmetro index ou usar um atributo de associação de modelo para configurar a associação evita esse problema:

    public IActionResult Post(string productIndex, List<Product> products)
    
  • O formato a seguir é compatível apenas com os dados de formulário:

    selectedCourses[]=1050&selectedCourses[]=2000
    
  • Para todos os formatos do exemplo anterior, o model binding passa uma matriz de dois itens para o parâmetro selectedCourses:

    • selectedCourses[0]=1050
    • selectedCourses[1]=2000

    Formatos de dados que usam números subscritos (... [0]... [1]...) devem garantir que eles estejam numerados em sequência, começando com zero. Se houver quaisquer intervalos na numeração de subscrito, todos os itens após o intervalo serão ignorados. Por exemplo, se os subscritos forem 0 e 2, em vez de 0 e 1, o segundo item será ignorado.

Dicionários

Para destinos Dictionary, o model binding procura correspondências para parameter_name ou property_name. Se nenhuma correspondência for encontrada, procurará um dos formatos compatíveis sem o prefixo. Por exemplo:

  • Suponha que o parâmetro de destino seja um Dictionary<int, string> chamado selectedCourses:

    public IActionResult OnPost(int? id, Dictionary<int, string> selectedCourses)
    
  • Os dados de cadeia de caracteres de consulta ou formulário postados podem se parecer com um dos exemplos a seguir:

    selectedCourses[1050]=Chemistry&selectedCourses[2000]=Economics
    
    [1050]=Chemistry&selectedCourses[2000]=Economics
    
    selectedCourses[0].Key=1050&selectedCourses[0].Value=Chemistry&
    selectedCourses[1].Key=2000&selectedCourses[1].Value=Economics
    
    [0].Key=1050&[0].Value=Chemistry&[1].Key=2000&[1].Value=Economics
    
  • Para todos os formatos do exemplo anterior, o model binding passa um dicionário de dois itens para o parâmetro selectedCourses:

    • selectedCourses["1050"]="Chemistry"
    • selectedCourses["2000"]="Economics"

Associação de construtor e tipos de registro

A associação de modelo requer que tipos complexos tenham um construtor sem parâmetros. Os formatadores de entrada baseados em System.Text.Json e Newtonsoft.Json dão suporte à desserialização de classes que não têm um construtor sem parâmetros.

Os tipos de registro são uma ótima maneira de representar de forma sucinta os dados pela rede. O ASP.NET Core dá suporte à associação de modelo e à validação de tipos de registro com um único construtor:

public record Person(
    [Required] string Name, [Range(0, 150)] int Age, [BindNever] int Id);

public class PersonController
{
    public IActionResult Index() => View();

    [HttpPost]
    public IActionResult Index(Person person)
    {
        // ...
    }
}

Person/Index.cshtml:

@model Person

Name: <input asp-for="Name" />
<br />
Age: <input asp-for="Age" />

Ao validar tipos de registro, o runtime pesquisa metadados de associação e validação especificamente em parâmetros e não em propriedades.

A estrutura permite associar e validar tipos de registro:

public record Person([Required] string Name, [Range(0, 100)] int Age);

Para que o anterior funcione, o tipo deve:

  • Ser um tipo de registro.
  • Ter exatamente um construtor público.
  • Conter parâmetros que tenham uma propriedade com o mesmo nome e tipo. Os nomes não devem ser diferentes por caso.

POCOs sem construtores sem parâmetros

POCOs que não têm construtores sem parâmetros não podem ser associados.

O código a seguir resulta em uma exceção informando que o tipo deve ter um construtor sem parâmetros:

public class Person(string Name)

public record Person([Required] string Name, [Range(0, 100)] int Age)
{
    public Person(string Name) : this (Name, 0);
}

Tipos de registro com construtores criados manualmente

Tipos de registro com construtores criados manualmente que se parecem com construtores primários funcionam

public record Person
{
    public Person([Required] string Name, [Range(0, 100)] int Age)
        => (this.Name, this.Age) = (Name, Age);

    public string Name { get; set; }
    public int Age { get; set; }
}

Tipos de registro, validação e metadados de associação

Para tipos de registro, os metadados de validação e associação em parâmetros são usados. Todos os metadados nas propriedades são ignorados

public record Person (string Name, int Age)
{
   [BindProperty(Name = "SomeName")] // This does not get used
   [Required] // This does not get used
   public string Name { get; init; }
}

Validação e metadados

A validação usa metadados no parâmetro, mas usa a propriedade para ler o valor. No caso comum com construtores primários, os dois seriam idênticos. No entanto, há maneiras de contornar isso:

public record Person([Required] string Name)
{
    private readonly string _name;

    // The following property is never null.
    // However this object could have been constructed as "new Person(null)".
    public string Name { get; init => _name = value ?? string.Empty; }
}

TryUpdateModel não atualiza parâmetros em um tipo de registro

public record Person(string Name)
{
    public int Age { get; set; }
}

var person = new Person("initial-name");
TryUpdateModel(person, ...);

Nesse caso, o MVC não tentará associar Name novamente. No entanto, Age pode ser atualizado

Comportamento de globalização de dados de rota de associação de modelo e cadeias de caracteres de consulta

O provedor de valor de rota ASP.NET Core e o provedor de valor da cadeia de caracteres de consulta:

  • Tratar valores como cultura invariável.
  • Espere que as URLs sejam invariáveis à cultura.

Por outro lado, os valores provenientes de dados de formulário passam por uma conversão sensível à cultura. Isso é esperado, para que as URLs sejam compartilháveis entre localidades.

Para que o provedor de valor de rota ASP.NET Core e o provedor de valor da cadeia de caracteres de consulta passem por uma conversão sensível à cultura:

public class CultureQueryStringValueProviderFactory : IValueProviderFactory
{
    public Task CreateValueProviderAsync(ValueProviderFactoryContext context)
    {
        _ = context ?? throw new ArgumentNullException(nameof(context));

        var query = context.ActionContext.HttpContext.Request.Query;
        if (query?.Count > 0)
        {
            context.ValueProviders.Add(
                new QueryStringValueProvider(
                    BindingSource.Query,
                    query,
                    CultureInfo.CurrentCulture));
        }

        return Task.CompletedTask;
    }
}
builder.Services.AddControllers(options =>
{
    var index = options.ValueProviderFactories.IndexOf(
        options.ValueProviderFactories.OfType<QueryStringValueProviderFactory>()
            .Single());

    options.ValueProviderFactories[index] =
        new CultureQueryStringValueProviderFactory();
});

Tipos de dados especiais

Há alguns tipos de dados especiais com o model binding pode lidar.

IFormFile e IFormFileCollection

Um arquivo carregado incluído na solicitação HTTP. Também compatível com IEnumerable<IFormFile> para vários arquivos.

CancellationToken

Opcionalmente, as ações podem associar um CancellationToken como um parâmetro. Isso associa RequestAborted que sinaliza quando a conexão subjacente à solicitação HTTP é anulada. As ações podem usar esse parâmetro para cancelar operações assíncronas de execução prolongada executadas como parte das ações do controlador.

FormCollection

Usado para recuperar todos os valores dos dados de formulário postados.

Formatadores de entrada

Dados no corpo da solicitação podem estar em JSON, XML ou algum outro formato. Para analisar esses dados, o model binding usa um formatador de entrada configurado para lidar com um determinado tipo de conteúdo. Por padrão, o ASP.NET Core inclui formatadores de entrada baseados em JSON para lidar com os dados JSON. Você pode adicionar outros formatadores para outros tipos de conteúdo.

O ASP.NET Core seleciona formatadores de entrada com base no atributo Consome. Se nenhum atributo estiver presente, ele usará o cabeçalho Content-Type.

Para usar os formatadores de entrada XML internos:

Personalizar a associação de modelo com formatadores de entrada

Um formatador de entrada assume total responsabilidade pela leitura de dados do corpo da solicitação. Para personalizar esse processo, configure as APIs usadas pelo formatador de entrada. Esta seção descreve como personalizar o formatador de entrada baseado em System.Text.Json para entender um tipo personalizado chamado ObjectId.

Considere o seguinte modelo, que contém uma propriedade personalizada ObjectId:

public class InstructorObjectId
{
    [Required]
    public ObjectId ObjectId { get; set; } = null!;
}

Para personalizar o processo de associação de modelo ao usar System.Text.Json, crie uma classe derivada de JsonConverter<T>:

internal class ObjectIdConverter : JsonConverter<ObjectId>
{
    public override ObjectId Read(
        ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        => new(JsonSerializer.Deserialize<int>(ref reader, options));

    public override void Write(
        Utf8JsonWriter writer, ObjectId value, JsonSerializerOptions options)
        => writer.WriteNumberValue(value.Id);
}

Para usar um conversor personalizado, aplique o atributo JsonConverterAttribute ao tipo. No exemplo a seguir, o tipo ObjectId é configurado com ObjectIdConverter como conversor personalizado:

[JsonConverter(typeof(ObjectIdConverter))]
public record ObjectId(int Id);

Para obter mais informações, confira Como gravar conversores personalizados.

Excluir tipos especificados do model binding

O comportamento do sistema de validação e model binding é orientado pelo ModelMetadata. Você pode personalizar ModelMetadata adicionando um provedor de detalhes MvcOptions.ModelMetadataDetailsProviders. Provedores de detalhes internos estão disponíveis para desabilitar o model binding ou a validação para tipos especificados.

Para desabilitar o model binding em todos os modelos de um tipo especificado, adicione um ExcludeBindingMetadataProvider em Program.cs. Por exemplo, para desabilitar o model binding em todos os modelos do tipo System.Version:

builder.Services.AddRazorPages()
    .AddMvcOptions(options =>
    {
        options.ModelMetadataDetailsProviders.Add(
            new ExcludeBindingMetadataProvider(typeof(Version)));
        options.ModelMetadataDetailsProviders.Add(
            new SuppressChildValidationMetadataProvider(typeof(Guid)));
    });

Para desabilitar a validação nas propriedades de um tipo especificado, adicione um SuppressChildValidationMetadataProvider em Program.cs. Por exemplo, para desabilitar a validação nas propriedades do tipo System.Guid:

builder.Services.AddRazorPages()
    .AddMvcOptions(options =>
    {
        options.ModelMetadataDetailsProviders.Add(
            new ExcludeBindingMetadataProvider(typeof(Version)));
        options.ModelMetadataDetailsProviders.Add(
            new SuppressChildValidationMetadataProvider(typeof(Guid)));
    });

Associadores de modelos personalizados

Você pode estender o model binding escrevendo um associador de modelos personalizado e usando o atributo [ModelBinder] para selecioná-la para um determinado destino. Saiba mais sobre o model binding personalizado.

Model binding manual

O model binding pode ser invocado manualmente usando o método TryUpdateModelAsync. O método é definido nas classes ControllerBase e PageModel. Sobrecargas de método permitem que você especifique o provedor de valor e prefixo a ser usado. O método retornará false se o model binding falhar. Veja um exemplo:

if (await TryUpdateModelAsync(
    newInstructor,
    "Instructor",
    x => x.Name, x => x.HireDate!))
{
    _instructorStore.Add(newInstructor);
    return RedirectToPage("./Index");
}

return Page();

TryUpdateModelAsync usa provedores de valor para obter dados do corpo do formulário, da cadeia de caracteres de consulta e dos dados de rota. TryUpdateModelAsync normalmente é:

  • Usado com páginas Razor e aplicativos MVC usando controladores e exibições para evitar o excesso de postagem.
  • Não usado com uma API Web, a menos que seja consumido de dados de formulário, cadeias de caracteres de consulta e dados de rota. Os pontos de extremidade da API Web que consomem JSON usam formatadores de entrada para desserializar o corpo da solicitação em um objeto.

Para obter mais informações, confira TryUpdateModelAsync.

Atributo [FromServices]

O nome do atributo segue o padrão dos atributos de model binding que especificam uma fonte de dados. Porém, não se trata de associar dados de um provedor de valor. Ele obtém uma instância de um tipo do contêiner de injeção de dependência. Sua finalidade é oferecer uma alternativa à injeção de construtor para quando você precisa de um serviço somente se um determinado método for chamado.

Se uma instância do tipo não estiver registrada no contêiner de injeção de dependência, o aplicativo gerará uma exceção ao tentar associar o parâmetro. Para tornar o parâmetro opcional, use uma das seguintes abordagens:

  • Torne o parâmetro anulável.
  • Defina um valor padrão para o parâmetro.

Para parâmetros anuláveis, verifique se o parâmetro não é null antes de acessá-lo.

Recursos adicionais

Este artigo explica o que é model binding, como ele funciona e como personalizar seu comportamento.

O que é o model binding

Controladores e páginas Razor funcionam com os dados provenientes de solicitações HTTP. Por exemplo, dados de rota podem fornecer uma chave de registro e campos de formulário postados podem fornecer valores para as propriedades do modelo. Escrever código para recuperar cada um desses valores e convertê-los de cadeias de caracteres em tipos .NET seria uma tarefa entediante e propensa a erro. O model binding automatiza esse processo. O sistema de model binding:

  • Recupera dados de várias fontes, como dados de rota, campos de formulário e cadeias de caracteres de consulta.
  • Fornece os dados a controladores e páginas Razor em parâmetros de método e propriedades públicas.
  • Converte dados de cadeia de caracteres em tipos .NET.
  • Atualiza as propriedades de tipos complexos.

Exemplo

Suponha que você tenha o seguinte método de ação:

[HttpGet("{id}")]
public ActionResult<Pet> GetById(int id, bool dogsOnly)

E o aplicativo receba uma solicitação com esta URL:

https://contoso.com/api/pets/2?DogsOnly=true

O model binding passa pelas etapas a seguir depois que o sistema de roteamento seleciona o método de ação:

  • Localiza o primeiro parâmetro de GetById, um número inteiro denominado id.
  • Examina as fontes disponíveis na solicitação HTTP e localiza id = "2" em dados de rota.
  • Converte a cadeia de caracteres "2" em inteiro 2.
  • Localiza o próximo parâmetro de GetById, um booliano chamado dogsOnly.
  • Examina as fontes e localiza "DogsOnly=true" na cadeia de consulta. A correspondência de nomes não diferencia maiúsculas de minúsculas.
  • Converte a cadeia de caracteres "true" no booliano true.

A estrutura então chama o método GetById, passando 2 para o parâmetro id e true para o parâmetro dogsOnly.

No exemplo anterior, os destinos do model binding são parâmetros de método que são tipos simples. Destinos também podem ser as propriedades de um tipo complexo. Depois de cada propriedade ser associada com êxito, a validação do modelo ocorre para essa propriedade. O registro de quais dados estão associados ao modelo, além de quaisquer erros de validação ou de associação, é armazenado em ControllerBase.ModelState ou PageModel.ModelState. Para descobrir se esse processo foi bem-sucedido, o aplicativo verifica o sinalizador ModelState.IsValid.

Destinos

O model binding tenta encontrar valores para os seguintes tipos de destinos:

  • Parâmetros do método de ação do controlador para o qual uma solicitação é roteada.
  • Os parâmetros do método do manipulador de páginas Razor para o qual uma solicitação é roteada.
  • Propriedades públicas de um controlador ou classe PageModel, se especificadas por atributos.

Atributo [BindProperty]

Pode ser aplicado a uma propriedade pública de um controlador ou classe PageModel para fazer o model binding ter essa propriedade como destino:

public class EditModel : PageModel
{
    [BindProperty]
    public Instructor? Instructor { get; set; }

    // ...
}

Atributo [BindProperties]

Pode ser aplicado a um controlador ou classe PageModel para informar o model binding para ter todas as propriedades públicas da classe como destino:

[BindProperties]
public class CreateModel : PageModel
{
    public Instructor? Instructor { get; set; }

    // ...
}

Model binding para solicitações HTTP GET

Por padrão, as propriedades não são vinculadas para solicitações HTTP GET. Normalmente, tudo o que você precisa para uma solicitação GET é um parâmetro de ID de registro. A ID do registro é usada para pesquisar o item no banco de dados. Portanto, não é necessário associar uma propriedade que contém uma instância do modelo. Em cenários em que você deseja associar propriedades a dados de solicitações GET, defina a propriedade SupportsGet como true:

[BindProperty(Name = "ai_user", SupportsGet = true)]
public string? ApplicationInsightsCookie { get; set; }

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.

Origens

Por padrão, o model binding obtém dados na forma de pares chave-valor das seguintes fontes em uma solicitação HTTP:

  1. Campos de formulário
  2. O corpo da solicitação (para controladores que têm o atributo [ApiController]).
  3. Rotear dados
  4. Parâmetros de cadeia de caracteres de consulta
  5. Arquivos carregados

Para cada propriedade ou parâmetro de destino, as fontes são verificadas na ordem indicada na lista anterior. Há algumas exceções:

  • Os valores de cadeia de caracteres de consulta e dados de rota são usados apenas para tipos simples.
  • Arquivos carregados são associados apenas a tipos de destino que implementam IFormFile ou IEnumerable<IFormFile>.

Se a origem padrão não estiver correta, use um dos seguintes atributos para especificar a origem:

  • [FromQuery] – obtém valores da cadeia de caracteres de consulta.
  • [FromRoute] – obtém valores dos dados de rota.
  • [FromForm] – obtém valores de campos de formulário postados.
  • [FromBody] – obtém valores do corpo da solicitação.
  • [FromHeader] – obtém valores de cabeçalhos HTTP.

Esses atributos:

  • São adicionados às propriedades de modelo individualmente, e não à classe de modelo, como no exemplo a seguir:

    public class Instructor
    {
        public int Id { get; set; }
    
        [FromQuery(Name = "Note")]
        public string? NoteFromQueryString { get; set; }
    
        // ...
    }
    
  • Opcionalmente, aceite um valor de nome de modelo no construtor. Essa opção é fornecida caso o nome da propriedade não corresponda ao valor na solicitação. Por exemplo, o valor na solicitação pode ser um cabeçalho com um hífen em seu nome, como no exemplo a seguir:

    public void OnGet([FromHeader(Name = "Accept-Language")] string language)
    

Atributo [FromBody]

Aplique o atributo [FromBody] a um parâmetro para preencher suas propriedades do corpo de uma solicitação HTTP. O runtime do ASP.NET Core delega a responsabilidade de ler o corpo a um formatador de entrada. O formatadores de entrada são explicados posteriormente neste artigo.

Quando [FromBody] é aplicado a um parâmetro de tipo complexo, todos os atributos de origem de associação aplicados às suas propriedades são ignorados. Por exemplo, a ação Create a seguir especifica que seu parâmetro pet é preenchido do corpo:

public ActionResult<Pet> Create([FromBody] Pet pet)

A classe Pet especifica que sua propriedade Breed é preenchida a partir de um parâmetro de cadeia de caracteres de consulta:

public class Pet
{
    public string Name { get; set; } = null!;

    [FromQuery] // Attribute is ignored.
    public string Breed { get; set; } = null!;
}

No exemplo anterior:

  • O atributo [FromQuery] é ignorado.
  • A propriedade Breed não é preenchida de um parâmetro de cadeia de caracteres de consulta.

Os formatadores de entrada leem apenas o corpo e não entendem os atributos de origem da associação. Se um valor adequado for encontrado no corpo, esse valor será usado para preencher a propriedade Breed.

Não aplique [FromBody] a mais de um parâmetro por método de ação. Depois que o fluxo da solicitação é lido por um formatador de entrada, ele não fica mais disponível para ser lido novamente para associar outros parâmetros [FromBody].

Fontes adicionais

Os dados de origem são fornecidos ao sistema de model binding pelos provedores de valor. Você pode escrever e registrar provedores de valor personalizados que obtêm dados para model binding de outras fontes. Por exemplo, talvez você queira dados de cookies ou estado de sessão. Para obter dados de uma nova fonte:

  • Crie uma classe que implementa IValueProvider.
  • Crie uma classe que implementa IValueProviderFactory.
  • Registre a classe de alocador em Program.cs.

A amostra inclui um provedor de valor e um exemplo de alocador que obtém valores de cookies. Registre fábricas de provedores de valor personalizado em Program.cs:

builder.Services.AddControllers(options =>
{
    options.ValueProviderFactories.Add(new CookieValueProviderFactory());
});

O código anterior coloca o provedor de valor personalizado depois de todos os provedores de valor internos. Para torná-lo o primeiro na lista, chame Insert(0, new CookieValueProviderFactory()), em vez de Add.

Nenhuma fonte para uma propriedade de modelo

Por padrão, um erro de estado de modelo não será criado se nenhum valor for encontrado para uma propriedade de modelo. A propriedade é definida como null ou um valor padrão:

  • Tipos simples anuláveis são definidos como null.
  • Tipos de valor não anuláveis são definidos como default(T). Por exemplo, um parâmetro int id é definido como 0.
  • Para tipos complexos, o model binding cria uma instância usando o construtor padrão sem definir propriedades.
  • As matrizes são definidas como Array.Empty<T>(), exceto que matrizes byte[] são definidas como null.

Se o estado do modelo precisar ser invalidado quando nada for encontrado em campos de formulário para uma propriedade de modelo, use o atributo [BindRequired].

Observe que este comportamento [BindRequired] se aplica ao model binding de dados de formulário postados, não a dados JSON ou XML em um corpo da solicitação. Dados do corpo da solicitação são tratados pelos formatadores de entrada.

Erros de conversão de tipos

Se uma fonte for encontrada, mas não puder ser convertida no tipo de destino, o estado do modelo será sinalizado como inválido. O parâmetro ou a propriedade de destino é definido como nulo ou um valor padrão, conforme observado na seção anterior.

Em um controlador de API que tem o atributo [ApiController], um estado de modelo inválido resulta em uma resposta HTTP 400 automática.

Em uma página Razor, exiba novamente a página com uma mensagem de erro:

public IActionResult OnPost()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    // ...

    return RedirectToPage("./Index");
}

Quando a página é exibida novamente pelo código anterior, a entrada inválida não é mostrada no campo de formulário. Isso ocorre porque a propriedade do modelo foi definida como nulo ou um valor padrão. A entrada inválida aparece em uma mensagem de erro. Se você quiser exibir novamente os dados inválidos no campo de formulário, considere tornar a propriedade do modelo uma cadeia de caracteres e fazer a conversão de dados manualmente.

A mesma estratégia será recomendada se você não desejar que erros de conversão de tipo resultem em erros de estado de modelo. Nesse caso, torne a propriedade de modelo uma cadeia de caracteres.

Tipos simples

Confira Model binding de tipos simples e complexos para obter uma explicação sobre tipos simples e complexos.

Os tipos simples em que o associador de modelos pode converter cadeias de caracteres de origem incluem os seguintes:

Associar a IParsable<T>.TryParse

A API IParsable<TSelf>.TryParse dá suporte a valores de parâmetro de ação do controlador de associação:

public static bool TryParse (string? s, IFormatProvider? provider, out TSelf result);

A seguinte classe DateRange implementa IParsable<TSelf> para dar suporte à associação de um intervalo de datas:

public class DateRange : IParsable<DateRange>
{
    public DateOnly? From { get; init; }
    public DateOnly? To { get; init; }

    public static DateRange Parse(string value, IFormatProvider? provider)
    {
        if (!TryParse(value, provider, out var result))
        {
           throw new ArgumentException("Could not parse supplied value.", nameof(value));
        }

        return result;
    }

    public static bool TryParse(string? value,
                                IFormatProvider? provider, out DateRange dateRange)
    {
        var segments = value?.Split(',', StringSplitOptions.RemoveEmptyEntries 
                                       | StringSplitOptions.TrimEntries);

        if (segments?.Length == 2
            && DateOnly.TryParse(segments[0], provider, out var fromDate)
            && DateOnly.TryParse(segments[1], provider, out var toDate))
        {
            dateRange = new DateRange { From = fromDate, To = toDate };
            return true;
        }

        dateRange = new DateRange { From = default, To = default };
        return false;
    }
}

O código anterior:

  • Converte uma cadeia de caracteres que representa duas datas em um objeto DateRange
  • O associador de modelo usa o método IParsable<TSelf>.TryParse para associar o DateRange.

A ação do controlador a seguir usa a classe DateRange para associar um intervalo de datas:

// GET /WeatherForecast/ByRange?range=7/24/2022,07/26/2022
public IActionResult ByRange([FromQuery] DateRange range)
{
    if (!ModelState.IsValid)
        return View("Error", ModelState.Values.SelectMany(v => v.Errors));

    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Where(wf => DateOnly.FromDateTime(wf.Date) >= range.From
                     && DateOnly.FromDateTime(wf.Date) <= range.To)
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d"),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int)(wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View("Index", weatherForecasts);
}

A classe Locale a seguir implementa IParsable<TSelf> para dar suporte à associação a CultureInfo:

public class Locale : CultureInfo, IParsable<Locale>
{
    public Locale(string culture) : base(culture)
    {
    }

    public static Locale Parse(string value, IFormatProvider? provider)
    {
        if (!TryParse(value, provider, out var result))
        {
           throw new ArgumentException("Could not parse supplied value.", nameof(value));
        }

        return result;
    }

    public static bool TryParse([NotNullWhen(true)] string? value,
                                IFormatProvider? provider, out Locale locale)
    {
        if (value is null)
        {
            locale = new Locale(CurrentCulture.Name);
            return false;
        }
        
        try
        {
            locale = new Locale(value);
            return true;
        }
        catch (CultureNotFoundException)
        {
            locale = new Locale(CurrentCulture.Name);
            return false;
        }
    }
}

A ação do controlador a seguir usa a classe Locale para associar uma cadeia de caracteres CultureInfo:

// GET /en-GB/WeatherForecast
public IActionResult Index([FromRoute] Locale locale)
{
    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d", locale),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int)(wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View(weatherForecasts);
}

A ação do controlador a seguir usa as classes DateRange e Locale para associar um intervalo de datas a CultureInfo:

// GET /af-ZA/WeatherForecast/RangeByLocale?range=2022-07-24,2022-07-29
public IActionResult RangeByLocale([FromRoute] Locale locale, [FromQuery] string range)
{
    if (!ModelState.IsValid)
        return View("Error", ModelState.Values.SelectMany(v => v.Errors));

    if (!DateRange.TryParse(range, locale, out DateRange rangeResult))
    {
        ModelState.TryAddModelError(nameof(range),
            $"Invalid date range: {range} for locale {locale.DisplayName}");

        return View("Error", ModelState.Values.SelectMany(v => v.Errors));
    }

    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Where(wf => DateOnly.FromDateTime(wf.Date) >= rangeResult.From
                     && DateOnly.FromDateTime(wf.Date) <= rangeResult.To)
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d", locale),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int) (wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View("Index", weatherForecasts);
}

O aplicativo de exemplo de API no GitHub mostra o exemplo anterior para um controlador de API.

Associar a TryParse

A API TryParse dá suporte a valores de parâmetro de ação do controlador de associação:

public static bool TryParse(string value, T out result);
public static bool TryParse(string value, IFormatProvider provider, T out result);

IParsable<T>.TryParse é a abordagem recomendada para associação de parâmetros porque, ao contrário de TryParse, ela não depende da reflexão.

A classe TryParse a seguir implementa DateRangeTP:

public class DateRangeTP
{
    public DateOnly? From { get; }
    public DateOnly? To { get; }

    public DateRangeTP(string from, string to)
    {
        if (string.IsNullOrEmpty(from))
            throw new ArgumentNullException(nameof(from));
        if (string.IsNullOrEmpty(to))
            throw new ArgumentNullException(nameof(to));

        From = DateOnly.Parse(from);
        To = DateOnly.Parse(to);
    }

    public static bool TryParse(string? value, out DateRangeTP? result)
    {
        var range = value?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
        if (range?.Length != 2)
        {
            result = default;
            return false;
        }

        result = new DateRangeTP(range[0], range[1]);
        return true;
    }
}

A ação do controlador a seguir usa a classe DateRangeTP para associar um intervalo de datas:

// GET /WeatherForecast/ByRangeTP?range=7/24/2022,07/26/2022
public IActionResult ByRangeTP([FromQuery] DateRangeTP range)
{
    if (!ModelState.IsValid)
        return View("Error", ModelState.Values.SelectMany(v => v.Errors));

    var weatherForecasts = Enumerable
        .Range(1, 5).Select(index => new WeatherForecast
        {
            Date = DateTime.Now.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        })
        .Where(wf => DateOnly.FromDateTime(wf.Date) >= range.From
                     && DateOnly.FromDateTime(wf.Date) <= range.To)
        .Select(wf => new WeatherForecastViewModel
        {
            Date = wf.Date.ToString("d"),
            TemperatureC = wf.TemperatureC,
            TemperatureF = 32 + (int)(wf.TemperatureC / 0.5556),
            Summary = wf.Summary
        });

    return View("Index", weatherForecasts);
}

Tipos complexos

Um tipo complexo deve ter um construtor padrão público e propriedades públicas graváveis para associar. Quando ocorre model binding, a classe é instanciada usando o construtor padrão público.

Para cada propriedade do tipo complexo, o model binding examina as fontes em busca do nome padrãoprefix.property_name. Se nada for encontrado, ela procurará apenas property_name sem o prefixo. A decisão de usar o prefixo não é tomada por propriedade. Por exemplo, com uma consulta que contém ?Instructor.Id=100&Name=foo, associada ao método OnGet(Instructor instructor), o objeto resultante do tipo Instructor contém:

  • Id definido como 100.
  • Name definido como null. A associação de modelo espera Instructor.Name porque Instructor.Id foi usado no parâmetro de consulta anterior.

Para associação a um parâmetro, o prefixo é o nome do parâmetro. Para associação a uma propriedade pública PageModel, o prefixo é o nome da propriedade pública. Alguns atributos têm uma propriedade Prefix que permite substituir o uso padrão do nome da propriedade ou do parâmetro.

Por exemplo, suponha que o tipo complexo seja a seguinte classe Instructor:

public class Instructor
{
    public int ID { get; set; }
    public string LastName { get; set; }
    public string FirstName { get; set; }
}

Prefixo = nome do parâmetro

Se o modelo a ser associado for um parâmetro chamado instructorToUpdate:

public IActionResult OnPost(int? id, Instructor instructorToUpdate)

O model binding começa examinando as fontes para a chave instructorToUpdate.ID. Se ela não for encontrada, procurará ID sem um prefixo.

Prefixo = nome da propriedade

Se o modelo a ser associado for uma propriedade chamada Instructor do controlador ou da classe PageModel:

[BindProperty]
public Instructor Instructor { get; set; }

O model binding começa examinando as fontes para a chave Instructor.ID. Se ela não for encontrada, procurará ID sem um prefixo.

Prefixo personalizado

Se o modelo a ser associado for um parâmetro denominado instructorToUpdate e um atributo Bind especificar Instructor como o prefixo:

public IActionResult OnPost(
    int? id, [Bind(Prefix = "Instructor")] Instructor instructorToUpdate)

O model binding começa examinando as fontes para a chave Instructor.ID. Se ela não for encontrada, procurará ID sem um prefixo.

Atributos para destinos de tipo complexo

Vários atributos internos estão disponíveis para controlar o model binding de tipos complexos:

Aviso

Esses atributos afetam o model binding quando os dados de formulário postados são a origem dos valores. Eles não afetam os formatadores de entrada, que processam corpos de solicitação XML e JSON postados. O formatadores de entrada são explicados posteriormente neste artigo.

Atributo [Bind]

Pode ser aplicado a uma classe ou a um parâmetro de método. Especifica quais propriedades de um modelo devem ser incluídas no model binding. [Bind]não afeta formatadores de entrada.

No exemplo a seguir, somente as propriedades especificadas do modelo Instructor são associadas quando qualquer método de ação ou o manipulador é chamado:

[Bind("LastName,FirstMidName,HireDate")]
public class Instructor

No exemplo a seguir, somente as propriedades especificadas do modelo Instructor são associadas quando o método OnPost é chamado:

[HttpPost]
public IActionResult OnPost(
    [Bind("LastName,FirstMidName,HireDate")] Instructor instructor)

O atributo [Bind] pode ser usado para proteção contra o excesso de postagem cenários de criar. Ele não funciona bem em cenários de edição, pois as propriedades excluídas são definidas como nulas ou um valor padrão, em vez de serem deixadas inalteradas. Para defesa contra o excesso de postagem, são recomendados modelos de exibição, em vez do atributo [Bind]. Para obter mais informações, veja Observação de segurança sobre o excesso de postagem.

Atributo [ModelBinder]

ModelBinderAttribute pode ser aplicado a tipos, propriedades ou parâmetros. Ele permite especificar o tipo de associador de modelo usado para associar a instância ou o tipo específico. Por exemplo:

[HttpPost]
public IActionResult OnPost(
    [ModelBinder(typeof(MyInstructorModelBinder))] Instructor instructor)

O atributo [ModelBinder] também pode ser usado para alterar o nome de uma propriedade ou parâmetro quando está sendo associado ao modelo:

public class Instructor
{
    [ModelBinder(Name = "instructor_id")]
    public string Id { get; set; }

    // ...
}

Atributo [BindRequired]

Faz com que o model binding adicione um erro de estado de modelo se a associação não puder ocorrer para a propriedade de um modelo. Veja um exemplo:

public class InstructorBindRequired
{
    // ...

    [BindRequired]
    public DateTime HireDate { get; set; }
}

Veja também a discussão sobre o atributo [Required] em Validação do modelo.

Atributo [BindNever]

Pode ser aplicado a uma propriedade ou um tipo. Impede que o model binding configure a propriedade de um modelo. Quando aplicado a um tipo, o sistema de associação de modelo exclui todas as propriedades definidas pelo tipo. Veja um exemplo:

public class InstructorBindNever
{
    [BindNever]
    public int Id { get; set; }

    // ...
}

Coleções

Para destinos que são coleções de tipos simples, o model binding procura correspondências para parameter_name ou property_name. Se nenhuma correspondência for encontrada, procurará um dos formatos compatíveis sem o prefixo. Por exemplo:

  • Suponha que o parâmetro a ser associado seja uma matriz chamada selectedCourses:

    public IActionResult OnPost(int? id, int[] selectedCourses)
    
  • Dados de cadeia de caracteres de consulta ou formulário podem estar em um dos seguintes formatos:

    selectedCourses=1050&selectedCourses=2000 
    
    selectedCourses[0]=1050&selectedCourses[1]=2000
    
    [0]=1050&[1]=2000
    
    selectedCourses[a]=1050&selectedCourses[b]=2000&selectedCourses.index=a&selectedCourses.index=b
    
    [a]=1050&[b]=2000&index=a&index=b
    

    Evite associar um parâmetro ou uma propriedade chamada index ou Index se estes forem adjacentes a um valor de coleção. A associação de modelo tenta usar index como o índice para a coleção, o que pode resultar em associação incorreta. Por exemplo, considere a seguinte ação.

    public IActionResult Post(string index, List<Product> products)
    

    No código anterior, o parâmetro de cadeia de caracteres de consulta index associa-se ao parâmetro de método index e também é usado para associar a coleção de produtos. Renomear o parâmetro index ou usar um atributo de associação de modelo para configurar a associação evita esse problema:

    public IActionResult Post(string productIndex, List<Product> products)
    
  • O formato a seguir é compatível apenas com os dados de formulário:

    selectedCourses[]=1050&selectedCourses[]=2000
    
  • Para todos os formatos do exemplo anterior, o model binding passa uma matriz de dois itens para o parâmetro selectedCourses:

    • selectedCourses[0]=1050
    • selectedCourses[1]=2000

    Formatos de dados que usam números subscritos (... [0]... [1]...) devem garantir que eles estejam numerados em sequência, começando com zero. Se houver quaisquer intervalos na numeração de subscrito, todos os itens após o intervalo serão ignorados. Por exemplo, se os subscritos forem 0 e 2, em vez de 0 e 1, o segundo item será ignorado.

Dicionários

Para destinos Dictionary, o model binding procura correspondências para parameter_name ou property_name. Se nenhuma correspondência for encontrada, procurará um dos formatos compatíveis sem o prefixo. Por exemplo:

  • Suponha que o parâmetro de destino seja um Dictionary<int, string> chamado selectedCourses:

    public IActionResult OnPost(int? id, Dictionary<int, string> selectedCourses)
    
  • Os dados de cadeia de caracteres de consulta ou formulário postados podem se parecer com um dos exemplos a seguir:

    selectedCourses[1050]=Chemistry&selectedCourses[2000]=Economics
    
    [1050]=Chemistry&selectedCourses[2000]=Economics
    
    selectedCourses[0].Key=1050&selectedCourses[0].Value=Chemistry&
    selectedCourses[1].Key=2000&selectedCourses[1].Value=Economics
    
    [0].Key=1050&[0].Value=Chemistry&[1].Key=2000&[1].Value=Economics
    
  • Para todos os formatos do exemplo anterior, o model binding passa um dicionário de dois itens para o parâmetro selectedCourses:

    • selectedCourses["1050"]="Chemistry"
    • selectedCourses["2000"]="Economics"

Associação de construtor e tipos de registro

A associação de modelo requer que tipos complexos tenham um construtor sem parâmetros. Os formatadores de entrada baseados em System.Text.Json e Newtonsoft.Json dão suporte à desserialização de classes que não têm um construtor sem parâmetros.

Os tipos de registro são uma ótima maneira de representar de forma sucinta os dados pela rede. O ASP.NET Core dá suporte à associação de modelo e à validação de tipos de registro com um único construtor:

public record Person(
    [Required] string Name, [Range(0, 150)] int Age, [BindNever] int Id);

public class PersonController
{
    public IActionResult Index() => View();

    [HttpPost]
    public IActionResult Index(Person person)
    {
        // ...
    }
}

Person/Index.cshtml:

@model Person

Name: <input asp-for="Name" />
<br />
Age: <input asp-for="Age" />

Ao validar tipos de registro, o runtime pesquisa metadados de associação e validação especificamente em parâmetros e não em propriedades.

A estrutura permite associar e validar tipos de registro:

public record Person([Required] string Name, [Range(0, 100)] int Age);

Para que o anterior funcione, o tipo deve:

  • Ser um tipo de registro.
  • Ter exatamente um construtor público.
  • Conter parâmetros que tenham uma propriedade com o mesmo nome e tipo. Os nomes não devem ser diferentes por caso.

POCOs sem construtores sem parâmetros

POCOs que não têm construtores sem parâmetros não podem ser associados.

O código a seguir resulta em uma exceção informando que o tipo deve ter um construtor sem parâmetros:

public class Person(string Name)

public record Person([Required] string Name, [Range(0, 100)] int Age)
{
    public Person(string Name) : this (Name, 0);
}

Tipos de registro com construtores criados manualmente

Tipos de registro com construtores criados manualmente que se parecem com construtores primários funcionam

public record Person
{
    public Person([Required] string Name, [Range(0, 100)] int Age)
        => (this.Name, this.Age) = (Name, Age);

    public string Name { get; set; }
    public int Age { get; set; }
}

Tipos de registro, validação e metadados de associação

Para tipos de registro, os metadados de validação e associação em parâmetros são usados. Todos os metadados nas propriedades são ignorados

public record Person (string Name, int Age)
{
   [BindProperty(Name = "SomeName")] // This does not get used
   [Required] // This does not get used
   public string Name { get; init; }
}

Validação e metadados

A validação usa metadados no parâmetro, mas usa a propriedade para ler o valor. No caso comum com construtores primários, os dois seriam idênticos. No entanto, há maneiras de contornar isso:

public record Person([Required] string Name)
{
    private readonly string _name;

    // The following property is never null.
    // However this object could have been constructed as "new Person(null)".
    public string Name { get; init => _name = value ?? string.Empty; }
}

TryUpdateModel não atualiza parâmetros em um tipo de registro

public record Person(string Name)
{
    public int Age { get; set; }
}

var person = new Person("initial-name");
TryUpdateModel(person, ...);

Nesse caso, o MVC não tentará associar Name novamente. No entanto, Age pode ser atualizado

Comportamento de globalização de dados de rota de associação de modelo e cadeias de caracteres de consulta

O provedor de valor de rota ASP.NET Core e o provedor de valor da cadeia de caracteres de consulta:

  • Tratar valores como cultura invariável.
  • Espere que as URLs sejam invariáveis à cultura.

Por outro lado, os valores provenientes de dados de formulário passam por uma conversão sensível à cultura. Isso é esperado, para que as URLs sejam compartilháveis entre localidades.

Para que o provedor de valor de rota ASP.NET Core e o provedor de valor da cadeia de caracteres de consulta passem por uma conversão sensível à cultura:

public class CultureQueryStringValueProviderFactory : IValueProviderFactory
{
    public Task CreateValueProviderAsync(ValueProviderFactoryContext context)
    {
        _ = context ?? throw new ArgumentNullException(nameof(context));

        var query = context.ActionContext.HttpContext.Request.Query;
        if (query?.Count > 0)
        {
            context.ValueProviders.Add(
                new QueryStringValueProvider(
                    BindingSource.Query,
                    query,
                    CultureInfo.CurrentCulture));
        }

        return Task.CompletedTask;
    }
}
builder.Services.AddControllers(options =>
{
    var index = options.ValueProviderFactories.IndexOf(
        options.ValueProviderFactories.OfType<QueryStringValueProviderFactory>()
            .Single());

    options.ValueProviderFactories[index] =
        new CultureQueryStringValueProviderFactory();
});

Tipos de dados especiais

Há alguns tipos de dados especiais com o model binding pode lidar.

IFormFile e IFormFileCollection

Um arquivo carregado incluído na solicitação HTTP. Também compatível com IEnumerable<IFormFile> para vários arquivos.

CancellationToken

Opcionalmente, as ações podem associar um CancellationToken como um parâmetro. Isso associa RequestAborted que sinaliza quando a conexão subjacente à solicitação HTTP é anulada. As ações podem usar esse parâmetro para cancelar operações assíncronas de execução prolongada executadas como parte das ações do controlador.

FormCollection

Usado para recuperar todos os valores dos dados de formulário postados.

Formatadores de entrada

Dados no corpo da solicitação podem estar em JSON, XML ou algum outro formato. Para analisar esses dados, o model binding usa um formatador de entrada configurado para lidar com um determinado tipo de conteúdo. Por padrão, o ASP.NET Core inclui formatadores de entrada baseados em JSON para lidar com os dados JSON. Você pode adicionar outros formatadores para outros tipos de conteúdo.

O ASP.NET Core seleciona formatadores de entrada com base no atributo Consome. Se nenhum atributo estiver presente, ele usará o cabeçalho Content-Type.

Para usar os formatadores de entrada XML internos:

Personalizar a associação de modelo com formatadores de entrada

Um formatador de entrada assume total responsabilidade pela leitura de dados do corpo da solicitação. Para personalizar esse processo, configure as APIs usadas pelo formatador de entrada. Esta seção descreve como personalizar o formatador de entrada baseado em System.Text.Json para entender um tipo personalizado chamado ObjectId.

Considere o seguinte modelo, que contém uma propriedade personalizada ObjectId:

public class InstructorObjectId
{
    [Required]
    public ObjectId ObjectId { get; set; } = null!;
}

Para personalizar o processo de associação de modelo ao usar System.Text.Json, crie uma classe derivada de JsonConverter<T>:

internal class ObjectIdConverter : JsonConverter<ObjectId>
{
    public override ObjectId Read(
        ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        => new(JsonSerializer.Deserialize<int>(ref reader, options));

    public override void Write(
        Utf8JsonWriter writer, ObjectId value, JsonSerializerOptions options)
        => writer.WriteNumberValue(value.Id);
}

Para usar um conversor personalizado, aplique o atributo JsonConverterAttribute ao tipo. No exemplo a seguir, o tipo ObjectId é configurado com ObjectIdConverter como conversor personalizado:

[JsonConverter(typeof(ObjectIdConverter))]
public record ObjectId(int Id);

Para obter mais informações, confira Como gravar conversores personalizados.

Excluir tipos especificados do model binding

O comportamento do sistema de validação e model binding é orientado pelo ModelMetadata. Você pode personalizar ModelMetadata adicionando um provedor de detalhes MvcOptions.ModelMetadataDetailsProviders. Provedores de detalhes internos estão disponíveis para desabilitar o model binding ou a validação para tipos especificados.

Para desabilitar o model binding em todos os modelos de um tipo especificado, adicione um ExcludeBindingMetadataProvider em Program.cs. Por exemplo, para desabilitar o model binding em todos os modelos do tipo System.Version:

builder.Services.AddRazorPages()
    .AddMvcOptions(options =>
    {
        options.ModelMetadataDetailsProviders.Add(
            new ExcludeBindingMetadataProvider(typeof(Version)));
        options.ModelMetadataDetailsProviders.Add(
            new SuppressChildValidationMetadataProvider(typeof(Guid)));
    });

Para desabilitar a validação nas propriedades de um tipo especificado, adicione um SuppressChildValidationMetadataProvider em Program.cs. Por exemplo, para desabilitar a validação nas propriedades do tipo System.Guid:

builder.Services.AddRazorPages()
    .AddMvcOptions(options =>
    {
        options.ModelMetadataDetailsProviders.Add(
            new ExcludeBindingMetadataProvider(typeof(Version)));
        options.ModelMetadataDetailsProviders.Add(
            new SuppressChildValidationMetadataProvider(typeof(Guid)));
    });

Associadores de modelos personalizados

Você pode estender o model binding escrevendo um associador de modelos personalizado e usando o atributo [ModelBinder] para selecioná-la para um determinado destino. Saiba mais sobre o model binding personalizado.

Model binding manual

O model binding pode ser invocado manualmente usando o método TryUpdateModelAsync. O método é definido nas classes ControllerBase e PageModel. Sobrecargas de método permitem que você especifique o provedor de valor e prefixo a ser usado. O método retornará false se o model binding falhar. Veja um exemplo:

if (await TryUpdateModelAsync(
    newInstructor,
    "Instructor",
    x => x.Name, x => x.HireDate!))
{
    _instructorStore.Add(newInstructor);
    return RedirectToPage("./Index");
}

return Page();

TryUpdateModelAsync usa provedores de valor para obter dados do corpo do formulário, da cadeia de caracteres de consulta e dos dados de rota. TryUpdateModelAsync normalmente é:

  • Usado com páginas Razor e aplicativos MVC usando controladores e exibições para evitar o excesso de postagem.
  • Não usado com uma API Web, a menos que seja consumido de dados de formulário, cadeias de caracteres de consulta e dados de rota. Os pontos de extremidade da API Web que consomem JSON usam formatadores de entrada para desserializar o corpo da solicitação em um objeto.

Para obter mais informações, confira TryUpdateModelAsync.

Atributo [FromServices]

O nome do atributo segue o padrão dos atributos de model binding que especificam uma fonte de dados. Porém, não se trata de associar dados de um provedor de valor. Ele obtém uma instância de um tipo do contêiner de injeção de dependência. Sua finalidade é oferecer uma alternativa à injeção de construtor para quando você precisa de um serviço somente se um determinado método for chamado.

Se uma instância do tipo não estiver registrada no contêiner de injeção de dependência, o aplicativo gerará uma exceção ao tentar associar o parâmetro. Para tornar o parâmetro opcional, use uma das seguintes abordagens:

  • Torne o parâmetro anulável.
  • Defina um valor padrão para o parâmetro.

Para parâmetros anuláveis, verifique se o parâmetro não é null antes de acessá-lo.

Recursos adicionais

Este artigo explica o que é model binding, como ele funciona e como personalizar seu comportamento.

O que é o model binding

Controladores e páginas Razor funcionam com os dados provenientes de solicitações HTTP. Por exemplo, dados de rota podem fornecer uma chave de registro e campos de formulário postados podem fornecer valores para as propriedades do modelo. Escrever código para recuperar cada um desses valores e convertê-los de cadeias de caracteres em tipos .NET seria uma tarefa entediante e propensa a erro. O model binding automatiza esse processo. O sistema de model binding:

  • Recupera dados de várias fontes, como dados de rota, campos de formulário e cadeias de caracteres de consulta.
  • Fornece os dados a controladores e páginas Razor em parâmetros de método e propriedades públicas.
  • Converte dados de cadeia de caracteres em tipos .NET.
  • Atualiza as propriedades de tipos complexos.

Exemplo

Suponha que você tenha o seguinte método de ação:

[HttpGet("{id}")]
public ActionResult<Pet> GetById(int id, bool dogsOnly)

E o aplicativo receba uma solicitação com esta URL:

https://contoso.com/api/pets/2?DogsOnly=true

O model binding passa pelas etapas a seguir depois que o sistema de roteamento seleciona o método de ação:

  • Localiza o primeiro parâmetro de GetById, um número inteiro denominado id.
  • Examina as fontes disponíveis na solicitação HTTP e localiza id = "2" em dados de rota.
  • Converte a cadeia de caracteres "2" em inteiro 2.
  • Localiza o próximo parâmetro de GetById, um booliano chamado dogsOnly.
  • Examina as fontes e localiza "DogsOnly=true" na cadeia de consulta. A correspondência de nomes não diferencia maiúsculas de minúsculas.
  • Converte a cadeia de caracteres "true" no booliano true.

A estrutura então chama o método GetById, passando 2 para o parâmetro id e true para o parâmetro dogsOnly.

No exemplo anterior, os destinos do model binding são parâmetros de método que são tipos simples. Destinos também podem ser as propriedades de um tipo complexo. Depois de cada propriedade ser associada com êxito, a validação do modelo ocorre para essa propriedade. O registro de quais dados estão associados ao modelo, além de quaisquer erros de validação ou de associação, é armazenado em ControllerBase.ModelState ou PageModel.ModelState. Para descobrir se esse processo foi bem-sucedido, o aplicativo verifica o sinalizador ModelState.IsValid.

Destinos

O model binding tenta encontrar valores para os seguintes tipos de destinos:

  • Parâmetros do método de ação do controlador para o qual uma solicitação é roteada.
  • Os parâmetros do método do manipulador de páginas Razor para o qual uma solicitação é roteada.
  • Propriedades públicas de um controlador ou classe PageModel, se especificadas por atributos.

Atributo [BindProperty]

Pode ser aplicado a uma propriedade pública de um controlador ou classe PageModel para fazer o model binding ter essa propriedade como destino:

public class EditModel : PageModel
{
    [BindProperty]
    public Instructor? Instructor { get; set; }

    // ...
}

Atributo [BindProperties]

Pode ser aplicado a um controlador ou classe PageModel para informar o model binding para ter todas as propriedades públicas da classe como destino:

[BindProperties]
public class CreateModel : PageModel
{
    public Instructor? Instructor { get; set; }

    // ...
}

Model binding para solicitações HTTP GET

Por padrão, as propriedades não são vinculadas para solicitações HTTP GET. Normalmente, tudo o que você precisa para uma solicitação GET é um parâmetro de ID de registro. A ID do registro é usada para pesquisar o item no banco de dados. Portanto, não é necessário associar uma propriedade que contém uma instância do modelo. Em cenários em que você deseja associar propriedades a dados de solicitações GET, defina a propriedade SupportsGet como true:

[BindProperty(Name = "ai_user", SupportsGet = true)]
public string? ApplicationInsightsCookie { get; set; }

Origens

Por padrão, o model binding obtém dados na forma de pares chave-valor das seguintes fontes em uma solicitação HTTP:

  1. Campos de formulário
  2. O corpo da solicitação (para controladores que têm o atributo [ApiController]).
  3. Rotear dados
  4. Parâmetros de cadeia de caracteres de consulta
  5. Arquivos carregados

Para cada propriedade ou parâmetro de destino, as fontes são verificadas na ordem indicada na lista anterior. Há algumas exceções:

  • Os valores de cadeia de caracteres de consulta e dados de rota são usados apenas para tipos simples.
  • Arquivos carregados são associados apenas a tipos de destino que implementam IFormFile ou IEnumerable<IFormFile>.

Se a origem padrão não estiver correta, use um dos seguintes atributos para especificar a origem:

  • [FromQuery] – obtém valores da cadeia de caracteres de consulta.
  • [FromRoute] – obtém valores dos dados de rota.
  • [FromForm] – obtém valores de campos de formulário postados.
  • [FromBody] – obtém valores do corpo da solicitação.
  • [FromHeader] – obtém valores de cabeçalhos HTTP.

Esses atributos:

  • São adicionados às propriedades de modelo individualmente, e não à classe de modelo, como no exemplo a seguir:

    public class Instructor
    {
        public int Id { get; set; }
    
        [FromQuery(Name = "Note")]
        public string? NoteFromQueryString { get; set; }
    
        // ...
    }
    
  • Opcionalmente, aceite um valor de nome de modelo no construtor. Essa opção é fornecida caso o nome da propriedade não corresponda ao valor na solicitação. Por exemplo, o valor na solicitação pode ser um cabeçalho com um hífen em seu nome, como no exemplo a seguir:

    public void OnGet([FromHeader(Name = "Accept-Language")] string language)
    

Atributo [FromBody]

Aplique o atributo [FromBody] a um parâmetro para preencher suas propriedades do corpo de uma solicitação HTTP. O runtime do ASP.NET Core delega a responsabilidade de ler o corpo a um formatador de entrada. O formatadores de entrada são explicados posteriormente neste artigo.

Quando [FromBody] é aplicado a um parâmetro de tipo complexo, todos os atributos de origem de associação aplicados às suas propriedades são ignorados. Por exemplo, a ação Create a seguir especifica que seu parâmetro pet é preenchido do corpo:

public ActionResult<Pet> Create([FromBody] Pet pet)

A classe Pet especifica que sua propriedade Breed é preenchida a partir de um parâmetro de cadeia de caracteres de consulta:

public class Pet
{
    public string Name { get; set; } = null!;

    [FromQuery] // Attribute is ignored.
    public string Breed { get; set; } = null!;
}

No exemplo anterior:

  • O atributo [FromQuery] é ignorado.
  • A propriedade Breed não é preenchida de um parâmetro de cadeia de caracteres de consulta.

Os formatadores de entrada leem apenas o corpo e não entendem os atributos de origem da associação. Se um valor adequado for encontrado no corpo, esse valor será usado para preencher a propriedade Breed.

Não aplique [FromBody] a mais de um parâmetro por método de ação. Depois que o fluxo da solicitação é lido por um formatador de entrada, ele não fica mais disponível para ser lido novamente para associar outros parâmetros [FromBody].

Fontes adicionais

Os dados de origem são fornecidos ao sistema de model binding pelos provedores de valor. Você pode escrever e registrar provedores de valor personalizados que obtêm dados para model binding de outras fontes. Por exemplo, talvez você queira dados de cookies ou estado de sessão. Para obter dados de uma nova fonte:

  • Crie uma classe que implementa IValueProvider.
  • Crie uma classe que implementa IValueProviderFactory.
  • Registre a classe de alocador em Program.cs.

A amostra inclui um provedor de valor e um exemplo de alocador que obtém valores de cookies. Registre fábricas de provedores de valor personalizado em Program.cs:

builder.Services.AddControllers(options =>
{
    options.ValueProviderFactories.Add(new CookieValueProviderFactory());
});

O código anterior coloca o provedor de valor personalizado depois de todos os provedores de valor internos. Para torná-lo o primeiro na lista, chame Insert(0, new CookieValueProviderFactory()), em vez de Add.

Nenhuma fonte para uma propriedade de modelo

Por padrão, um erro de estado de modelo não será criado se nenhum valor for encontrado para uma propriedade de modelo. A propriedade é definida como null ou um valor padrão:

  • Tipos simples anuláveis definidos como null.
  • Tipos de valor não anuláveis são definidos como default(T). Por exemplo, um parâmetro int id é definido como 0.
  • Para tipos complexos, o model binding cria uma instância usando o construtor padrão sem definir propriedades.
  • As matrizes são definidas como Array.Empty<T>(), exceto que matrizes byte[] são definidas como null.

Se o estado do modelo precisar ser invalidado quando nada for encontrado em campos de formulário para uma propriedade de modelo, use o atributo [BindRequired].

Observe que este comportamento [BindRequired] se aplica ao model binding de dados de formulário postados, não a dados JSON ou XML em um corpo da solicitação. Dados do corpo da solicitação são tratados pelos formatadores de entrada.

Erros de conversão de tipos

Se uma fonte for encontrada, mas não puder ser convertida no tipo de destino, o estado do modelo será sinalizado como inválido. O parâmetro ou a propriedade de destino é definido como nulo ou um valor padrão, conforme observado na seção anterior.

Em um controlador de API que tem o atributo [ApiController], um estado de modelo inválido resulta em uma resposta HTTP 400 automática.

Em uma página Razor, exiba novamente a página com uma mensagem de erro:

public IActionResult OnPost()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    // ...

    return RedirectToPage("./Index");
}

Quando a página é exibida novamente pelo código anterior, a entrada inválida não é mostrada no campo de formulário. Isso ocorre porque a propriedade do modelo foi definida como nulo ou um valor padrão. A entrada inválida aparece em uma mensagem de erro. Se você quiser exibir novamente os dados inválidos no campo de formulário, considere tornar a propriedade do modelo uma cadeia de caracteres e fazer a conversão de dados manualmente.

A mesma estratégia será recomendada se você não desejar que erros de conversão de tipo resultem em erros de estado de modelo. Nesse caso, torne a propriedade de modelo uma cadeia de caracteres.

Tipos simples

Os tipos simples em que o associador de modelos pode converter cadeias de caracteres de origem incluem os seguintes:

Tipos complexos

Um tipo complexo deve ter um construtor padrão público e propriedades públicas graváveis para associar. Quando ocorre model binding, a classe é instanciada usando o construtor padrão público.

Para cada propriedade do tipo complexo, o model binding examina as fontes em busca do nome padrãoprefix.property_name. Se nada for encontrado, ela procurará apenas property_name sem o prefixo. A decisão de usar o prefixo não é tomada por propriedade. Por exemplo, com uma consulta que contém ?Instructor.Id=100&Name=foo, associada ao método OnGet(Instructor instructor), o objeto resultante do tipo Instructor contém:

  • Id definido como 100.
  • Name definido como null. A associação de modelo espera Instructor.Name porque Instructor.Id foi usado no parâmetro de consulta anterior.

Para associação a um parâmetro, o prefixo é o nome do parâmetro. Para associação a uma propriedade pública PageModel, o prefixo é o nome da propriedade pública. Alguns atributos têm uma propriedade Prefix que permite substituir o uso padrão do nome da propriedade ou do parâmetro.

Por exemplo, suponha que o tipo complexo seja a seguinte classe Instructor:

public class Instructor
{
    public int ID { get; set; }
    public string LastName { get; set; }
    public string FirstName { get; set; }
}

Prefixo = nome do parâmetro

Se o modelo a ser associado for um parâmetro chamado instructorToUpdate:

public IActionResult OnPost(int? id, Instructor instructorToUpdate)

O model binding começa examinando as fontes para a chave instructorToUpdate.ID. Se ela não for encontrada, procurará ID sem um prefixo.

Prefixo = nome da propriedade

Se o modelo a ser associado for uma propriedade chamada Instructor do controlador ou da classe PageModel:

[BindProperty]
public Instructor Instructor { get; set; }

O model binding começa examinando as fontes para a chave Instructor.ID. Se ela não for encontrada, procurará ID sem um prefixo.

Prefixo personalizado

Se o modelo a ser associado for um parâmetro denominado instructorToUpdate e um atributo Bind especificar Instructor como o prefixo:

public IActionResult OnPost(
    int? id, [Bind(Prefix = "Instructor")] Instructor instructorToUpdate)

O model binding começa examinando as fontes para a chave Instructor.ID. Se ela não for encontrada, procurará ID sem um prefixo.

Atributos para destinos de tipo complexo

Vários atributos internos estão disponíveis para controlar o model binding de tipos complexos:

Aviso

Esses atributos afetam o model binding quando os dados de formulário postados são a origem dos valores. Eles não afetam os formatadores de entrada, que processam corpos de solicitação XML e JSON postados. O formatadores de entrada são explicados posteriormente neste artigo.

Atributo [Bind]

Pode ser aplicado a uma classe ou a um parâmetro de método. Especifica quais propriedades de um modelo devem ser incluídas no model binding. [Bind]não afeta formatadores de entrada.

No exemplo a seguir, somente as propriedades especificadas do modelo Instructor são associadas quando qualquer método de ação ou o manipulador é chamado:

[Bind("LastName,FirstMidName,HireDate")]
public class Instructor

No exemplo a seguir, somente as propriedades especificadas do modelo Instructor são associadas quando o método OnPost é chamado:

[HttpPost]
public IActionResult OnPost(
    [Bind("LastName,FirstMidName,HireDate")] Instructor instructor)

O atributo [Bind] pode ser usado para proteção contra o excesso de postagem cenários de criar. Ele não funciona bem em cenários de edição, pois as propriedades excluídas são definidas como nulas ou um valor padrão, em vez de serem deixadas inalteradas. Para defesa contra o excesso de postagem, são recomendados modelos de exibição, em vez do atributo [Bind]. Para obter mais informações, veja Observação de segurança sobre o excesso de postagem.

Atributo [ModelBinder]

ModelBinderAttribute pode ser aplicado a tipos, propriedades ou parâmetros. Ele permite especificar o tipo de associador de modelo usado para associar a instância ou o tipo específico. Por exemplo:

[HttpPost]
public IActionResult OnPost(
    [ModelBinder(typeof(MyInstructorModelBinder))] Instructor instructor)

O atributo [ModelBinder] também pode ser usado para alterar o nome de uma propriedade ou parâmetro quando está sendo associado ao modelo:

public class Instructor
{
    [ModelBinder(Name = "instructor_id")]
    public string Id { get; set; }

    // ...
}

Atributo [BindRequired]

Faz com que o model binding adicione um erro de estado de modelo se a associação não puder ocorrer para a propriedade de um modelo. Veja um exemplo:

public class InstructorBindRequired
{
    // ...

    [BindRequired]
    public DateTime HireDate { get; set; }
}

Veja também a discussão sobre o atributo [Required] em Validação do modelo.

Atributo [BindNever]

Pode ser aplicado a uma propriedade ou um tipo. Impede que o model binding configure a propriedade de um modelo. Quando aplicado a um tipo, o sistema de associação de modelo exclui todas as propriedades definidas pelo tipo. Veja um exemplo:

public class InstructorBindNever
{
    [BindNever]
    public int Id { get; set; }

    // ...
}

Coleções

Para destinos que são coleções de tipos simples, o model binding procura correspondências para parameter_name ou property_name. Se nenhuma correspondência for encontrada, procurará um dos formatos compatíveis sem o prefixo. Por exemplo:

  • Suponha que o parâmetro a ser associado seja uma matriz chamada selectedCourses:

    public IActionResult OnPost(int? id, int[] selectedCourses)
    
  • Dados de cadeia de caracteres de consulta ou formulário podem estar em um dos seguintes formatos:

    selectedCourses=1050&selectedCourses=2000 
    
    selectedCourses[0]=1050&selectedCourses[1]=2000
    
    [0]=1050&[1]=2000
    
    selectedCourses[a]=1050&selectedCourses[b]=2000&selectedCourses.index=a&selectedCourses.index=b
    
    [a]=1050&[b]=2000&index=a&index=b
    

    Evite associar um parâmetro ou uma propriedade chamada index ou Index se estes forem adjacentes a um valor de coleção. A associação de modelo tenta usar index como o índice para a coleção, o que pode resultar em associação incorreta. Por exemplo, considere a seguinte ação.

    public IActionResult Post(string index, List<Product> products)
    

    No código anterior, o parâmetro de cadeia de caracteres de consulta index associa-se ao parâmetro de método index e também é usado para associar a coleção de produtos. Renomear o parâmetro index ou usar um atributo de associação de modelo para configurar a associação evita esse problema:

    public IActionResult Post(string productIndex, List<Product> products)
    
  • O formato a seguir é compatível apenas com os dados de formulário:

    selectedCourses[]=1050&selectedCourses[]=2000
    
  • Para todos os formatos do exemplo anterior, o model binding passa uma matriz de dois itens para o parâmetro selectedCourses:

    • selectedCourses[0]=1050
    • selectedCourses[1]=2000

    Formatos de dados que usam números subscritos (... [0]... [1]...) devem garantir que eles estejam numerados em sequência, começando com zero. Se houver quaisquer intervalos na numeração de subscrito, todos os itens após o intervalo serão ignorados. Por exemplo, se os subscritos forem 0 e 2, em vez de 0 e 1, o segundo item será ignorado.

Dicionários

Para destinos Dictionary, o model binding procura correspondências para parameter_name ou property_name. Se nenhuma correspondência for encontrada, procurará um dos formatos compatíveis sem o prefixo. Por exemplo:

  • Suponha que o parâmetro de destino seja um Dictionary<int, string> chamado selectedCourses:

    public IActionResult OnPost(int? id, Dictionary<int, string> selectedCourses)
    
  • Os dados de cadeia de caracteres de consulta ou formulário postados podem se parecer com um dos exemplos a seguir:

    selectedCourses[1050]=Chemistry&selectedCourses[2000]=Economics
    
    [1050]=Chemistry&selectedCourses[2000]=Economics
    
    selectedCourses[0].Key=1050&selectedCourses[0].Value=Chemistry&
    selectedCourses[1].Key=2000&selectedCourses[1].Value=Economics
    
    [0].Key=1050&[0].Value=Chemistry&[1].Key=2000&[1].Value=Economics
    
  • Para todos os formatos do exemplo anterior, o model binding passa um dicionário de dois itens para o parâmetro selectedCourses:

    • selectedCourses["1050"]="Chemistry"
    • selectedCourses["2000"]="Economics"

Associação de construtor e tipos de registro

A associação de modelo requer que tipos complexos tenham um construtor sem parâmetros. Os formatadores de entrada baseados em System.Text.Json e Newtonsoft.Json dão suporte à desserialização de classes que não têm um construtor sem parâmetros.

Os tipos de registro são uma ótima maneira de representar de forma sucinta os dados pela rede. O ASP.NET Core dá suporte à associação de modelo e à validação de tipos de registro com um único construtor:

public record Person(
    [Required] string Name, [Range(0, 150)] int Age, [BindNever] int Id);

public class PersonController
{
    public IActionResult Index() => View();

    [HttpPost]
    public IActionResult Index(Person person)
    {
        // ...
    }
}

Person/Index.cshtml:

@model Person

Name: <input asp-for="Name" />
<br />
Age: <input asp-for="Age" />

Ao validar tipos de registro, o runtime pesquisa metadados de associação e validação especificamente em parâmetros e não em propriedades.

A estrutura permite associar e validar tipos de registro:

public record Person([Required] string Name, [Range(0, 100)] int Age);

Para que o anterior funcione, o tipo deve:

  • Ser um tipo de registro.
  • Ter exatamente um construtor público.
  • Conter parâmetros que tenham uma propriedade com o mesmo nome e tipo. Os nomes não devem ser diferentes por caso.

POCOs sem construtores sem parâmetros

POCOs que não têm construtores sem parâmetros não podem ser associados.

O código a seguir resulta em uma exceção informando que o tipo deve ter um construtor sem parâmetros:

public class Person(string Name)

public record Person([Required] string Name, [Range(0, 100)] int Age)
{
    public Person(string Name) : this (Name, 0);
}

Tipos de registro com construtores criados manualmente

Tipos de registro com construtores criados manualmente que se parecem com construtores primários funcionam

public record Person
{
    public Person([Required] string Name, [Range(0, 100)] int Age)
        => (this.Name, this.Age) = (Name, Age);

    public string Name { get; set; }
    public int Age { get; set; }
}

Tipos de registro, validação e metadados de associação

Para tipos de registro, os metadados de validação e associação em parâmetros são usados. Todos os metadados nas propriedades são ignorados

public record Person (string Name, int Age)
{
   [BindProperty(Name = "SomeName")] // This does not get used
   [Required] // This does not get used
   public string Name { get; init; }
}

Validação e metadados

A validação usa metadados no parâmetro, mas usa a propriedade para ler o valor. No caso comum com construtores primários, os dois seriam idênticos. No entanto, há maneiras de contornar isso:

public record Person([Required] string Name)
{
    private readonly string _name;

    // The following property is never null.
    // However this object could have been constructed as "new Person(null)".
    public string Name { get; init => _name = value ?? string.Empty; }
}

TryUpdateModel não atualiza parâmetros em um tipo de registro

public record Person(string Name)
{
    public int Age { get; set; }
}

var person = new Person("initial-name");
TryUpdateModel(person, ...);

Nesse caso, o MVC não tentará associar Name novamente. No entanto, Age pode ser atualizado

Comportamento de globalização de dados de rota de associação de modelo e cadeias de caracteres de consulta

O provedor de valor de rota ASP.NET Core e o provedor de valor da cadeia de caracteres de consulta:

  • Tratar valores como cultura invariável.
  • Espere que as URLs sejam invariáveis à cultura.

Por outro lado, os valores provenientes de dados de formulário passam por uma conversão sensível à cultura. Isso é esperado, para que as URLs sejam compartilháveis entre localidades.

Para que o provedor de valor de rota ASP.NET Core e o provedor de valor da cadeia de caracteres de consulta passem por uma conversão sensível à cultura:

public class CultureQueryStringValueProviderFactory : IValueProviderFactory
{
    public Task CreateValueProviderAsync(ValueProviderFactoryContext context)
    {
        _ = context ?? throw new ArgumentNullException(nameof(context));

        var query = context.ActionContext.HttpContext.Request.Query;
        if (query?.Count > 0)
        {
            context.ValueProviders.Add(
                new QueryStringValueProvider(
                    BindingSource.Query,
                    query,
                    CultureInfo.CurrentCulture));
        }

        return Task.CompletedTask;
    }
}
builder.Services.AddControllers(options =>
{
    var index = options.ValueProviderFactories.IndexOf(
        options.ValueProviderFactories.OfType<QueryStringValueProviderFactory>()
            .Single());

    options.ValueProviderFactories[index] =
        new CultureQueryStringValueProviderFactory();
});

Tipos de dados especiais

Há alguns tipos de dados especiais com o model binding pode lidar.

IFormFile e IFormFileCollection

Um arquivo carregado incluído na solicitação HTTP. Também compatível com IEnumerable<IFormFile> para vários arquivos.

CancellationToken

Opcionalmente, as ações podem associar um CancellationToken como um parâmetro. Isso associa RequestAborted que sinaliza quando a conexão subjacente à solicitação HTTP é anulada. As ações podem usar esse parâmetro para cancelar operações assíncronas de execução prolongada executadas como parte das ações do controlador.

FormCollection

Usado para recuperar todos os valores dos dados de formulário postados.

Formatadores de entrada

Dados no corpo da solicitação podem estar em JSON, XML ou algum outro formato. Para analisar esses dados, o model binding usa um formatador de entrada configurado para lidar com um determinado tipo de conteúdo. Por padrão, o ASP.NET Core inclui formatadores de entrada baseados em JSON para lidar com os dados JSON. Você pode adicionar outros formatadores para outros tipos de conteúdo.

O ASP.NET Core seleciona formatadores de entrada com base no atributo Consome. Se nenhum atributo estiver presente, ele usará o cabeçalho Content-Type.

Para usar os formatadores de entrada XML internos:

Personalizar a associação de modelo com formatadores de entrada

Um formatador de entrada assume total responsabilidade pela leitura de dados do corpo da solicitação. Para personalizar esse processo, configure as APIs usadas pelo formatador de entrada. Esta seção descreve como personalizar o formatador de entrada baseado em System.Text.Json para entender um tipo personalizado chamado ObjectId.

Considere o seguinte modelo, que contém uma propriedade personalizada ObjectId:

public class InstructorObjectId
{
    [Required]
    public ObjectId ObjectId { get; set; } = null!;
}

Para personalizar o processo de associação de modelo ao usar System.Text.Json, crie uma classe derivada de JsonConverter<T>:

internal class ObjectIdConverter : JsonConverter<ObjectId>
{
    public override ObjectId Read(
        ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        => new(JsonSerializer.Deserialize<int>(ref reader, options));

    public override void Write(
        Utf8JsonWriter writer, ObjectId value, JsonSerializerOptions options)
        => writer.WriteNumberValue(value.Id);
}

Para usar um conversor personalizado, aplique o atributo JsonConverterAttribute ao tipo. No exemplo a seguir, o tipo ObjectId é configurado com ObjectIdConverter como conversor personalizado:

[JsonConverter(typeof(ObjectIdConverter))]
public record ObjectId(int Id);

Para obter mais informações, confira Como gravar conversores personalizados.

Excluir tipos especificados do model binding

O comportamento do sistema de validação e model binding é orientado pelo ModelMetadata. Você pode personalizar ModelMetadata adicionando um provedor de detalhes MvcOptions.ModelMetadataDetailsProviders. Provedores de detalhes internos estão disponíveis para desabilitar o model binding ou a validação para tipos especificados.

Para desabilitar o model binding em todos os modelos de um tipo especificado, adicione um ExcludeBindingMetadataProvider em Program.cs. Por exemplo, para desabilitar o model binding em todos os modelos do tipo System.Version:

builder.Services.AddRazorPages()
    .AddMvcOptions(options =>
    {
        options.ModelMetadataDetailsProviders.Add(
            new ExcludeBindingMetadataProvider(typeof(Version)));
        options.ModelMetadataDetailsProviders.Add(
            new SuppressChildValidationMetadataProvider(typeof(Guid)));
    });

Para desabilitar a validação nas propriedades de um tipo especificado, adicione um SuppressChildValidationMetadataProvider em Program.cs. Por exemplo, para desabilitar a validação nas propriedades do tipo System.Guid:

builder.Services.AddRazorPages()
    .AddMvcOptions(options =>
    {
        options.ModelMetadataDetailsProviders.Add(
            new ExcludeBindingMetadataProvider(typeof(Version)));
        options.ModelMetadataDetailsProviders.Add(
            new SuppressChildValidationMetadataProvider(typeof(Guid)));
    });

Associadores de modelos personalizados

Você pode estender o model binding escrevendo um associador de modelos personalizado e usando o atributo [ModelBinder] para selecioná-la para um determinado destino. Saiba mais sobre o model binding personalizado.

Model binding manual

O model binding pode ser invocado manualmente usando o método TryUpdateModelAsync. O método é definido nas classes ControllerBase e PageModel. Sobrecargas de método permitem que você especifique o provedor de valor e prefixo a ser usado. O método retornará false se o model binding falhar. Veja um exemplo:

if (await TryUpdateModelAsync(
    newInstructor,
    "Instructor",
    x => x.Name, x => x.HireDate!))
{
    _instructorStore.Add(newInstructor);
    return RedirectToPage("./Index");
}

return Page();

TryUpdateModelAsync usa provedores de valor para obter dados do corpo do formulário, da cadeia de caracteres de consulta e dos dados de rota. TryUpdateModelAsync normalmente é:

  • Usado com páginas Razor e aplicativos MVC usando controladores e exibições para evitar o excesso de postagem.
  • Não usado com uma API Web, a menos que seja consumido de dados de formulário, cadeias de caracteres de consulta e dados de rota. Os pontos de extremidade da API Web que consomem JSON usam formatadores de entrada para desserializar o corpo da solicitação em um objeto.

Para obter mais informações, confira TryUpdateModelAsync.

Atributo [FromServices]

O nome do atributo segue o padrão dos atributos de model binding que especificam uma fonte de dados. Porém, não se trata de associar dados de um provedor de valor. Ele obtém uma instância de um tipo do contêiner de injeção de dependência. Sua finalidade é oferecer uma alternativa à injeção de construtor para quando você precisa de um serviço somente se um determinado método for chamado.

Se uma instância do tipo não estiver registrada no contêiner de injeção de dependência, o aplicativo gerará uma exceção ao tentar associar o parâmetro. Para tornar o parâmetro opcional, use uma das seguintes abordagens:

  • Torne o parâmetro anulável.
  • Defina um valor padrão para o parâmetro.

Para parâmetros anuláveis, verifique se o parâmetro não é null antes de acessá-lo.

Recursos adicionais

Este artigo explica o que é model binding, como ele funciona e como personalizar seu comportamento.

Exibir ou baixar um código de exemplo (como baixar).

O que é o model binding

Controladores e páginas Razor funcionam com os dados provenientes de solicitações HTTP. Por exemplo, dados de rota podem fornecer uma chave de registro e campos de formulário postados podem fornecer valores para as propriedades do modelo. Escrever código para recuperar cada um desses valores e convertê-los de cadeias de caracteres em tipos .NET seria uma tarefa entediante e propensa a erro. O model binding automatiza esse processo. O sistema de model binding:

  • Recupera dados de várias fontes, como dados de rota, campos de formulário e cadeias de caracteres de consulta.
  • Fornece os dados a controladores e páginas Razor em parâmetros de método e propriedades públicas.
  • Converte dados de cadeia de caracteres em tipos .NET.
  • Atualiza as propriedades de tipos complexos.

Exemplo

Suponha que você tenha o seguinte método de ação:

[HttpGet("{id}")]
public ActionResult<Pet> GetById(int id, bool dogsOnly)

E o aplicativo receba uma solicitação com esta URL:

http://contoso.com/api/pets/2?DogsOnly=true

O model binding passa pelas etapas a seguir depois que o sistema de roteamento seleciona o método de ação:

  • Localiza o primeiro parâmetro de GetById, um número inteiro denominado id.
  • Examina as fontes disponíveis na solicitação HTTP e localiza id = "2" em dados de rota.
  • Converte a cadeia de caracteres "2" em inteiro 2.
  • Localiza o próximo parâmetro de GetById, um booliano chamado dogsOnly.
  • Examina as fontes e localiza "DogsOnly=true" na cadeia de consulta. A correspondência de nomes não diferencia maiúsculas de minúsculas.
  • Converte a cadeia de caracteres "true" no booliano true.

A estrutura então chama o método GetById, passando 2 para o parâmetro id e true para o parâmetro dogsOnly.

No exemplo anterior, os destinos do model binding são parâmetros de método que são tipos simples. Destinos também podem ser as propriedades de um tipo complexo. Depois de cada propriedade ser associada com êxito, a validação do modelo ocorre para essa propriedade. O registro de quais dados estão associados ao modelo, além de quaisquer erros de validação ou de associação, é armazenado em ControllerBase.ModelState ou PageModel.ModelState. Para descobrir se esse processo foi bem-sucedido, o aplicativo verifica o sinalizador ModelState.IsValid.

Destinos

O model binding tenta encontrar valores para os seguintes tipos de destinos:

  • Parâmetros do método de ação do controlador para o qual uma solicitação é roteada.
  • Os parâmetros do método do manipulador de páginas Razor para o qual uma solicitação é roteada.
  • Propriedades públicas de um controlador ou classe PageModel, se especificadas por atributos.

Atributo [BindProperty]

Pode ser aplicado a uma propriedade pública de um controlador ou classe PageModel para fazer o model binding ter essa propriedade como destino:

public class EditModel : InstructorsPageModel
{
    [BindProperty]
    public Instructor Instructor { get; set; }

Atributo [BindProperties]

Disponível no ASP.NET Core 2.1 e posteriores. Pode ser aplicado a um controlador ou classe PageModel para informar o model binding para ter todas as propriedades públicas da classe como destino:

[BindProperties(SupportsGet = true)]
public class CreateModel : InstructorsPageModel
{
    public Instructor Instructor { get; set; }

Model binding para solicitações HTTP GET

Por padrão, as propriedades não são vinculadas para solicitações HTTP GET. Normalmente, tudo o que você precisa para uma solicitação GET é um parâmetro de ID de registro. A ID do registro é usada para pesquisar o item no banco de dados. Portanto, não é necessário associar uma propriedade que contém uma instância do modelo. Em cenários em que você deseja associar propriedades a dados de solicitações GET, defina a propriedade SupportsGet como true:

[BindProperty(Name = "ai_user", SupportsGet = true)]
public string ApplicationInsightsCookie { get; set; }

Origens

Por padrão, o model binding obtém dados na forma de pares chave-valor das seguintes fontes em uma solicitação HTTP:

  1. Campos de formulário
  2. O corpo da solicitação (para controladores que têm o atributo [ApiController]).
  3. Rotear dados
  4. Parâmetros de cadeia de caracteres de consulta
  5. Arquivos carregados

Para cada propriedade ou parâmetro de destino, as fontes são verificadas na ordem indicada na lista anterior. Há algumas exceções:

  • Os valores de cadeia de caracteres de consulta e dados de rota são usados apenas para tipos simples.
  • Arquivos carregados são associados apenas a tipos de destino que implementam IFormFile ou IEnumerable<IFormFile>.

Se a origem padrão não estiver correta, use um dos seguintes atributos para especificar a origem:

  • [FromQuery] – obtém valores da cadeia de caracteres de consulta.
  • [FromRoute] – obtém valores dos dados de rota.
  • [FromForm] – obtém valores de campos de formulário postados.
  • [FromBody] – obtém valores do corpo da solicitação.
  • [FromHeader] – obtém valores de cabeçalhos HTTP.

Esses atributos:

  • São adicionados às propriedades de modelo individualmente (não à classe de modelo), como no exemplo a seguir:

    public class Instructor
    {
        public int ID { get; set; }
    
        [FromQuery(Name = "Note")]
        public string NoteFromQueryString { get; set; }
    
  • Opcionalmente, aceite um valor de nome de modelo no construtor. Essa opção é fornecida caso o nome da propriedade não corresponda ao valor na solicitação. Por exemplo, o valor na solicitação pode ser um cabeçalho com um hífen em seu nome, como no exemplo a seguir:

    public void OnGet([FromHeader(Name = "Accept-Language")] string language)
    

Atributo [FromBody]

Aplique o atributo [FromBody] a um parâmetro para preencher suas propriedades do corpo de uma solicitação HTTP. O runtime do ASP.NET Core delega a responsabilidade de ler o corpo a um formatador de entrada. O formatadores de entrada são explicados posteriormente neste artigo.

Quando [FromBody] é aplicado a um parâmetro de tipo complexo, todos os atributos de origem de associação aplicados às suas propriedades são ignorados. Por exemplo, a ação Create a seguir especifica que seu parâmetro pet é preenchido do corpo:

public ActionResult<Pet> Create([FromBody] Pet pet)

A classe Pet especifica que sua propriedade Breed é preenchida a partir de um parâmetro de cadeia de caracteres de consulta:

public class Pet
{
    public string Name { get; set; }

    [FromQuery] // Attribute is ignored.
    public string Breed { get; set; }
}

No exemplo anterior:

  • O atributo [FromQuery] é ignorado.
  • A propriedade Breed não é preenchida de um parâmetro de cadeia de caracteres de consulta.

Os formatadores de entrada leem apenas o corpo e não entendem os atributos de origem da associação. Se um valor adequado for encontrado no corpo, esse valor será usado para preencher a propriedade Breed.

Não aplique [FromBody] a mais de um parâmetro por método de ação. Depois que o fluxo da solicitação é lido por um formatador de entrada, ele não fica mais disponível para ser lido novamente para associar outros parâmetros [FromBody].

Fontes adicionais

Os dados de origem são fornecidos ao sistema de model binding pelos provedores de valor. Você pode escrever e registrar provedores de valor personalizados que obtêm dados para model binding de outras fontes. Por exemplo, talvez você queira dados de cookies ou estado de sessão. Para obter dados de uma nova fonte:

  • Crie uma classe que implementa IValueProvider.
  • Crie uma classe que implementa IValueProviderFactory.
  • Registre a classe de alocador em Startup.ConfigureServices.

O aplicativo de exemplo inclui um provedor de valor e um exemplo de alocador que obtém valores de cookies. Aqui está o código de registro em Startup.ConfigureServices:

services.AddRazorPages()
    .AddMvcOptions(options =>
{
    options.ValueProviderFactories.Add(new CookieValueProviderFactory());
    options.ModelMetadataDetailsProviders.Add(
        new ExcludeBindingMetadataProvider(typeof(System.Version)));
    options.ModelMetadataDetailsProviders.Add(
        new SuppressChildValidationMetadataProvider(typeof(System.Guid)));
})
.AddXmlSerializerFormatters();

O código mostrado coloca o provedor de valor personalizado depois de todos os provedores de valor internos. Para torná-lo o primeiro na lista, chame Insert(0, new CookieValueProviderFactory()), em vez de Add.

Nenhuma fonte para uma propriedade de modelo

Por padrão, um erro de estado de modelo não será criado se nenhum valor for encontrado para uma propriedade de modelo. A propriedade é definida como null ou um valor padrão:

  • Tipos simples anuláveis definidos como null.
  • Tipos de valor não anuláveis são definidos como default(T). Por exemplo, um parâmetro int id é definido como 0.
  • Para tipos complexos, o model binding cria uma instância usando o construtor padrão sem definir propriedades.
  • As matrizes são definidas como Array.Empty<T>(), exceto que matrizes byte[] são definidas como null.

Se o estado do modelo precisar ser invalidado quando nada for encontrado em campos de formulário para uma propriedade de modelo, use o atributo [BindRequired].

Observe que este comportamento [BindRequired] se aplica ao model binding de dados de formulário postados, não a dados JSON ou XML em um corpo da solicitação. Dados do corpo da solicitação são tratados pelos formatadores de entrada.

Erros de conversão de tipos

Se uma fonte for encontrada, mas não puder ser convertida no tipo de destino, o estado do modelo será sinalizado como inválido. O parâmetro ou a propriedade de destino é definido como nulo ou um valor padrão, conforme observado na seção anterior.

Em um controlador de API que tem o atributo [ApiController], um estado de modelo inválido resulta em uma resposta HTTP 400 automática.

Em uma página Razor, exiba novamente a página com uma mensagem de erro:

public IActionResult OnPost()
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    _instructorsInMemoryStore.Add(Instructor);
    return RedirectToPage("./Index");
}

Validação no lado do cliente captura a maioria dos dados inválidos que de outra forma seriam enviados a um formulário de páginas Razor. Essa validação torna difícil disparar o código realçado anterior. O aplicativo de exemplo inclui um botão Enviar com Data Inválida que coloca os dados inválidos no campo Data de Contratação e envia o formulário. Esse botão mostra como o código para exibir novamente a página funciona quando ocorrem erros de conversão de dados.

Quando a página é exibida novamente pelo código anterior, a entrada inválida não é mostrada no campo de formulário. Isso ocorre porque a propriedade do modelo foi definida como nulo ou um valor padrão. A entrada inválida aparece em uma mensagem de erro. Porém, se você quiser exibir novamente os dados inválidos no campo de formulário, considere tornar a propriedade do modelo uma cadeia de caracteres e fazer a conversão de dados manualmente.

A mesma estratégia será recomendada se você não desejar que erros de conversão de tipo resultem em erros de estado de modelo. Nesse caso, torne a propriedade de modelo uma cadeia de caracteres.

Tipos simples

Os tipos simples em que o associador de modelos pode converter cadeias de caracteres de origem incluem os seguintes:

Tipos complexos

Um tipo complexo deve ter um construtor padrão público e propriedades públicas graváveis para associar. Quando ocorre model binding, a classe é instanciada usando o construtor padrão público.

Para cada propriedade do tipo complexo, o model binding examina as fontes em busca do nome padrão prefix.property_name. Se nada for encontrado, ela procurará apenas property_name sem o prefixo.

Para associação a um parâmetro, o prefixo é o nome do parâmetro. Para associação a uma propriedade pública PageModel, o prefixo é o nome da propriedade pública. Alguns atributos têm uma propriedade Prefix que permite substituir o uso padrão do nome da propriedade ou do parâmetro.

Por exemplo, suponha que o tipo complexo seja a seguinte classe Instructor:

public class Instructor
{
    public int ID { get; set; }
    public string LastName { get; set; }
    public string FirstName { get; set; }
}

Prefixo = nome do parâmetro

Se o modelo a ser associado for um parâmetro chamado instructorToUpdate:

public IActionResult OnPost(int? id, Instructor instructorToUpdate)

O model binding começa examinando as fontes para a chave instructorToUpdate.ID. Se ela não for encontrada, procurará ID sem um prefixo.

Prefixo = nome da propriedade

Se o modelo a ser associado for uma propriedade chamada Instructor do controlador ou da classe PageModel:

[BindProperty]
public Instructor Instructor { get; set; }

O model binding começa examinando as fontes para a chave Instructor.ID. Se ela não for encontrada, procurará ID sem um prefixo.

Prefixo personalizado

Se o modelo a ser associado for um parâmetro denominado instructorToUpdate e um atributo Bind especificar Instructor como o prefixo:

public IActionResult OnPost(
    int? id, [Bind(Prefix = "Instructor")] Instructor instructorToUpdate)

O model binding começa examinando as fontes para a chave Instructor.ID. Se ela não for encontrada, procurará ID sem um prefixo.

Atributos para destinos de tipo complexo

Vários atributos internos estão disponíveis para controlar o model binding de tipos complexos:

  • [Bind]
  • [BindRequired]
  • [BindNever]

Aviso

Esses atributos afetam o model binding quando os dados de formulário postados são a origem dos valores. Eles não afetam os formatadores de entrada, que processam corpos de solicitação XML e JSON postados. O formatadores de entrada são explicados posteriormente neste artigo.

Atributo [Bind]

Pode ser aplicado a uma classe ou a um parâmetro de método. Especifica quais propriedades de um modelo devem ser incluídas no model binding. [Bind]não afeta formatadores de entrada.

No exemplo a seguir, somente as propriedades especificadas do modelo Instructor são associadas quando qualquer método de ação ou o manipulador é chamado:

[Bind("LastName,FirstMidName,HireDate")]
public class Instructor

No exemplo a seguir, somente as propriedades especificadas do modelo Instructor são associadas quando o método OnPost é chamado:

[HttpPost]
public IActionResult OnPost([Bind("LastName,FirstMidName,HireDate")] Instructor instructor)

O atributo [Bind] pode ser usado para proteção contra o excesso de postagem cenários de criar. Ele não funciona bem em cenários de edição, pois as propriedades excluídas são definidas como nulas ou um valor padrão, em vez de serem deixadas inalteradas. Para defesa contra o excesso de postagem, são recomendados modelos de exibição, em vez do atributo [Bind]. Para obter mais informações, veja Observação de segurança sobre o excesso de postagem.

Atributo [ModelBinder]

ModelBinderAttribute pode ser aplicado a tipos, propriedades ou parâmetros. Ele permite especificar o tipo de associador de modelo usado para associar a instância ou o tipo específico. Por exemplo:

[HttpPost]
public IActionResult OnPost([ModelBinder(typeof(MyInstructorModelBinder))] Instructor instructor)

O atributo [ModelBinder] também pode ser usado para alterar o nome de uma propriedade ou parâmetro quando está sendo associado ao modelo:

public class Instructor
{
    [ModelBinder(Name = "instructor_id")]
    public string Id { get; set; }

    public string Name { get; set; }
}

Atributo [BindRequired]

Somente pode ser aplicado às propriedades de modelo, não aos parâmetros do método. Faz com que o model binding adicione um erro de estado de modelo se a associação não puder ocorrer para a propriedade de um modelo. Veja um exemplo:

public class InstructorWithCollection
{
    public int ID { get; set; }

    [DataType(DataType.Date)]
    [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
    [Display(Name = "Hire Date")]
    [BindRequired]
    public DateTime HireDate { get; set; }

Veja também a discussão sobre o atributo [Required] em Validação do modelo.

Atributo [BindNever]

Somente pode ser aplicado às propriedades de modelo, não aos parâmetros do método. Impede que o model binding configure a propriedade de um modelo. Veja um exemplo:

public class InstructorWithDictionary
{
    [BindNever]
    public int ID { get; set; }

Coleções

Para destinos que são coleções de tipos simples, o model binding procura correspondências para parameter_name ou property_name. Se nenhuma correspondência for encontrada, procurará um dos formatos compatíveis sem o prefixo. Por exemplo:

  • Suponha que o parâmetro a ser associado seja uma matriz chamada selectedCourses:

    public IActionResult OnPost(int? id, int[] selectedCourses)
    
  • Dados de cadeia de caracteres de consulta ou formulário podem estar em um dos seguintes formatos:

    selectedCourses=1050&selectedCourses=2000 
    
    selectedCourses[0]=1050&selectedCourses[1]=2000
    
    [0]=1050&[1]=2000
    
    selectedCourses[a]=1050&selectedCourses[b]=2000&selectedCourses.index=a&selectedCourses.index=b
    
    [a]=1050&[b]=2000&index=a&index=b
    

    Evite associar um parâmetro ou uma propriedade chamada index ou Index se estes forem adjacentes a um valor de coleção. A associação de modelo tenta usar index como o índice para a coleção, o que pode resultar em associação incorreta. Por exemplo, considere a seguinte ação.

    public IActionResult Post(string index, List<Product> products)
    

    No código anterior, o parâmetro de cadeia de caracteres de consulta index associa-se ao parâmetro de método index e também é usado para associar a coleção de produtos. Renomear o parâmetro index ou usar um atributo de associação de modelo para configurar a associação evita esse problema:

    public IActionResult Post(string productIndex, List<Product> products)
    
  • O formato a seguir é compatível apenas com os dados de formulário:

    selectedCourses[]=1050&selectedCourses[]=2000
    
  • Para todos os formatos do exemplo anterior, o model binding passa uma matriz de dois itens para o parâmetro selectedCourses:

    • selectedCourses[0]=1050
    • selectedCourses[1]=2000

    Formatos de dados que usam números subscritos (... [0]... [1]...) devem garantir que eles estejam numerados em sequência, começando com zero. Se houver quaisquer intervalos na numeração de subscrito, todos os itens após o intervalo serão ignorados. Por exemplo, se os subscritos forem 0 e 2, em vez de 0 e 1, o segundo item será ignorado.

Dicionários

Para destinos Dictionary, o model binding procura correspondências para parameter_name ou property_name. Se nenhuma correspondência for encontrada, procurará um dos formatos compatíveis sem o prefixo. Por exemplo:

  • Suponha que o parâmetro de destino seja um Dictionary<int, string> chamado selectedCourses:

    public IActionResult OnPost(int? id, Dictionary<int, string> selectedCourses)
    
  • Os dados de cadeia de caracteres de consulta ou formulário postados podem se parecer com um dos exemplos a seguir:

    selectedCourses[1050]=Chemistry&selectedCourses[2000]=Economics
    
    [1050]=Chemistry&selectedCourses[2000]=Economics
    
    selectedCourses[0].Key=1050&selectedCourses[0].Value=Chemistry&
    selectedCourses[1].Key=2000&selectedCourses[1].Value=Economics
    
    [0].Key=1050&[0].Value=Chemistry&[1].Key=2000&[1].Value=Economics
    
  • Para todos os formatos do exemplo anterior, o model binding passa um dicionário de dois itens para o parâmetro selectedCourses:

    • selectedCourses["1050"]="Chemistry"
    • selectedCourses["2000"]="Economics"

Associação de construtor e tipos de registro

A associação de modelo requer que tipos complexos tenham um construtor sem parâmetros. Os formatadores de entrada baseados em System.Text.Json e Newtonsoft.Json dão suporte à desserialização de classes que não têm um construtor sem parâmetros.

O C# 9 apresenta tipos de registro, que são uma ótima maneira de representar dados de forma sucinta pela rede. O ASP.NET Core adiciona suporte para associação de modelo e validação de tipos de registro com um único construtor:

public record Person([Required] string Name, [Range(0, 150)] int Age, [BindNever] int Id);

public class PersonController
{
   public IActionResult Index() => View();

   [HttpPost]
   public IActionResult Index(Person person)
   {
       ...
   }
}

Person/Index.cshtml:

@model Person

Name: <input asp-for="Name" />
...
Age: <input asp-for="Age" />

Ao validar tipos de registro, o runtime pesquisa metadados de associação e validação especificamente em parâmetros e não em propriedades.

A estrutura permite associar e validar tipos de registro:

public record Person([Required] string Name, [Range(0, 100)] int Age);

Para que o anterior funcione, o tipo deve:

  • Ser um tipo de registro.
  • Ter exatamente um construtor público.
  • Conter parâmetros que tenham uma propriedade com o mesmo nome e tipo. Os nomes não devem ser diferentes por caso.

POCOs sem construtores sem parâmetros

POCOs que não têm construtores sem parâmetros não podem ser associados.

O código a seguir resulta em uma exceção informando que o tipo deve ter um construtor sem parâmetros:

public class Person(string Name)

public record Person([Required] string Name, [Range(0, 100)] int Age)
{
   public Person(string Name) : this (Name, 0);
}

Tipos de registro com construtores criados manualmente

Tipos de registro com construtores criados manualmente que se parecem com construtores primários funcionam

public record Person
{
   public Person([Required] string Name, [Range(0, 100)] int Age) => (this.Name, this.Age) = (Name, Age);

   public string Name { get; set; }
   public int Age { get; set; }
}

Tipos de registro, validação e metadados de associação

Para tipos de registro, os metadados de validação e associação em parâmetros são usados. Todos os metadados nas propriedades são ignorados

public record Person (string Name, int Age)
{
   [BindProperty(Name = "SomeName")] // This does not get used
   [Required] // This does not get used
   public string Name { get; init; }
}

Validação e metadados

A validação usa metadados no parâmetro, mas usa a propriedade para ler o valor. No caso comum com construtores primários, os dois seriam idênticos. No entanto, há maneiras de contornar isso:

public record Person([Required] string Name)
{
   private readonly string _name;
   public Name { get; init => _name = value ?? string.Empty; } // Now this property is never null. However this object could have been constructed as `new Person(null);`
}

TryUpdateModel não atualiza parâmetros em um tipo de registro

public record Person(string Name)
{
   public int Age { get; set; }
}

var person = new Person("initial-name");
TryUpdateModel(person, ...);

Nesse caso, o MVC não tentará associar Name novamente. No entanto, Age pode ser atualizado

Comportamento de globalização de dados de rota de associação de modelo e cadeias de caracteres de consulta

O provedor de valor de rota ASP.NET Core e o provedor de valor da cadeia de caracteres de consulta:

  • Tratar valores como cultura invariável.
  • Espere que as URLs sejam invariáveis à cultura.

Por outro lado, os valores provenientes de dados de formulário passam por uma conversão sensível à cultura. Isso é esperado, para que as URLs sejam compartilháveis entre localidades.

Para que o provedor de valor de rota ASP.NET Core e o provedor de valor da cadeia de caracteres de consulta passem por uma conversão sensível à cultura:

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews(options =>
    {
        var index = options.ValueProviderFactories.IndexOf(
            options.ValueProviderFactories.OfType<QueryStringValueProviderFactory>().Single());
        options.ValueProviderFactories[index] = new CulturedQueryStringValueProviderFactory();
    });
}
public class CulturedQueryStringValueProviderFactory : IValueProviderFactory
{
    public Task CreateValueProviderAsync(ValueProviderFactoryContext context)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        var query = context.ActionContext.HttpContext.Request.Query;
        if (query != null && query.Count > 0)
        {
            var valueProvider = new QueryStringValueProvider(
                BindingSource.Query,
                query,
                CultureInfo.CurrentCulture);

            context.ValueProviders.Add(valueProvider);
        }

        return Task.CompletedTask;
    }
}

Tipos de dados especiais

Há alguns tipos de dados especiais com o model binding pode lidar.

IFormFile e IFormFileCollection

Um arquivo carregado incluído na solicitação HTTP. Também compatível com IEnumerable<IFormFile> para vários arquivos.

CancellationToken

Opcionalmente, as ações podem associar um CancellationToken como um parâmetro. Isso associa RequestAborted que sinaliza quando a conexão subjacente à solicitação HTTP é anulada. As ações podem usar esse parâmetro para cancelar operações assíncronas de execução prolongada executadas como parte das ações do controlador.

FormCollection

Usado para recuperar todos os valores dos dados de formulário postados.

Formatadores de entrada

Dados no corpo da solicitação podem estar em JSON, XML ou algum outro formato. Para analisar esses dados, o model binding usa um formatador de entrada configurado para lidar com um determinado tipo de conteúdo. Por padrão, o ASP.NET Core inclui formatadores de entrada baseados em JSON para lidar com os dados JSON. Você pode adicionar outros formatadores para outros tipos de conteúdo.

O ASP.NET Core seleciona formatadores de entrada com base no atributo Consome. Se nenhum atributo estiver presente, ele usará o cabeçalho Content-Type.

Para usar os formatadores de entrada XML internos:

  • Instale o pacote do NuGet Microsoft.AspNetCore.Mvc.Formatters.Xml.

  • Em Startup.ConfigureServices, chame AddXmlSerializerFormatters ou AddXmlDataContractSerializerFormatters.

    services.AddRazorPages()
        .AddMvcOptions(options =>
    {
        options.ValueProviderFactories.Add(new CookieValueProviderFactory());
        options.ModelMetadataDetailsProviders.Add(
            new ExcludeBindingMetadataProvider(typeof(System.Version)));
        options.ModelMetadataDetailsProviders.Add(
            new SuppressChildValidationMetadataProvider(typeof(System.Guid)));
    })
    .AddXmlSerializerFormatters();
    
  • Aplique o atributo Consumes às classes de controlador ou aos métodos de ação que devem esperar XML no corpo da solicitação.

    [HttpPost]
    [Consumes("application/xml")]
    public ActionResult<Pet> Create(Pet pet)
    

    Para obter mais informações, veja Introdução à serialização XML.

Personalizar a associação de modelo com formatadores de entrada

Um formatador de entrada assume total responsabilidade pela leitura de dados do corpo da solicitação. Para personalizar esse processo, configure as APIs usadas pelo formatador de entrada. Esta seção descreve como personalizar o formatador de entrada baseado em System.Text.Json para entender um tipo personalizado chamado ObjectId.

Considere o seguinte modelo, que contém uma propriedade personalizada ObjectId chamada Id:

public class ModelWithObjectId
{
    public ObjectId Id { get; set; }
}

Para personalizar o processo de associação de modelo ao usar System.Text.Json, crie uma classe derivada de JsonConverter<T>:

internal class ObjectIdConverter : JsonConverter<ObjectId>
{
    public override ObjectId Read(
        ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        return new ObjectId(JsonSerializer.Deserialize<int>(ref reader, options));
    }

    public override void Write(
        Utf8JsonWriter writer, ObjectId value, JsonSerializerOptions options)
    {
        writer.WriteNumberValue(value.Id);
    }
}

Para usar um conversor personalizado, aplique o atributo JsonConverterAttribute ao tipo. No exemplo a seguir, o tipo ObjectId é configurado com ObjectIdConverter como conversor personalizado:

[JsonConverter(typeof(ObjectIdConverter))]
public struct ObjectId
{
    public ObjectId(int id) =>
        Id = id;

    public int Id { get; }
}

Para obter mais informações, confira Como gravar conversores personalizados.

Excluir tipos especificados do model binding

O comportamento do sistema de validação e model binding é orientado pelo ModelMetadata. Você pode personalizar ModelMetadata adicionando um provedor de detalhes MvcOptions.ModelMetadataDetailsProviders. Provedores de detalhes internos estão disponíveis para desabilitar o model binding ou a validação para tipos especificados.

Para desabilitar o model binding em todos os modelos de um tipo especificado, adicione um ExcludeBindingMetadataProvider em Startup.ConfigureServices. Por exemplo, para desabilitar o model binding em todos os modelos do tipo System.Version:

services.AddRazorPages()
    .AddMvcOptions(options =>
{
    options.ValueProviderFactories.Add(new CookieValueProviderFactory());
    options.ModelMetadataDetailsProviders.Add(
        new ExcludeBindingMetadataProvider(typeof(System.Version)));
    options.ModelMetadataDetailsProviders.Add(
        new SuppressChildValidationMetadataProvider(typeof(System.Guid)));
})
.AddXmlSerializerFormatters();

Para desabilitar a validação nas propriedades de um tipo especificado, adicione um SuppressChildValidationMetadataProvider em Startup.ConfigureServices. Por exemplo, para desabilitar a validação nas propriedades do tipo System.Guid:

services.AddRazorPages()
    .AddMvcOptions(options =>
{
    options.ValueProviderFactories.Add(new CookieValueProviderFactory());
    options.ModelMetadataDetailsProviders.Add(
        new ExcludeBindingMetadataProvider(typeof(System.Version)));
    options.ModelMetadataDetailsProviders.Add(
        new SuppressChildValidationMetadataProvider(typeof(System.Guid)));
})
.AddXmlSerializerFormatters();

Associadores de modelos personalizados

Você pode estender o model binding escrevendo um associador de modelos personalizado e usando o atributo [ModelBinder] para selecioná-la para um determinado destino. Saiba mais sobre o model binding personalizado.

Model binding manual

O model binding pode ser invocado manualmente usando o método TryUpdateModelAsync. O método é definido nas classes ControllerBase e PageModel. Sobrecargas de método permitem que você especifique o provedor de valor e prefixo a ser usado. O método retornará false se o model binding falhar. Veja um exemplo:

if (await TryUpdateModelAsync<InstructorWithCollection>(
    newInstructor,
    "Instructor",
    i => i.FirstMidName, i => i.LastName, i => i.HireDate))
{
    _instructorsInMemoryStore.Add(newInstructor);
    return RedirectToPage("./Index");
}
PopulateAssignedCourseData(newInstructor);
return Page();

TryUpdateModelAsync usa provedores de valor para obter dados do corpo do formulário, da cadeia de caracteres de consulta e dos dados de rota. TryUpdateModelAsync normalmente é:

  • Usado com páginas Razor e aplicativos MVC usando controladores e exibições para evitar o excesso de postagem.
  • Não usado com uma API Web, a menos que seja consumido de dados de formulário, cadeias de caracteres de consulta e dados de rota. Os pontos de extremidade da API Web que consomem JSON usam formatadores de entrada para desserializar o corpo da solicitação em um objeto.

Para obter mais informações, confira TryUpdateModelAsync.

Atributo [FromServices]

O nome do atributo segue o padrão dos atributos de model binding que especificam uma fonte de dados. Porém, não se trata de associar dados de um provedor de valor. Ele obtém uma instância de um tipo do contêiner de injeção de dependência. Sua finalidade é oferecer uma alternativa à injeção de construtor para quando você precisa de um serviço somente se um determinado método for chamado.

Recursos adicionais