Razor mvc

Escopo: C#/.NET. Guia baseado em ASP.NET Core .NET 10 com C# 14.

Razor Pages e MVC (Model-View-Controller · Modelo-Visão-Controlador) são as duas formas de montar a página no servidor e enviá-la pronta ao navegador. No Razor Pages, cada página é um par de arquivos: o .cshtml com a marcação e o PageModel com o código que a alimenta. Isso encaixa bem em fluxos fechados, como um formulário de cadastro. No MVC, um Controller (Controlador) atende várias telas da mesma entidade, e cada tela é uma View (Visão) separada. Escolha o MVC quando a mesma entidade aparece em muitas telas diferentes.

Este guia aplica os princípios de methods.md e api-design.md.

Conceitos fundamentais

ConceitoO que é
PageModel (modelo de página)Classe base de uma Razor Page: define handlers OnGet/OnPost e propriedades disponíveis na view
[BindProperty] (atributo de vinculação)Atributo que vincula uma propriedade do PageModel ao POST do formulário automaticamente
Tag Helper (auxiliar de tag)Extensão de atributo HTML (prefixo asp-) que gera markup correto a partir do model
ModelState (estado do modelo)Dicionário de erros de validação preenchido pelo framework após o binding
IActionResult (resultado de ação)Tipo de retorno polimórfico de action methods MVC; concretizado por View(), Ok(), RedirectToAction()
ViewModel (modelo da view)Classe tipada criada para a view: contém exatamente os dados que a página precisa, sem expor a entidade
Partial View (View parcial)Fragmento de marcação reutilizável renderizado com <partial name="..." />
Layout (layout mestre)Template mestre declarado com Layout; define estrutura HTML compartilhada entre páginas

Razor Pages

Cada página são dois arquivos: Pages/Orders/Create.cshtml guarda a marcação e Pages/Orders/Create.cshtml.cs guarda o PageModel. O PageModel responde a dois momentos: o OnGet prepara o que a tela precisa exibir quando o usuário chega, e o OnPost recebe o formulário quando ele é enviado.

Fluxo GET: GET /orders/create → OnGet → View Fluxo POST: POST /orders/create → Binding → OnPost → Validate → Service → Redirect

❌ Ruim: lógica de negócio no PageModel; validação ad hoc; acesso direto ao banco
// Pages/Orders/Create.cshtml.cs
public class CreateModel(AppDbContext db) : PageModel
{
    [BindProperty]
    public string ProductName { get; set; } = string.Empty;

    [BindProperty]
    public int Quantity { get; set; }

    public async Task<IActionResult> OnPostAsync()
    {
        if (string.IsNullOrWhiteSpace(ProductName))  // validação ad hoc (improvisada)
        {
            ModelState.AddModelError("ProductName", "Required");
            return Page();
        }

        var order = new Order(ProductName, Quantity, DateTimeOffset.UtcNow);  // lógica no PageModel
        await db.Orders.AddAsync(order);
        await db.SaveChangesAsync();

        return RedirectToPage("./Index");
    }
}
✅ Bom: PageModel delega para Service; validação via DataAnnotations
// Pages/Orders/Create.cshtml.cs
public class CreateModel(IOrderService orderService) : PageModel
{
    [BindProperty]
    public required OrderInput Input { get; set; }

    public void OnGet() { }

    public async Task<IActionResult> OnPostAsync(CancellationToken ct)
    {
        if (!ModelState.IsValid)
            return Page();

        var result = await orderService.CreateAsync(Input, ct);
        if (result.IsFailure)
        {
            ModelState.AddModelError(string.Empty, result.Error!.Message);
            return Page();
        }

        return RedirectToPage("./Index");
    }
}
// Models/OrderInput.cs
public class OrderInput
{
    [Required(ErrorMessage = "Nome do produto obrigatório")]
    [MaxLength(200)]
    public string ProductName { get; set; } = string.Empty;

    [Range(1, 1000, ErrorMessage = "Quantidade entre 1 e 1000")]
    public int Quantity { get; set; }
}

Tag Helpers

Tag Helper é um atributo que o ASP.NET Core lê no servidor e transforma no HTML (HyperText Markup Language · Linguagem de Marcação de Hipertexto) final. asp-for liga o campo à propriedade do PageModel, e daí saem sozinhos o name, o id e as regras de validação que a propriedade já declara. asp-validation-for mostra o erro daquele campo. asp-page monta a URL (Uniform Resource Locator · Localizador Uniforme de Recurso) do formulário e inclui o token que protege contra requisição forjada de outro site.

❌ Ruim: HTML manual sem Tag Helpers; sem anti-forgery; erros hardcoded
<form method="post" action="/orders/create">
    <input type="text" name="Input.ProductName" placeholder="Produto" />
    <span class="text-danger">Campo obrigatório</span>

    <input type="number" name="Input.Quantity" />

    <button type="submit">Salvar</button>
</form>
✅ Bom: Tag Helpers vinculam ao model; anti-forgery e erros automáticos
<form asp-page="./Create" method="post">
    <div asp-validation-summary="ModelOnly" class="text-danger"></div>

    <div>
        <label asp-for="Input.ProductName">Produto</label>
        <input asp-for="Input.ProductName" class="form-control" />
        <span asp-validation-for="Input.ProductName" class="text-danger"></span>
    </div>

    <div>
        <label asp-for="Input.Quantity">Quantidade</label>
        <input asp-for="Input.Quantity" class="form-control" />
        <span asp-validation-for="Input.Quantity" class="text-danger"></span>
    </div>

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

MVC: o controller é um adaptador

O controller faz a tradução entre o mundo do HTTP (HyperText Transfer Protocol · Protocolo de Transferência de Hipertexto) e o mundo do domínio: recebe a requisição, chama um Service ou Handler (classe que atende a operação) e transforma o Result<T> que voltou em IActionResult. A regra de negócio mora no service, e o acesso ao banco passa pelo Repository. Um DbContext injetado no controller é o sinal de que esse limite caiu, e a partir dali a mesma regra passa a ser reescrita em cada tela que precisar dela.

Fluxo: HTTP Request → Controller → Service → Result<T> → IActionResult → HTTP Response

❌ Ruim: lógica de negócio no controller; DbContext injetado diretamente
[ApiController]
[Route("orders")]
public class OrdersController(AppDbContext db) : ControllerBase
{
    [HttpPost]
    public async Task<IActionResult> CreateAsync(OrderRequest req, CancellationToken ct)
    {
        var exists = await db.Products.AnyAsync(p => p.Id == req.ProductId, ct);  // acesso direto ao banco
        if (!exists) return NotFound("Product not found");

        var order = new Order(req.ProductId, req.Quantity, DateTimeOffset.UtcNow);  // lógica no controller
        db.Orders.Add(order);
        await db.SaveChangesAsync(ct);

        return Ok(order);
    }
}
✅ Bom: controller delega para Service; traduz Result em HTTP no boundary
[ApiController]
[Route("orders")]
public class OrdersController(IOrderService orderService) : ControllerBase
{
    [HttpPost]
    public async Task<IActionResult> CreateAsync(OrderRequest request, CancellationToken ct)
    {
        var result = await orderService.CreateAsync(request, ct);
        if (result.IsFailure)
            return BadRequest(result.Error);

        var order = result.Value!;

        return CreatedAtAction(nameof(GetByIdAsync), new { id = order.Id }, order);
    }

    [HttpGet("{id:guid}")]
    public async Task<IActionResult> GetByIdAsync(Guid id, CancellationToken ct)
    {
        var result = await orderService.FindByIdAsync(id, ct);
        if (result.IsFailure)
            return NotFound(result.Error);

        var order = result.Value!;

        return Ok(order);
    }
}

ViewModels

O ViewModel é um tipo criado para a tela, com os campos que aquela tela mostra. Entregar a entidade de domínio direto para a view leva junto tudo o que ela carrega, e o PasswordHash que ninguém pretendia publicar acaba a um @Model. de distância de aparecer no HTML.

❌ Ruim: entidade de domínio passada direto para a view; campos sensíveis expostos
// Controllers/OrdersController.cs
public async Task<IActionResult> DetailAsync(Guid id, CancellationToken ct)
{
    var order = await orderRepository.FindByIdAsync(id, ct);
    if (order is null)
        return NotFound();

    return View(order);  // entidade completa na view; expõe InternalCost, AuditLog, etc.
}
✅ Bom: ViewModel tipado projetado para a view
// Controllers/OrdersController.cs
public async Task<IActionResult> DetailAsync(Guid id, CancellationToken ct)
{
    var result = await orderService.FindDetailAsync(id, ct);
    if (result.IsFailure)
        return NotFound();

    var viewModel = result.Value!;
    return View(viewModel);
}
// ViewModels/OrderDetailViewModel.cs
public record OrderDetailViewModel(
    Guid Id,
    string ProductName,
    int Quantity,
    decimal Total,
    string Status,
    DateTimeOffset CreatedAt
);

Layouts e Partial Views

O Layout guarda o que toda página tem em volta: cabeçalho, menu, rodapé, os arquivos de estilo. O @RenderBody() marca o ponto onde o conteúdo da página entra. A Partial View guarda um pedaço que se repete em várias telas, como a tabela de itens ou o cartão de produto, e passa a ser editada num lugar só.

❌ Ruim: HTML estrutural duplicado em cada página
@* Pages/Orders/Index.cshtml: sem layout *@
<!DOCTYPE html>
<html>
<head><title>Pedidos</title></head>
<body>
    <nav><!-- navegação duplicada em cada página --></nav>
    <main>
        <h1>Pedidos</h1>
    </main>
    <footer><!-- rodapé duplicado em cada página --></footer>
</body>
</html>
✅ Bom: _Layout.cshtml compartilhado; partial view para fragmentos
@* Shared/_Layout.cshtml *@
<!DOCTYPE html>
<html>
<head><title>@ViewData["Title"]: Pedidos</title></head>
<body>
    <partial name="_NavigationBar" />
    <main class="container">
        @RenderBody()
    </main>
    <partial name="_Footer" />
</body>
</html>
@* Pages/Orders/Index.cshtml *@
@model OrderIndexViewModel
@{
    Layout = "_Layout";
    ViewData["Title"] = "Pedidos";
}

<h1>Pedidos</h1>
<partial name="_OrderTable" model="Model.Orders" />
@* Shared/_OrderTable.cshtml *@
@model IEnumerable<OrderSummary>

<table class="table">
    <thead>
        <tr>
            <th>Produto</th>
            <th>Quantidade</th>
            <th>Status</th>
        </tr>
    </thead>
    <tbody>
        @foreach (var order in Model)
        {
            <tr>
                <td>@order.ProductName</td>
                <td>@order.Quantity</td>
                <td>@order.Status</td>
            </tr>
        }
    </tbody>
</table>

DoDocs v3.7.0 · Desenvolvido por @thiagocajadev · Baseado no trabalho de pmndrs/docs · Poimandres.