[{"data":1,"prerenderedAt":1201},["ShallowReactive",2],{"blog-\u002Fblog\u002Fdotnet-aspnet-core-routing":3},{"id":4,"title":5,"body":6,"description":1186,"difficulty":1187,"extension":1188,"framework":1189,"frameworkSlug":1190,"meta":1191,"navigation":213,"order":77,"path":1192,"qaPath":1193,"seo":1194,"stem":1195,"subtopic":1196,"topic":1197,"topicSlug":1198,"updated":1199,"__hash__":1200},"blog\u002Fblog\u002Fdotnet-aspnet-core-routing.md","Routing in ASP.NET Core",{"type":7,"value":8,"toc":1174},"minimark",[9,14,18,22,58,93,111,115,125,168,174,284,289,312,316,411,414,434,438,441,500,503,579,597,601,604,726,733,781,785,788,933,948,952,959,1031,1034,1049,1053,1056,1118,1124,1139,1142,1146,1170],[10,11,13],"h2",{"id":12},"why-routing-knowledge-is-tested-in-net-interviews","Why routing knowledge is tested in .NET interviews",[15,16,17],"p",{},"Routing is how every HTTP request finds its handler. Interviewers probe it because wrong\nroute design causes security holes (wrong controller handles a sensitive route), incorrect\nbehavior (ambiguous routes match the wrong action), and broken clients (URLs change and break\nexisting integrations). Understanding routing deeply signals architectural maturity.",[10,19,21],{"id":20},"how-endpoint-routing-works","How endpoint routing works",[15,23,24,28,29,33,34,37,38,42,43,46,47,50,51,53,54,57],{},[25,26,27],"strong",{},"Endpoint routing"," (ASP.NET Core 3.0+) separates route ",[30,31,32],"em",{},"matching"," from route ",[30,35,36],{},"dispatching",".\n",[39,40,41],"code",{},"UseRouting"," matches the request to an endpoint and stores the result in ",[39,44,45],{},"HttpContext",".\nMiddleware running ",[30,48,49],{},"between"," ",[39,52,41],{}," and ",[39,55,56],{},"MapControllers"," can read that endpoint metadata:",[59,60,65],"pre",{"className":61,"code":62,"language":63,"meta":64,"style":64},"language-csharp shiki shiki-themes github-light github-dark","app.UseRouting();            \u002F\u002F 1. Match: sets HttpContext.GetEndpoint()\napp.UseAuthentication();     \u002F\u002F 2. Can now inspect [Authorize] on the matched endpoint\napp.UseAuthorization();      \u002F\u002F 3. Enforce auth before dispatch\napp.MapControllers();        \u002F\u002F 4. Dispatch: actually run the matched action\n","csharp","",[39,66,67,75,81,87],{"__ignoreMap":64},[68,69,72],"span",{"class":70,"line":71},"line",1,[68,73,74],{},"app.UseRouting();            \u002F\u002F 1. Match: sets HttpContext.GetEndpoint()\n",[68,76,78],{"class":70,"line":77},2,[68,79,80],{},"app.UseAuthentication();     \u002F\u002F 2. Can now inspect [Authorize] on the matched endpoint\n",[68,82,84],{"class":70,"line":83},3,[68,85,86],{},"app.UseAuthorization();      \u002F\u002F 3. Enforce auth before dispatch\n",[68,88,90],{"class":70,"line":89},4,[68,91,92],{},"app.MapControllers();        \u002F\u002F 4. Dispatch: actually run the matched action\n",[15,94,95,96,99,100,102,103,106,107,110],{},"This is why ",[39,97,98],{},"UseAuthorization"," must come after ",[39,101,41],{}," — authorization middleware needs\nto know ",[30,104,105],{},"which"," endpoint matched before it can check ",[39,108,109],{},"[Authorize]"," attributes.",[10,112,114],{"id":113},"conventional-routing-vs-attribute-routing","Conventional routing vs attribute routing",[15,116,117,120,121,124],{},[25,118,119],{},"Conventional routing"," defines URL patterns centrally. Every controller that doesn't have\nits own ",[39,122,123],{},"[Route]"," is matched by these patterns:",[59,126,128],{"className":61,"code":127,"language":63,"meta":64,"style":64},"\u002F\u002F MVC — one pattern for all controllers:\napp.MapControllerRoute(\n    name: \"default\",\n    pattern: \"{controller=Home}\u002F{action=Index}\u002F{id?}\");\n\u002F\u002F \u002FProducts\u002FDetails\u002F5 → ProductsController.Details(id: 5)\n\u002F\u002F \u002F           → HomeController.Index()\n\u002F\u002F \u002FBlog\u002FPost\u002F42 → BlogController.Post(id: 42)\n",[39,129,130,135,140,145,150,156,162],{"__ignoreMap":64},[68,131,132],{"class":70,"line":71},[68,133,134],{},"\u002F\u002F MVC — one pattern for all controllers:\n",[68,136,137],{"class":70,"line":77},[68,138,139],{},"app.MapControllerRoute(\n",[68,141,142],{"class":70,"line":83},[68,143,144],{},"    name: \"default\",\n",[68,146,147],{"class":70,"line":89},[68,148,149],{},"    pattern: \"{controller=Home}\u002F{action=Index}\u002F{id?}\");\n",[68,151,153],{"class":70,"line":152},5,[68,154,155],{},"\u002F\u002F \u002FProducts\u002FDetails\u002F5 → ProductsController.Details(id: 5)\n",[68,157,159],{"class":70,"line":158},6,[68,160,161],{},"\u002F\u002F \u002F           → HomeController.Index()\n",[68,163,165],{"class":70,"line":164},7,[68,166,167],{},"\u002F\u002F \u002FBlog\u002FPost\u002F42 → BlogController.Post(id: 42)\n",[15,169,170,173],{},[25,171,172],{},"Attribute routing"," puts templates directly on controllers and actions:",[59,175,177],{"className":61,"code":176,"language":63,"meta":64,"style":64},"[ApiController]\n[Route(\"api\u002F[controller]\")]          \u002F\u002F [controller] expands to \"products\"\npublic class ProductsController : ControllerBase\n{\n    [HttpGet]                        \u002F\u002F GET \u002Fapi\u002Fproducts\n    public IActionResult List() => Ok(_products);\n\n    [HttpGet(\"{id:int}\")]            \u002F\u002F GET \u002Fapi\u002Fproducts\u002F5\n    public IActionResult Get(int id) => Ok(Find(id));\n\n    [HttpPost]                       \u002F\u002F POST \u002Fapi\u002Fproducts\n    public IActionResult Create([FromBody] CreateProductDto dto) { ... }\n\n    [HttpPut(\"{id:int}\")]            \u002F\u002F PUT \u002Fapi\u002Fproducts\u002F5\n    public IActionResult Update(int id, [FromBody] UpdateProductDto dto) { ... }\n\n    [HttpDelete(\"{id:int}\")]         \u002F\u002F DELETE \u002Fapi\u002Fproducts\u002F5\n    public IActionResult Delete(int id) { ... }\n}\n",[39,178,179,184,189,194,199,204,209,215,221,227,232,238,244,249,255,261,266,272,278],{"__ignoreMap":64},[68,180,181],{"class":70,"line":71},[68,182,183],{},"[ApiController]\n",[68,185,186],{"class":70,"line":77},[68,187,188],{},"[Route(\"api\u002F[controller]\")]          \u002F\u002F [controller] expands to \"products\"\n",[68,190,191],{"class":70,"line":83},[68,192,193],{},"public class ProductsController : ControllerBase\n",[68,195,196],{"class":70,"line":89},[68,197,198],{},"{\n",[68,200,201],{"class":70,"line":152},[68,202,203],{},"    [HttpGet]                        \u002F\u002F GET \u002Fapi\u002Fproducts\n",[68,205,206],{"class":70,"line":158},[68,207,208],{},"    public IActionResult List() => Ok(_products);\n",[68,210,211],{"class":70,"line":164},[68,212,214],{"emptyLinePlaceholder":213},true,"\n",[68,216,218],{"class":70,"line":217},8,[68,219,220],{},"    [HttpGet(\"{id:int}\")]            \u002F\u002F GET \u002Fapi\u002Fproducts\u002F5\n",[68,222,224],{"class":70,"line":223},9,[68,225,226],{},"    public IActionResult Get(int id) => Ok(Find(id));\n",[68,228,230],{"class":70,"line":229},10,[68,231,214],{"emptyLinePlaceholder":213},[68,233,235],{"class":70,"line":234},11,[68,236,237],{},"    [HttpPost]                       \u002F\u002F POST \u002Fapi\u002Fproducts\n",[68,239,241],{"class":70,"line":240},12,[68,242,243],{},"    public IActionResult Create([FromBody] CreateProductDto dto) { ... }\n",[68,245,247],{"class":70,"line":246},13,[68,248,214],{"emptyLinePlaceholder":213},[68,250,252],{"class":70,"line":251},14,[68,253,254],{},"    [HttpPut(\"{id:int}\")]            \u002F\u002F PUT \u002Fapi\u002Fproducts\u002F5\n",[68,256,258],{"class":70,"line":257},15,[68,259,260],{},"    public IActionResult Update(int id, [FromBody] UpdateProductDto dto) { ... }\n",[68,262,264],{"class":70,"line":263},16,[68,265,214],{"emptyLinePlaceholder":213},[68,267,269],{"class":70,"line":268},17,[68,270,271],{},"    [HttpDelete(\"{id:int}\")]         \u002F\u002F DELETE \u002Fapi\u002Fproducts\u002F5\n",[68,273,275],{"class":70,"line":274},18,[68,276,277],{},"    public IActionResult Delete(int id) { ... }\n",[68,279,281],{"class":70,"line":280},19,[68,282,283],{},"}\n",[15,285,286],{},[25,287,288],{},"When to use which:",[290,291,292,296,303],"ul",{},[293,294,295],"li",{},"Attribute routing: Web APIs, REST endpoints, fine-grained URL control.",[293,297,298,299,302],{},"Conventional routing: MVC apps with consistent ",[39,300,301],{},"controller\u002Faction\u002Fid"," URLs.",[293,304,305,50,308,311],{},[39,306,307],{},"[ApiController]",[30,309,310],{},"requires"," attribute routing — it disables conventional routing for the controller.",[10,313,315],{"id":314},"route-templates-in-depth","Route templates in depth",[59,317,319],{"className":61,"code":318,"language":63,"meta":64,"style":64},"\u002F\u002F Literal segment — exact match required:\n[HttpGet(\"api\u002Fv1\u002Fstatus\")]          \u002F\u002F only \u002Fapi\u002Fv1\u002Fstatus\n\n\u002F\u002F Route parameter — captures one path segment:\n[HttpGet(\"products\u002F{id}\")]          \u002F\u002F \u002Fproducts\u002F42 → id = \"42\"\n\n\u002F\u002F Optional parameter — segment may be absent:\n[HttpGet(\"search\u002F{term?}\")]         \u002F\u002F \u002Fsearch and \u002Fsearch\u002Fshoes both match\npublic IActionResult Search(string? term) { ... }\n\n\u002F\u002F Default value:\n[HttpGet(\"page\u002F{number=1}\")]        \u002F\u002F \u002Fpage → number=1; \u002Fpage\u002F3 → number=3\n\n\u002F\u002F Multiple parameters:\n[HttpGet(\"{year:int}\u002F{month:int}\u002F{slug}\")]\n\u002F\u002F \u002F2026\u002F6\u002Fmy-post → year=2026, month=6, slug=\"my-post\"\n\n\u002F\u002F Catch-all parameter — captures the rest including slashes:\n[HttpGet(\"files\u002F{**path}\")]         \u002F\u002F \u002Ffiles\u002Fa\u002Fb\u002Fc.txt → path = \"a\u002Fb\u002Fc.txt\"\n",[39,320,321,326,331,335,340,345,349,354,359,364,368,373,378,382,387,392,397,401,406],{"__ignoreMap":64},[68,322,323],{"class":70,"line":71},[68,324,325],{},"\u002F\u002F Literal segment — exact match required:\n",[68,327,328],{"class":70,"line":77},[68,329,330],{},"[HttpGet(\"api\u002Fv1\u002Fstatus\")]          \u002F\u002F only \u002Fapi\u002Fv1\u002Fstatus\n",[68,332,333],{"class":70,"line":83},[68,334,214],{"emptyLinePlaceholder":213},[68,336,337],{"class":70,"line":89},[68,338,339],{},"\u002F\u002F Route parameter — captures one path segment:\n",[68,341,342],{"class":70,"line":152},[68,343,344],{},"[HttpGet(\"products\u002F{id}\")]          \u002F\u002F \u002Fproducts\u002F42 → id = \"42\"\n",[68,346,347],{"class":70,"line":158},[68,348,214],{"emptyLinePlaceholder":213},[68,350,351],{"class":70,"line":164},[68,352,353],{},"\u002F\u002F Optional parameter — segment may be absent:\n",[68,355,356],{"class":70,"line":217},[68,357,358],{},"[HttpGet(\"search\u002F{term?}\")]         \u002F\u002F \u002Fsearch and \u002Fsearch\u002Fshoes both match\n",[68,360,361],{"class":70,"line":223},[68,362,363],{},"public IActionResult Search(string? term) { ... }\n",[68,365,366],{"class":70,"line":229},[68,367,214],{"emptyLinePlaceholder":213},[68,369,370],{"class":70,"line":234},[68,371,372],{},"\u002F\u002F Default value:\n",[68,374,375],{"class":70,"line":240},[68,376,377],{},"[HttpGet(\"page\u002F{number=1}\")]        \u002F\u002F \u002Fpage → number=1; \u002Fpage\u002F3 → number=3\n",[68,379,380],{"class":70,"line":246},[68,381,214],{"emptyLinePlaceholder":213},[68,383,384],{"class":70,"line":251},[68,385,386],{},"\u002F\u002F Multiple parameters:\n",[68,388,389],{"class":70,"line":257},[68,390,391],{},"[HttpGet(\"{year:int}\u002F{month:int}\u002F{slug}\")]\n",[68,393,394],{"class":70,"line":263},[68,395,396],{},"\u002F\u002F \u002F2026\u002F6\u002Fmy-post → year=2026, month=6, slug=\"my-post\"\n",[68,398,399],{"class":70,"line":268},[68,400,214],{"emptyLinePlaceholder":213},[68,402,403],{"class":70,"line":274},[68,404,405],{},"\u002F\u002F Catch-all parameter — captures the rest including slashes:\n",[68,407,408],{"class":70,"line":280},[68,409,410],{},"[HttpGet(\"files\u002F{**path}\")]         \u002F\u002F \u002Ffiles\u002Fa\u002Fb\u002Fc.txt → path = \"a\u002Fb\u002Fc.txt\"\n",[15,412,413],{},"Token replacement:",[290,415,416,422,428],{},[293,417,418,421],{},[39,419,420],{},"[controller]"," → controller class name minus \"Controller\" suffix",[293,423,424,427],{},[39,425,426],{},"[action]"," → action method name",[293,429,430,433],{},[39,431,432],{},"[area]"," → area name",[10,435,437],{"id":436},"route-constraints-right-tool-for-the-right-job","Route constraints — right tool for the right job",[15,439,440],{},"Constraints restrict which requests a route matches. They are specified with a colon:",[59,442,444],{"className":61,"code":443,"language":63,"meta":64,"style":64},"[HttpGet(\"{id:int}\")]             \u002F\u002F id must be parseable as int\n[HttpGet(\"{id:int:min(1)}\")]      \u002F\u002F int ≥ 1\n[HttpGet(\"{id:guid}\")]            \u002F\u002F GUID format\n[HttpGet(\"{slug:alpha}\")]         \u002F\u002F letters only\n[HttpGet(\"{date:datetime}\")]      \u002F\u002F parseable DateTime\n[HttpGet(\"{code:length(5)}\")]     \u002F\u002F exactly 5 characters\n[HttpGet(\"{zip:regex(^\\\\d{{5}}$)}\")] \u002F\u002F 5-digit zip code\n\n\u002F\u002F These two routes are unambiguous because of constraints:\n[HttpGet(\"{id:int}\")]             \u002F\u002F matches \u002Fproducts\u002F5\n[HttpGet(\"{name:alpha}\")]         \u002F\u002F matches \u002Fproducts\u002Fwidget\n",[39,445,446,451,456,461,466,471,476,481,485,490,495],{"__ignoreMap":64},[68,447,448],{"class":70,"line":71},[68,449,450],{},"[HttpGet(\"{id:int}\")]             \u002F\u002F id must be parseable as int\n",[68,452,453],{"class":70,"line":77},[68,454,455],{},"[HttpGet(\"{id:int:min(1)}\")]      \u002F\u002F int ≥ 1\n",[68,457,458],{"class":70,"line":83},[68,459,460],{},"[HttpGet(\"{id:guid}\")]            \u002F\u002F GUID format\n",[68,462,463],{"class":70,"line":89},[68,464,465],{},"[HttpGet(\"{slug:alpha}\")]         \u002F\u002F letters only\n",[68,467,468],{"class":70,"line":152},[68,469,470],{},"[HttpGet(\"{date:datetime}\")]      \u002F\u002F parseable DateTime\n",[68,472,473],{"class":70,"line":158},[68,474,475],{},"[HttpGet(\"{code:length(5)}\")]     \u002F\u002F exactly 5 characters\n",[68,477,478],{"class":70,"line":164},[68,479,480],{},"[HttpGet(\"{zip:regex(^\\\\d{{5}}$)}\")] \u002F\u002F 5-digit zip code\n",[68,482,483],{"class":70,"line":217},[68,484,214],{"emptyLinePlaceholder":213},[68,486,487],{"class":70,"line":223},[68,488,489],{},"\u002F\u002F These two routes are unambiguous because of constraints:\n",[68,491,492],{"class":70,"line":229},[68,493,494],{},"[HttpGet(\"{id:int}\")]             \u002F\u002F matches \u002Fproducts\u002F5\n",[68,496,497],{"class":70,"line":234},[68,498,499],{},"[HttpGet(\"{name:alpha}\")]         \u002F\u002F matches \u002Fproducts\u002Fwidget\n",[15,501,502],{},"Custom route constraint:",[59,504,506],{"className":61,"code":505,"language":63,"meta":64,"style":64},"public class EvenConstraint : IRouteConstraint\n{\n    public bool Match(HttpContext? httpContext, IRouter? route,\n        string routeKey, RouteValueDictionary values, RouteDirection direction)\n    {\n        return values.TryGetValue(routeKey, out var val)\n            && int.TryParse(val?.ToString(), out int n)\n            && n % 2 == 0;\n    }\n}\n\nbuilder.Services.Configure\u003CRouteOptions>(o =>\n    o.ConstraintMap[\"even\"] = typeof(EvenConstraint));\n\napp.MapGet(\"\u002Fitems\u002F{id:even}\", (int id) => $\"Even: {id}\");\n",[39,507,508,513,517,522,527,532,537,542,547,552,556,560,565,570,574],{"__ignoreMap":64},[68,509,510],{"class":70,"line":71},[68,511,512],{},"public class EvenConstraint : IRouteConstraint\n",[68,514,515],{"class":70,"line":77},[68,516,198],{},[68,518,519],{"class":70,"line":83},[68,520,521],{},"    public bool Match(HttpContext? httpContext, IRouter? route,\n",[68,523,524],{"class":70,"line":89},[68,525,526],{},"        string routeKey, RouteValueDictionary values, RouteDirection direction)\n",[68,528,529],{"class":70,"line":152},[68,530,531],{},"    {\n",[68,533,534],{"class":70,"line":158},[68,535,536],{},"        return values.TryGetValue(routeKey, out var val)\n",[68,538,539],{"class":70,"line":164},[68,540,541],{},"            && int.TryParse(val?.ToString(), out int n)\n",[68,543,544],{"class":70,"line":217},[68,545,546],{},"            && n % 2 == 0;\n",[68,548,549],{"class":70,"line":223},[68,550,551],{},"    }\n",[68,553,554],{"class":70,"line":229},[68,555,283],{},[68,557,558],{"class":70,"line":234},[68,559,214],{"emptyLinePlaceholder":213},[68,561,562],{"class":70,"line":240},[68,563,564],{},"builder.Services.Configure\u003CRouteOptions>(o =>\n",[68,566,567],{"class":70,"line":246},[68,568,569],{},"    o.ConstraintMap[\"even\"] = typeof(EvenConstraint));\n",[68,571,572],{"class":70,"line":251},[68,573,214],{"emptyLinePlaceholder":213},[68,575,576],{"class":70,"line":257},[68,577,578],{},"app.MapGet(\"\u002Fitems\u002F{id:even}\", (int id) => $\"Even: {id}\");\n",[15,580,581,584,585,588,589,592,593,596],{},[25,582,583],{},"Important:"," route constraints are for ",[30,586,587],{},"routing",", not ",[30,590,591],{},"validation",". A constraint of\n",[39,594,595],{},"{id:int:min(1)}"," rejects non-integer URLs at the routing layer, but you still need business\nvalidation in the action for input correctness. A URL that doesn't match any route returns 404,\nnot 400 — the client may be confused if you rely on constraints for validation.",[10,598,600],{"id":599},"link-generation","Link generation",[15,602,603],{},"Always generate URLs programmatically — hard-coded strings break when you rename a route:",[59,605,607],{"className":61,"code":606,"language":63,"meta":64,"style":64},"\u002F\u002F In a controller — Url helper:\n[HttpPost]\npublic IActionResult Create([FromBody] CreateOrderDto dto)\n{\n    var order = _svc.Create(dto);\n    var url = Url.Action(\"Get\", \"Orders\", new { id = order.Id });\n    return Created(url, order); \u002F\u002F Location: \u002Fapi\u002Forders\u002F42\n}\n\n\u002F\u002F Named route:\n[HttpGet(\"{id:int}\", Name = \"GetOrder\")]\npublic IActionResult Get(int id) => Ok(Find(id));\n\nvar url = Url.RouteUrl(\"GetOrder\", new { id = 5 }); \u002F\u002F \u002Fapi\u002Forders\u002F5\n\n\u002F\u002F LinkGenerator in DI (works outside controllers):\npublic class NotificationService\n{\n    private readonly LinkGenerator _links;\n    public NotificationService(LinkGenerator links) => _links = links;\n\n    public string GetOrderUrl(int orderId)\n        => _links.GetPathByName(\"GetOrder\", new { id = orderId })!;\n}\n",[39,608,609,614,619,624,628,633,638,643,647,651,656,661,666,670,675,679,684,689,693,698,704,709,715,721],{"__ignoreMap":64},[68,610,611],{"class":70,"line":71},[68,612,613],{},"\u002F\u002F In a controller — Url helper:\n",[68,615,616],{"class":70,"line":77},[68,617,618],{},"[HttpPost]\n",[68,620,621],{"class":70,"line":83},[68,622,623],{},"public IActionResult Create([FromBody] CreateOrderDto dto)\n",[68,625,626],{"class":70,"line":89},[68,627,198],{},[68,629,630],{"class":70,"line":152},[68,631,632],{},"    var order = _svc.Create(dto);\n",[68,634,635],{"class":70,"line":158},[68,636,637],{},"    var url = Url.Action(\"Get\", \"Orders\", new { id = order.Id });\n",[68,639,640],{"class":70,"line":164},[68,641,642],{},"    return Created(url, order); \u002F\u002F Location: \u002Fapi\u002Forders\u002F42\n",[68,644,645],{"class":70,"line":217},[68,646,283],{},[68,648,649],{"class":70,"line":223},[68,650,214],{"emptyLinePlaceholder":213},[68,652,653],{"class":70,"line":229},[68,654,655],{},"\u002F\u002F Named route:\n",[68,657,658],{"class":70,"line":234},[68,659,660],{},"[HttpGet(\"{id:int}\", Name = \"GetOrder\")]\n",[68,662,663],{"class":70,"line":240},[68,664,665],{},"public IActionResult Get(int id) => Ok(Find(id));\n",[68,667,668],{"class":70,"line":246},[68,669,214],{"emptyLinePlaceholder":213},[68,671,672],{"class":70,"line":251},[68,673,674],{},"var url = Url.RouteUrl(\"GetOrder\", new { id = 5 }); \u002F\u002F \u002Fapi\u002Forders\u002F5\n",[68,676,677],{"class":70,"line":257},[68,678,214],{"emptyLinePlaceholder":213},[68,680,681],{"class":70,"line":263},[68,682,683],{},"\u002F\u002F LinkGenerator in DI (works outside controllers):\n",[68,685,686],{"class":70,"line":268},[68,687,688],{},"public class NotificationService\n",[68,690,691],{"class":70,"line":274},[68,692,198],{},[68,694,695],{"class":70,"line":280},[68,696,697],{},"    private readonly LinkGenerator _links;\n",[68,699,701],{"class":70,"line":700},20,[68,702,703],{},"    public NotificationService(LinkGenerator links) => _links = links;\n",[68,705,707],{"class":70,"line":706},21,[68,708,214],{"emptyLinePlaceholder":213},[68,710,712],{"class":70,"line":711},22,[68,713,714],{},"    public string GetOrderUrl(int orderId)\n",[68,716,718],{"class":70,"line":717},23,[68,719,720],{},"        => _links.GetPathByName(\"GetOrder\", new { id = orderId })!;\n",[68,722,724],{"class":70,"line":723},24,[68,725,283],{},[15,727,728,729,732],{},"For minimal APIs, use ",[39,730,731],{},".WithName()"," to enable link generation:",[59,734,736],{"className":61,"code":735,"language":63,"meta":64,"style":64},"app.MapGet(\"\u002Forders\u002F{id:int}\", (int id) => GetOrder(id))\n   .WithName(\"get-order\");\n\napp.MapPost(\"\u002Forders\", (CreateOrderDto dto, LinkGenerator links) =>\n{\n    var order = CreateOrder(dto);\n    var url = links.GetPathByName(\"get-order\", new { id = order.Id });\n    return Results.Created(url, order);\n});\n",[39,737,738,743,748,752,757,761,766,771,776],{"__ignoreMap":64},[68,739,740],{"class":70,"line":71},[68,741,742],{},"app.MapGet(\"\u002Forders\u002F{id:int}\", (int id) => GetOrder(id))\n",[68,744,745],{"class":70,"line":77},[68,746,747],{},"   .WithName(\"get-order\");\n",[68,749,750],{"class":70,"line":83},[68,751,214],{"emptyLinePlaceholder":213},[68,753,754],{"class":70,"line":89},[68,755,756],{},"app.MapPost(\"\u002Forders\", (CreateOrderDto dto, LinkGenerator links) =>\n",[68,758,759],{"class":70,"line":152},[68,760,198],{},[68,762,763],{"class":70,"line":158},[68,764,765],{},"    var order = CreateOrder(dto);\n",[68,767,768],{"class":70,"line":164},[68,769,770],{},"    var url = links.GetPathByName(\"get-order\", new { id = order.Id });\n",[68,772,773],{"class":70,"line":217},[68,774,775],{},"    return Results.Created(url, order);\n",[68,777,778],{"class":70,"line":223},[68,779,780],{},"});\n",[10,782,784],{"id":783},"minimal-apis-and-route-groups","Minimal APIs and route groups",[15,786,787],{},"Minimal APIs define endpoints with lambdas — no controller class needed:",[59,789,791],{"className":61,"code":790,"language":63,"meta":64,"style":64},"\u002F\u002F Route group: common prefix + shared policies\nvar orders = app.MapGroup(\"\u002Fapi\u002Forders\")\n                .RequireAuthorization()\n                .WithTags(\"Orders\");\n\norders.MapGet(\"\u002F\", async (AppDb db) =>\n    await db.Orders.ToListAsync());\n\norders.MapGet(\"\u002F{id:int}\", async (int id, AppDb db) =>\n    await db.Orders.FindAsync(id) is Order o ? Results.Ok(o) : Results.NotFound());\n\norders.MapPost(\"\u002F\", async (CreateOrderDto dto, AppDb db) =>\n{\n    var order = new Order { Item = dto.Item };\n    db.Orders.Add(order);\n    await db.SaveChangesAsync();\n    return Results.Created($\"\u002Fapi\u002Forders\u002F{order.Id}\", order);\n});\n\norders.MapDelete(\"\u002F{id:int}\", async (int id, AppDb db) =>\n{\n    if (await db.Orders.FindAsync(id) is Order o)\n    {\n        db.Orders.Remove(o);\n        await db.SaveChangesAsync();\n        return Results.NoContent();\n    }\n    return Results.NotFound();\n});\n",[39,792,793,798,803,808,813,817,822,827,831,836,841,845,850,854,859,864,869,874,878,882,887,891,896,900,905,911,917,922,928],{"__ignoreMap":64},[68,794,795],{"class":70,"line":71},[68,796,797],{},"\u002F\u002F Route group: common prefix + shared policies\n",[68,799,800],{"class":70,"line":77},[68,801,802],{},"var orders = app.MapGroup(\"\u002Fapi\u002Forders\")\n",[68,804,805],{"class":70,"line":83},[68,806,807],{},"                .RequireAuthorization()\n",[68,809,810],{"class":70,"line":89},[68,811,812],{},"                .WithTags(\"Orders\");\n",[68,814,815],{"class":70,"line":152},[68,816,214],{"emptyLinePlaceholder":213},[68,818,819],{"class":70,"line":158},[68,820,821],{},"orders.MapGet(\"\u002F\", async (AppDb db) =>\n",[68,823,824],{"class":70,"line":164},[68,825,826],{},"    await db.Orders.ToListAsync());\n",[68,828,829],{"class":70,"line":217},[68,830,214],{"emptyLinePlaceholder":213},[68,832,833],{"class":70,"line":223},[68,834,835],{},"orders.MapGet(\"\u002F{id:int}\", async (int id, AppDb db) =>\n",[68,837,838],{"class":70,"line":229},[68,839,840],{},"    await db.Orders.FindAsync(id) is Order o ? Results.Ok(o) : Results.NotFound());\n",[68,842,843],{"class":70,"line":234},[68,844,214],{"emptyLinePlaceholder":213},[68,846,847],{"class":70,"line":240},[68,848,849],{},"orders.MapPost(\"\u002F\", async (CreateOrderDto dto, AppDb db) =>\n",[68,851,852],{"class":70,"line":246},[68,853,198],{},[68,855,856],{"class":70,"line":251},[68,857,858],{},"    var order = new Order { Item = dto.Item };\n",[68,860,861],{"class":70,"line":257},[68,862,863],{},"    db.Orders.Add(order);\n",[68,865,866],{"class":70,"line":263},[68,867,868],{},"    await db.SaveChangesAsync();\n",[68,870,871],{"class":70,"line":268},[68,872,873],{},"    return Results.Created($\"\u002Fapi\u002Forders\u002F{order.Id}\", order);\n",[68,875,876],{"class":70,"line":274},[68,877,780],{},[68,879,880],{"class":70,"line":280},[68,881,214],{"emptyLinePlaceholder":213},[68,883,884],{"class":70,"line":700},[68,885,886],{},"orders.MapDelete(\"\u002F{id:int}\", async (int id, AppDb db) =>\n",[68,888,889],{"class":70,"line":706},[68,890,198],{},[68,892,893],{"class":70,"line":711},[68,894,895],{},"    if (await db.Orders.FindAsync(id) is Order o)\n",[68,897,898],{"class":70,"line":717},[68,899,531],{},[68,901,902],{"class":70,"line":723},[68,903,904],{},"        db.Orders.Remove(o);\n",[68,906,908],{"class":70,"line":907},25,[68,909,910],{},"        await db.SaveChangesAsync();\n",[68,912,914],{"class":70,"line":913},26,[68,915,916],{},"        return Results.NoContent();\n",[68,918,920],{"class":70,"line":919},27,[68,921,551],{},[68,923,925],{"class":70,"line":924},28,[68,926,927],{},"    return Results.NotFound();\n",[68,929,931],{"class":70,"line":930},29,[68,932,780],{},[15,934,935,936,939,940,943,944,947],{},"Route groups eliminate repetition — the ",[39,937,938],{},"\u002Fapi\u002Forders"," prefix, ",[39,941,942],{},"RequireAuthorization()",", and\nthe ",[39,945,946],{},"WithTags(\"Orders\")"," Swagger tag apply to every endpoint in the group.",[10,949,951],{"id":950},"routing-ambiguity-and-how-to-avoid-it","Routing ambiguity — and how to avoid it",[15,953,954,955,958],{},"When two routes match with equal specificity, ",[39,956,957],{},"AmbiguousMatchException"," is thrown at runtime:",[59,960,962],{"className":61,"code":961,"language":63,"meta":64,"style":64},"\u002F\u002F Ambiguous — both match GET \u002Fproducts\u002F5:\n[HttpGet(\"{id}\")]\npublic IActionResult GetById(string id) { ... }\n\n[HttpGet(\"{name}\")]\npublic IActionResult GetByName(string name) { ... }\n\n\u002F\u002F Fixed with constraints:\n[HttpGet(\"{id:int}\")]      \u002F\u002F only integers\n[HttpGet(\"{name:alpha}\")]  \u002F\u002F only letters\n\n\u002F\u002F Or with distinct templates:\n[HttpGet(\"by-id\u002F{id:int}\")]\n[HttpGet(\"by-name\u002F{name}\")]\n",[39,963,964,969,974,979,983,988,993,997,1002,1007,1012,1016,1021,1026],{"__ignoreMap":64},[68,965,966],{"class":70,"line":71},[68,967,968],{},"\u002F\u002F Ambiguous — both match GET \u002Fproducts\u002F5:\n",[68,970,971],{"class":70,"line":77},[68,972,973],{},"[HttpGet(\"{id}\")]\n",[68,975,976],{"class":70,"line":83},[68,977,978],{},"public IActionResult GetById(string id) { ... }\n",[68,980,981],{"class":70,"line":89},[68,982,214],{"emptyLinePlaceholder":213},[68,984,985],{"class":70,"line":152},[68,986,987],{},"[HttpGet(\"{name}\")]\n",[68,989,990],{"class":70,"line":158},[68,991,992],{},"public IActionResult GetByName(string name) { ... }\n",[68,994,995],{"class":70,"line":164},[68,996,214],{"emptyLinePlaceholder":213},[68,998,999],{"class":70,"line":217},[68,1000,1001],{},"\u002F\u002F Fixed with constraints:\n",[68,1003,1004],{"class":70,"line":223},[68,1005,1006],{},"[HttpGet(\"{id:int}\")]      \u002F\u002F only integers\n",[68,1008,1009],{"class":70,"line":229},[68,1010,1011],{},"[HttpGet(\"{name:alpha}\")]  \u002F\u002F only letters\n",[68,1013,1014],{"class":70,"line":234},[68,1015,214],{"emptyLinePlaceholder":213},[68,1017,1018],{"class":70,"line":240},[68,1019,1020],{},"\u002F\u002F Or with distinct templates:\n",[68,1022,1023],{"class":70,"line":246},[68,1024,1025],{},"[HttpGet(\"by-id\u002F{id:int}\")]\n",[68,1027,1028],{"class":70,"line":251},[68,1029,1030],{},"[HttpGet(\"by-name\u002F{name}\")]\n",[15,1032,1033],{},"For conventional routing, more specific routes must be registered first:",[59,1035,1037],{"className":61,"code":1036,"language":63,"meta":64,"style":64},"app.MapControllerRoute(\"featured\", \"products\u002Ffeatured\", ...); \u002F\u002F specific first\napp.MapControllerRoute(\"default\",  \"products\u002F{id}\",     ...); \u002F\u002F generic second\n",[39,1038,1039,1044],{"__ignoreMap":64},[68,1040,1041],{"class":70,"line":71},[68,1042,1043],{},"app.MapControllerRoute(\"featured\", \"products\u002Ffeatured\", ...); \u002F\u002F specific first\n",[68,1045,1046],{"class":70,"line":77},[68,1047,1048],{},"app.MapControllerRoute(\"default\",  \"products\u002F{id}\",     ...); \u002F\u002F generic second\n",[10,1050,1052],{"id":1051},"http-verb-attributes-and-rest-conventions","HTTP verb attributes and REST conventions",[15,1054,1055],{},"The HTTP verb attributes constrain routes to specific methods and follow REST semantics:",[59,1057,1059],{"className":61,"code":1058,"language":63,"meta":64,"style":64},"[ApiController]\n[Route(\"api\u002F[controller]\")]\npublic class OrdersController : ControllerBase\n{\n    [HttpGet]                     \u002F\u002F GET \u002Fapi\u002Forders — list collection\n    [HttpGet(\"{id:int}\")]         \u002F\u002F GET \u002Fapi\u002Forders\u002F5 — get single\n    [HttpPost]                    \u002F\u002F POST \u002Fapi\u002Forders — create\n    [HttpPut(\"{id:int}\")]         \u002F\u002F PUT \u002Fapi\u002Forders\u002F5 — full replace\n    [HttpPatch(\"{id:int}\")]       \u002F\u002F PATCH \u002Fapi\u002Forders\u002F5 — partial update\n    [HttpDelete(\"{id:int}\")]      \u002F\u002F DELETE \u002Fapi\u002Forders\u002F5 — delete\n    [HttpHead(\"{id:int}\")]        \u002F\u002F HEAD \u002Fapi\u002Forders\u002F5 — existence check\n}\n",[39,1060,1061,1065,1070,1075,1079,1084,1089,1094,1099,1104,1109,1114],{"__ignoreMap":64},[68,1062,1063],{"class":70,"line":71},[68,1064,183],{},[68,1066,1067],{"class":70,"line":77},[68,1068,1069],{},"[Route(\"api\u002F[controller]\")]\n",[68,1071,1072],{"class":70,"line":83},[68,1073,1074],{},"public class OrdersController : ControllerBase\n",[68,1076,1077],{"class":70,"line":89},[68,1078,198],{},[68,1080,1081],{"class":70,"line":152},[68,1082,1083],{},"    [HttpGet]                     \u002F\u002F GET \u002Fapi\u002Forders — list collection\n",[68,1085,1086],{"class":70,"line":158},[68,1087,1088],{},"    [HttpGet(\"{id:int}\")]         \u002F\u002F GET \u002Fapi\u002Forders\u002F5 — get single\n",[68,1090,1091],{"class":70,"line":164},[68,1092,1093],{},"    [HttpPost]                    \u002F\u002F POST \u002Fapi\u002Forders — create\n",[68,1095,1096],{"class":70,"line":217},[68,1097,1098],{},"    [HttpPut(\"{id:int}\")]         \u002F\u002F PUT \u002Fapi\u002Forders\u002F5 — full replace\n",[68,1100,1101],{"class":70,"line":223},[68,1102,1103],{},"    [HttpPatch(\"{id:int}\")]       \u002F\u002F PATCH \u002Fapi\u002Forders\u002F5 — partial update\n",[68,1105,1106],{"class":70,"line":229},[68,1107,1108],{},"    [HttpDelete(\"{id:int}\")]      \u002F\u002F DELETE \u002Fapi\u002Forders\u002F5 — delete\n",[68,1110,1111],{"class":70,"line":234},[68,1112,1113],{},"    [HttpHead(\"{id:int}\")]        \u002F\u002F HEAD \u002Fapi\u002Forders\u002F5 — existence check\n",[68,1115,1116],{"class":70,"line":240},[68,1117,283],{},[15,1119,1120,1123],{},[39,1121,1122],{},"[AcceptVerbs]"," for multi-verb actions:",[59,1125,1127],{"className":61,"code":1126,"language":63,"meta":64,"style":64},"[AcceptVerbs(\"GET\", \"HEAD\")]\npublic IActionResult GetOrHead(int id) { ... }\n",[39,1128,1129,1134],{"__ignoreMap":64},[68,1130,1131],{"class":70,"line":71},[68,1132,1133],{},"[AcceptVerbs(\"GET\", \"HEAD\")]\n",[68,1135,1136],{"class":70,"line":77},[68,1137,1138],{},"public IActionResult GetOrHead(int id) { ... }\n",[15,1140,1141],{},"Never use GET for operations with side effects — caches and proxies assume GET is safe and\nidempotent, which breaks if GET mutates state.",[10,1143,1145],{"id":1144},"recap","Recap",[15,1147,1148,1149,1151,1152,1154,1155,1157,1158,1161,1162,1165,1166,1169],{},"Endpoint routing separates matching (",[39,1150,41],{},") from dispatch (",[39,1153,56],{},"), letting\nmiddleware between them inspect endpoint metadata. Attribute routing gives per-action URL\ncontrol and is required with ",[39,1156,307],{},". Route constraints restrict matching — use them\nfor routing, not input validation. Always generate URLs with ",[39,1159,1160],{},"IUrlHelper"," or ",[39,1163,1164],{},"LinkGenerator",",\nnever hard-code strings. Use ",[39,1167,1168],{},"MapGroup"," in minimal APIs to share prefixes and policies across\nrelated endpoints. Resolve ambiguity with constraints or distinct templates — never rely on\nregistration order for correctness.",[1171,1172,1173],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":64,"searchDepth":77,"depth":77,"links":1175},[1176,1177,1178,1179,1180,1181,1182,1183,1184,1185],{"id":12,"depth":77,"text":13},{"id":20,"depth":77,"text":21},{"id":113,"depth":77,"text":114},{"id":314,"depth":77,"text":315},{"id":436,"depth":77,"text":437},{"id":599,"depth":77,"text":600},{"id":783,"depth":77,"text":784},{"id":950,"depth":77,"text":951},{"id":1051,"depth":77,"text":1052},{"id":1144,"depth":77,"text":1145},"How ASP.NET Core routing maps requests to handlers — attribute vs conventional routing, route constraints, link generation, minimal API route groups, and how to debug ambiguous routes.","medium","md",".NET Core","dotnet",{},"\u002Fblog\u002Fdotnet-aspnet-core-routing","\u002Fdotnet\u002Faspnet-core\u002Frouting",{"title":5,"description":1186},"blog\u002Fdotnet-aspnet-core-routing","Routing","ASP.NET Core","aspnet-core","2026-06-23","nUxHmcHP4WtZY1CfPAJopXC-nUstWW4K8GAhjxD0IDo",1782244085882]