The foundation: what the official documentation explains
ASP.NET Core MVC separates request handling, presentation, and application data. A controller action can return a view that Razor renders into HTML. ASP.NET Core can also host HTTP API endpoints in the same application. These capabilities let a page use server-rendered navigation while fetching data independently; MVC does not require every database operation to live inside a controller.
Implementation context: The examples use a .NET 10 MVC application organized into feature folders (Vertical Slice Architecture). Basic C# classes and async/await are assumed. Reference excerpts show selected parts of that application; separately labeled teaching adaptations explain alternatives. They are not complete standalone projects.
Start with a screen you can explain
Imagine an administrator opening a Country list and adding a new country. There are three different things to identify: the address of the screen, the address that accepts its data, and the C# method that performs the change. Treating all three as one controller action makes an unfamiliar codebase harder to read.
The example application targets .NET 10. Its Country feature lives under Areas/Admin/Country. The examples here are excerpts from that working structure, not a standalone application or a requirement to adopt a particular template. You need basic C# classes, methods, and async/await to follow the flow.
First trip: render the MVC page
A browser navigation to /Admin/Country/Create matches the area, controller, and action. The action below selects an explicit Razor file. It does not insert a Country and does not receive the JSON submitted later. Authorization is declared on the controller in the reference implementation.
Reference excerpt: Areas/Admin/Country/Controllers/CountryController.cs
public IActionResult Create()
{
return View("~/Areas/Admin/Country/Views/Create.cshtml");
}
This small action is useful precisely because it answers only the presentation question: which screen should be shown? The full view uses a shared layout and loads a collocated JavaScript file. Rendering that view is the end of this first request; the database write is a different request initiated by the browser.
Second trip: submit a JSON request
The Create page collects a code, name, and description. Its JavaScript sends a POST to /api/country. The URL contains no MVC area because it belongs to an explicitly mapped API route. This split lets the screen and the write operation evolve independently without moving the feature into another deployed service.
Reference excerpt: Areas/Admin/Country/Views/Create.cshtml.js
const response = await fetch('/api/country', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
code: form.code,
name: form.name,
description: form.description || null
})
});
Third step: delegate the use case
Inside the API route, ASP.NET Core supplies the request object, a database context, and the request cancellation token. The endpoint constructs a feature-specific handler and awaits it. The handler checks input, checks the supplied code for duplicates, maps the request into an entity, and saves it. The response exposes the created identifier and code rather than returning the complete tracked entity.
Reference excerpt: Areas/Admin/Country/Endpoints/CountryEndpoint.cs
// POST /api/country
group.MapPost("/", async (CreateCountryRequest request, AppDbContext db, CancellationToken ct) =>
{
var handler = new CreateCountryHandler(db);
var result = await handler.HandleAsync(request, ct);
return result.Success
? Results.Created($"/api/country/{result.Data?.Id}", result)
: Results.BadRequest(result);
})
.WithName("CreateCountry");
Read the feature in this order
Open the controller, then the matching view, then its JavaScript, then the endpoint, and finally the handler and validator. Write down the URL at each boundary. For Country, the successful endpoint result is 201 Created; a handler-reported validation failure maps to 400 Bad Request. These are the endpoint-level results. Middleware can affect the final response, so the browser Network panel remains the place to inspect what actually arrived.
The browser consumes result.success and result.data. Those properties come from the application-defined ApiResponse<T> wrapper. They are not built-in MVC response fields. Likewise, the Cqrs folder and direct handler construction are project organization choices. No message broker or separate read database is required by the code shown here.
A practical tracing exercise
In a local development copy, put breakpoints in the Create action and the create handler. Opening the screen should reach the action; submitting valid data should reach the handler through the API route. Then remove the name from the JSON in an API client. The server should reject that input even if the browser normally prevents submission. Use a disposable country code so the exercise does not alter important data.
If the page opens but saving fails, inspect the POST rather than repeatedly debugging the GET action. Check its method, content type, authentication, response body, and status. If the POST never appears, investigate the JavaScript load or client-side validation. This narrows the investigation before you touch database configuration.
Key Takeaways
- A page navigation and an API call are distinct requests.
- Follow URLs and payloads before studying architectural terminology.
- MVC, Minimal APIs, and EF Core can cooperate inside one process.
FAQ
Does MVC require all CRUD code inside controllers?
No. Controllers can delegate application work. This example goes further: MVC actions render pages while Minimal API endpoints call feature handlers.
Is this a microservice architecture?
No. Feature folders organize one application. A separate endpoint does not imply a separate process or database.
Do I need Vue to understand this flow?
No. Vue manages this example screen, but the same HTTP boundaries can be used by plain JavaScript or another frontend.