Formulários e validação: o site recebe dados

Formulários e validação: o site recebe dados

A vitrine da Livraria passa a receber dados: busca por GET com URL compartilhável, cotação de frete com validação no servidor e no navegador, proteção automática contra CSRF, Post/Redirect/Get com TempData contra envios duplicados e erros da API exibidos junto ao campo certo.
Linguagem C#

• • 22 min de leitura

Um formulário HTML é o jeito mais antigo e mais robusto de um site receber dados: funciona em qualquer navegador, com ou sem JavaScript. Neste artigo a Livraria ganha três formulários, e com eles o binding de dados postados, a validação nos dois lados, a proteção contra CSRF e o padrão que evita envios duplicados.

Três formulários, três lições

Formulário Verbo Onde Lição principal
Busca no catálogo GET vitrine formulários que não alteram nada vão por GET
Cotação de frete POST página do livro validação, reexibição com erros, handlers nomeados
"Avise-me quando chegar" POST livro esgotado Post/Redirect/Get, consentimento, erros vindos da API

A busca: um formulário GET

Uma busca não altera nada no servidor, então é uma leitura, e leituras são GET, pela mesma semântica de "A API da Livraria". Um formulário GET coloca os campos na query string:

<form method="get" role="search" class="busca">
    <label asp-for="Termo">Buscar livros</label>
    <input asp-for="Termo" type="search" placeholder="título ou autor" />
    <button type="submit">Buscar</button>
</form>
public sealed class IndexModel(ICatalogoDaLoja catalogo) : PageModel
{
    [BindProperty(SupportsGet = true)]
    [StringLength(100)]
    public string? Termo { get; set; }

    public PaginaDe<ProdutoResposta> Vitrine { get; private set; } = default!;

    public async Task OnGetAsync(int pagina = 1, CancellationToken ct = default)
    {
        var termo = ModelState.IsValid ? Termo?.Trim() : null;
        Vitrine = await catalogo.ListarAsync(Math.Max(pagina, 1), 12, termo, ct);
    }
}

O resultado é uma URL como /?Termo=machado, que pode ser compartilhada, favoritada e indexada. Um formulário POST para busca produziria uma página sem endereço, e o botão Voltar mostraria o aviso "reenviar formulário?".

[BindProperty] liga uma propriedade do PageModel aos dados da requisição. Por padrão só em POST, e SupportsGet = true estende o binding ao GET. Os links de paginação passam a carregar o termo com asp-route-Termo="@Model.Termo".

Do lado da API, GET /v1/produtos ganha o parâmetro opcional termo, e cada repositório busca com o recurso do seu banco: unaccent com ILIKE no PostgreSQL, a collation ai_ci no MySQL e o índice de texto no Mongo, todos vistos na Fase 5. É por isso que "memorias postumas" encontra "Memórias Póstumas".

O modelo de entrada da cotação

Na página do livro, o visitante informa o CEP e a quantidade e vê o frete. A entrada tem uma classe própria, com os mesmos atributos de validação de "Validação, ProblemDetails e OpenAPI":

public sealed class EntradaDeFrete
{
    [Required(ErrorMessage = "Informe o CEP.")]
    [RegularExpression(@"^\d{5}-?\d{3}$", ErrorMessage = "Use o formato 00000-000.")]
    [Display(Name = "CEP")]
    public string Cep { get; set; } = "";

    [Range(1, 20, ErrorMessage = "Escolha entre 1 e 20 exemplares.")]
    [Display(Name = "Quantidade")]
    public int Quantidade { get; set; } = 1;
}

Aqui a classe tem setters públicos e construtor sem parâmetros, ao contrário dos records da API. O model binding de formulários do Razor Pages cria o objeto vazio e preenche propriedade por propriedade. [Display(Name = ...)] define o rótulo que os tag helpers vão gerar e o nome que aparece nas mensagens padrão.

A marcação: tag helpers de formulário

<section aria-labelledby="titulo-frete" class="frete">
    <h2 id="titulo-frete">Calcule o frete</h2>

    <form method="post" asp-page-handler="Cotar" asp-route-slug="@Model.Slug" novalidate>
        <div asp-validation-summary="ModelOnly" role="alert"></div>

        <label asp-for="Frete.Cep"></label>
        <input asp-for="Frete.Cep" inputmode="numeric" autocomplete="postal-code" />
        <span asp-validation-for="Frete.Cep" class="erro"></span>

        <label asp-for="Frete.Quantidade"></label>
        <input asp-for="Frete.Quantidade" min="1" max="20" />
        <span asp-validation-for="Frete.Quantidade" class="erro"></span>

        <button type="submit">Calcular</button>
    </form>

    @if (Model.Cotacao is { } c)
    {
        <p role="status">
            @c.Transportadora: <strong>@c.Valor.ToString("C")</strong>,
            em até @c.PrazoDiasUteis dias úteis.
        </p>
    }
</section>

Cada tag helper gera bastante marcação a partir de uma linha:

  • <label asp-for> escreve o texto do [Display] e o atributo for apontando para o id do campo. Um leitor de tela anuncia "CEP, campo de edição", e clicar no rótulo foca o campo.
  • <input asp-for> gera id, name="Frete.Cep", o type a partir do tipo C# (int vira type="number"), o valor atual e os atributos data-val-* que a validação no navegador lê.
  • <span asp-validation-for> exibe a mensagem de erro daquele campo.
  • asp-validation-summary="ModelOnly" exibe os erros que não pertencem a um campo específico, como "serviço de frete indisponível".
  • <form method="post"> acrescenta um campo oculto __RequestVerificationToken, que é o assunto da seção sobre CSRF.
  • asp-page-handler="Cotar" faz o formulário postar para o handler OnPostCotarAsync. Uma página pode ter vários formulários, cada um com seu handler.

Os atributos inputmode="numeric" e autocomplete="postal-code" não têm nada de ASP.NET. São HTML puro: o celular abre o teclado numérico e o navegador oferece o CEP salvo. Pequenos detalhes de formulário decidem se alguém termina uma compra no celular.

O handler: validar, reexibir, agir

public sealed class DetalheModel(ICatalogoDaLoja catalogo, ICotadorDeFrete cotador) : PageModel
{
    public ProdutoResposta Produto { get; private set; } = default!;
    public string Slug { get; private set; } = "";

    public EntradaDeFrete Frete { get; set; } = new();
    public EntradaDeAviso Aviso { get; set; } = new();
    public CotacaoResposta? Cotacao { get; private set; }

    [TempData]
    public string? Mensagem { get; set; }

    public async Task<IActionResult> OnGetAsync(int id, string? slug, CancellationToken ct)
    {
        // igual ao artigo anterior: carrega, 404 ou redireciona para o slug canônico
    }

    public async Task<IActionResult> OnPostCotarAsync(
        int id,
        [Bind(Prefix = nameof(Frete))] EntradaDeFrete frete,
        CancellationToken ct)
    {
        if (!await CarregarAsync(id, ct))
            return NotFound();

        Frete = frete;

        if (!ModelState.IsValid)
            return Page();

        var resultado = await cotador.CotarAsync(frete.Cep, id, frete.Quantidade, ct);

        switch (resultado)
        {
            case ResultadoDeCotacao.Sucesso s:
                Cotacao = s.Cotacao;
                break;
            case ResultadoDeCotacao.Invalida inv:
                ModelState.AddModelError("Frete.Cep", inv.Mensagem);
                break;
            case ResultadoDeCotacao.Indisponivel:
                ModelState.AddModelError(string.Empty,
                    "Não foi possível calcular o frete agora. Tente novamente em instantes.");
                break;
        }

        return Page();
    }

    private async Task<bool> CarregarAsync(int id, CancellationToken ct)
    {
        var produto = await catalogo.ObterAsync(id, ct);
        if (produto is null) return false;
        Produto = produto;
        Slug = Slugs.De(produto.Titulo);
        return true;
    }
}

Há três pontos que tropeçam quem está começando.

O POST precisa recarregar a página inteira. O HTTP não guarda estado. A requisição POST não sabe nada do GET que exibiu a página antes: o Produto não está lá. Se o handler só calculasse o frete e chamasse Page(), o template quebraria em Model.Produto.Titulo. Por isso CarregarAsync roda em todos os handlers. Não há como pular essa etapa, só evitar repeti-la escrevendo-a uma vez.

ModelState é o registro de tudo o que deu errado no binding e na validação. Quando o formulário chega, o framework tenta converter cada campo (texto "abc" num int gera um erro de conversão), executa os atributos de validação e anota cada problema com a chave do campo (Frete.Cep). ModelState.IsValid é falso se houver qualquer anotação. AddModelError permite acrescentar erros próprios: com a chave de um campo, a mensagem aparece junto ao campo; com a chave vazia, aparece no resumo.

return Page() depois de um erro reexibe o formulário com o que o usuário digitou. Os tag helpers leem os valores do ModelState, e não só do modelo, e por isso até um valor inválido, como "abc" no campo de quantidade, volta ao campo para ser corrigido, em vez de sumir.

Por que [Bind(Prefix)] e não [BindProperty]

A página tem dois formulários: frete e aviso. Se Frete e Aviso fossem [BindProperty], ambos seriam preenchidos e validados a cada POST, qualquer que fosse o formulário enviado. Postar o frete deixaria Aviso.Email vazio, o [Required] dele falharia, e ModelState.IsValid seria falso para sempre, com uma mensagem "informe o e-mail" aparecendo no formulário que ninguém tocou. Receber cada entrada como parâmetro do seu handler, com o prefixo que corresponde aos nomes gerados (Frete.Cep), faz o framework ligar e validar só o que foi enviado. As propriedades sem atributo continuam servindo para a exibição.

[BindProperty] num modelo grande também é o caminho clássico para o overposting de "A API da Livraria": qualquer propriedade pública com setter no modelo ligado pode ser preenchida por um campo extra forjado no formulário. Um modelo de entrada que contém só os campos do formulário elimina o risco pela mesma razão que o DTO eliminava na API.

Validação no navegador

A validação no servidor é a que vale, porque qualquer um pode enviar um POST sem passar pelo navegador. Mesmo assim, avisar "CEP inválido" antes de uma ida e volta à rede melhora muito a experiência. O template já traz a validação unobtrusive: os atributos data-val-* gerados a partir dos atributos C# são lidos pelas bibliotecas jQuery Validation e jQuery Validation Unobtrusive, que validam ao sair do campo e ao enviar:

@section Scripts {
    <partial name="_ValidationScriptsPartial" />
}

[Required], [StringLength], [Range], [RegularExpression], [EmailAddress] e [Compare] têm equivalentes no navegador. Atributos próprios, como o [Isbn] de "Validação, ProblemDetails e OpenAPI", só validam no servidor, a menos que se escreva o adaptador JavaScript correspondente. Isso é aceitável: o servidor pega o erro e a página volta com a mensagem.

O novalidate no formulário desliga a validação nativa do HTML5 (os balões do required e do type="email"), para que o usuário não receba dois estilos de mensagem ao mesmo tempo. A alternativa, sem jQuery, é usar só a validação nativa do navegador, que cobre bem os casos simples.

A armadilha da vírgula. O jQuery Validation valida números com ponto decimal. Num site em português, "39,90" num campo decimal é marcado como inválido no navegador, embora o servidor, com UseRequestLocalization("pt-BR") do artigo anterior, o aceitasse. Os formulários desta página só têm inteiros, mas qualquer campo de preço num formulário como estes vai precisar lidar com isso: ou substituindo o método number do validador por uma versão que aceite vírgula, ou usando type="text" com inputmode="decimal" e deixando a validação desse campo para o servidor.

CSRF: o token escondido

CSRF (Cross-Site Request Forgery) é o ataque em que um site malicioso faz o navegador da vítima enviar um formulário para outro site onde ela está logada:

<!-- em site-malicioso.exemplo -->
<form action="https://loja.livraria.exemplo/conta/endereco" method="post">
    <input type="hidden" name="Cep" value="endereco-do-atacante" />
</form>
<script>document.forms[0].submit()</script>

Se a loja autentica por cookie, o navegador anexa o cookie da vítima automaticamente, e a loja vê uma requisição legítima. O CORS não ajuda: ele impede o site malicioso de ler a resposta, mas o POST foi executado mesmo assim.

A defesa é o token antiforgery. O servidor gera um valor aleatório, coloca-o num cookie e num campo oculto do formulário, e na chegada do POST confere se os dois correspondem. O site malicioso não consegue ler nem o cookie nem a página da loja, e por isso não tem como forjar o campo. Nas Razor Pages a proteção é automática: o <form method="post"> gera o campo, e todo handler POST valida o token, respondendo 400 se ele faltar ou não conferir. Não é preciso escrever nada, desde que não se desligue a proteção.

O atributo SameSite=Lax, padrão dos cookies de autenticação do ASP.NET Core, já impede o navegador de enviar o cookie nesse tipo de POST vindo de outro site, e isso é uma segunda camada. O token continua necessário para navegadores antigos e para cenários em que SameSite não se aplica, como subdomínios do mesmo site. A API da Fase 6 não precisa de token antiforgery porque se autentica por cabeçalho Authorization, que o navegador nunca anexa sozinho. É o mesmo raciocínio do exercício 1 de "Produção: CORS, rate limiting, versionamento e OpenTelemetry".

"Avise-me quando chegar": Post/Redirect/Get

Quando o livro está esgotado, a parcial _Disponibilidade mostra um segundo formulário:

public sealed class EntradaDeAviso
{
    [Required(ErrorMessage = "Informe seu e-mail.")]
    [EmailAddress(ErrorMessage = "E-mail inválido.")]
    [StringLength(254)]
    [Display(Name = "Seu e-mail")]
    public string Email { get; set; } = "";

    [Range(typeof(bool), "true", "true",
        ErrorMessage = "Precisamos da sua autorização para enviar o aviso.")]
    [Display(Name = "Autorizo a Livraria a me enviar um único e-mail quando este livro chegar.")]
    public bool Consentimento { get; set; }

    // armadilha para robôs: campo invisível que humanos não preenchem
    public string? Site { get; set; }
}
<form method="post" asp-page-handler="AviseMe" asp-route-slug="@Model.Slug">
    <label asp-for="Aviso.Email"></label>
    <input asp-for="Aviso.Email" autocomplete="email" />
    <span asp-validation-for="Aviso.Email" class="erro"></span>

    <div class="consentimento">
        <input asp-for="Aviso.Consentimento" />
        <label asp-for="Aviso.Consentimento"></label>
        <span asp-validation-for="Aviso.Consentimento" class="erro"></span>
    </div>

    <div class="armadilha" aria-hidden="true">
        <label for="Aviso_Site">Não preencha</label>
        <input asp-for="Aviso.Site" tabindex="-1" autocomplete="off" />
    </div>

    <button type="submit">Avise-me</button>
</form>

@if (Model.Mensagem is not null)
{
    <p role="status" class="sucesso">@Model.Mensagem</p>
}
public async Task<IActionResult> OnPostAviseMeAsync(
    int id,
    [Bind(Prefix = nameof(Aviso))] EntradaDeAviso aviso,
    CancellationToken ct)
{
    if (!await CarregarAsync(id, ct))
        return NotFound();

    Aviso = aviso;

    if (!string.IsNullOrEmpty(aviso.Site))                 // robô: finge sucesso e ignora
        return RedirectToPage(new { id, slug = Slug });

    if (!ModelState.IsValid)
        return Page();

    var resultado = await catalogo.RegistrarAvisoAsync(id, aviso.Email, ct);

    if (resultado.Erros is { } erros)
    {
        foreach (var (campo, mensagens) in erros)
            foreach (var mensagem in mensagens)
                ModelState.AddModelError($"{nameof(Aviso)}.{campo}", mensagem);
        return Page();
    }

    if (resultado.LimiteExcedido)
    {
        ModelState.AddModelError(string.Empty, "Muitas tentativas. Aguarde um minuto e tente de novo.");
        return Page();
    }

    Mensagem = "Pronto! Vamos avisar você quando este livro chegar.";
    return RedirectToPage(new { id, slug = Slug });        // Post/Redirect/Get
}

Por que redirecionar

Se o handler de sucesso devolvesse Page() direto, a página exibida seria a resposta a um POST. Um F5 reenviaria o formulário (o navegador pergunta, mas quase todo mundo clica "sim"), e o botão Voltar faria o mesmo. Para um aviso de disponibilidade isso é só incômodo. Num formulário de pedido, seria uma compra em dobro.

O padrão Post/Redirect/Get (PRG) resolve: depois de um POST bem-sucedido, o servidor responde 303 See Other (ou 302) para uma URL de GET. O navegador segue o redirecionamento, e a página na tela passa a ser a resposta a um GET, que pode ser recarregado à vontade. Os erros de validação não redirecionam: reexibem a página, porque é preciso mostrar os valores digitados e as mensagens, e nada foi gravado.

Como a mensagem de sucesso sobrevive ao redirecionamento se o HTTP não guarda estado? Pelo TempData: um dicionário gravado, por padrão, num cookie criptografado, que dura até ser lido uma vez. O atributo [TempData] na propriedade Mensagem faz o PageModel gravá-la no POST e lê-la de volta no GET seguinte, exibe o aviso e o descarta. Um F5 posterior já não mostra a mensagem.

Erros que vêm da API

O site valida o e-mail, mas a API valida de novo, porque é ela quem garante o contrato para todos os clientes. Quando a API recusa com o 400 de validação, o corpo é o HttpValidationProblemDetails de "Validação, ProblemDetails e OpenAPI", com o dicionário errors por campo. O cliente do site o traduz para as chaves do formulário:

public async Task<ResultadoDeAviso> RegistrarAvisoAsync(int produtoId, string email, CancellationToken ct)
{
    using var resposta = await http.PostAsJsonAsync(
        $"v1/produtos/{produtoId}/avisos", new { email }, ct);

    return resposta.StatusCode switch
    {
        HttpStatusCode.Accepted or HttpStatusCode.NoContent => ResultadoDeAviso.Ok,
        HttpStatusCode.TooManyRequests => ResultadoDeAviso.Limitado,
        HttpStatusCode.BadRequest => ResultadoDeAviso.ComErros(
            (await resposta.Content.ReadFromJsonAsync<HttpValidationProblemDetails>(ct))!.Errors
                .ToDictionary(e => Capitalizar(e.Key), e => e.Value)),
        _ => throw new HttpRequestException($"Resposta inesperada da API: {(int)resposta.StatusCode}")
    };
}

Os nomes de campo da API podem chegar em camelCase (email), enquanto as chaves do formulário usam o nome C# (Aviso.Email). O Capitalizar alinha as duas convenções. É um detalhe pequeno, mas se for esquecido os erros da API vão parar numa chave que nenhum asp-validation-for exibe, e o usuário vê o formulário voltar sem explicação.

Do lado da API, o endpoint POST /v1/produtos/{id}/avisos é anônimo, protegido pela política de rate limiting "Leitura" (ou uma mais estrita), e guarda o e-mail num conjunto do Redis por produto. Quando o evento produto.reposto passar pelo RabbitMQ, um consumidor no Livraria.Worker envia os avisos e apaga os e-mails. O consentimento pedia um único aviso, e a LGPD pede que o dado pessoal não fique guardado além da finalidade para a qual foi coletado.

O campo-armadilha

O campo Site fica escondido por CSS (.armadilha { position: absolute; left: -9999px; }), fora da ordem de tabulação e oculto de leitores de tela. Pessoas não o veem nem o preenchem. Robôs de spam, que preenchem todos os campos que encontram, preenchem. O handler finge sucesso para não ensinar ao robô que foi detectado. É uma defesa barata e sem atrito, que não substitui o rate limiting mas elimina a maior parte do spam trivial, sem obrigar pessoas a resolver um CAPTCHA.

Acessibilidade dos formulários

Formulários são onde a acessibilidade mais falha, e boa parte do trabalho os tag helpers já fazem: rótulos ligados aos campos, nomes consistentes e mensagens junto ao campo. O resto depende de nós:

  • Todo campo tem um <label> visível. Um placeholder não é rótulo: desaparece ao digitar e muitos leitores de tela não o anunciam.
  • Erros são anunciados. O resumo tem role="alert", e a mensagem de sucesso tem role="status", para que leitores de tela os leiam sem que o usuário precise procurá-los.
  • A cor não é a única pista. O campo com erro recebe também um ícone ou um texto, não apenas uma borda vermelha.
  • O teclado basta. Os formulários podem ser preenchidos e enviados só com Tab e Enter, e o foco fica visível.
  • autocomplete correto (email, postal-code) ajuda a todos e é exigido pelas diretrizes WCAG 2.2 para dados pessoais.

A Livraria agora escuta seus visitantes: busca por GET com URLs compartilháveis, cotação de frete com validação nos dois lados e mensagens junto aos campos, e um aviso de chegada protegido contra CSRF, robôs e envios duplicados, que trata os erros vindos da API como se tivessem nascido no próprio formulário. Cada interação, porém, ainda custa uma ida e volta completa ao servidor e uma página inteira redesenhada. Para uma vitrine isso é ótimo. Para um carrinho que soma itens, recalcula o frete e atualiza o total enquanto o cliente mexe nas quantidades, é pouco.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Um desenvolvedor transformou a cotação de frete num formulário GET (/livros/7?cep=01310100&quantidade=2) "para poder compartilhar o link". Isso é aceitável? E se fosse o "avise-me"?

Ver resposta

✓ Resposta: Para a cotação é aceitável, e até tem vantagens. Cotar não altera nada no servidor, então é uma leitura segura e idempotente, e o link compartilhável é um benefício real. Os cuidados: o CEP fica em logs de servidor e de proxy e no histórico do navegador, o que para um CEP costuma ser aceitável, e buscadores podem seguir esses links. Vale marcar as páginas de cotação com <meta name="robots" content="noindex"> para não gerar milhares de páginas duplicadas. Para o "avise-me", não: ele grava um dado pessoal. Por GET, um pré-carregamento do navegador, um robô de busca ou um link numa imagem cadastrariam e-mails sem ação do usuário, e o e-mail ficaria exposto em logs e no histórico. E não haveria proteção contra CSRF, que as Razor Pages não aplicam a GET. Tudo o que altera estado vai por POST.

Exercício 2

O formulário de frete foi enviado com o CEP "abc" e quantidade "3". O handler devolve Page(). Descreva o que o usuário vê em cada campo, e por que o campo CEP mostra "abc" e não um valor vazio.

Ver resposta

✓ Resposta: O campo CEP mostra "abc" com a mensagem "Use o formato 00000-000." abaixo dele. O campo quantidade mostra "3" sem mensagem. Os tag helpers de input buscam o valor primeiro no ModelState, que guarda o valor tentado (o texto bruto enviado) junto com os erros, e só depois no modelo. Isso é o que permite devolver ao usuário exatamente o que ele digitou, mesmo quando o valor nem pode ser convertido para o tipo da propriedade: "abc" num int geraria erro de conversão e o modelo ficaria com 0, mas o campo mostraria "abc". Uma consequência que surpreende: se o handler alterar Frete.Cep no código e chamar Page(), o campo continua mostrando o valor antigo do ModelState. Para substituí-lo é preciso chamar ModelState.Remove("Frete.Cep") antes.

Exercício 3

Explique por que o handler de sucesso do "avise-me" redireciona e o de erro de validação não. Qual seria o problema de também redirecionar no erro?

Ver resposta

✓ Resposta: No sucesso, algo foi gravado, e a página que resulta do POST não deve poder ser reenviada por F5 ou pelo botão Voltar. Redirecionar para um GET faz a tela exibir uma resposta segura de recarregar, e o TempData leva a mensagem de sucesso através do redirecionamento. No erro, nada foi gravado: reenviar o mesmo formulário inválido só produziria o mesmo erro, sem dano. E a página precisa mostrar os valores digitados e as mensagens por campo, que vivem no ModelState da requisição atual. Um redirecionamento os perderia: o GET seguinte começaria com o formulário vazio e sem mensagens. Para preservá-los seria necessário serializar o ModelState no TempData, uma complicação sem benefício. Por isso a regra do PRG é: redirecionar depois de sucesso, reexibir depois de erro.

Exercício 4

Um atacante descobre o endpoint e envia 10 mil POSTs ao "avise-me" com e-mails de terceiros, cada um com um token antiforgery válido obtido com um GET antes. Quais das defesas do artigo o impedem, e o que mais seria recomendável?

Ver resposta

✓ Resposta: O token antiforgery não impede: ele protege contra requisições forjadas a partir do navegador de uma vítima, e não contra um robô que obtém um token legítimo para si. O campo-armadilha também não, se o atacante estudar o formulário. O que contém o ataque é o rate limiting na API, e no site, por IP, que limita o volume. A real proteção de terceiros vem do desenho: o sistema envia um único e-mail por inscrição e só quando o livro chegar, então o dano para quem teve o endereço cadastrado é pequeno. Recomendações adicionais: (1) confirmação dupla (double opt-in): enviar um e-mail com link de confirmação e só ativar o aviso depois do clique, o que é a prática certa quando se coleta e-mail de desconhecidos; (2) limitar inscrições por e-mail e por produto no próprio conjunto do Redis; (3) um CAPTCHA acessível ou um desafio de prova de trabalho só quando o volume por IP for anômalo.

Exercício 5

Acrescente ao modelo de frete um campo opcional "Presente?" (bool) e explique o que acontece se o usuário desmarcar a caixa. Por que o tag helper gera um <input type="hidden"> extra para checkboxes?

Ver resposta

✓ Resposta: Acrescente [Display(Name = "É para presente")] public bool Presente { get; set; } e <input asp-for="Frete.Presente" /><label asp-for="Frete.Presente"></label>. O HTML tem uma peculiaridade: uma checkbox desmarcada não é enviada no formulário. Nem o nome, nem um valor. Sem mais nada, o binder não receberia o campo, e o valor do modelo ficaria no padrão, o que funciona para bool mas é ambíguo ("desmarcou" e "o campo não existia" ficam iguais), e é um problema em edições em que o valor anterior era true. Por isso o tag helper gera, junto com a checkbox, um <input type="hidden" name="Frete.Presente" value="false">. Marcada, o formulário envia true e false, e o binder usa o primeiro. Desmarcada, envia só o false do campo oculto. O resultado é que o valor sempre chega de forma explícita. Nas versões recentes, esse campo oculto é emitido no fim do formulário.

Comentários

Mais em Linguagem C#

Do Código-Fonte ao Processo: Instalando o .NET e Entendendo o que Acontece ao Rodar
Do Código-Fonte ao Processo: Instalando o .NET e Entendendo o que Acontece ao Rodar

Instalar o .NET 10 e entender o caminho do código até o processador: SDK e…

Delegates, Lambdas e Eventos: Passando Comportamento Como Se Fosse Dado
Delegates, Lambdas e Eventos: Passando Comportamento Como Se Fosse Dado

Comportamento como dado em C#: um filtro que recebe o critério, delegates…

Validação, ProblemDetails e OpenAPI: uma API que explica a si mesma
Validação, ProblemDetails e OpenAPI: uma API que explica a si mesma

Uma API que diz ao cliente o que ele errou: validação nativa do .NET 10 com…