Updated on

MapControllers() registers the controller actions that carry route attributes. MapControllerRoute() registers a URL pattern that ASP.NET Core matches against controller and action names. A controller is reached through one or the other, never both.

To download the source code for this article, you can visit our GitHub repository.

They are not competing settings, though, and one application can call both. That is what the sample project below does, and it is what most applications that serve HTML pages alongside an API end up doing.

What Is the Difference Between MapControllers and MapControllerRoute?

MapControllers() registers the actions that carry routing attributes. MapControllerRoute() registers a URL pattern that ASP.NET Core matches against controller and action names.

The split is per controller, not per application. Microsoft Learn is blunt about it: “Any route attribute on the controller makes all actions in the controller attribute routed.”

Once a controller is attribute routed, a conventional pattern can no longer reach it, and the reverse holds as well. The documentation states that we “can’t reach actions that define attribute routes through the conventional routes, and vice versa.”

So the two methods are not competing settings. They are two ways of saying where an action lives, and one application can use both at once: conventional routes for the controllers that return HTML pages, attribute routes for the controllers that serve an API.

The sample project for this article does exactly that. It calls both methods in the Program class, and each of its two controllers is reached through one of them.

Both quoted sentences come from Routing to controller actions in ASP.NET Core on Microsoft Learn.

To learn more about routing, check out our great article Routing in ASP.NET Core MVC.

What Does MapControllers Need in Program.cs?

MapControllers() needs the MVC services registered before it runs. A call to builder.Services.AddControllers() covers an application that only serves an API, and AddControllersWithViews() covers one that also returns views, which is what this article’s sample calls.

Miss the registration and the application throws at startup rather than at the first request. The exception names the method to add, so the failure is loud and the fix is obvious once it is seen.

The MapControllerRoute() method carries the same requirement. Both methods run the same service check before they register anything, so neither of them works on its own.

The call to UseRouting() is a different matter. Microsoft Learn says applications “typically don’t need to call UseRouting or UseEndpoints”, because the web application builder already wraps the middleware added in the startup file in both. Calling it explicitly only matters when other middleware has to run before route matching.

The order never changes: register the services on the builder, build the application, then map the controllers.

In a minimal application that serves both views and an API, the whole of it looks like this:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddControllersWithViews();

var app = builder.Build();

app.MapControllers();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

app.Run();

Leave the registration out and this is what starts up instead:

Unable to find the required services. Please add all the required services by calling 'IServiceCollection.AddControllers' inside the call to 'ConfigureServices(...)' in the application startup code.

The message names AddControllers even when the application needs AddControllersWithViews, because the arguments to it are fixed.

Every action mapped here lands in the application’s OpenAPI document by default, and hiding one of them from the generated OpenAPI document takes one attribute.

How Do We Use MapControllers for Attribute Routing?

We use the MapControllers() method to enable attribute-based routing at the controller level of our application. Moreover, this allows us to express controller routes using attributes.

Configure MapControllers

The MapControllers() method enhances control and flexibility, which allows detailed, attribute-focused routing for application route definitions.

Let’s see how we can configure attribute routing in our ASP.NET Core MVC application:

app.MapControllers();

Inside the Program class, we call the app.MapControllers() method to indicate to the application that we want to allow attribute routing.

Creating a Controller for MapControllers

After that, let’s create a new Controller and name it CustomersController. Let’s go ahead and add the attribute base routing to the controller:

[Route("Customers")]
public class CustomersController : Controller
{
    [Route("")]
    [Route("Index")]
    public IActionResult Index()
    {
        return Ok("Customers Index");
    }

    [HttpGet("Info/{id}")]
    public IActionResult Detail(int id)
    {
        return Ok($"Customer {id} Info");
    }
}

Here, we use the RouteAttribute at the controller level which takes one string parameter template to establish a route template for all actions within our CustomersController class. This means that the base path for actions in this controller will start with /Customers.

Additionally, for the Index action, we use two RouteAttribute to specify two route templates "" and Index, which makes the action accessible through two URL paths: /Customers and /Customers/Index. Both routes lead to the same action, which provides flexibility to access the Index action.

Also, the Detail action is a parameterized route which uses the HttpGetAttribute and accepts a string parameter template Info/{id}. As a result, the action is accessible through an HTTP GET request via the URL path /Customers/Info/4 where 4 is the value of the id parameter in the route template string.

Our CustomersController derives from Controller here, but a controller that serves an API would normally also carry the [ApiController] attribute that usually travels with attribute routing, which switches on automatic model validation and a few binding conventions.

Once a request reaches one of these actions, reading the request body inside an action is usually the next thing it does.

Lastly, let’s look at a few more examples of using attributes, still inside CustomersController:

[Route("Order")]
[Route("Customer/Order")]
public IActionResult Order()
{
    return Ok("Customers Order");
}

[HttpGet("/Special")]
public IActionResult SpecialRoute()
{
    return Ok("Customers SpecialRoute");
}

Here, we associate the Order action with two RouteAttribute, Order and Customer/Order. Both templates are relative to the controller-level [Route("Customers")], so the action is accessible through two distinct URL paths: /Customers/Order and /Customers/Customer/Order.

For the SpecialRoute() action, we configure it with an absolute path route with HttpGetAttribute, we use /Special as our parameter. Furthermore, this specifies that the action can be directly accessible through a GET request using the URL path /Special regardless of the controller-level route of the CustomersController class. This differs from relative routes, it will bypass the usual controller-level route templates.

How Do We Use MapControllerRoute for Conventional Routing?

The MapControllerRoute() method allows us to configure a consistent mapping between URL patterns and controller actions. Therefore, it allows us a convention-based approach to define routing templates that follow common URL structures for our web app.

That is, it allows routing configuration without explicitly using attributes on controllers and actions. Instead, it depends on conventions and it presents a practical choice for situations where an attribute-based approach may be less fitting or where a combination of conventions and attributes is preferred.

Configure MapControllerRoute

Next, let’s look at the usage of MapControllerRoute() in our application:

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

Here, we configure a default route for controllers in the Program class of our application. Let’s break the route configuration down for better understanding.

We use the name parameter to give the route a name. In this case, we name it default.

For the second parameter, pattern we use a template to define the route with a specific pattern:

PatternExplanation
{controller=Home}This part of the pattern represents the controller. If the URL does not explicitly specify a controller, it defaults to Home
{action=Index}This part stands for the action name. If the URL does not specify an action, it defaults to Index
{id?}This part is for an id parameter which can be null. The ? indicates that the id parameter is optional. If present in the URL, its value is captured; otherwise, it's null.

The question mark on the last token is the only part of the pattern that changes what a URL may leave out, and we cover optional route parameters and what {id?} does to a URL in a separate article.

Evidently, MapControllerRoute() allows us to define routes to make it easier to understand and manage. Specifically, MapControllerRoute is sometimes called conventional routing because it follows a convention-based approach to define routes.

Creating a Controller for MapControllerRoute

The pattern’s Home and Index defaults have to resolve to something, so let’s add the controller they name:

public class HomeController : Controller
{
    public IActionResult Index()
    {
        return View();
    }
}

There is no route attribute anywhere on this controller, which is what leaves it reachable through the conventional route. The URLs /, /Home and /Home/Index all arrive at Index(), and it returns the matching view.

What Happens When We Call Both MapControllers and MapControllerRoute?

Nothing breaks, and no endpoint is registered twice. Both calls reach the same ControllerActionEndpointDataSource, and ASP.NET Core builds one endpoint per action rather than one endpoint per registration call.

The data source walks every action once. An action with a route attribute gets an endpoint built from that attribute. An action without one gets an endpoint for each conventional route that can reach it. An action that already carries an attribute is skipped by the conventional routes, so the two sets never overlap.

That is why calling both is safe, and it is also why MapControllerRoute() on its own is often enough. Microsoft Learn describes it as mapping “both conventionally routed controllers and attribute routed controllers”, while MapControllers() maps only the attribute-routed ones.

Real duplicates come from somewhere else. Two actions carrying the same route template collide, and the request then fails with an AmbiguousMatchException. The analyzer ASP0023 flags the pair at build time. Calling both mapping methods together does not cause that.

Both MapControllers and MapControllerRoute register endpoints through one shared ControllerActionEndpointDataSource, which gives attribute-routed actions their endpoint once and conventional routes their own.

The sample project makes all of this concrete. These are every URL it exposes, and the routing style that put each one there:

URLRouting styleController and action
/ConventionalHomeController.Index()
/HomeConventionalHomeController.Index()
/Home/IndexConventionalHomeController.Index()
/CustomersAttributeCustomersController.Index()
/Customers/IndexAttributeCustomersController.Index()
/Customers/Info/4AttributeCustomersController.Detail(4)
/Customers/OrderAttributeCustomersController.Order()
/Customers/Customer/OrderAttributeCustomersController.Order()
/SpecialAttributeCustomersController.SpecialRoute()

Every attribute row here would still work with app.MapControllers() deleted, because MapControllerRoute() maps attribute-routed controllers too. The conventional rows are the three that need it.

Once an application has settled which style each controller uses, putting a prefix in front of every route in the application becomes the next question worth asking.

How Do We Choose Between MapControllers and MapControllerRoute?

Choose per controller, not per application. Each controller answers one question: does its URL belong to a browsing path, or to an API surface?

Reach for MapControllerRoute() when the URLs follow the shape of the application. A pattern such as {controller=Home}/{action=Index}/{id?} gives every new controller a working URL the moment it is added, with no attribute to write.

Reach for MapControllers() and route attributes when the URL is a contract. API routes get versioned, published, and depended on by code we do not own, so they belong next to the action they serve, where renaming a class cannot quietly move them.

Applications that do both keep the two apart: conventional routes for the controllers that return views, attribute routes for the controllers that return data. Microsoft Learn recommends that arrangement, and it is the one the sample project uses: “Typically, you use conventional routes for controllers that serve HTML pages to browsers, and attribute routing for controllers that serve REST APIs.”

MapControllersMapControllerRoute
Routes configuration is done through attributes.Route configuration is convention-based
Suitable for Web API applicationsSuitable for conventional ASP.NET Core MVC applications
Ideal when routing requirements are complexIdeal for simple routing requirements
The route is written next to the action, so a rename cannot change the URL by accidentThe route is written once in Program.cs, so a new controller gets a URL without touching it
Ideal for explicit route definitions and customization requirementsIdeal for situations where a standardized routing method is preferred

Conclusion

MapControllers is attribute-based routing at the controller level, which allows detailed and flexible route definition using attributes.

MapControllerRoute is a convention-based routing, it provides a standardized mapping between URL patterns and controller actions without the explicit use of attributes.

The choice is made per controller rather than per application. A controller that serves HTML pages takes the conventional route. A controller that serves an API takes route attributes.

Applications that do both call both methods, and the two never collide: an action reached by an attribute is not reachable through a conventional pattern, and the endpoints are built once either way.

Tested with .NET 10.0.10.