Routing in .NET

Routing is one of those fundamental pieces that any .NET developer needs to master. It’s what allows HTTP requests to reach the right place in our web applications, directing them to the correct controllers and actions based on the URL. If you're working with .NET 7 or .NET 8, you're in luck because the routing system has evolved quite a bit, becoming more powerful and flexible. In this guide, we'll explore the latest improvements and see how to make the most of this tool, with clear examples and some useful tips to enhance the performance of your applications.

What is Routing in .NET?

Imagine your web application as a large city. Routing is the system of signs and roads that ensures visitors (HTTP requests) arrive exactly where they need to go: to the correct controller and the right action. In ASP.NET Core, routing is configured through middleware that intercepts requests and decides which controller and action they should be sent to. It sounds simple, but in reality, you can do much more with this system.

Let’s Start with Basic Routes in .NET 7/8

To begin, let’s see how to configure the most basic routes in an ASP.NET Core project. It all starts in the Program.cs file:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/", () => "Hello, Codú!");

app.MapGet("/products/{id:int}", (int id) =>
{
    return $"Product ID: {id}";
});

app.MapPost("/products", (Product product) =>
{
    // Logic to add a new product
    return Results.Created($"/products/{product.Id}", product);
});

app.Run();

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

Here, we are defining some simple routes: one for the home page ("/"), another that accepts a product ID as a parameter, and another that allows adding new products. It’s a good starting point.

Controller-Based Routing: The Classic Way

In larger projects, it's common for routes to be defined within controllers. Here’s an example of how you could do it:

[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
    [HttpGet]
    public IActionResult GetAll()
    {
        // Logic to get all products
        return Ok(new List<Product>());
    }

    [HttpGet("{id:int}")]
    public IActionResult GetById(int id)
    {
        // Logic to get a product by ID
        return Ok(new Product { Id = id, Name = "Product 1", Price = 99.99M });
    }

    [HttpPost]
    public IActionResult Create(Product product)
    {
        // Logic to create a new product
        return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
    }
}

This approach allows you to better organise the code, especially when you have multiple routes related to the same functionality. Additionally, it is much more readable and easier to maintain as your application grows.

What’s New in Routing with .NET 7/8?

Endpoint Routing: A Modern Approach

Since .NET Core 3.0, "Endpoint Routing" has become the standard, unifying routing for controllers and middleware into a single, more coherent approach. .NET 7/8 has further improved this functionality, making it more efficient and flexible.

Example of Endpoint Routing:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.UseRouting();

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllers();  // Map controller routes
    endpoints.MapGet("/health", () => "Healthy"); // Additional routes if needed
});

app.Run();

This approach is ideal when you want more control over how requests are handled within your application.

Attribute Routing: Total Flexibility

If you like having total control, attribute routing allows you to define exactly how you want your routes to behave, directly in the controller actions:

[ApiController]
[Route("api/[controller]")]
public class OrdersController : ControllerBase
{
    [HttpGet("latest")]
    public IActionResult GetLatest()
    {
        // Logic to get the latest orders
        return Ok(new List<Order>());
    }

    [HttpGet("status/{status}")]
    public IActionResult GetByStatus(string status)
    {
        // Logic to get orders by status
        return Ok(new List<Order>());
    }
}

With this approach, you have the flexibility to create more customised routes that respond to specific needs, such as filtering orders by status.

Route Segments and Constraints

In .NET 7/8, you can enhance routes by using constraints, which allow you to define criteria that URL segments must meet.

Example with Constraints:

[HttpGet("products/{category}/{id:int:min(1)}")]
public IActionResult GetProductByCategory(string category, int id)
{
    // Logic to get a product by category and ID
    return Ok(new Product { Id = id, Name = "Product 1", Price = 99.99M });
}

This example ensures that the id must be an integer greater than 1, which is useful for avoiding malformed requests.

Flexible Result Types

With .NET 7/8, you have greater flexibility in defining how your routes respond. You can use Results to build more complex responses tailored to the needs of each situation.

app.MapGet("/status/{id:int}", (int id) =>
{
    if (id < 1)
        return Results.BadRequest("Invalid ID");
    
    var product = new Product { Id = id, Name = "Sample", Price = 10.99M };
    return Results.Ok(product);
});

Routing with Minimal APIs: Simple and Direct

Minimal APIs in .NET 7/8 allow you to configure routes in a more concise and straightforward way, ideal for small projects or microservices.

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

app.MapGet("/users/{id:int}", (int id) => new { Id = id, Name = "User Name" });

app.MapPost("/users", (User user) => Results.Created($"/users/{user.Id}", user));

app.Run();

public record User(int Id, string Name);

This minimalist approach is perfect when you need something quick and effective, without the need for all the infrastructure of controllers.

Advanced Tips for Optimising Routing

Optimise Routing Performance

Routing can significantly impact the performance of your application, especially when you have many or complex routes. Here are some tips to keep everything running smoothly:

  1. Prefer Static Routes: Static routes (without parameters) are faster to process. Use static routes whenever possible.

  2. Evaluation Order: Routes are evaluated in the order in which they are defined. Place more specific routes before more general ones to avoid unnecessary evaluations.

  3. Use Constraints Sparingly: While constraints are useful, they can slow down route processing if overused. Use them only when you really need them.

Routing and Security: Integrating Authorisation

Often, routes need to be protected by authorisation policies. You can easily integrate routing with authorisation to ensure that only the right users have access to certain routes.

Example:

[Authorize(Roles = "Admin")]
[HttpGet("admin/reports")]
public IActionResult GetAdminReports()
{
    // Logic to get admin reports
    return Ok(new { Report = "Admin Report" });
}

With this setup, only users with the Admin role will be able to access the /admin/reports route.

Routing in Microservice Architectures

If you're working with a microservices architecture, routing can become more complex. In these cases, consider using an API Gateway that centralises and manages routing to the various microservices.

Conclusion

Routing in .NET has evolved to be more powerful and flexible, especially with the improvements introduced in .NET 7 and .NET 8. From simple Minimal APIs to sophisticated routes with constraints and authorisation, these tools allow you to build robust and maintainable web applications.

Web DevelopmentRoutingDotnetCsharp
Avatar for Adrián Bailador

Written by Adrián Bailador

🚀 Full-Stack Dev 👨🏻‍💻 .NET Engineer 👾 Geek & Friki 💡 Talks about #dotnet, #csharp, #azure, #visualstudio and a little bit of #nextjs.

Loading

Fetching comments

Hey! 👋

Got something to say?

or to leave a comment.