Routing is a core part of ASP.NET’s request processing pipeline. In the early days of the MVC pattern—back in the 2010s—it was largely invisible, as conventional routing was the only available option. Routing became a more explicit and independent layer when attribute-based routing was introduced alongside the conventional model. The underlying routing engine remained largely consistent during the transition from the .NET Framework to .NET Core. More recently, with the advent of Minimal APIs, routing has undergone yet another evolution.
This article sheds light on the different forms of routing available in modern ASP.NET Core and explains how they fit into the framework’s unified routing infrastructure.
What Is Routing?
The ASP.NET Core routing engine is centered around the UseRouting middleware, which performs a single, very specific job: matching incoming HTTP requests to one of the endpoints defined in the application. So, what exactly is an endpoint?
In ASP.NET Core, an endpoint is essentially a logical destination for an HTTP request. More formally, it is a combination of route information and an associated request handler that executes when a request matches the route. In other words, an endpoint defines what code should run in response to a specific URL.
When the application starts, all endpoints registered through MapGet, MapPost, MapControllerRoute, MapRazorPages, and similar methods are collected into an endpoint data source. The UseRouting middleware, when executed, looks up the request path and HTTP method against this list of endpoints and—if it finds a match—attaches the corresponding metadata to the HTTP context. Note that at this point, routing is resolved but not executed: the actual handler—whether a lambda, controller method, or page—is not yet invoked. How does ASP.NET Core invoke the handler?
Invoking the Request Handler
The process depends on whether you’re using Minimal APIs, MVC controllers, or Razor Pages. In general, the steps are those outlined below.
- Incoming request
- Preliminary middleware UseRouting to resolve the endpoint
- Optional middleware like Authorization
- Response returned
Acceptable endpoints for an application are usually registered within the UseEndpoints middleware. This is necessary for all types of endpoints except Minimal APIs. Note that MapGet (and similar methods) can be called at any point after the WebApplication is built, but its position determines which middleware runs before the endpoint executes. MapGet simply registers an endpoint; it is not middleware itself, though it exists within the middleware pipeline and interacts with it.
The UseEndpoints middleware is responsible for executing the matched endpoint. It examines the associated delegate or request handler and invokes it. Depending on the endpoint type, this may involve:
- Dispatching to an MVC controller action
- Rendering a Razor Page
- Executing a custom endpoint defined via IEndpointConventionBuilder
- Executing the lambda of a Minimal API endpoint
Here’s an example:
app.UseEndpoints(endpoints =>
{
// Map MVC controllers (attribute routing)
endpoints.MapControllers();
// Map Razor Pages
endpoints.MapRazorPages();
// Map Minimal API endpoints
endpoints.MapGet("/hello", () => "Hello World!");
// Map a custom endpoint
endpoints.Map("/custom", async context =>
{
await context.Response.WriteAsync("This is a custom endpoint!");
});
});
In Minimal APIs, the execution of the captured handler—the lambda registered with methods like MapGet—is essentially immediate. In contrast, MVC-style applications require additional steps and configuration before the corresponding controller action executes.
Delayed Start of Execution for MVC Endpoints
In MVC-based applications, UseEndpoints must activate the MVC middleware to determine the actual code to execute. The metadata associated with the endpoint provides information about which controller class to instantiate, which action method to call, and its signature.
Before the action method can run, the ASP.NET Core runtime needs an instance of the controller class, which is typically obtained via reflection. This process includes multiple injection points, where the application can provide custom behavior or services to the controller. These additional steps can introduce some delay before the actual execution of the request handler begins.
With Minimal APIs, the code to execute is a ready-to-run lambda, so execution begins almost immediately.
That said, there are still a few steps the request must pass through before the developer’s code fully takes the spotlight.
From Start to Actual Code
However flexible it might appear, handling an HTTP request isn’t a straight call into your code. Both Minimal API and MVC endpoints flow through a pipeline of filters that can block, customize, or enrich the execution context. The actual list of filters depends on the type of endpoint—Minimal API or traditional ASP.NET Core.
In Minimal APIs, filters are registered classes that implement the IEndpointFilter interface or lambdas added via AddEndpointFilter.
app.MapPost("/items", async (Item item) =>
{
// handler logic
})
.AddEndpointFilter(async (context, next) =>
{
// pre-handler logic
var result = await next(context); // next filter or handler
// post-handler logic
return result;
});
Endpoint filters form a single chain of components that run in the order they were registered. You can use filters for cross-cutting concerns like validation, logging or ad hoc permissions.
app.MapGet("/data", () => Results.Ok(new { Value = 123 }))
.AddEndpointFilter<ApiKeyFilter>();
The following example shows how to check for a specific header key.
public async ValueTask<object> InvokeAsync(EndpointFilterInvocationContext context,
EndpointFilterDelegate next)
{
var http = context.HttpContext;
if (!http.Request.Headers.TryGetValue(HeaderName, out var key) ||
key != Secret)
return Results.Unauthorized();
return await next(context);
}
The preliminaries of execution are more sophisticated for other endpoints and involve three distinct layers: action filters, model binding, and model validation. Furthermore, action filters come in five different types, each with a specific role in the pipeline. Here’s the list of supported action filters.
- Authorization. Executed before any other filters; responsible for performing authorization checks.
- Resource. Executed before model binding and again after the result has been executed.
- Action. Executed immediately before and after the action method runs.
- Result. Executed immediately before and after the action result is processed (after the action itself).
- Exception. Executed whenever an unhandled exception occurs during action or result execution.
The execution of an endpoint is split into three steps:
- Model binding – Responsible for populating the input parameters of the method to be called.
- Code execution – The selected method executes using the model-bound parameters. This step produces the action result metadata (e.g., an IActionResult), but does not generate the actual HTTP response.
- Result generation – Processes the metadata from step 2 to produce the final HTTP response sent to the client.
Action filters run around step 2. Result filters run around step 3. Resource filters wrap the entire process (step 1 to 3).
Model Binding
Generally, model binding in ASP.NET Core is the process of taking data from the HTTP request (route, query string, headers, cookies, or body) and mapping it to the parameters of an endpoint handler.
In Minimal APIs, model binding is tied directly to the endpoint invocation and provides the same core capabilities as MVC. In MVC, model binding is richer, offering additional features such as automatic model validation.
That said, the capabilities of model binding in Minimal APIs are approaching parity with MVC. For example, automatic validation is supported in Minimal APIs starting with .NET 10. Both Minimal APIs and MVC allow explicit specification of the binding source (body, header, query) via attributes. However, more sophisticated or custom binding for complex types in Minimal APIs can often be handled directly by the target class, whereas in MVC it typically requires additional helper classes or custom model binders.
Key Takeaway
Here’s the key takeaway for ASP.NET Core routing: UseRouting matches the request to an endpoint, while UseEndpoints executes the matched handler. MVC, Razor Pages, and Minimal APIs all share the same routing infrastructure, but Minimal APIs streamline the process by embedding routing automatically and reducing middleware layers.
In short, routing in ASP.NET Core is unified, composable, and endpoint-driven. What differs across models is not the routing system itself, but where and how the final execution occurs.
Routing first appeared in classic ASP.NET (before .NET Core) as a control-specific mechanism. In .NET Core, however, it has evolved into a universal mechanism at the heart of request processing.