Types of Web Projects in ASP.NET Core
Key Points
dotnet new --listis the menu — but most teams pick the wrong template because they don't know what each one ships with by default. The right starting template saves a week of removing scaffolding.- Web API is the modern default for HTTP services. .NET 9+ ships OpenAPI on by default, controllers + minimal API support, and
MapOpenApi()wired in. - Blazor Web App (
dotnet new blazor, .NET 8+) unified Server, WASM, and SSR under one project with per-component render modes. The oldblazorservertemplate is gone. - Worker Service is the foundation for headless work — also the shape of Azure Functions isolated worker and any cron container.
- Razor Pages is not deprecated. Page-focused apps still ship faster with Razor Pages than with MVC controllers + views.
- Aspire AppHost is an orchestrator, not a host — it composes other projects (web, worker, gRPC) and provisions their dependencies for local dev.
Concepts (deep dive)
The whole template menu
Run dotnet new --list in .NET 9/10 and you see roughly:
Web API webapi
ASP.NET Core Empty web
ASP.NET Core MVC mvc
Razor Pages razor
Blazor Web App blazor
Blazor WebAssembly Standalone blazorwasm
Worker Service worker
gRPC Service grpc
.NET Aspire App Host aspire-apphost
.NET Aspire Service Defaults aspire-servicedefaults
Razor Class Library razorclasslib
ASP.NET Core SignalR App (community / preview)
.NET MAUI Blazor Hybrid App maui-blazor
Each ships a different Program.cs, a different set of NuGet packages, and a different folder layout. Pick once; live with the choice for the project's lifetime.
Web API — dotnet new webapi
What it ships in .NET 9+:
- OpenAPI on by default.
builder.Services.AddOpenApi()andapp.MapOpenApi(). - Controllers + Minimal API support in the same project.
HttpsRedirectionmiddleware.appsettings.json+appsettings.Development.json.- A sample
WeatherForecastendpoint demonstrating typed results.
Use the -minimal flag to drop controllers and start pure Minimal API:
Use this template for nearly every new HTTP service. See Minimal APIs and MVC & Razor Pages for the deeper dives.
Minimal API only — dotnet new web
The web template is the bare bones — app.MapGet("/", () => "Hello"). No OpenAPI. No appsettings layout. Almost no one starts here unless they want a deliberately tiny demo or are wiring middleware-only services.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.Run();
Prefer webapi --use-minimal-apis for real Minimal API projects — same shape, more batteries.
MVC — dotnet new mvc
Server-rendered HTML via Razor .cshtml views. Ships with:
- Controllers under
/Controllers, views under/Views/{Controller}/{Action}.cshtml. _Layout.cshtml,_ViewStart.cshtml,_ViewImports.cshtml.- Bootstrap-flavored sample site.
- Identity scaffolding available via
--auth Individual.
Still the right call when:
- You're building a multi-page server-rendered app with complex routing and a lot of forms.
- You want traditional model binding + view templates familiar to a long-running team.
- You need anti-forgery and form-post workflows (file uploads, multi-step wizards).
See MVC & Razor Pages for the controller/view conventions.
Razor Pages — dotnet new razor
A page is a .cshtml + a PageModel code-behind. Routing is folder-based (/Pages/Orders/Index.cshtml → /Orders). Each page has its own handler methods (OnGet, OnPost).
public class IndexModel(IOrderRepo repo) : PageModel
{
public IList<Order> Orders { get; private set; } = [];
public async Task OnGetAsync() => Orders = await repo.AllAsync();
public async Task<IActionResult> OnPostDeleteAsync(int id)
{
await repo.DeleteAsync(id);
return RedirectToPage();
}
}
When Razor Pages beats MVC:
- Page-focused apps where each URL is one form/screen (admin tools, internal apps).
- Less ceremony per feature. No controller mediator between route and view.
- Model binding co-located with the page. Easier code review.
When MVC beats Razor Pages:
- One controller serves many actions / formats (HTML + JSON + PDF).
- Heavy use of API-style action results.
Blazor Web App — dotnet new blazor
The .NET 8+ unified template. Replaces the old blazorserver template entirely.
What's unified:
- Static SSR for pages that don't need interactivity (fastest first paint).
- Interactive Server render mode for low-latency UI with a SignalR connection.
- Interactive WebAssembly render mode for client-side execution.
- Interactive Auto — start as Server, transparently swap to WASM once the runtime is downloaded.
Render mode is chosen per component, not per project:
See Render Modes for when to pick each.
Blazor WebAssembly Standalone — dotnet new blazorwasm
Pure SPA. No server project. The output is static files (wwwroot) you can host on:
- Azure Static Web Apps.
- GitHub Pages.
- S3 + CloudFront.
- Any CDN or static host.
Still the right pick when:
- You want a truly static-host SPA with no .NET server.
- You're calling external APIs (own or third-party) with token-based auth.
- You're embedding into a non-.NET host (shipped as a sub-app).
If you're greenfield on a .NET stack with a server already in the picture, prefer Blazor Web App with the WebAssembly render mode — same WASM bytes, but with SSR and unified routing.
Blazor Hybrid (.NET MAUI) — dotnet new maui-blazor
Native desktop or mobile app that hosts Razor components in a BlazorWebView.
<BlazorWebView HostPage="wwwroot/index.html">
<RootComponents>
<RootComponent Selector="#app" ComponentType="{x:Type local:Main}" />
</RootComponents>
</BlazorWebView>
You ship a native Windows / macOS / iOS / Android app, but write the UI in Razor. Components run in-process — full access to native APIs (file system, camera, sensors). See Blazor Hybrid & MAUI.
Worker Service — dotnet new worker
Program.cs is Host.CreateApplicationBuilder (not WebApplication):
var builder = Host.CreateApplicationBuilder(args);
builder.Services.AddHostedService<Worker>();
var host = builder.Build();
host.Run();
What you do not get by default:
- No Kestrel.
- No HTTP routing.
- No middleware pipeline.
- No
wwwroot.
What you do get:
Microsoft.Extensions.Hostinghost with logging, configuration, DI.- A sample
Worker : BackgroundServicethat loops.
This is also the shape of an Azure Functions isolated worker and the typical container for a Kubernetes CronJob. Cross-link to Hosted Services & Background Work.
gRPC Service — dotnet new grpc
Ships:
- A
.protofile under/Protos. - A generated service base class.
- HTTP/2 endpoints via
app.MapGrpcService<PricingService>(). - gRPC reflection in development.
Use when:
- You're building service-to-service RPC with strict contracts.
- You need streaming (server, client, or bidirectional).
- You want Protobuf schema-as-source-of-truth with cross-language clients.
See gRPC for the deep dive.
Aspire AppHost — dotnet new aspire-apphost
dotnet new aspire-apphost -n Storefront.AppHost
dotnet new aspire-servicedefaults -n Storefront.ServiceDefaults
The AppHost is not a runtime app — it's a dev-time orchestrator that composes your other projects:
var builder = DistributedApplication.CreateBuilder(args);
var redis = builder.AddRedis("cache");
var sql = builder.AddSqlServer("sql").AddDatabase("orders");
var api = builder.AddProject<Projects.Orders_Api>("api")
.WithReference(redis)
.WithReference(sql);
builder.AddProject<Projects.Storefront_Web>("web")
.WithReference(api);
builder.Build().Run();
Run the AppHost; it spins up Redis (containerized), SQL (containerized), the API, the web frontend, wires their connection strings, and gives you a dashboard at localhost:18888 showing logs, traces, and metrics across all of them.
In production, each composed project deploys independently. The AppHost stays in dev. See Aspire & Cloud-Native.
Briefly mentioned templates
- SignalR Hub — a community / preview template; in practice, you add SignalR to a Web API project (
builder.Services.AddSignalR()+app.MapHub<T>("/hub")). - Razor Class Library (RCL) —
dotnet new razorclasslib. Ships Razor components/pages as a NuGet package. Use for shared component libraries across multiple Blazor / MVC apps. - ASP.NET Core Empty (
web) — covered above; the bare Minimal API. - Class Library — for shared code (DTOs, services); not a web template per se.
How it works under the hood
dotnet new <template>
│
▼
[ template.json ] ── parameters → tokens
│
▼
[ Generator copies + transforms files ]
│
▼
[ Project files: .csproj + Program.cs + appsettings + sample code ]
│
▼
[ Restore: NuGet pulls Microsoft.* packages from the template's PackageReference list ]
Every template is a content package (NuGet) registered with the dotnet CLI. The shipped templates live in the Microsoft.DotNet.Web.ProjectTemplates.* NuGet packages. You can author your own template, publish to NuGet, and others install it via dotnet new install <pkg>.
The differences between templates are mostly:
- Which
WebApplication.CreateBuildershape is used. - Which middleware is registered by default.
- Which packages are referenced.
- Which sample code is included (and which folder layout).
You can always start with one template and add the others' packages later. The choice mostly affects how fast the first feature ships.
Code: correct vs wrong
❌ Wrong: starting with dotnet new web for an HTTP API
dotnet new web -n Orders.Api
# ...then manually add OpenAPI, controllers, JSON options, problem details, ...
You'll spend a day reproducing what webapi ships in 30 seconds.
✅ Correct: pick the closest-fit template
dotnet new webapi -n Orders.Api
# OpenAPI, controllers, sample endpoint, HTTPS, dev settings — all wired.
❌ Wrong: separate Server and WASM projects in 2026
dotnet new blazorserver -n Web
dotnet new blazorwasm -n WebClient
# Then plumb shared models, routing, auth across both. Ouch.
(blazorserver no longer ships in .NET 8+ for this reason.)
✅ Correct: Blazor Web App with mixed render modes
One project, one routing system, one auth surface. Pick render mode per component.
❌ Wrong: Worker Service for an HTTP-receiving service
If it accepts HTTP, start from webapi and add a BackgroundService to it.
✅ Correct: Web API + hosted services
Design patterns for this topic
Pattern 1 — "Pick the closest-fit template, never the bare one"
- Intent: start with the most batteries; remove what you don't need.
Pattern 2 — "Aspire AppHost as the dev composition root"
- Intent: describe the topology once; let local dev mirror prod shape.
Pattern 3 — "Razor Class Library for shared components"
- Intent: ship a reusable Razor library across multiple host apps.
Pattern 4 — "Worker Service for headless containers"
- Intent: uniform shape across cron, queue workers, and Functions isolated.
Pattern 5 — "Blazor Web App with per-component render mode"
- Intent: SSR for content pages, interactive for the dashboards — one project.
Pros & cons / trade-offs
| Template | Pros | Cons |
|---|---|---|
| webapi | Modern default; OpenAPI on; controllers + Minimal | Slightly heavier than web |
| web (empty) | Tiny; explicit | You add everything |
| mvc | Mature; flexible | More ceremony than Razor Pages |
| razor (Pages) | Page-focused; low ceremony | Awkward for many-action endpoints |
| blazor (Web App) | Unified Server/WASM/SSR | Render-mode mental model |
| blazorwasm | Static-host SPA; no .NET server | No SSR; longer first paint |
| maui-blazor | Native + Razor | MAUI tooling overhead |
| worker | Headless; Functions-shaped | No HTTP by default |
| grpc | Strict contracts; streaming | Not browser-friendly without grpc-web |
| aspire-apphost | Dev orchestration + dashboard | Dev-only; not a runtime |
Decision matrix — "I want X → start with Y"
| I want… | Start with |
|---|---|
| A REST/HTTP API | webapi |
| A pure Minimal API service | webapi --use-minimal-apis |
| Server-rendered multi-page web app | mvc |
| Server-rendered admin / form-heavy app | razor |
| Modern interactive web UI in C# | blazor |
| Static-hostable SPA in C# | blazorwasm |
| Native desktop/mobile app in C# | maui-blazor |
| A scheduled job / queue consumer | worker |
| Service-to-service RPC | grpc |
| Local dev orchestration of many projects | aspire-apphost |
| Reusable component library | razorclasslib |
Deployment-target reference
| Template | Best deployment targets |
|---|---|
| webapi | App Service, Container Apps, AKS, Functions (isolated repackage) |
| mvc / razor | App Service, Container Apps, AKS |
| blazor (Web App) | App Service, Container Apps, AKS |
| blazorwasm | Static Web Apps, S3+CloudFront, GitHub Pages |
| maui-blazor | Microsoft Store, Mac App Store, App Store, Play Store |
| worker | Container Apps Jobs, AKS, Container Instances, on-prem service |
| grpc | Container Apps, AKS (HTTP/2 required; App Service supports HTTP/2 with caveats) |
When to use / when to avoid
- Use webapi for nearly every new HTTP service in .NET 9+.
- Use Razor Pages when each URL is one screen; MVC when one controller serves many formats.
- Use Blazor Web App for new interactive apps; standalone WASM only when you genuinely need static hosting.
- Use Worker Service for headless, long-running, or scheduled processes.
- Use Aspire AppHost in dev for any multi-project topology.
- Avoid
web(empty) for real services — the savings vanish in five minutes. - Avoid the old
blazorservermuscle memory — it's been replaced byblazor.
Interview Q&A
Q1. What's new in dotnet new webapi in .NET 9+ that wasn't in .NET 6? OpenAPI on by default (Microsoft.AspNetCore.OpenApi), MapOpenApi() wired, controllers + Minimal API supported in the same project, TypedResults examples, and stronger problem-details defaults.
Q2. When do you choose Razor Pages over MVC? Page-focused apps where each URL is one form or screen. Razor Pages co-locates handler with view; less ceremony per feature.
Q3. How did Blazor Web App unify the old Server vs WASM split? One project, one routing system, one auth surface. Render mode is chosen per component (Static SSR, InteractiveServer, InteractiveWebAssembly, InteractiveAuto). The old blazorserver template no longer ships.
Q4. When does Blazor WebAssembly Standalone still make sense? When you want a static-host SPA with no .NET server — Static Web Apps, GitHub Pages, S3+CloudFront, or embedding inside a non-.NET host.
Q5. Difference between Worker Service and Web API host? Worker uses Host.CreateApplicationBuilder — no Kestrel, no middleware, no wwwroot. Web API uses WebApplication.CreateBuilder with Kestrel and routing.
Q6. What is .NET Aspire AppHost? Dev-time orchestrator that composes other projects (web, worker, gRPC, plus containerized deps) and gives a dashboard with logs/traces/metrics. Not a runtime — only runs in dev.
Q7. Razor Class Library — when? When you want to ship reusable Razor components/pages as a NuGet package consumed by multiple Blazor or MVC apps.
Q8. Migrating from MVC to Minimal API — when worth it? Worth it when controllers became thin shells around services. Not worth it when you have heavy filter pipelines, model binding, and view-rendering — keep MVC for those.
Q9. gRPC vs Web API — when do you pick gRPC? Service-to-service with strict Protobuf contracts, streaming, and cross-language clients. Pick Web API when callers are browsers or the contract is JSON-shaped.
Q10. SignalR template — does it exist? Not as a first-party template. Add SignalR to a Web API project: AddSignalR() + MapHub<T>("/hub").
Q11. Why dotnet new web (empty) almost never? You spend a day rebuilding what webapi ships in 30 seconds. Pick webapi and remove what you don't need.
Q12. How do you migrate from one template to another? Templates are just initial scaffolding. To "migrate," you copy missing packages and Program.cs wiring from the target template into your existing project. Often easiest: scaffold a fresh target project and move feature code in.
Gotchas / common mistakes
- ⚠️ Starting with
web(empty) when you really wantedwebapi— costly time loss. - ⚠️
blazorservermuscle memory — that template is gone; useblazor(Web App). - ⚠️ Razor Pages assumed deprecated — it's not. Still first-class in .NET 9+.
- ⚠️ Worker Service for HTTP intake — bolting Kestrel onto a worker is wrong shape; start from
webapi. - ⚠️ Aspire AppHost confused with a runtime — it's a dev orchestrator only.
- ⚠️ Mixing
blazorwasm(standalone) andblazor(Web App) in one solution — pick one for new code. - ⚠️ Per-template OpenAPI defaults differ — only
webapiships it on by default.