Módulo 3 · Arquitetura MVC — Capítulo 08

Projeto Prático MVC com Procedures

Fechando o módulo: uma tela de checkout completa, do clique do usuário até a procedure usp_PedidoCheckout do Módulo 2 — juntando cada peça vista nos 7 capítulos anteriores.

1. O que este projeto amarra

CapítuloPeça aplicada no projeto
01Separação Model (Repository) / Controller / View
02CarrinhoController com actions GET (exibir) e POST (finalizar)
03View do carrinho com Razor + Layout compartilhado
04CarrinhoViewModel distinto da entidade Pedido
05Validação do formulário antes de chamar o checkout
06Injeção de IPedidoRepository via construtor
07Filter tratando a exceção de estoque insuficiente

2. O ViewModel do carrinho

C# CarrinhoViewModel.cs
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

C# PedidoRepository.cs
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

C# CarrinhoController.cs
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

Razor Views/Carrinho/Exibir.cshtml
@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>
Dica

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

C# Program.cs — juntando tudo
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.

📌 Resumo do capítulo

  • Um fluxo MVC completo conecta ViewModel → Controller → Repository → procedure, com validação e tratamento de erro em cada camada.
  • Exceções específicas do banco (SqlException com número customizado) viram mensagens amigáveis via ModelState, nunca stack traces expostos ao usuário.
  • O padrão Post/Redirect/Get (capítulo 2) fecha o fluxo: POST processa, redireciona para uma tela de confirmação via GET.

✏️ Praticando

  1. Implemente o fluxo completo deste capítulo, incluindo a tela de confirmação com o Id do pedido criado.
  2. Adicione uma tela de listagem de produtos (ProdutoController) para popular o carrinho antes do checkout.
  3. Teste o cenário de erro (estoque insuficiente) e confirme que a mensagem aparece corretamente na tela, sem expor detalhes internos do banco.