.NET Web Apps
Overview
ASP.NET Core provides multiple models for building web applications: MVC (Model-View-Controller) for complex server-rendered apps, Razor Pages for page-focused scenarios, Minimal APIs for lightweight HTTP services, and Blazor for interactive web UIs with C#. Each model runs on the same ASP.NET Core pipeline and shares the dependency injection, configuration, middleware, and authentication infrastructure. Choosing the right model depends on the application's complexity, team familiarity, and whether the UI is server-rendered, client-rendered, or API-driven.
MVC Pattern
Use the MVC pattern for applications with complex routing, multiple views per controller, and shared layouts.
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
builder.Services.AddScoped<IProductService, ProductService>();
var app = builder.Build();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
app.Run();
// Controllers/ProductsController.cs
using Microsoft.AspNetCore.Mvc;
namespace MyApp.Controllers;
public class ProductsController : Controller
{
private readonly IProductService _productService;
public ProductsController(IProductService productService)
{
_productService = productService;
}
public async Task<IActionResult> Index(string? category, int page = 1)
{
var products = await _productService.GetPagedAsync(category, page, pageSize: 20);
ViewBag.CurrentCategory = category;
return View(products);
}
public async Task<IActionResult> Details(int id)
{
var product = await _productService.GetByIdAsync(id);
if (product is null) return NotFound();
return View(product);
}
[HttpGet]
public IActionResult Create() => View();
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(CreateProductViewModel model)
{
if (!ModelState.IsValid) return View(model);
await _productService.CreateAsync(model);
TempData["Success"] = "Product created successfully.";
return RedirectToAction(nameof(Index));
}
}
Razor Pages
Use Razor Pages for page-centric applications where each URL maps to a single page with its own model.
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorPages();
builder.Services.AddScoped<IContactService, ContactService>();
var app = builder.Build();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapRazorPages();
app.Run();
// Pages/Contacts/Create.cshtml.cs
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using System.ComponentModel.DataAnnotations;
namespace MyApp.Pages.Contacts;
public class CreateModel : PageModel
{
private readonly IContactService _contactService;
public CreateModel(IContactService contactService)
{
_contactService = contactService;
}
[BindProperty]
public ContactInput Input { get; set; } = new();
public void OnGet() { }
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid) return Page();
await _contactService.CreateAsync(new Contact
{
Name = Input.Name,
Email = Input.Email,
Message = Input.Message
});
TempData["Success"] = "Contact submitted.";
return RedirectToPage("/Contacts/Index");
}
public class ContactInput
{
[Required, StringLength(100)]
public string Name { get; set; } = string.Empty;
[Required, EmailAddress]
public string Email { get; set; } = string.Empty;
[Required, StringLength(2000)]
public string Message { get; set; } = string.Empty;
}
}
Minimal APIs with Endpoint Groups
Use minimal APIs for lightweight microservices and API-only projects.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddScoped<IOrderService, OrderService>();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.MapGroup("/api/orders")
.WithTags("Orders")
.MapOrderEndpoints();
app.Run();
// Extensions/OrderEndpoints.cs
public static class OrderEndpoints
{
public static RouteGroupBuilder MapOrderEndpoints(this RouteGroupBuilder group)
{
group.MapGet("/", async (IOrderService service, int page = 1) =>
Results.Ok(await service.GetPagedAsync(page)));
group.MapGet("/{id:int}", async (int id, IOrderService service) =>
{
var order = await service.GetByIdAsync(id);
return order is not null ? Results.Ok(order) : Results.NotFound();
});
group.MapPost("/", async (CreateOrderDto dto, IOrderService service) =>
{
var order = await service.CreateAsync(dto);
return Results.Created($"/api/orders/{order.Id}", order);
});
return group;
}
}
Static Server-Side Rendering with Blazor
Use Blazor SSR for server-rendered pages with optional interactive islands.
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
var app = builder.Build();
app.UseStaticFiles();
app.UseAntiforgery();
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
app.Run();
Web Application Model Comparison
| Feature |
MVC |
Razor Pages |
Minimal APIs |
Blazor SSR |
| Best for |
Complex web apps |
Page-centric apps |
Microservices/APIs |
Interactive web UI |
| Routing |
Convention + attribute |
File/folder-based |
Lambda-based |
Component-based |
| Views |
Razor views (.cshtml) |
Razor pages (.cshtml) |
JSON responses |
Razor components (.razor) |
| Model binding |
[FromForm], [FromBody] |
[BindProperty] |
Parameter injection |
@bind, EditForm |
| Testability |
Controller unit tests |
PageModel unit tests |
Endpoint delegate tests |
Component tests |
| SEO |
Server-rendered HTML |
Server-rendered HTML |
N/A (API) |
Server-rendered HTML |
| Complexity |
Higher |
Moderate |
Lowest |
Moderate |
| Areas/Sections |
Yes (Areas) |
Yes (folders) |
Groups |
Layouts |
Best Practices
Choose Razor Pages for page-centric web apps where each URL corresponds to a single page (e.g., contact forms, dashboards, admin panels), and MVC only when multiple actions per controller are genuinely needed (e.g., a products controller with CRUD + search + bulk operations sharing the same service dependencies).
Organize minimal API endpoints into static extension methods (e.g., MapOrderEndpoints(), MapUserEndpoints()) in separate files under an Endpoints/ folder, rather than defining all routes in Program.cs, to keep the startup file under 50 lines and make each endpoint group independently navigable.
Use MapGroup() to share route prefixes, tags, filters, and authorization policies across related endpoints instead of duplicating .RequireAuthorization() and .WithTags() on every individual endpoint, reducing boilerplate and ensuring policy consistency when new endpoints are added.
Apply [ValidateAntiForgeryToken] on every MVC [HttpPost] action and Razor Page OnPost handler that processes form submissions, and add app.UseAntiforgery() to the middleware pipeline, to prevent cross-site request forgery attacks on state-changing operations.
Use TempData for post-redirect-get (PRG) success messages in MVC and Razor Pages instead of passing messages via query strings or storing them in session, because TempData is automatically cleared after the next request and does not persist across browser refreshes.
Set [BindProperty] on Razor Page properties that receive form data and use a nested Input class to group all bound properties, rather than binding directly to the domain model, to prevent over-posting attacks where malicious users submit fields that should not be user-editable.
Configure AddControllersWithViews() or AddRazorPages() with AddJsonOptions(options => options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase) to ensure JSON responses use camelCase property names, matching JavaScript conventions and preventing front-end mapping errors.
Use the IWebHostEnvironment.IsDevelopment() check to conditionally enable Swagger, detailed error pages, and developer exception page so that sensitive diagnostic information is never exposed in production; app.UseDeveloperExceptionPage() leaks stack traces and connection strings.
Implement IAsyncActionFilter or endpoint filters for cross-cutting validation rather than repeating ModelState.IsValid checks in every controller action, centralizing validation logic and ensuring no action accidentally skips the check.
Deploy behind a reverse proxy (NGINX, Azure App Gateway, YARP) and configure ForwardedHeaders middleware to preserve the original client IP, scheme, and host from the X-Forwarded-* headers, because without this configuration, HttpContext.Connection.RemoteIpAddress returns the proxy's IP and Request.Scheme returns http instead of https.
1---2name: dotnet-web-apps3description: USE FOR: Choosing between and implementing .NET web application patterns including MVC, Razor Pages, Minimal APIs, and Blazor. Use when deciding on architecture, project structure, and routing strategies for ASP.NET Core web applications. DO NOT USE FOR: Native mobile/desktop apps (use MAUI or Avalonia), game development (use Unity or MonoGame), or projects that exclusively need a REST API without any server-rendered content (use the aspnet-core skill directly).4license: MIT5---67# .NET Web Apps89## Overview1011ASP.NET Core provides multiple models for building web applications: MVC (Model-View-Controller) for complex server-rendered apps, Razor Pages for page-focused scenarios, Minimal APIs for lightweight HTTP services, and Blazor for interactive web UIs with C#. Each model runs on the same ASP.NET Core pipeline and shares the dependency injection, configuration, middleware, and authentication infrastructure. Choosing the right model depends on the application's complexity, team familiarity, and whether the UI is server-rendered, client-rendered, or API-driven.1213## MVC Pattern1415Use the MVC pattern for applications with complex routing, multiple views per controller, and shared layouts.1617```csharp18// Program.cs19var builder = WebApplication.CreateBuilder(args);20builder.Services.AddControllersWithViews();21builder.Services.AddScoped<IProductService, ProductService>();2223var app = builder.Build();24app.UseStaticFiles();25app.UseRouting();26app.UseAuthorization();27app.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");28app.Run();2930// Controllers/ProductsController.cs31using Microsoft.AspNetCore.Mvc;3233namespace MyApp.Controllers;3435public class ProductsController : Controller36{37 private readonly IProductService _productService;3839 public ProductsController(IProductService productService)40 {41 _productService = productService;42 }4344 public async Task<IActionResult> Index(string? category, int page = 1)45 {46 var products = await _productService.GetPagedAsync(category, page, pageSize: 20);47 ViewBag.CurrentCategory = category;48 return View(products);49 }5051 public async Task<IActionResult> Details(int id)52 {53 var product = await _productService.GetByIdAsync(id);54 if (product is null) return NotFound();55 return View(product);56 }5758 [HttpGet]59 public IActionResult Create() => View();6061 [HttpPost]62 [ValidateAntiForgeryToken]63 public async Task<IActionResult> Create(CreateProductViewModel model)64 {65 if (!ModelState.IsValid) return View(model);6667 await _productService.CreateAsync(model);68 TempData["Success"] = "Product created successfully.";69 return RedirectToAction(nameof(Index));70 }71}72```7374## Razor Pages7576Use Razor Pages for page-centric applications where each URL maps to a single page with its own model.7778```csharp79// Program.cs80var builder = WebApplication.CreateBuilder(args);81builder.Services.AddRazorPages();82builder.Services.AddScoped<IContactService, ContactService>();8384var app = builder.Build();85app.UseStaticFiles();86app.UseRouting();87app.UseAuthorization();88app.MapRazorPages();89app.Run();9091// Pages/Contacts/Create.cshtml.cs92using Microsoft.AspNetCore.Mvc;93using Microsoft.AspNetCore.Mvc.RazorPages;94using System.ComponentModel.DataAnnotations;9596namespace MyApp.Pages.Contacts;9798public class CreateModel : PageModel99{100 private readonly IContactService _contactService;101102 public CreateModel(IContactService contactService)103 {104 _contactService = contactService;105 }106107 [BindProperty]108 public ContactInput Input { get; set; } = new();109110 public void OnGet() { }111112 public async Task<IActionResult> OnPostAsync()113 {114 if (!ModelState.IsValid) return Page();115116 await _contactService.CreateAsync(new Contact117 {118 Name = Input.Name,119 Email = Input.Email,120 Message = Input.Message121 });122123 TempData["Success"] = "Contact submitted.";124 return RedirectToPage("/Contacts/Index");125 }126127 public class ContactInput128 {129 [Required, StringLength(100)]130 public string Name { get; set; } = string.Empty;131132 [Required, EmailAddress]133 public string Email { get; set; } = string.Empty;134135 [Required, StringLength(2000)]136 public string Message { get; set; } = string.Empty;137 }138}139```140141## Minimal APIs with Endpoint Groups142143Use minimal APIs for lightweight microservices and API-only projects.144145```csharp146var builder = WebApplication.CreateBuilder(args);147builder.Services.AddEndpointsApiExplorer();148builder.Services.AddSwaggerGen();149builder.Services.AddScoped<IOrderService, OrderService>();150151var app = builder.Build();152153if (app.Environment.IsDevelopment())154{155 app.UseSwagger();156 app.UseSwaggerUI();157}158159app.MapGroup("/api/orders")160 .WithTags("Orders")161 .MapOrderEndpoints();162163app.Run();164165// Extensions/OrderEndpoints.cs166public static class OrderEndpoints167{168 public static RouteGroupBuilder MapOrderEndpoints(this RouteGroupBuilder group)169 {170 group.MapGet("/", async (IOrderService service, int page = 1) =>171 Results.Ok(await service.GetPagedAsync(page)));172173 group.MapGet("/{id:int}", async (int id, IOrderService service) =>174 {175 var order = await service.GetByIdAsync(id);176 return order is not null ? Results.Ok(order) : Results.NotFound();177 });178179 group.MapPost("/", async (CreateOrderDto dto, IOrderService service) =>180 {181 var order = await service.CreateAsync(dto);182 return Results.Created($"/api/orders/{order.Id}", order);183 });184185 return group;186 }187}188```189190## Static Server-Side Rendering with Blazor191192Use Blazor SSR for server-rendered pages with optional interactive islands.193194```csharp195// Program.cs196var builder = WebApplication.CreateBuilder(args);197builder.Services.AddRazorComponents()198 .AddInteractiveServerComponents();199200var app = builder.Build();201app.UseStaticFiles();202app.UseAntiforgery();203app.MapRazorComponents<App>()204 .AddInteractiveServerRenderMode();205app.Run();206```207208## Web Application Model Comparison209210| Feature | MVC | Razor Pages | Minimal APIs | Blazor SSR |211|---|---|---|---|---|212| Best for | Complex web apps | Page-centric apps | Microservices/APIs | Interactive web UI |213| Routing | Convention + attribute | File/folder-based | Lambda-based | Component-based |214| Views | Razor views (.cshtml) | Razor pages (.cshtml) | JSON responses | Razor components (.razor) |215| Model binding | `[FromForm]`, `[FromBody]` | `[BindProperty]` | Parameter injection | `@bind`, `EditForm` |216| Testability | Controller unit tests | PageModel unit tests | Endpoint delegate tests | Component tests |217| SEO | Server-rendered HTML | Server-rendered HTML | N/A (API) | Server-rendered HTML |218| Complexity | Higher | Moderate | Lowest | Moderate |219| Areas/Sections | Yes (Areas) | Yes (folders) | Groups | Layouts |220221## Best Practices2222231. **Choose Razor Pages for page-centric web apps** where each URL corresponds to a single page (e.g., contact forms, dashboards, admin panels), and MVC only when multiple actions per controller are genuinely needed (e.g., a products controller with CRUD + search + bulk operations sharing the same service dependencies).2242252. **Organize minimal API endpoints into static extension methods** (e.g., `MapOrderEndpoints()`, `MapUserEndpoints()`) in separate files under an `Endpoints/` folder, rather than defining all routes in `Program.cs`, to keep the startup file under 50 lines and make each endpoint group independently navigable.2262273. **Use `MapGroup()` to share route prefixes, tags, filters, and authorization policies** across related endpoints instead of duplicating `.RequireAuthorization()` and `.WithTags()` on every individual endpoint, reducing boilerplate and ensuring policy consistency when new endpoints are added.2282294. **Apply `[ValidateAntiForgeryToken]` on every MVC `[HttpPost]` action and Razor Page `OnPost` handler** that processes form submissions, and add `app.UseAntiforgery()` to the middleware pipeline, to prevent cross-site request forgery attacks on state-changing operations.2302315. **Use `TempData` for post-redirect-get (PRG) success messages** in MVC and Razor Pages instead of passing messages via query strings or storing them in session, because `TempData` is automatically cleared after the next request and does not persist across browser refreshes.2322336. **Set `[BindProperty]` on Razor Page properties that receive form data** and use a nested `Input` class to group all bound properties, rather than binding directly to the domain model, to prevent over-posting attacks where malicious users submit fields that should not be user-editable.2342357. **Configure `AddControllersWithViews()` or `AddRazorPages()` with `AddJsonOptions(options => options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase)`** to ensure JSON responses use camelCase property names, matching JavaScript conventions and preventing front-end mapping errors.2362378. **Use the `IWebHostEnvironment.IsDevelopment()` check to conditionally enable Swagger, detailed error pages, and developer exception page** so that sensitive diagnostic information is never exposed in production; `app.UseDeveloperExceptionPage()` leaks stack traces and connection strings.2382399. **Implement `IAsyncActionFilter` or endpoint filters for cross-cutting validation** rather than repeating `ModelState.IsValid` checks in every controller action, centralizing validation logic and ensuring no action accidentally skips the check.24024110. **Deploy behind a reverse proxy (NGINX, Azure App Gateway, YARP) and configure `ForwardedHeaders` middleware** to preserve the original client IP, scheme, and host from the `X-Forwarded-*` headers, because without this configuration, `HttpContext.Connection.RemoteIpAddress` returns the proxy's IP and `Request.Scheme` returns `http` instead of `https`.