1. O que este projeto amarra
| Capítulo | Peça aplicada no projeto |
|---|---|
| 01 | Separação Model (Repository) / Controller / View |
| 02 | CarrinhoController com actions GET (exibir) e POST (finalizar) |
| 03 | View do carrinho com Razor + Layout compartilhado |
| 04 | CarrinhoViewModel distinto da entidade Pedido |
| 05 | Validação do formulário antes de chamar o checkout |
| 06 | Injeção de IPedidoRepository via construtor |
| 07 | Filter tratando a exceção de estoque insuficiente |
2. O ViewModel do carrinho
public class CarrinhoViewModel
{
[Required]
public int ClienteId { get; set; }
[MinLength(1, ErrorMessage = "O carrinho não pode estar vazio")]
public List<ItemCarrinhoViewModel> Itens { get; set; } = new();
}
public class ItemCarrinhoViewModel
{
[Required]
public int ProdutoId { get; set; }
[Range(1, 999, ErrorMessage = "Quantidade inválida")]
public int Quantidade { get; set; }
}
3. O Repository, chamando a procedure do Módulo 2
public interface IPedidoRepository
{
Task<int> CheckoutAsync(int clienteId, List<ItemCarrinhoViewModel> itens);
}
public class PedidoRepository : IPedidoRepository
{
private readonly string _connectionString;
public PedidoRepository(string connectionString) => _connectionString = connectionString;
public async Task<int> CheckoutAsync(int clienteId, List<ItemCarrinhoViewModel> itens)
{
var tabela = new DataTable();
tabela.Columns.Add("ProdutoId", typeof(int));
tabela.Columns.Add("Quantidade", typeof(int));
foreach (var item in itens)
tabela.Rows.Add(item.ProdutoId, item.Quantidade);
var parametros = new DynamicParameters();
parametros.Add("@ClienteId", clienteId);
parametros.Add("@Itens", tabela.AsTableValuedParameter("dbo.ItemCarrinhoTableType"));
parametros.Add("@PedidoId", dbType: DbType.Int32, direction: ParameterDirection.Output);
await using var conexao = new SqlConnection(_connectionString);
await conexao.ExecuteAsync("dbo.usp_PedidoCheckout", parametros,
commandType: CommandType.StoredProcedure);
return parametros.Get<int>("@PedidoId");
}
}
4. O Controller
public class CarrinhoController : Controller
{
private readonly IPedidoRepository _repositorio;
public CarrinhoController(IPedidoRepository repositorio) => _repositorio = repositorio;
[HttpGet("carrinho")]
public IActionResult Exibir() => View(new CarrinhoViewModel());
[HttpPost("carrinho/finalizar")]
public async Task<IActionResult> Finalizar(CarrinhoViewModel modelo)
{
if (!ModelState.IsValid)
return View("Exibir", modelo);
try
{
int pedidoId = await _repositorio.CheckoutAsync(modelo.ClienteId, modelo.Itens);
return RedirectToAction(nameof(Confirmacao), new { id = pedidoId });
}
catch (SqlException ex) when (ex.Number == 50010)
{
ModelState.AddModelError(string.Empty, "Um ou mais produtos não têm estoque suficiente.");
return View("Exibir", modelo);
}
}
[HttpGet("carrinho/confirmacao/{id:int}")]
public IActionResult Confirmacao(int id) => View(new { PedidoId = id });
}
Repare a linha inteira do fluxo: o catch específico do
SqlException com número 50010 (o mesmo código customizado definido em
usp_PedidoCheckout, capítulo 14 do Módulo 2) vira uma mensagem de erro
amigável em ModelState, exibida de volta na mesma tela — nada de
stack trace cru chegando ao usuário.
5. A View
@model CarrinhoViewModel
<h1>Finalizar compra</h1>
<div asp-validation-summary="All"></div>
<form asp-controller="Carrinho" asp-action="Finalizar" method="post">
<input type="hidden" asp-for="ClienteId" />
@for (int i = 0; i < Model.Itens.Count; i++)
{
<div class="linha-item">
<input type="hidden" asp-for="Itens[i].ProdutoId" />
<input asp-for="Itens[i].Quantidade" />
</div>
}
<button type="submit">Confirmar pedido</button>
</form>
asp-validation-summary="All" exibe automaticamente qualquer erro
adicionado com ModelState.AddModelError(string.Empty, ...) — é assim
que a mensagem de estoque insuficiente do Controller aparece na tela sem código adicional na View.
6. Registro no Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.AddScoped<IPedidoRepository>(_ =>
new PedidoRepository(builder.Configuration.GetConnectionString("Default")!));
var app = builder.Build();
app.UseExceptionHandler("/Erro");
app.UseStaticFiles();
app.UseRouting();
app.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();
7. Onde isso te leva
Este projeto cobre a experiência de um usuário navegando pelo browser. O próximo módulo aborda o outro consumidor mais comum de procedures em sistemas modernos: uma API que devolve JSON para front-ends separados (SPA, mobile, outros serviços) em vez de HTML renderizado no servidor.