Skip to content

Instantly share code, notes, and snippets.

@rynowak
Last active January 7, 2019 17:43
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save rynowak/ba75c1c3f72d1fe83cc6e23876273941 to your computer and use it in GitHub Desktop.
Save rynowak/ba75c1c3f72d1fe83cc6e23876273941 to your computer and use it in GitHub Desktop.

Routing and MVC

ASP.NET MVC uses the Routing middleware to match the URLs of incoming requests and map them to actions. Routes are defined in startup code or using attributes and describe how URL paths should be matched to actions. MVC also uses the same set of routes to generate URLs to include as links in responses.

This document will explain the interactions between MVC and routing, and how typical MVC applications make sure of routing features. See the for details on how to write more advanced routes.

Setting up Routing Middleware

In your Configure method you may see code like:

app.UseMvc(routes =>
{
    routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
});

Inside the call to UseMvc, MapRoute is used to create a single route, which we'll refer to as the default route. Most MVC applications will use a route with a template very similar to the default route.

The route template "{controller=Home}/{action=Index}/{id?}" can match a URL path like /Products/Details/5 and will extract the route values { controller = Products, action = Details, id = 5 } from tokenizing the path. MVC would then attempt to locate a controller named ProductsController and execute the action Details such as in this example:

public class ProductsController : Controller
{
    public IActionResult Details(int id) { ... }
}

Note that in this example, model binding would use the value of id = 5 to set the id parameter to 5 when invoking this action. See the for more details on how model binding works.

In the default route "{controller=Home}/{action=Index}/{id?}", the route template syntax {controller=Home} defines the default value of Home for controller and {action=Index} defines the default value of Index for action. {id?} defines the id as optional. Optional route parameters and those with default values do not need to be present in the URL path for it to match. See the for a detailed description of route template syntax.

"{controller=Home}/{action=Index}/{id?}" can match a URL path like / and will produce the route values { controller = Home, action = Index }. The values for controller and action make use of the default values, id does not produce a value since there is no corresponding segment in the URL path. MVC would use these route values to select the HomeController and Index action.

public class HomeController : Controller
{
    public IActionResult Index() { ... }
}

Using this controller definition and route template, the HomeController.Index action would be executed for any of the following URL paths:

  • /Home/Index/17
  • /Home/Index
  • /Home
  • /

The default route is so common, MVC provides a convenience method to configure routing using only the default route.

app.UseMvcWithDefaultRoute();

Both UseMvc(...) and UseMvcWithDefaultRoute() add an instance of RouterMiddleware to the middleware pipeline. MVC doesn't interact directly with middleware, and uses routing to handle requests. MVC is connected to the routes through an instance of MvcRouteHandler. The code inside of UseMvc(...) is similar to the following:

var routes = new RouteBuilder(app);

 // Add connection to MVC, will be hooked up by calls to MapRoute
routes.DefaultHandler = new MvcRouteHandler(...);

// Execute callback to register routes
// routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");

// Create route collection and add the middleware
app.UseRouter(routes.Build());

MVC also provides the UseMvc() method which doesn't directly define any routes. What it does is add a placeholder to the route collection for the attribute route. In fact all three of UseMvc(), UseMvc(...), and UseMvcWithDefaultRoute(...) add the attribute route to the route collection. The will include more information about how to configure attribute routing.

Conventional Routing

The default route, created by routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}"), is an example of a conventional routing. We call this style conventional routing because it establishes a convention for URL paths that the first path segment maps to the controller name, and the second maps to the action name. This also establishes the convention that the third segment is used for an optional id used to map to a model entity.

The URL path /Products/List maps to the ProductsController.List(...) action, and /Blog/Article/17 maps to BlogController.Article(...). This mapping is based on the controller and action names only and is not based on namespaces, source file locations, or method parameters.

.. tip: Using conventional routing with the default route allows you to build the application quickly without having to come up with a new URL pattern for each action you define. For an application with CRUD style actions, having consistency for the URLs across your controllers can help simplify things.

.. warning: The id is defined as optional by the route template, meaning that your actions can execute without the id provided as part of the URL. Usually what will happen if id is omitted from the URL is that it will be set to 0 by model binding, and as a result no entity will be found in the database matching id == 0. Attribute routing can give you fine-grained control to make the id required for some actions and not for others. By convention the documentation will include optional parameters like id when they are likely to appear in correct usage.

Multiple Routes

You can add multiple routes inside UseMvc(...) by adding more calls to MapRoute, this can allow you to define multiple conventions, or to add conventional routes that are dedicated to a specific action, such as:

app.UseMvc(routes =>
{
    routes.MapRoute("blog", "blog/{*article}", defaults: new { controller = "Blog", action = "Article" });
    routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
}

The blog route here is a dedicated conventional route, meaning that it uses the conventional routing system, but is dedicated to a specific action. Since controller and action don't appear in the route template as parameters, they can only ever have the default values, and thus this route will always map to the action BlogController.Article(...).

Routes in the route collection are ordered, and will be processed in the order they are added. So in this example, the blog route will be tried before the default route.

.. note: Note that dedicated conventional routes often use catch-all route parameters like {*article} to capture the remaining portion of the URL path. This can make a route 'too greedy' meaning that it matches URLs that you intended to be matched by other routes. Put the 'greedy' routes later in the route table to solve this.

Fallback

As part of request processing, MVC will verify that the route values can be used to find a controller and action in your application. If the route values don't match an action then the route is not considered a match, and the next route will be tried. We call this fallback, and it's intended to simplify cases where conventional routes overlap.

Disambiguating Actions

When two actions match through routing, MVC must disambiguate to choose the 'best' candidate or else throw an exception, as in this example:

public class ProductsController : Controller
{
    public IActionResult Edit(int id) { ... }
    
    [HttpPost]
    public IActionResult Edit(int id, Product product) { ... }
}

This controller defines two actions that would match the URL path /Products/Edit/17 and route data { controller = Products, action = Edit, id = 17 }. This is a typical pattern for MVC controllers where Edit(int) would show a form to edit a product, and Edit(int, Product) would process submissions of the form. To make this possible MVC would need to choose Edit(int, Product) when the request is an HTTP POST and Edit(int) when the HTTP verb is anything else.

The HttpPostAttribute is an implemenation of IActionConstraint that will only allow the action to be selected with the HTTP verb is POST. The presence of an IActionConstraint makes the Edit(int, Product) a 'better' match than Edit(int), so Edit(int, Product) will be tried first. See for a more thorough explanation of IActionConstraint.

You will only need to write custom IActionConstraint implementations in specialized scenarios, but it's important to understand the role of attributes like HttpPostAttribute - similar attributes are defined for other HTTP verbs. In conventional routing it's common for actions to use the same action name when they are part of a show form -> submit form workflow. The convenience of this pattern will become more apparent after understanding .

Route Names

The strings like "blog" and "default" in the following example are route names.

app.UseMvc(routes =>
{
    routes.MapRoute("blog", "blog/{*article}", defaults: new { controller = "Blog", action = "Article" });
    routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
}

The route names give the route a logical name so that the desired route can be used specifically for URL generation. This is a powerful override for cases where the ordering of routes makes URL generation complicated. Routes names must be unique application-wide.

Route names have no impact on URL matching or handling of requests, and are only used for URL generation. The has more information on URL generation in detail, and includes information about using URL generation in MVC-specific helpers.

Attribute Routing

Attribute routing uses a set of attributes to map actions directly to route templates. In this example, the HomeController.Index() action will match a set of URLs similar to what the default route {controller=Home}/{action=Index}/{id?} would match.

public class HomeController : Controller
{
    [Route("")]
    [Route("Home")]
    [Route("Home/Index")]
    public IActionResult Index() { }
}

The HomeController.Index() action will be executed for any of the URL paths /, /Home, or /Home/Index.

.. note: This example highlights a key philosophical difference between attribute routing and conventional routing. Attribute routing requires more input to specify something the conventional default route can do quite easily. However, attribute routing, allows (and requires) precise control of which route templates apply to each action.

Note that with attribute routing the controller name and action name play no role in which action is chosen. This example will match the same URLs as the above example.

public class MyHomeController : Controller
{
    [Route("")]
    [Route("Home")]
    [Route("Home/Index")]
    public IActionResult MyIndex() { }
}

.. note: Notice that these route templates don't define route parameters for action, area, and controller. In fact, these route parameters are not allowed in attribute routes. Since the route template is already assocated with an action, it wouldn't make sense to parse the action name from the URL.

Attribute routing can also make use of attributes like HttpPostAttribute as all of these attributes can accept a route template. This example shows two actions with match the same route template:

public class ProductsApiController : Controller
{
    [HttpGet("/products")]
    public IActionResult ListProducts() { ... }
    
    [HttpPut("/products")]
    public IActionResult CreateProduct(...) { ... }
}

For a URL path like /products the ProductsApi.ListProducts() action will be executed when the HTTP verb is GET and ProductsApi.CreateProduct(...) will be executed when the HTTP verb is PUT. Attribute routing first matches the URL against the set of route templates defined by route attributes. Once a route template matches, IActionConstraints are applied to determine which actions can be executed.

.. tip: When building a REST API, it's rare that you will want to use [Route(...)] on an action method. It's better to use the more specific Http*Verb*Attributes to be precise about what your API supports. Clients of REST APIs are expected to know what paths and HTTP verbs map to specific logical operations.

Since an attribute route applies to a specific action, it's easy to make parameters required as part of the route template definition. In this example, the id is required as part of the URL path.

public class ProductsApiController : Controller
{
    [HttpGet("/products/{id}", Name = "Products_List")]
    public IActionResult GetProducts(int id) { ... }
}

The ProductsApi.GetProducts(int) action will be executed for a URL path like /products/3 but not for a URL path like /products. See for a full description of route templates and related options.

This route attribute also defines route name of Products_List - route names can be used to generate a URL based on a specific route. Route names have no impact on the URL matching behavior of routing and are only used for URL generation. Route names must be unique application-wide.

.. note: Contrast this with the conventional default route, which defines the id parameter as optional ({id?}). This is an advantage for specifying APIs as it's precise. This allows /products and /products/5 to be dispatched to different actions.

Combining Routes

To make attribute routing less repetative, route attributes on the controller are combined with route attributes on the individual actions. Any route templates defined on the controller are preprended to route templates on the actions. Placing a route attribute on the controller makes all actions in the controller use attribute routing.

[Route("products")]
public class ProductsApiController : Controller
{
    [HttpGet]
    public IActionResult ListProducts() { ... }
    
    [HttpGet("{id}")]
    public ActionResult GetProduct(int id) { ... }
}

In this example the URL path /products can match ProductsApi.ListProducts(), and the URL path /products/5 can match ProductsApi.GetProduct(int). Both of these actions only match HTTP GET since they have the HttpGetAttribute applied.

Route templates applied to an action that begin with a / do not get combined with route templates applied to the controller. This example matches a set of URL paths similar to the default route.

[Route("Home")]
public class HomeController : Controller
{
    [Route("")] // Combines to define the route template "Home"
    [Route("Index")] // Combines to define the route template "Home/Index"
    [Route("/")] // Does not combine, defines the route template ""
    public IActionResult Index() { ... }
}

Ordering attribute routes

In contrast to conventional routes, which execute in a defined order, attribute routing builds a tree, and matches all routes simultaneously. This behaves as-if the route entries where placed in an ideal ordering the most specific routes have a chance to execute before the more general routes.

For example, a route like blog/search/{topic} is more specific than a route like blog/{*article}. Logically speaking the blog/search/{topic} route 'runs' first, by default, because that's the only sensible ordering. Using conventional routing you, the developer, are responsible for placing routes in the desired order.

Attribute routes can configure an order, using the Order property of all of the framework provided route attributes. Routes are processed according to an ascending sort of the Order property, that means that a route using Order = -1 will routes that don't set an order, and a route using Order = 1 will run after.

.. tip: Use the Order property very sparingly. If your URL-space requires explicit order values to route correctly, then it's likely confusing to clients as well. In general attribute routing will do the right thing for you for URL matching. If the default order used for URL generation isn't working out the way you want, using route name as an override is usually simpler.

Token replacement in route templates ([controller], [action], [area])

For convenience, attribute routes support a token replacement by enclosing a token in square-braces ([, ]]). The tokens [action], [area], and [controller] will be replaced with the values of the action name, area name, and controller name from the action where the route is defined. In this example the actions can match URL paths as described in the comments:

[Route("[controller]/[action]")]
public class ProductsController : Controller
{
    [HttpGet] // Matches '/Products/List'
    public IActionResult List() { ... }

    [HttpGet("{id}")] // Matches '/Products/Edit/{id}'
    public IActionResult Edit(int id) { ... }
}

Token replacement occurs as the last step of building the attribute routes. The above example will behave the same as the following code:

public class ProductsController : Controller
{
    [HttpGet("[controller]/[action]")] // Matches '/Products/List'
    public IActionResult List() { ... }

    [HttpGet("[controller]/[action]/{id}")] // Matches '/Products/Edit/{id}'
    public IActionResult Edit(int id) { ... }
}

Attribute routes can also be combined with inheritance. This is particularly powerful combined with token replacement.

[Route("api/[controller]")]
public abstract class MyBaseController : Controller { ... }

public class ProductsController : MyBaseController
{
    [HttpGet] // Matches '/api/Products'
    public IActionResult List() { ... }

    [HttpPost("{id}")] // Matches '/api/Products/{id}'
    public IActionResult Edit(int id) { ... }
}

Token replacement also applies to route names defined by attribute routes. [Route("[controller]/[action]", Name="[controller]_[action]")] will generate a unique route name for each action.

Multiple Routes

Attribute routing supports defining multiple routes that reach the same action. The most common usage of this is to mimic the behavior of the default conventional route as shown in the following example:

[Route("[controller]")]
public class ProductsController : Controller
{
    [Route("")] // Matches 'Products'
    [Route("Index")] // Matches 'Products/Index'
    public IActionResult Index()
}

Putting multiple route attributes on the controller means that each one will combine with each of the route attributes on the action methods.

[Route("Store")]
[Route("[controller]")]
public class ProductsController : Controller
{
    [HttpPost("Buy")] // Matches 'Products/Buy' and 'Store/Buy'
    [HttpPost("Checkout")] // Matches 'Products/Checkout' and 'Store/Checkout'
    public IActionResult Buy()
}

When multiple route attributes that implement IActionConstraints are placed on an action, then each action constraint combines with the route template from the attribute that defined it.

[Route("api/[controller]")]
public class ProductsController : Controller
{
    [HttpPut("Buy")] // Matches PUT 'api/Products/Buy'
    [HttpPost("Checkout")] // Matches POST 'api/Products/Checkout'
    public IActionResult Buy()
}

.. tip: While using multiple routes on actions can seem powerful, it's better to keep your application's URL space simple and well-defined. Use multiple routes on actions only where needed to support existing clients.

Custom route attributes using IRouteTemplateProvider

All of the route attributes provided in the framework ([Route(...)], [HttpGet(...)], etc) implement the IRouteTemplateProvider interface. MVC looks for attributes on controller classes and action methods when the application starts and uses the ones that implement IRouteTemplateProvider to build the intial set of routes.

You can implement IRouteTemplateProvider to define your own route attributes, but each IRouteTemplateProvider only allows you to define a single route with a custom route template, order, and name.

public class ApiControllerAttribute : Attribute, IRouteTemplateProvider
{
    public string Template => "api/[controller]";

    public int? Order { get; set; }

    public string Name { get; set; }
}

The attribute from the above example automatically sets the Template to "api/[controller]" wherever it's used.

Using Application Model to customize attribute routes

The application model is an object model created at startup with all of the metadata used by MVC to route to and execute your actions. The application model includes all of the data gathered from route attributes (via IRouteTemplateProvider). You can write conventions to modify the application model at startup time to customize how routing behaves.

This section will provide a simple example of customizing routing using application model. See the application model section for detailed documentation.

public class NamespaceRoutingConvention : IControllerModelConvention
{
    private readonly string _baseNamespace;

    public NamespaceRoutingConvention(string baseNamespace)
    {
        _baseNamespace = baseNamespace;
    }

    public void Apply(ControllerModel controller)
    {
        var hasRouteAttributes = controller.Selectors.Any(selector => selector.AttributeRouteModel != null);
        if (hasRouteAttributes)
        {
            // This controller manually defined some routes, let's treat this as an override and not apply the convention here.
            return;
        }

        // Let's use the namespace and controller name to infer a route for the controller.
        //
        // Example:
        //
        //  controller.ControllerTypeInfo ->    "My.Application.Admin.UsersController"
        //  baseNamespace ->                    "My.Application"
        //
        //  template =>                         "Admin/[controller]"
        //
        // This makes your routes roughly line up with the folder structure of your project.
        //
        var @namespace = controller.ControllerTypeInfo.Namespace;

        var template = new StringBuilder();
        template.Append(@namespace, _baseNamespace.Length + 1, @namespace.Length - _baseNamespace.Length - 1);
        template.Replace('.', '/');
        template.Append("/[controller]")

        foreach (var selector in controller.Selectors)
        {
            selector.AttributeRouteModel = new AttributeRouteModel()
            {
                Template = template.ToString();
            }
        }
    }
}

Mixed Routing

MVC applications can mix the use of conventional routing and attribute routing. It's possible, for instance, to use conventional routes for controllers serving HTML pages for browsers, and attribute routing for controllers serving REST APIs.

The only limitation is that each action is either conventional routed or attribute routed. Actions that define attribute routes cannot be reached via the conventional routes and vice-versa.

.. note: What distinguishes the two types of routing systems is the process applied to after a URL matches a route template. In conventional routing, the route values from the match are used to choose the action and controller from a lookup table of all conventional routed actions. In attribute routing, each template is already associated with an action, and no further lookup is needed.

URL Generation

MVC applications can use routing's URL generation features to generate URL links to actions. This helps you avoid hardcoding URLs in your application code. This section will focus on the URL generation features provided by MVC and will only cover basics of how these features work. See [Routing Document] for a detailed description of URL generation.

The IUrlHelper interface is the underlying piece of infrastructure between MVC and routing for URL generation. You'll find instance of IUrlHelper available through the Url property in controllers, views, and view components.

In this example, the IUrlHelper interface is used, through the Controller.Url property, to generate a URL to another action.

app.UseMvcWithDefaultRoute();
...

public class UrlGenerationController : Controller
{
    public IActionResult Source()
    {
        var url = Url.Action("Destination"); // Generates /UrlGeneration/Destination
        return Content($"Go check out {url}, it's really great.");
    }

    public IActionResult Destination(() { };
}

If the application is using the default conventional route, the value of the url variable will be the URL path string /UrlGeneration/Destination. This URL path is created by routing by combining the route values from the current request (ambient values), with the values passed to Url.Action and substituting those values into the route template.

ambient values: { controller = "UrlGeneration", action = "Source" }
values passed to Url.Action: { controller = "UrlGeneration", action = "Destination" }
route template: {controller}/{action}/{id?}

result: /UrlGeneration/Destination

Each route parameter in the route template has its value substituted by matching names with the values and ambient values. A route parameter that does not have a value can use a default value if it has one, or be skipped if it is optional (as in the case of id in this example). URL generation will fail if any required route parameter doesn't have a corresponding value. In that case routing will try again with the next route until all routes have been tried or a match is found.

The example of Url.Action above assumes conventional routing, but URL generation works similarly with attribute routing, though the concepts are different. With conventional routing, the route values are used to expand a template, and the route values for controller and action usually appear in that template - this works because the URLs matched by routing adhere to a convention. In attribute routing, the route values for controller and action are not allowed to appear in the template - they are instead used to look up which template to use.

This example uses attribute routing:

app.UseMvc();
...

public class UrlGenerationController : Controller
{
    [HttpGet("")]
    public IActionResult Source()
    {
        var url = Url.Action("Destination"); // Generates /custom/url/to/destination
        return Content($"Go check out {url}, it's really great.");
    }

    [HttpGet("custom/url/to/destination")]
    public IActionResult Destination(() { };
}

MVC builds a lookup table of all attribute routed actions, and will match the controller and action values to select the route template to use for URL generation, in this case custom/url/to/destination.

Generating URLs by action name

Url.Action and all related overloads all are based on that idea that you want to specify what you're linking to by specifying a controller name and action name.

.. note: When using Url.Action, the current route values for controller and action are specified for you - the value of controller and action are part of both ambient values and values. The method Url.Action(), always uses the current values of action and controller and will generate a URL path that routes to the current action.

.. note: Routing tries to use the values in ambient values to fill in information that you didn't provide when generating a URL. Using a route like {a}/{b}/{c}/{d} and ambient values { a = Alice, b = Bob, c = Carol, d = David }, routing has enough information to generate a URL without any additional values - since all route paramaters have a value. If you added the value { d = Donovan }, the the value { d = David } would be ignored, and the generated URL path would be Alice/Bob/Carol/Donovan. But, there's a tricky rule here, URL paths are assumed to be hierarchical. So if instead, added the value { c = Cheryl }, both of the values { c = Carol, d = David } would be ignored - in this case we no longer have a value for d and URL generation will fail. You would need to specify the desired value of c and d. You might expect to hit this problem with the default route ({controller}/{action}/{id?})will rarely encounter this behavior in practice as Url.Action will always explicitly specify a controller and action value.

Longer overloads of Url.Action also take an additional route values object to provide values for route parameters other than controller and action. You will most commonly see this used with id like Url.Action("Buy", "Products", new { id = 17 }). By convention the route values object is usually an object of anonymous type, but it can also be an IDictionary<> or a 'plain old .NET object'. Any additional route values that don't match route parameters are put in the query string.

// Generates /Products/Buy/17?color=red
var url = Url.Action("Buy", "Products", new { id = 17, color = "red" })`

.. tip: To create an absolute URL, use an overload that accepts a protocol like: Url.Action("Buy", "Products", new { id = 17 }, protocol: Request.Scheme)

Generating URLs by route

Besides generating a URL to an action by controller and action name, IUrlHelper also provides the Url.RouteUrl family of methods. These methods are similar to Url.Action, but they do not copy the current values of action and controller to the route values. The most common usage is to specify a route name to use a specific route to generate the URL, most frequently without specifying a controller name or action name.

app.UseMvc();
...

public class UrlGenerationController : Controller
{
    [HttpGet("")]
    public IActionResult Source()
    {
        var url = Url.RouteUrl("Destination_Route"); // Generates /custom/url/to/destination
        return Content($"Go check out {url}, it's really great.");
    }

    [HttpGet("custom/url/to/destination", Name = "Destination_Route")]
    public IActionResult Destination(() { };
}

Generating URLs in HTML

The IHtmlHelper provides the Html.BeginForm and Html.ActionLink methods to generate <form> and <a> elements respectively. These methods use the Url.Action method to generate a URL and accept similar arguments. The Url.RouteUrl companions for HTML are Html.BeginRouteForm and Html.RouteLink which have similar functionality. See the [HTML Helpers] document for more details.

TagHelpers generate URLs through the <form> TagHelper and the <a> TagHelper. Both of these use IUrlHelper for their implementation as well. See the $MVC TagHelpers$ document for details.

Inside views, the IUrlHelper is available via the Url property for any ad-hoc URL generation not covered by the above.

Generating URLS in Action Results

While we've shown examples of using IUrlHelper in a controller, the most common usage in a controller is to generate a URL as part of an action result.

The ControllerBase and Controller base classes provide convenience methods for action results that reference another action. One typical usage is to redirect after accepting user input.

public Task<IActionResult> Edit(int id, Customer customer)
{
    if (ModelState.IsValid)
    {
        // Update datebase with new details
        return RedirectToAction("Index", "Customer");
    }
}

The action results factory methods follow a similar pattern to the methods on IUrlHelper.

Special case for dedicated conventional routes

Conventional routing also sometimes uses a special kind of route definition we call a dedicated conventional route. In the below example, the route named blog is a dedicated conventional route.

app.UseMvc(routes =>
{
    routes.MapRoute("blog", "blog/{*article}", defaults: new { controller = "Blog", action = "Article" });
    routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
}

Using these route definitions, calling Url.Action("Index", "Home") will generate the URL path / via the default route, but why? It seems like the route values { controller = Home, action = Index } would be enough to generate a URL using blog, and result would be /blog?action=Index&controller=Home.

Dedicated conventional routes rely on a special behavior of default values that don't have a corresponding route parameter that prevents the route from being "too greedy" with URL generation. In this case the default values are { controller = Blog, action = Article }, and neither controller nor action appears as a route parameter. When routing performs URL generation in this case, the values provided must match the default values. So URL generation using blog will fail because the values { controller = Home, action = Index } don't match { controller = Blog, action = Article }. Routing then falls back to try default which succeeds.

Areas

Areas are an MVC feature used to organize related functionality into a group as a separate namespace (for routing) and folder structure (for views). Using areas allows an application to have multiple controllers with the same name - as long as they have different areas. Using areas creates a hierarchy for the purpose of routing by adding another route parameter, area, to controller and action. This section will discuss how routing interacts with areas - see [View Engine Document] for details about how areas are used with views.

The following example configures MVC to use the default conventional route and an area route for an area named Admin:

app.UseMvc(routes =>
{
    routes.MapAreaRoute("admin_route", "Admin", "Manage/{controller}/{action}/{id?}");
    routes.MapRoute("default_route", "{controller}/{action}/{id?}");
})

When matching a URL path like /Manage/Users/AddUser the first route will produce the route values { area = Admin, controller = Users, action = AddUser }. The area route value is produced by a defalt value for area, in fact the route created by MapAreaRoute is equivalent to the following example.

app.UseMvc(routes =>
{
    routes.MapRoute("admin_route", "Manage/{controller}/{action}/{id?}", defaults: new { area = "Admin" }, constraints: new { area = "Admin" });
    ...
})

MapAreaRoute creates a route using both a default value and constraint for area using the provided area name, in this case it's Admin. The default value ensures that the route always produces { area = Admin, ... }, the constraint requires the value { area = Admin, ... } for URL generation.

.. tip: Remember, conventional routing is order-dependent. In general, routes with areas should be placed earlier in the route table as they are more specific than routes without an area.

Using the above example, the route values would match the following action:

[Area("Admin")]
public class UsersController : Controller
{
    public IActionResult AddUser() { ... }

}

The AreaAttribute is what denotes a controller as part of an area, we say that this controller is in the Admin area. Controllers without an AreaAttribute are not members of any area, and will not match when the area route value is provided by routing. In the following example, only the first controller listed can match the route values { area = Admin, controller = Users, action = AddUser }.

namespace MyApp.Namespace1
{
    // Matches { area = Admin, controller = Users, action = AddUser }
    [Area("Admin")]
    public class UsersController : Controller
    {
        public IActionResult AddUser() { ... }

    }
}

namespace MyApp.Namespace2
{
    // Matches { area = Admin, controller = Users, action = AddUser }
    [Area("Blog")]
    public class UsersController : Controller
    {
        public IActionResult AddUser() { ... }

    }
}

namespace MyApp.Namespace3
{
    // Matches { area = string.Empty, controller = Users, action = AddUser }
    // Matches { area = null, controller = Users, action = AddUser }
    // Matches { controller = Users, action = AddUser }
    public class UsersController : Controller
    {
        public IActionResult AddUser() { ... }

    }
}

.. note: The namespace of each controller is shown here for completeness - otherwise the controllers have a naming conflict, and would not compile. Class namespaces have no effect on MVC's routing.

The first two controllers are members of areas, and only match when their respective area name is provided by the area route value. The third controller is not a member of any area, and can only match when no value for area is provided by routing.

.. note: In terms of matching no value, the absence of the area value is the same as if the value for area were null or the empty string.

When executing an action inside an area, the route value for area will be available as an ambient value for routing to use for URL generation. This means that by default areas act sticky for URL generation as demonstrated by the following sample.

// Assume routes {
app.UseMvc(routes =>
{
    routes.MapAreaRoute("admin_route", "Admin", "Manage/{controller}/{action}/{id?}");
    routes.MapRoute("default_route", "Manage/{controller}/{action}/{id?}");
})

[Area("Admin")]
public class UsersController : Controller
{
    public IActionResult GenerateURLInArea()
    {
        // Uses the 'ambient' value of area
        var url = Url.Action("Index", "Home"); // returns /Manage
    }

    public IActionResult GenerateURLOutsideOfArea()
    {
        // Uses the empty value for area
        var url = Url.Action("Index", "Home", new { area = "" }); // returns /
    }
}

Understanding IActionConstraint

.. note: This section is a deep-dive on framework internals and how MVC chooses an action to execute. A typical application won't use any custom IActionConstraints.

You have likely already used IActionConstraint even if you're not familar with the interface. The HttpGetAttribute and similar attributes implement IActionConstraint in order to limit the execution of an action method.

public class ProductsController : Controller
{
    [HttpGet]
    public IActionResult Edit() { }

    public IActionResult Edit(...) { }
}

Assuming the default conventional route, the URL path /Products/Edit would produce the values { controller = Products, action = Edit }, which would match both of the actions shown here. In IActionConstraint terminology we would say that both of these actions are considered candidates - as they both match the route data.

When the HttpGetAttribute executes, it will say that Edit() is a match for GET and is not a match for any other HTTP verb. The Edit(...) action doesn't have any constraints defined, and so will match any HTTP verb. So assuming a POST - only Edit(...) matches. But, for a GET both actions can still match - however, an action with an IActionConstraint is always considered better than an action without. So because Edit() has [HttpGet] it is considered more specified, and will be selected if both actions can match.

Conceptually, IActionConstraint is a form of overloading, but instead of overloading methods with the same name, it is overloading between actions that match the same URL. Attribute routing also uses IActionConstraint and can result in actions from different controllers both being considered candidates.

Implementing IActionConstraint

The simplest way to implement an IActionConstraint is to create class derived from System.Attribute and place it on your actions and controllers. MVC will automatically discover any IActionConstraints that are applied as attributes. You can also use the application model to apply constraints, and this is probably the most flexible approach as it allows you to metaprogram how they are applied.

Here's an example. This constraint chooses an action based on a country code from the route data. See the full sample here: https://github.com/aspnet/Entropy/blob/dev/samples/Mvc.ActionConstraintSample.Web/CountrySpecificAttribute.cs

public class CountrySpecificAttribute : Attribute, IActionConstraint
{
    private readonly string _countryCode;

    public CountrySpecificAttribute(string countryCode)
    {
        _countryCode = countryCode;
    }

    public int Order
    {
        get
        {
            return 0;
        }
    }

    public bool Accept(ActionConstraintContext context)
    {
        return string.Equals(
            context.RouteContext.RouteData.Values["country"].ToString(),
            _countryCode,
            StringComparison.OrdinalIgnoreCase);
    }
}

You are responsible for implementing the Accept(...) method and choosing an 'Order' for the constraint to execute. In this case, the Accept method returns true to say the action is a match when the country route value matches. This is different from a RouteValueAttribute in that it allows fallback to a non-attributed action. The sample shows that if you define an en-US action then a country code like fr-FR will fall back to a more generic controller that does not have [CountrySpecific(...)] applied.

The Order property decides which stage the constraint is part of. Action constraints run in groups based on the Order. For example all of the framework provided HTTP method attributes use the same Order value so that they run in the same stage. You can have as many stages as you need to implement your desired policies.

.. tip: To decide on a value for Order think about whether or not your constraint should be applied before HTTP methods. Lower numbers run first.

**Note for Rick: we've got a few existing samples already for IActionConstraint - example: https://github.com/aspnet/Entropy/blob/dev/samples/Mvc.ActionConstraintSample.Web/CountrySpecificAttribute.cs

I feel like this is already a pretty advanced topic and we can add more details about implementing constraints as asked. **

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment