Descomplicando o Model Binding no ASP.NET Core
O model binding é o mecanismo do ASP.NET Core que pega os dados de uma requisição HTTP e os transforma em parâmetros e objetos .NET prontos para uso. Neste artigo, você vai conhecer as principais formas de manipular essas informações com os atributos [Bind], [FromQuery], [FromRoute], [FromForm] e [FromBody].
O que é model binding no ASP.NET Core
Controllers e Razor Pages trabalham com dados que chegam em requisições HTTP. Um id pode vir na rota, um filtro pode vir na query string e um cadastro inteiro pode vir em um formulário ou em um JSON.
Escrever código para ler cada valor e convertê-lo de string para o tipo certo seria tedioso e propenso a erros. Por isso, o model binding automatiza esse processo.
Segundo a documentação oficial de model binding, esse sistema realiza quatro tarefas:
- Recupera dados de várias fontes, como rota, campos de formulário e query string
- Entrega esses dados aos controllers e Razor Pages como parâmetros de métodos e propriedades públicas
- Converte strings em tipos .NET
- Atualiza as propriedades de tipos complexos
Como o model binding funciona passo a passo
Considere uma action que recebe um id e um filtro booleano. A requisição chega com a URL https://contoso.com/api/products/2?activeOnly=true.
[HttpGet("{id}")]
public ActionResult<Product> GetById(int id, bool activeOnly)
=> throw new NotImplementedException();
Depois que o roteamento escolhe a action, o model binding procura o primeiro parâmetro, id. Em seguida, encontra o valor "2" nos dados de rota e o converte para o inteiro 2.
Então, ele passa para o parâmetro activeOnly. Dessa vez, o valor vem da query string, e a comparação de nomes não diferencia maiúsculas de minúsculas.
Por fim, o framework chama o método GetById com os valores já convertidos. Além disso, o resultado do binding e eventuais erros ficam registrados em ModelState.
De onde o model binding busca os dados
Por padrão, o model binding procura pares de chave e valor nestas fontes, nesta ordem:
- Campos de formulário
- Corpo da requisição, em controllers com
[ApiController] - Dados de rota
- Parâmetros da query string
- Arquivos enviados
Existem, porém, algumas exceções. Dados de rota e query string servem apenas para tipos simples, e arquivos só chegam a parâmetros do tipo IFormFile ou IEnumerable<IFormFile>.
Quando a fonte padrão não é a correta, você indica a origem explicitamente com um atributo. É exatamente isso que veremos nas próximas seções.
Bind: obtendo propriedades específicas
Durante o desenvolvimento, pode surgir a necessidade de obter apenas algumas propriedades de um objeto e ignorar as outras. Para isso, usamos o atributo [Bind].
[HttpPost]
public IActionResult OnPost([Bind("Name,Email,IsActive")] User user)
=> throw new NotImplementedException();
No código acima, o método OnPost recebe um usuário como parâmetro. Como só temos interesse em três campos, declaramos esses nomes no [Bind].
Com isso, o model binding preenche apenas Name, Email e IsActive. As demais propriedades ficam nulas ou recebem o valor padrão do tipo.
O atributo também pode decorar a própria classe. Nesse caso, a restrição vale para qualquer action que receba aquele tipo.
[Bind("Name,Email,IsActive")]
public class User
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public bool IsActive { get; set; }
public DateTime? LastUpdateDate { get; set; }
}
Por que o Bind não serve para edição
Esse comportamento com as propriedades não listadas faz do [Bind] uma opção ruim para edição. Ele não mantém o valor anterior das propriedades ignoradas, que passam a valer nulo ou o padrão do tipo.
Imagine um formulário de edição que envia só o nome e o e-mail. Se você salvar o objeto recebido, LastUpdateDate e outras colunas serão sobrescritas com valores vazios.
Por outro lado, o [Bind] ajuda a evitar overposting em cenários de criação. Overposting acontece quando o cliente envia campos que não deveria, como um IsAdmin escondido.
Ainda assim, a Microsoft recomenda view models no lugar do [Bind] para essa proteção. Um DTO com apenas os campos permitidos deixa o contrato explícito, e o artigo sobre boas práticas de segurança em APIs ASP.NET Core aprofunda esse cuidado.
Há outro detalhe importante: o [Bind] atua sobre dados de formulário. Ele não afeta os input formatters, que processam JSON e XML no corpo da requisição.
FromQuery: model binding com a query string
Antes de obter valores da query string, vale deixar claro o que ela é. A query string, ou cadeia de caracteres de consulta, é um modelo para atribuir valores a parâmetros diretamente na URL.
Anatomia de uma query string
Observe a URL de exemplo abaixo:
https://yourdomain.com/users/search?Name=John&Email=mail@mail.com&IsActive=false
O trecho que começa em ? é a query string. Depois do sinal de interrogação, os parâmetros seguem o formato Parametro=valor e ficam separados por &.
Vinculando a query string a um objeto
Para recuperar esses dados e convertê-los em um objeto do tipo User, usamos o atributo [FromQuery].
[HttpGet("search")]
public IActionResult Search([FromQuery] User user)
=> throw new NotImplementedException();
Com esse código, o model binding procura na query string parâmetros com o mesmo nome das propriedades de User. Em seguida, converte cada valor para o tipo da propriedade correspondente.
Na prática, o [FromQuery] aparece muito em filtros e paginação. Um objeto UserFilter com Page, PageSize e Name deixa a assinatura da action limpa.
Você também pode renomear o parâmetro esperado. Por exemplo, [FromQuery(Name = "q")] string term lê o valor de ?q=balta e o entrega na variável term.
FromRoute: valores dos dados de rota
Aqui também recuperamos dados da URL, mas de uma forma diferente do [FromQuery]. Em vez de pares de chave e valor, o dado faz parte do próprio caminho.
https://yourdomain.com/users/mail@mail.com
Para recuperar o endereço de e-mail informado nessa URL, usamos o [FromRoute].
[HttpGet("users/{email}")]
public IActionResult GetByEmail([FromRoute] string email)
=> throw new NotImplementedException();
O atributo [HttpGet] recebe um modelo de rota com o segmento {email}. Então, o model binding associa esse segmento ao parâmetro de mesmo nome.
Também é possível refinar a definição da rota com restrições, como {id:int} ou {slug:minlength(3)}. Esse assunto merece um artigo próprio, então aqui mantemos o foco no model binding.
FromForm: campos de formulários postados
Como o nome sugere, o [FromForm] recupera valores de um formulário enviado. Seguindo o mesmo padrão dos atributos anteriores, os nomes dos campos precisam coincidir com os nomes das propriedades.
[HttpPost]
public IActionResult Create([FromForm] User user)
=> throw new NotImplementedException();
No HTML, cada input precisa ter o atributo name igual à propriedade correspondente. Se quiser revisar esse lado da história, o artigo sobre formulários no HTML cobre os campos e seus atributos.
<form method="post" action="/users">
<input name="Name" />
<input name="Email" type="email" />
<input name="IsActive" type="checkbox" value="true" />
<button type="submit">Save</button>
</form>
O [FromForm] é característico de requisições que enviam o conteúdo como application/x-www-form-urlencoded ou multipart/form-data. Por isso, ele quase sempre acompanha o [HttpPost], porque formulários HTML só enviam dados no corpo com o método POST.
Além disso, o [FromForm] também recebe arquivos. Um parâmetro IFormFile avatar lê o arquivo enviado em um campo name="avatar" de um formulário multipart/form-data.
FromBody: model binding com o corpo da requisição
Quando a requisição envia o conteúdo em um formato como JSON, você usa o [FromBody] para recuperar as informações. Veja um exemplo de entrada:
{
"name": "John Doe",
"email": "mail@mail.com"
}
A declaração no código para ler esses valores fica assim:
[HttpPost]
public ActionResult<User> Create([FromBody] User user)
=> throw new NotImplementedException();
Nesse caso, o model binding delega a leitura do corpo a um input formatter. Por padrão, o ASP.NET Core usa o System.Text.Json para desserializar o JSON.
O ASP.NET Core configura o System.Text.Json com os padrões web. Na prática, isso significa nomes em camelCase e comparação de propriedades sem diferenciar maiúsculas de minúsculas.
Por isso, a chave name do JSON preenche a propriedade Name da classe sem nenhuma configuração extra. Se o cliente usar outro padrão de nomes, ajuste as opções do serializador ou use [JsonPropertyName] na propriedade.
Cuidados com o FromBody
Use o [FromBody] em no máximo um parâmetro por action. Depois que o input formatter lê o stream da requisição, ele não fica disponível para uma segunda leitura.
Outro ponto pega muita gente de surpresa. Quando o [FromBody] decora um tipo complexo, o framework ignora atributos de fonte aplicados às propriedades dele, como um [FromQuery] em Email.
Isso acontece porque os input formatters leem apenas o corpo e não entendem atributos de binding. Da mesma forma, [Bind], [BindRequired] e [BindNever] não têm efeito sobre JSON.
Outras fontes do model binding
Além dos atributos clássicos, o ASP.NET Core oferece mais duas fontes muito usadas. O [FromHeader] lê valores de headers HTTP, e o [FromServices] injeta um serviço registrado no container.
[HttpGet]
public IActionResult GetGreeting(
[FromHeader(Name = "Accept-Language")] string language,
[FromServices] ILogger<UsersController> logger)
{
logger.LogInformation("Requested language: {Language}", language);
return Ok();
}
O parâmetro Name resolve casos em que o nome na requisição não pode virar um identificador C#. Um header com hífen, como Accept-Language, é o exemplo clássico.
Se as fontes nativas não bastam, você pode criar um value provider próprio. A documentação mostra, por exemplo, um provider que lê valores de cookies a partir das interfaces IValueProvider e IValueProviderFactory.
Model binding com ApiController
Em controllers marcados com [ApiController], você raramente precisa escrever esses atributos. Isso acontece porque o framework infere a fonte de cada parâmetro.
A inferência segue regras simples:
- Tipos complexos registrados no container de injeção de dependência viram
[FromServices] - Os demais tipos complexos viram
[FromBody] - Parâmetros
IFormFileeIFormFileCollectionviram[FromForm] - Nomes que aparecem no template da rota viram
[FromRoute] - Os demais parâmetros simples viram
[FromQuery]
Portanto, o model binding de uma API moderna costuma funcionar sem atributo algum. Ainda assim, declarar a fonte deixa o código explícito e evita surpresas quando alguém altera a rota.
Além disso, o [ApiController] devolve automaticamente um 400 quando o ModelState fica inválido. Dessa forma, erros de conversão e de validação chegam ao cliente antes de executar a action.
Model binding de tipos complexos e coleções
Até aqui, vimos de onde vêm os dados. Agora, vale entender como o model binding monta objetos e listas a partir de pares de chave e valor.
Tipos complexos e prefixos
Um tipo complexo precisa de um construtor público sem parâmetros e de propriedades públicas com setter. Primeiro, o model binding cria a instância; depois, preenche cada propriedade.
Para cada propriedade, ele procura uma chave no formato prefixo.propriedade. Quando o binding acontece em um parâmetro, o prefixo é o nome do parâmetro.
[HttpPost]
public IActionResult Update(int? id, User userToUpdate)
=> throw new NotImplementedException();
Nesse exemplo, o model binding busca primeiro a chave userToUpdate.Name. Se não encontrar, tenta apenas Name, sem o prefixo.
Você também pode definir um prefixo próprio com [Bind(Prefix = "User")]. Assim, o formulário pode enviar User.Name mesmo que o parâmetro tenha outro nome.
Coleções no model binding
Para coleções de tipos simples, o model binding aceita vários formatos de entrada. Considere uma action que recebe um array de ids de cursos:
[HttpPost]
public IActionResult Enroll(int? id, int[] selectedCourses)
=> throw new NotImplementedException();
Tanto a query string quanto o formulário podem enviar selectedCourses=1050&selectedCourses=2000. O formato com índices, selectedCourses[0]=1050&selectedCourses[1]=2000, também funciona.
Os índices numéricos precisam começar em zero e seguir sem lacunas. Se a sequência pular de 0 para 2, por exemplo, o framework ignora todos os itens depois da lacuna.
Outro cuidado: evite um parâmetro chamado index ao lado de uma coleção. O model binding tenta usar esse valor como índice da lista, o que pode gerar um binding incorreto.
Quando o model binding não encontra valores
Por padrão, a ausência de um valor não gera erro no ModelState. Nesse caso, o model binding aplica valores padrão:
- Tipos simples anuláveis recebem
null - Um tipo de valor não anulável recebe
default(T), então umint idvale 0 - Tipos complexos ganham uma instância criada pelo construtor padrão, sem propriedades preenchidas
- Arrays recebem
Array.Empty<T>(), excetobyte[], que ficanull
Quando um valor é obrigatório em formulários, use o [BindRequired]. Ele adiciona um erro ao ModelState se o model binding não encontrar a propriedade.
Por outro lado, quando o valor existe mas não pode ser convertido, o ModelState fica inválido. Um texto "abc" enviado para um int, por exemplo, gera um erro de conversão.
Já o [BindNever] faz o caminho oposto. Ele impede que o model binding preencha uma propriedade, o que protege campos como Id em cenários de formulário.
Conclusão
O model binding elimina boa parte do código repetitivo que lê e converte dados de requisições. Com os atributos certos, você define com precisão de onde cada valor vem.
Em resumo, o [Bind] limita propriedades em formulários, o [FromQuery] lê a query string e o [FromRoute] lê segmentos da URL. Já o [FromForm] atende formulários postados, e o [FromBody] cuida de JSON e XML.
Entender essas fontes também ajuda a diagnosticar bugs. Quando um parâmetro chega vazio, quase sempre o model binding está procurando o valor no lugar errado.
Continue lendo
Para seguir estudando ASP.NET Core, veja como configurar logs no ASP.NET com Serilog. Em seguida, pratique tudo isso criando uma API do zero com .NET Core 3.1 e EF Core.
FAQ
O que é model binding no ASP.NET Core?
É o mecanismo que lê dados da rota, da query string, de formulários, de headers e do corpo da requisição. Em seguida, ele converte esses dados em parâmetros e objetos .NET.
Qual a diferença entre FromQuery e FromRoute?
O [FromQuery] lê pares de chave e valor depois do ? na URL. Já o [FromRoute] lê segmentos do próprio caminho definidos no template da rota.
Por que o Bind não funciona com JSON?
O [Bind] atua sobre dados de formulário, e o JSON passa por input formatters que ignoram esse atributo. Para JSON, use um DTO com apenas os campos permitidos.
Posso usar FromBody em dois parâmetros?
Não, porque o corpo da requisição só pode ser lido uma vez. Agrupe os dados em um único objeto e receba esse objeto com [FromBody].
Preciso escrever FromBody em controllers com ApiController?
Não é obrigatório, já que o [ApiController] infere [FromBody] para tipos complexos. Mesmo assim, declarar a fonte deixa o código mais explícito.