[FromRoute]
Bind route-template values to parameters, rename them with Name, and know when a bad value gives 404 versus 400.
[FromRoute(Name = "orderId")] int id- Added in .NET ASP.NET Core 1.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$Nameoptional | string?= null (the parameter name) | The route parameter to read when it differs from the C# parameter name, for example "orderId" in "{orderId:int}". |
Return value
n/aBinds the parameter from the route values that endpoint routing extracted from the URL path when it matched the template.
Errors
MVC: no error if the name does not exist in the template; the parameter never receives a route value. Minimal APIs: InvalidOperationException ('x' is not a route parameter.) when the endpoint is built, normally on the first request. A value that fails conversion is a model-state error (MVC, 400 under [ApiController]) or a 400 (minimal APIs).
Changelog
- PHP ASP.NET Core 6.0Minimal APIs honour [FromRoute]; parameters whose name matches a template parameter are inferred from the route.
When endpoint routing matches /api/orders/42 against api/orders/{orderId:int}, it stores orderId = "42" in HttpContext.Request.RouteValues. [FromRoute] binds a parameter from those values. Both MVC and minimal APIs already infer route binding when the parameter name equals a template parameter name, so the attribute earns its place when the names differ or when you want the source stated.
The silent name mismatch
The most common route bug is a template that says {orderId} and a handler parameter called id. Neither model throws: the route value is ignored and id is inferred from the query string instead. A minimal API with a non-nullable id then answers 400 for every request; a controller with an int id gets 0 and looks up order 0. Fix it by renaming the parameter or with [FromRoute(Name = "orderId")]. The route analyzers flag some of these cases in the IDE, so do not suppress their warnings.
Constraint or conversion: 404 or 400
| Template | Request | Result |
|---|---|---|
{id:int} | /orders/abc | 404: no endpoint matched |
{id}, parameter int id | /orders/abc | 400: matched, then conversion failed |
{sku:length(3,32)} | 40-character sku | 404 |
Constraints are for choosing between endpoints ({id:int} versus {slug}), not for validation. A 404 for a malformed id is acceptable; for a value that is the right type but out of range, validate in the handler and return 400 or 404 deliberately.
Encoding and catch-alls
Route values are URL-decoded, except that an encoded slash %2F stays encoded in a normal parameter, because the path was split into segments first. A SKU or file name that may contain / needs a catch-all parameter {**path}, which keeps slashes. Values are converted with the invariant culture.
Production notes
- Never trust a route id for authorisation:
/api/customers/{customerId}/ordersmust still check that the caller may see that customer. - Prefer
int,longorGuidids with matching constraints so non-numeric probes hit a cheap 404 before any database call.
Example
When to use it
- Read orderId from /api/orders/{orderId:int} into a parameter named id without renaming the public URL
- Share customerId from a /api/customers/{customerId:int}/orders route group across every endpoint in the group
- Serve /files/invoices/2026/INV-77.pdf with a catch-all route that keeps the slashes in the path value
Frequently asked questions
Why is my route parameter always null or 0?
The handler parameter's name does not match the template parameter, so it was bound from the query string. Rename it or add [FromRoute(Name = "...")].
Should I use route constraints for validation?
No. Constraints decide which endpoint matches, and a failure is a 404. Use them for type shapes (int, guid) and validate business rules in the handler so clients get a 400 with a message.
How do I accept a value containing a slash?
Use a catch-all parameter, {*path} or {**path}. A normal parameter is one segment, and an encoded %2F stays encoded in its value.
More examples
A route group that shares a parent id
Route values from a <code>MapGroup</code> prefix are available to every endpoint in the group, exactly like values from the endpoint's own template. Writing <code>[FromRoute]</code> on both makes it clear that neither id can come from the query string by accident.
using Microsoft.AspNetCore.Mvc;
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
var customerOrders = app.MapGroup("/api/customers/{customerId:int}/orders");
// customerId comes from the group prefix; orderId from the endpoint's own template.
customerOrders.MapGet("/", ([FromRoute] int customerId) =>
TypedResults.Ok(new { customerId, orders = new[] { 1001, 1002 } }));
customerOrders.MapGet("/{orderId:int}", ([FromRoute] int customerId, [FromRoute] int orderId) =>
TypedResults.Ok(new { customerId, orderId, total = 89.90m }));
app.Run();
Validating a value the constraint cannot
A constraint such as <code>{invoiceId:min(1)}</code> would turn <code>/api/invoices/0</code> into a 404, which tells the client nothing. Checking in the action returns a 400 with a field-level message, which is the better contract when the value is well-formed but invalid.
using Microsoft.AspNetCore.Mvc;
namespace Shop.Api;
[ApiController]
[Route("api/invoices")]
public class InvoicesController : ControllerBase
{
// The int constraint picks the endpoint; the range check is business validation.
[HttpGet("{invoiceId:int}")]
public IActionResult Get([FromRoute] int invoiceId)
{
if (invoiceId <= 0)
{
return ValidationProblem(new ValidationProblemDetails(new Dictionary<string, string[]>
{
["invoiceId"] = ["Invoice ids start at 1."],
}));
}
return Ok(new { invoiceId, amount = 412.30m, currency = "EUR" });
}
}
Testing that ids are read from the path
The decoy <code>?id=ORD-9</code> makes a query-bound handler return 200 for the wrong order, so the content assertion, not only the status code, proves that the value came from the path. <code>Program</code> must be the API's entry point; the comment on the stand-in says how the real project exposes it.
using System.Net;
using Microsoft.AspNetCore.Mvc.Testing;
using Xunit;
public class RouteBindingTests(WebApplicationFactory<Program> factory) : IClassFixture<WebApplicationFactory<Program>>
{
[Fact]
public async Task Order_id_comes_from_the_path_not_the_query()
{
var client = factory.CreateClient();
var response = await client.GetAsync("/fixed/orders/ORD-1001?id=ORD-9");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Contains("ORD-1001", await response.Content.ReadAsStringAsync(), StringComparison.Ordinal);
}
}
// Stand-in so this file compiles alone. In a real test project delete it: the API's
// Program.cs ends with "public partial class Program;" in the global namespace, this
// project references the API project, and WebApplicationFactory<Program> then boots
// the real API. Declared here inside a namespace, it would be a different, empty class.
public partial class Program;
Discussion