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:


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


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

An example HTTP request with elements correlating to the suggested order above.
public class SearchController : Controller
    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


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

public class QuestionsController : Controller
    public IActionResult Show(
        [FromRoute] long id,
        [FromRoute] string slug

Example: Twitter Settings

public class SettingsController : Controller
    public IActionResult YourTwitterData()

Example: GitHub Gists

public class GistsController : Controller
    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

public class PhotoController : Controller
    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].

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")

Model Naming Conventions


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


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


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