This tutorial creates your first ASP.NET Core MVC application on .NET 10, walks through every folder and file the template gives you, and follows one request through the MVC pattern — so the project structure stops being a mystery and becomes a map. Everything here works the same on .NET 8 and 9; only the target framework differs.

What MVC means in ASP.NET Core
MVC separates an application into three responsibilities:
- Model — your data and business logic: entity classes, view models, services.
- View — Razor templates (
.cshtml) that render HTML from model data. - Controller — receives the HTTP request, works with models, and picks the view (or other action result) to return.
A request for /Products/Details/5 flows: routing → ProductsController.Details(5) → the controller loads the model → returns View(model) → Razor renders Views/Products/Details.cshtml → HTML goes back to the browser.
Create the project
From the terminal:
dotnet new mvc -n GeekStore.Mvc
cd GeekStore.Mvc
dotnet run
In Visual Studio: File → New → Project → ASP.NET Core Web App (Model-View-Controller), name it GeekStore.Mvc, pick the latest framework.
dotnet run prints the URL it is listening on — open it and the template home page renders. That is a complete, working MVC app before you have written a line.
The project structure
GeekStore.Mvc/
├── Controllers/
│ └── HomeController.cs
├── Models/
│ └── ErrorViewModel.cs
├── Views/
│ ├── Home/ Index.cshtml, Privacy.cshtml
│ ├── Shared/ _Layout.cshtml, Error.cshtml
│ ├── _ViewImports.cshtml
│ └── _ViewStart.cshtml
├── wwwroot/ css/, js/, lib/ — static files
├── appsettings.json
└── Program.cs
- Controllers/ — one class per resource; MVC finds them by the
Controllersuffix. - Views/ — one folder per controller; a view file per action.
Shared/holds layouts and partials every page can use. _Layout.cshtml— the page chrome (header, nav, footer); each view's output is injected where the layout calls@RenderBody()._ViewStart.cshtml— runs before every view; this is where the default layout is assigned._ViewImports.cshtml—@usingdirectives and tag helpers shared by all views.- wwwroot/ — the only folder served directly to browsers: CSS, JavaScript, images.
- appsettings.json — configuration (connection strings, logging), with
appsettings.Development.jsonoverriding it during development.
Program.cs — the application in one file
The template's entire startup:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllersWithViews();
var app = builder.Build();
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.MapStaticAssets();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}")
.WithStaticAssets();
app.Run();
Two halves: services (everything before Build()) register what the app can use — MVC itself, and later your own services via dependency injection. Middleware (everything after) is the request pipeline, in order: error handling, HTTPS redirect, routing, authorization, static files.
The route pattern {controller=Home}/{action=Index}/{id?} is why / runs HomeController.Index() and /Home/Privacy runs Privacy() — controller and action default when the URL omits them, and id is optional. More in ASP.NET Core MVC routing.
MapStaticAssets (new since .NET 9) replaces the old UseStaticFiles for app assets — it fingerprints and compresses everything in wwwroot at build time, so browsers cache aggressively and still pick up new versions on deploy.
Add your own page — model, controller, view
Model — Models/Product.cs:
namespace GeekStore.Mvc.Models;
public class Product
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
}
Controller — Controllers/ProductsController.cs:
using GeekStore.Mvc.Models;
using Microsoft.AspNetCore.Mvc;
namespace GeekStore.Mvc.Controllers;
public class ProductsController : Controller
{
public IActionResult Index()
{
List<Product> products =
[
new() { Id = 1, Name = "Galaxy A15 Mobile", Price = 390 },
new() { Id = 2, Name = "Air Pods", Price = 200 },
];
return View(products);
}
}
View — Views/Products/Index.cshtml:
@model List<Product>
<h1>Products</h1>
<table class="table">
<thead><tr><th>Name</th><th>Price</th></tr></thead>
<tbody>
@foreach (var p in Model)
{
<tr><td>@p.Name</td><td>@p.Price.ToString("0.00")</td></tr>
}
</tbody>
</table>
Run again and browse to /Products — the default route maps it to ProductsController.Index(), which hands the list to the view. That is the whole MVC loop, and every page you ever build in this framework is a variation of these three files.
Where to go next
- Bind form posts to models: ASP.NET Core MVC model binding
- Validate input with attributes: model validation using DataAnnotations
- Store data with EF Core: code-first migrations
- Building an API instead of pages? ASP.NET Core Web API with Entity Framework
Comments (0)
No comments yet — be the first to share your thoughts.