Adicionando um Endpoint de Inserção POST com Validação em .NET Aspire no Linux
[[academy-video-youtube({"vid": "oAMMHR8kKnw", "start_time": "0", "title": "Adding a POST Insert Endpoint with Validation in .NET Aspire on Linux", "creator": "Tim Corey", "length": "20m 31s"})]]
Ler dados de uma API é apenas metade da história. Eventualmente, todo aplicativo precisa aceitar novos registros, e isso significa construir um endpoint POST que recebe um corpo de solicitação, valida a entrada, persiste no banco de dados e retorna um código de status significativo. Pular a etapa de validação é tentador durante a prototipação, mas APIs de produção que aceitam entrada não validada tornam-se uma fonte de dados corrompidos que é mais difícil de limpar do que de prevenir.
No vídeo "Adicionando um Endpoint POST Insert com Validação em .NET Aspire no Linux", Tim Corey adiciona um endpoint de inserção à API do Tiny Ticket, cria um tipo de registro de entrada dedicado, conecta o pipeline de validação embutido do .NET para APIs mínimas e configura o Swagger para iniciar automaticamente quando a API começa. O episódio cobre o ciclo completo desde o procedimento armazenado até o endpoint testado, incluindo o formato de resposta de erro de validação que .NET retorna nativamente. Se você está seguindo a série C# no Linux ou adicionando operações de gravação a uma API mínima pela primeira vez, este artigo aborda cada etapa.
Criando o Tipo de Registro de Inserção
[1:46 - 4:43] Antes de construir o endpoint, Tim cria um objeto de transferência de dados que representa a forma de uma solicitação de inserção. O existente TicketModel inclui campos como Id e CreatedDate que o banco de dados gera automaticamente. Aceitar esses em um corpo POST seria ignorado ou causaria conflitos, então um tipo separado limita a entrada apenas aos campos que o chamador deve fornecer.
public record TicketInsertRecord(string Title, string Description, int Priority);
public record TicketInsertRecord(string Title, string Description, int Priority);
Usar um record em vez de um class é uma escolha deliberada. Registros oferecem igualdade baseada em valor e imutabilidade por padrão, o que se adapta à semântica de um payload de solicitação: os dados chegam, são validados, são passados para o banco de dados, e nunca são modificados no meio do processo. As três propriedades (Title, Description, Priority) mapeiam diretamente para os parâmetros do procedimento armazenado spTickets_Insert.
Mapeando o Endpoint POST
[4:43 - 9:51] Com o tipo de registro definido, o registro do endpoint segue o mesmo padrão das rotas GET, mas usa MapPost e vincula o corpo da solicitação ao registro de inserção:
app.MapPost("/api/tickets", async (TicketInsertRecord ticket, IDbConnection db) =>
{
await db.SaveDataAsync("spTickets_Insert", ticket);
return Results.NoContent();
});
app.MapPost("/api/tickets", async (TicketInsertRecord ticket, IDbConnection db) =>
{
await db.SaveDataAsync("spTickets_Insert", ticket);
return Results.NoContent();
});
Observe que a rota é /api/tickets sem um segmento de ID, correspondendo às convenções do REST onde POST para uma URL de coleção cria um novo recurso. O manipulador chama o procedimento armazenado com o objeto inteiro ticket como o conjunto de parâmetros. Dapper mapeia as propriedades do registro para os parâmetros SQL por nome.
Retornar Results.NoContent() envia um código de status 204. Tim explica o raciocínio: a inserção foi bem-sucedida, mas não há nada significativo para retornar no corpo da resposta. Algumas APIs retornam o objeto recém-criado com um status 201 Created e um cabeçalho Location apontando para o novo recurso, que é uma alternativa válida. Para o projeto Tiny Ticket, 204 mantém as coisas simples.
Testando a Inserção pelo Swagger
[9:51 - 14:43] Tim launches the project and navigates to Swagger. O endpoint POST aparece com um esquema de corpo de solicitação correspondendo às propriedades TicketInsertRecord. Ele preenche um ticket de teste com um título, descrição e prioridade e então executa a solicitação.
Um 204 retorna, confirmando que a inserção foi bem-sucedida. Para verificar se os dados realmente persistiram, ele alterna para o endpoint GET all e executa-o. O novo ticket aparece na lista ao lado dos registros de teste originais.
O que o teste também revela é a lacuna sem validação: enviar um título vazio, uma descrição ausente ou uma prioridade de 99 todos têm sucesso com um 204. O banco de dados aceita o que quer que a API envie. Essa lacuna motiva a próxima seção.
Adicionando Validação Embutida
[14:43 - 18:28] Começando com .NET 10, APIs mínimas suportam um pipeline de validação embutido que lê atributos de anotação de dados do tipo de entrada e rejeita solicitações inválidas antes que o manipulador execute. Tim conecta-a em duas etapas.
Primeiro, registre os serviços de validação em Program.cs. Essa única linha ativa o pipeline completo:
builder.Services.AddValidation();
builder.Services.AddValidation();
Com o serviço registrado, o framework inspeciona cada corpo de solicitação quanto a atributos de validação antes que o manipulador execute. A segunda etapa é anotar o registro de inserção com as regras que cada campo deve satisfazer:
public record TicketInsertRecord(
[Required, MinLength(1)] string Title,
[Required] string Description,
[Range(1, 5)] int Priority
);
public record TicketInsertRecord(
[Required, MinLength(1)] string Title,
[Required] string Description,
[Range(1, 5)] int Priority
);
[Required] garante que o campo esteja presente e não seja nulo. [MinLength(1)] impede que strings vazias passem a verificação de obrigatoriedade (já que uma string vazia tecnicamente não é nula). [Range(1, 5)] restringe a prioridade a um nível válido. Esses atributos são os mesmos tipos System.ComponentModel.DataAnnotations que controladores ASP.NET MVC usaram por anos, mas agora funcionam em APIs mínimas sem middleware adicional.
Após salvar e reiniciar, Tim envia uma solicitação com um título vazio e uma prioridade de 10. A resposta retorna um 400 Bad Request com um corpo de erro estruturado:
{
"errors": {
"Title": ["The Title field is required."],
"Priority": ["The field Priority must be between 1 and 5."]
}
}
O pipeline de validação interrompe a solicitação antes que o manipulador execute, então nenhum dado inválido atinge o banco de dados. A resposta de erro segue o formato RFC 7807 Problem Details, que os consumidores de API podem analisar programaticamente.
Auto-Lançamento do Swagger na Inicialização
[19:44 - 20:31] Uma pequena melhoria de qualidade de vida fecha o episódio. Toda vez que Tim iniciava a API, ele tinha que digitar manualmente /swagger na URL do navegador. Para automatizar isso, ele abre o Properties/launchSettings.json do projeto da API e adiciona uma propriedade launchUrl ao perfil HTTPS:
{
"profiles": {
"https": {
"launchUrl": "swagger"
}
}
}
Na próxima inicialização, o navegador abre diretamente para a Swagger UI em vez da página padrão. Isso economiza alguns segundos por ciclo de depuração, o que se acumula ao longo de uma sessão completa de desenvolvimento.
Conclusão
[20:09 - 20:31] Adicionar um endpoint POST a uma API mínima envolve criar um registro de entrada dedicado, mapeá-lo para MapPost com a URL da coleção, e chamar o procedimento armazenado com o registro como objeto de parâmetro. A validação no .NET 10 requer um registro de serviço e atributos de anotação de dados padrão nas propriedades do registro. O framework lida com o formato da resposta 400 automaticamente.
Navegação na série: Este artigo faz parte da série C# no Linux construindo o app Tiny Ticket. Anterior: Adicionando um Endpoint Get By ID. Próximo: Adicionando um Endpoint PUT Update.
Dica de exemplo: Quando seu procedimento armazenado de inserção retorna o ID do novo registro, mude o retorno de Results.NoContent() para Results.Created($"/api/tickets/{newId}", result) para dar aos chamadores um status 201 com um cabeçalho de localização que eles podem seguir para buscar o recurso criado.
Assista o vídeo completo no canal do YouTube dele e obtenha mais insights sobre como construir endpoints de gravação na série C# no Linux.
