ASP.NET Core Routing from the Outside In

In this article, I will show you my conventions for attribute routing and how you can make your controllers shine.

ASP.NET Core Routing from the Outside In

I still run across ASP.NET Core MVC projects that use conventional routing. In this article, I will show you my conventions for attribute routing and how you can make your controllers shine.

Conventional vs. Attribute Routing

Conventional Routing

Conventional routing is based on conventions defined in Startup and is also the original routing mechanism from ASP.NET MVC in the .NET Framework era. It’s what you get by default when generating a new .NET project.

Traditionally, .NET routing used PascalCase path segments which exactly matched the controller and method names they mapped to with a route template that looks like this:

/Controller/Method/Id?

The question mark signifies that the Id bit is optional. This approach yields routes like these:

/Questions/Show/1
/Settings/YourTwitterData
/Gists/Mine
/Photo/Show/58469288

But being the astute developer you are, you know that URLs are case-sensitive and should be lowercase, preferably with hyphens as word separators. I don’t think I have ever seen a web application with PascalCase path segments outside of .NET.

Microsoft recommends conventional routing for MVC apps and attribute routing for APIs partly because conventional routing is not RESTful and can’t easily express complex routes.

I recommend attribute routing in all situations. Let’s dive in, and at the end, I will revisit the routes above using attribute routing to match how they look in the wild.

HTTP Method Attributes

Use HTTP method attributes such as [HttpGet], [HttpPost], and [HttpPut] to constrain action methods to HTTP methods. While this is optional, action methods without any HTTP method constraints can be invoked via any HTTP method.

Model Binding Constraints

Consider using model binding constraint attributes to indicate where bindings should come from.

  • [FromRoute]
  • [FromQuery]
  • [FromHeader]
  • [FromBody]
  • [FromForm]
  • [FromFile]
  • [FromServices]

I recommend placing these elements in the order they’re listed, which roughly maps to how they appear in an HTTP request. [FromServices] comes last, just before the CancellationToken (for async methods).

GET /search?q=dotnet+routing HTTP/1.1
Host: www.google.com

body_or_form_content=goes_here
An example HTTP request with elements correlating to the suggested order above.
[Route("search")]
public class SearchController : Controller
{
    [HttpGet]
    public IActionResult Search([FromQuery(Name = "q")] string query)
    {
    }
}

Controller and Method Naming

I recommend choosing controller and action names that match your URL path segments where possible.

For CRUD resources, I follow the Ruby on Rails resourceful routing conventions, as shown in the following table.

HTTP Method Path Controller#Action Used for
GET /photos Photos#Index display a list of all photos
GET /photos/new Photos#New return an HTML form for creating a new photo
POST /photos Photos#Create create a new photo
GET /photos/{id:int} Photos#Show display a specific photo
GET /photos/{id:int}/edit Photos#Edit return an HTML form for editing a photo
PATCH/PUT /photos/{id:int} Photos#Update update a specific photo
DELETE /photos/{id:int} Photos#Destroy delete a specific photo

Examples

Let's take a look at some example URLs from around the web and how we would declare these routes in ASP.NET Core.

Example: Stack Overflow Question

https://stackoverflow.com/questions/1/where-oh-where-did-the-joel-data-go

[Route("questions")]
public class QuestionsController : Controller
{
    [HttpGet("{id:long}/{slug}")]
    public IActionResult Show(
        [FromRoute] long id,
        [FromRoute] string slug
    )
    {
    } 
}

Example: Twitter Settings

https://twitter.com/settings/your_twitter_data

[Route("settings")]
public class SettingsController : Controller
{
    [HttpGet("your_twitter_data")]
    public IActionResult YourTwitterData()
    {
    } 
}

Example: GitHub Gists

https://gist.github.com/mine

public class GistsController : Controller
{
    [HttpGet("mine")]
    public IActionResult Mine()
    {
    } 
}

While Gists run on a subdomain, they're a minor feature that fits under a single controller (or area).

Example: 500px Photos

https://500px.com/photo/58469288/end-of-day-by-steven-benitez

[Route("photo")]
public class PhotoController : Controller
{
    [HttpGet("{id:long}/{slug}")]
    public IActionResult Show(
        [FromRoute] long id,
        [FromRoute] string slug
    )
    {
    } 
}

500px chose the /photo segment rather than /photos – presumably, because they have different routes for browsing photos (/upcoming, /fresh, etc.). So the PhotoController is singular because it is only concerned with showing individual photos rather than a collection.

Token Replacements

If you prefer, you can use token replacements in routes to derive the name of an [area], [controller], or [action].

[Route("[controller]")]
public class PhotoController : Controller

By default, this will use the PascalCase name, so if you do want to go this route, you should use a custom IOutboundParameterTransformer to keep your URLs lowercase.

public class LowercaseWordParameterTransformer : IOutboundParameterTransformer
{
    public string? TransformOutbound(object? value)
    {
        return value == null ? null : Regex.Replace(value.ToString()!, "([a-z])([A-Z])", "$1-$2")
            .ToLower();
    }
}

Model Naming Conventions

MVC

For MVC applications, I use the Form suffix for form data being sent from the browser and the ViewModel suffix for data being passed to a Razor view that is not posted back.

  • ContactsViewModel
  • ContactViewModel
  • CreateContactForm
  • UpdateContactForm
  • LoginForm
  • UserProfileViewModel

API

For API applications, I use the Request suffix for inbound JSON representations and the Response suffix for outbound JSON representations.

  • ContactsResponse
  • ContactResponse
  • CreateContactRequest
  • UpdateContactRequest
  • UserProfileResponse

Takeaway

Attribute routing provides a lot more control over the structure of your URLs and provides a uniform look to your MVC and API controllers.

Additional Resources

Mastodon