MapControllers() registra las acciones de controlador que llevan atributos de ruta. MapControllerRoute() registra un patrón de URL que ASP.NET Core compara con los nombres de controlador y de acción. A una acción se llega mediante sus atributos de ruta o mediante un patrón de URL, nunca por las dos vías.
Aun así, no son opciones excluyentes, y una misma aplicación puede llamar a las dos. Es lo que hace el proyecto de ejemplo que veremos a continuación, y es lo que acaban haciendo la mayoría de las aplicaciones que sirven páginas HTML junto con una API.
¿Cuál es la diferencia entre MapControllers y MapControllerRoute?
MapControllers() registra las acciones que llevan atributos de ruta. MapControllerRoute() registra un patrón de URL que ASP.NET Core compara con los nombres de controlador y de acción.
La división se hace por controlador, no por aplicación. Microsoft Learn lo dice sin rodeos: “Cualquier atributo de ruta en el controlador hace que todas las acciones del controlador usen enrutamiento por atributos”.
Una vez que un controlador usa enrutamiento por atributos, un patrón convencional ya no puede llegar a él, y lo contrario también se cumple. La documentación afirma que “no se puede llegar a las acciones que definen rutas por atributos a través de las rutas convencionales, y viceversa”.
Así que los dos métodos no son opciones excluyentes. Son dos formas de indicar dónde vive una acción, y una misma aplicación puede usar ambas a la vez: rutas convencionales para los controladores que devuelven páginas HTML y rutas por atributos para los controladores que sirven una API.
El proyecto de ejemplo de este artículo hace exactamente eso. Llama a los dos métodos en la clase Program, y a cada uno de sus dos controladores se llega a través de uno de ellos.
Las dos frases citadas proceden de Routing to controller actions in ASP.NET Core, en Microsoft Learn.
¿Qué necesita MapControllers en Program.cs?
MapControllers() necesita que los servicios de MVC estén registrados antes de ejecutarse. Una llamada a builder.Services.AddControllers() basta para una aplicación que solo sirve una API, y AddControllersWithViews(), para una que además devuelve vistas. El ejemplo de este artículo llama a este último.
Si falta el registro, la aplicación lanza una excepción al arrancar, no en la primera solicitud. La excepción nombra el método que hay que añadir, así que el fallo no pasa inadvertido y la solución resulta obvia en cuanto se ve.
El método MapControllerRoute() tiene el mismo requisito. Los dos métodos ejecutan la misma comprobación de servicios antes de registrar nada, así que ninguno de ellos funciona por sí solo.
La llamada a UseRouting() es otra cuestión. Microsoft Learn dice que las aplicaciones “normalmente no necesitan llamar a UseRouting ni a UseEndpoints”, porque el generador de la aplicación web ya envuelve en ambos el middleware que se añade en el archivo de inicio. Llamarlo explícitamente solo importa cuando otro middleware tiene que ejecutarse antes de la coincidencia de rutas.
El orden nunca cambia: registrar los servicios en el generador, construir la aplicación y después mapear los controladores.
En una aplicación mínima que sirve tanto vistas como una API, todo queda así:
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();
Si dejas fuera el registro, esto es lo que aparece al arrancar:
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.
El mensaje nombra AddControllers incluso cuando la aplicación necesita AddControllersWithViews, porque sus argumentos son fijos.
Las acciones de los controladores marcados con [ApiController] aparecen de forma predeterminada en el documento OpenAPI de la aplicación, y ocultar una de ellas del documento OpenAPI generado requiere un solo atributo.
¿Cómo usar MapControllers para el enrutamiento por atributos?
Usamos el método MapControllers() para habilitar el enrutamiento por atributos a nivel de controlador en la aplicación. Además, esto nos permite expresar las rutas de los controladores mediante atributos.
Configurar MapControllers
El método MapControllers() mejora el control y la flexibilidad, lo que permite un enrutamiento detallado y centrado en atributos para las definiciones de rutas de la aplicación.
Veamos cómo configurar el enrutamiento por atributos en nuestra aplicación ASP.NET Core MVC:
app.MapControllers();

Ebook gratis
¿Tu Web API está lista para producción?
33 puntos que comprobar antes de desplegarla, con la solución de cada uno. Un PDF gratuito de 76 páginas para .NET 10.
El ebook está en inglés.
Descarga la checklist gratisPDF gratuito. Un solo correo para enviártelo. Puedes darte de baja cuando quieras.
Dentro de la clase Program, llamamos al método app.MapControllers() para indicar a la aplicación que queremos permitir el enrutamiento por atributos.
Crear un controlador para MapControllers
Después, vamos a crear un controlador nuevo y llamarlo CustomersController. A continuación, añadimos el enrutamiento por atributos al controlador:
[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");
}
}
Aquí usamos RouteAttribute a nivel de controlador, que recibe un parámetro de cadena, template, para establecer una plantilla de ruta para todas las acciones de la clase CustomersController. Esto significa que la ruta base de las acciones de este controlador empezará por /Customers.
Además, para la acción Index, usamos dos RouteAttribute para especificar dos plantillas de ruta, "" y Index, con lo que la acción es accesible a través de dos rutas de URL: /Customers y /Customers/Index. Las dos rutas llevan a la misma acción, lo que da flexibilidad para acceder a la acción Index.
Por otro lado, la acción Detail es una ruta con parámetros que usa HttpGetAttribute y acepta una plantilla de parámetro de cadena, Info/{id}. Como resultado, se puede acceder a la acción con una solicitud HTTP GET mediante la ruta de URL /Customers/Info/4, donde 4 es el valor del parámetro id en la cadena de la plantilla de ruta.
Aquí, nuestro CustomersController deriva de Controller, pero un controlador que sirve una API normalmente llevaría además el atributo [ApiController] que suele acompañar al enrutamiento por atributos, el cual activa la validación automática del modelo y algunas convenciones de enlace.
Cuando una solicitud llega a una de estas acciones, leer el cuerpo de la solicitud dentro de una acción suele ser lo siguiente que hace.
Por último, veamos algunos ejemplos más de uso de atributos, todavía dentro de CustomersController:
[Route("Order")]
[Route("Customer/Order")]
public IActionResult Order()
{
return Ok("Customers Order");
}
[HttpGet("/Special")]
public IActionResult SpecialRoute()
{
return Ok("Customers SpecialRoute");
}
Aquí asociamos la acción Order con dos RouteAttribute, Order y Customer/Order. Las dos plantillas son relativas al [Route("Customers")] de nivel de controlador, así que la acción es accesible a través de dos rutas de URL distintas: /Customers/Order y /Customers/Customer/Order.
La acción SpecialRoute() la configuramos con una ruta absoluta mediante HttpGetAttribute, y usamos /Special como parámetro. Además, esto especifica que se puede acceder a la acción directamente con una solicitud GET mediante la ruta de URL /Special, sea cual sea la ruta de nivel de controlador de la clase CustomersController. Esto la distingue de las rutas relativas: se salta las plantillas de ruta habituales de nivel de controlador.
¿Cómo usar MapControllerRoute para el enrutamiento convencional?
El método MapControllerRoute() nos permite configurar una correspondencia coherente entre patrones de URL y acciones de controlador. Por lo tanto, nos ofrece un enfoque basado en convenciones para definir plantillas de ruta que siguen estructuras de URL habituales en nuestra aplicación web.
Es decir, permite configurar el enrutamiento sin usar explícitamente atributos en los controladores y las acciones. En su lugar, se basa en convenciones, y es una opción práctica para los casos en los que un enfoque basado en atributos puede encajar peor o en los que se prefiere una combinación de convenciones y atributos.
Configurar MapControllerRoute
A continuación, veamos cómo se usa MapControllerRoute() en la aplicación:
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
Aquí configuramos una ruta predeterminada para los controladores en la clase Program de la aplicación. Vamos a desglosar la configuración de la ruta para entenderla mejor.
Usamos el parámetro name para dar un nombre a la ruta. En este caso, la llamamos default.
Para el segundo parámetro, pattern, usamos una plantilla que define la ruta con un patrón concreto:
| Patrón | Explicación |
|---|---|
| {controller=Home} | Esta parte del patrón representa el controlador. Si la URL no especifica explícitamente un controlador, se usa Home de forma predeterminada |
| {action=Index} | Esta parte corresponde al nombre de la acción. Si la URL no especifica una acción, se usa Index de forma predeterminada |
| {id?} | Esta parte corresponde a un parámetro id que puede ser null. El signo ? indica que el parámetro id es opcional. Si aparece en la URL, se captura su valor; si no, es null. |
El signo de interrogación del último token, al igual que los valores predeterminados Home y Index, cambia lo que una URL puede omitir. Tratamos los parámetros de ruta opcionales y lo que {id?} hace con una URL en un artículo aparte.
Evidentemente, MapControllerRoute() nos permite definir rutas de forma que sean más fáciles de entender y de gestionar. En concreto, a MapControllerRoute a veces se le llama enrutamiento convencional porque sigue un enfoque basado en convenciones para definir rutas.
Crear un controlador para MapControllerRoute
Los valores predeterminados Home y Index del patrón tienen que resolverse en algo, así que vamos a añadir el controlador al que apuntan:
public class HomeController : Controller
{
public IActionResult Index()
{
return View();
}
}
Este controlador no tiene ningún atributo de ruta, y eso es lo que permite llegar a él a través de la ruta convencional. Las URL /, /Home y /Home/Index llegan todas a Index(), que devuelve la vista correspondiente.
¿Qué ocurre si llamamos a MapControllers y a MapControllerRoute a la vez?
No se rompe nada y ningún endpoint se registra dos veces. Las dos llamadas llegan al mismo ControllerActionEndpointDataSource, y ASP.NET Core crea un endpoint por acción, no un endpoint por cada llamada de registro.
El origen de datos recorre cada acción una vez. Una acción con un atributo de ruta obtiene un endpoint construido a partir de ese atributo. Una acción sin él obtiene un endpoint por cada ruta convencional que puede llegar a ella. Las rutas convencionales omiten las acciones que ya llevan un atributo, así que los dos conjuntos nunca se solapan.
Por eso es seguro llamar a ambos, y también por eso MapControllerRoute() por sí solo suele bastar. Microsoft Learn lo describe como un método que mapea “tanto los controladores con enrutamiento convencional como los controladores con enrutamiento por atributos”, mientras que MapControllers() solo mapea los de enrutamiento por atributos.

Ebook gratis
¿Tu Web API está lista para producción?
33 puntos que comprobar antes de desplegarla, con la solución de cada uno. Un PDF gratuito de 76 páginas para .NET 10.
El ebook está en inglés.
Descarga la checklist gratisPDF gratuito. Un solo correo para enviártelo. Puedes darte de baja cuando quieras.
Los duplicados reales vienen de otro sitio. Dos acciones con la misma plantilla de ruta chocan, y entonces la solicitud falla con una AmbiguousMatchException. El analizador ASP0023 señala el par en tiempo de compilación. Llamar a los dos métodos de mapeo a la vez no provoca eso.

El proyecto de ejemplo lo concreta todo. Estas son todas las URL que expone y el estilo de enrutamiento que puso ahí a cada una:
| URL | Estilo de enrutamiento | Controlador y acción |
|---|---|---|
/ | Convencional | HomeController.Index() |
/Home | Convencional | HomeController.Index() |
/Home/Index | Convencional | HomeController.Index() |
/Customers | Por atributos | CustomersController.Index() |
/Customers/Index | Por atributos | CustomersController.Index() |
/Customers/Info/4 | Por atributos | CustomersController.Detail(4) |
/Customers/Order | Por atributos | CustomersController.Order() |
/Customers/Customer/Order | Por atributos | CustomersController.Order() |
/Special | Por atributos | CustomersController.SpecialRoute() |
Todas las filas de atributos seguirían funcionando aunque se eliminara app.MapControllers(), porque MapControllerRoute() también mapea los controladores con enrutamiento por atributos. Las tres filas convencionales son las que lo necesitan.
Una vez que la aplicación ha decidido qué estilo usa cada controlador, añadir un prefijo delante de todas las rutas de la aplicación pasa a ser la siguiente pregunta que vale la pena plantearse.
¿Cómo elegir entre MapControllers y MapControllerRoute?
Elige por controlador, no por aplicación. Cada controlador responde a una pregunta: ¿su URL pertenece a una ruta de navegación o a la superficie de una API?
Recurre a MapControllerRoute() cuando las URL siguen la forma de la aplicación. Un patrón como {controller=Home}/{action=Index}/{id?} da a cada controlador nuevo una URL que funciona desde el momento en que se añade, sin escribir ningún atributo.
Recurre a MapControllers() y a los atributos de ruta cuando la URL es un contrato. Las rutas de una API se versionan, se publican y de ellas depende código que no controlamos, así que deben estar junto a la acción a la que sirven, donde cambiar el nombre de una clase no puede moverlas sin que nadie se dé cuenta.
Las aplicaciones que hacen ambas cosas las mantienen separadas: rutas convencionales para los controladores que devuelven vistas y rutas por atributos para los que devuelven datos. Microsoft Learn recomienda esa organización, y es la que usa el proyecto de ejemplo: “Normalmente, se usan rutas convencionales para los controladores que sirven páginas HTML a los navegadores y enrutamiento por atributos para los controladores que sirven API REST”.
| MapControllers | MapControllerRoute |
|---|---|
| Las rutas se configuran mediante atributos | La configuración de rutas se basa en convenciones |
| Adecuado para aplicaciones Web API | Adecuado para aplicaciones ASP.NET Core MVC convencionales |
| Ideal cuando los requisitos de enrutamiento son complejos | Ideal para requisitos de enrutamiento sencillos |
| La ruta se escribe junto a la acción, así que un cambio de nombre no puede cambiar la URL por accidente | La ruta se escribe una sola vez en Program.cs, así que un controlador nuevo obtiene una URL sin tocarlo |
| Ideal para definiciones de rutas explícitas y necesidades de personalización | Ideal para casos en los que se prefiere un método de enrutamiento estandarizado |
Conclusión
MapControllers es el enrutamiento por atributos a nivel de controlador, que permite definir rutas de forma detallada y flexible mediante atributos.
MapControllerRoute es un enrutamiento basado en convenciones: ofrece una correspondencia estandarizada entre patrones de URL y acciones de controlador sin usar atributos explícitos.
La elección se hace por controlador y no por aplicación. Un controlador que sirve páginas HTML usa la ruta convencional. Un controlador que sirve una API usa atributos de ruta.
Las aplicaciones que hacen ambas cosas llaman a los dos métodos, y los dos nunca chocan: a una acción a la que se llega por un atributo no se puede llegar mediante un patrón convencional, y los endpoints se crean una sola vez en cualquier caso.
Probado con .NET 10.0.10.