ASP.NET Core端点路由中三种让人困惑的路由函数

3/1/2022 2:00:42 PM
782
0

使用UseRoutingUseEndpoints中间件配置路由。要使用控制器:

动作要么是传统路由的,要么是属性路由的在控制器或动作上放置路由使其成为属性路由。有关详细信息,请参阅混合路由

 

常规路线名称

以下示例中的字符串 "blog"和是常规路由名称:"default"

C#复制

 

app.MapControllerRoute(name: "blog",
                pattern: "blog/{*article}",
                defaults: new { controller = "Blog", action = "Article" });
app.MapControllerRoute(name: "default",
               pattern: "{controller=Home}/{action=Index}/{id?}");

路由名称给路由一个逻辑名称。命名路由可用于 URL 生成。当路由的顺序可能使 URL 生成变得复杂时,使用命名路由可以简化 URL 创建。路由名称在应用程序范围内必须是唯一的。

路线名称:

  • 对 URL 匹配或请求处理没有影响。
  • 仅用于 URL 生成。

路由名称概念在路由中表示为IEndpointNameMetadata。术语路由名称端点名称

  • 可以互换。
  • 文档和代码中使用哪一个取决于所描述的 API。

 

 

 

REST API 的属性路由

REST API 应该使用属性路由将应用程序的功能建模为一组资源,其中操作由HTTP 动词表示。

属性路由使用一组属性将操作直接映射到路由模板。以下代码是 REST API 的典型代码,将在下一个示例中使用:

app.MapControllers();

在前面的代码中,内部调用了MapControllersUseEndpoints来映射属性路由控制器。

在以下示例中:

  • HomeController匹配一组类似于默认常规路由{controller=Home}/{action=Index}/{id?}匹配的 URL。

 

public class HomeController : Controller
{
    [Route("")]
    [Route("Home")]
    [Route("Home/Index")]
    [Route("Home/Index/{id?}")]
    public IActionResult Index(int? id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }

    [Route("Home/About")]
    [Route("Home/About/{id?}")]
    public IActionResult About(int? id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}

 

这个例子突出了属性路由和传统路由之间的关键编程区别。属性路由需要更多输入来指定路由。传统的默认路由处理路由更简洁。但是,属性路由允许并且需要精确控制哪些路由模板适用于每个操作

使用属性路由,控制器和动作名称在动作匹配中不起任何作用,除非使用令牌替换。以下示例匹配与上一个示例相同的 URL:

public class MyDemoController : Controller
{
    [Route("")]
    [Route("Home")]
    [Route("Home/Index")]
    [Route("Home/Index/{id?}")]
    public IActionResult MyIndex(int? id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }

    [Route("Home/About")]
    [Route("Home/About/{id?}")]
    public IActionResult MyAbout(int? id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}

以下代码对actionand使用令牌替换controller:

public class HomeController : Controller
{
    [Route("")]
    [Route("Home")]
    [Route("[controller]/[action]")]
    public IActionResult Index()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    [Route("[controller]/[action]")]
    public IActionResult About()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
}

HTTP 动词模板

ASP.NET Core 具有以下 HTTP 动词模板:

 

路线模板

ASP.NET Core 具有以下路由模板:

带有 Http 动词属性的属性路由

考虑以下控制器:

[Route("api/[controller]")]
[ApiController]
public class Test2Controller : ControllerBase
{
    [HttpGet]   // GET /api/test2
    public IActionResult ListProducts()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    [HttpGet("{id}")]   // GET /api/test2/xyz
    public IActionResult GetProduct(string id)
    {
       return ControllerContext.MyDisplayRouteInfo(id);
    }

    [HttpGet("int/{id:int}")] // GET /api/test2/int/3
    public IActionResult GetIntProduct(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }

    [HttpGet("int2/{id}")]  // GET /api/test2/int2/3
    public IActionResult GetInt2Product(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}

 

在前面的代码中:

  • 每个操作都包含[HttpGet]属性,该属性将匹配仅限于 HTTP GET 请求。
  • 该GetProduct操作包括"{id}"模板,因此id附加到"api/[controller]"控制器上的模板。方法模板是"api/[controller]/"{id}"". /api/test2/xyz因此,此操作仅匹配表单、/api/test2/123、/api/test2/{any string}等的 GET 请求。

C#复制

 

  • 该GetIntProduct操作包含"int/{id:int}")模板。:int模板的一部分将id路由值限制为可以转换为整数的字符串。一个 GET 请求/api/test2/int/abc:

C#复制

 

  • 该GetInt2Product操作包含{id}在模板中,但不限id于可以转换为整数的值。一个 GET 请求/api/test2/int2/abc:
    • 符合这条路线。
    • 模型绑定无法转换abc为整数。该id方法的参数是整数。
    • 返回400 错误请求,因为模型绑定无法转换abc为整数。

C#复制

 

属性路由可以使用HttpMethodAttribute属性,例如HttpPostAttributeHttpPutAttributeHttpDeleteAttribute。所有HTTP 谓词属性都接受一个路由模板。以下示例显示了匹配同一路由模板的两个操作:

[HttpGet("int2/{id}")]  // GET /api/test2/int2/3
public IActionResult GetInt2Product(int id)
{
    return ControllerContext.MyDisplayRouteInfo(id);
}
[HttpGet("int/{id:int}")] // GET /api/test2/int/3
public IActionResult GetIntProduct(int id)
{
    return ControllerContext.MyDisplayRouteInfo(id);
}
[HttpGet("{id}")]   // GET /api/test2/xyz
public IActionResult GetProduct(string id)
{
   return ControllerContext.MyDisplayRouteInfo(id);
}

属性路由可以使用HttpMethodAttribute属性,例如HttpPostAttributeHttpPutAttributeHttpDeleteAttribute。所有HTTP 谓词属性都接受一个路由模板。以下示例显示了匹配同一路由模板的两个操作:

[ApiController]
public class MyProductsController : ControllerBase
{
    [HttpGet("/products3")]
    public IActionResult ListProducts()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    [HttpPost("/products3")]
    public IActionResult CreateProduct(MyProduct myProduct)
    {
        return ControllerContext.MyDisplayRouteInfo(myProduct.Name);
    }
}

使用 URL 路径/products3:

  • 当HTTP 动词为时,该MyProductsController.ListProducts操作运行。GET
  • 当HTTP 动词为时,该MyProductsController.CreateProduct操作运行。POST

在构建 REST API 时,您很少需要[Route(...)]在操作方法上使用,因为该操作接受所有 HTTP 方法。最好使用更具体的HTTP 动词属性来准确了解您的 API 支持什么。REST API 的客户端应该知道哪些路径和 HTTP 动词映射到特定的逻辑操作。

REST API 应该使用属性路由将应用程序的功能建模为一组资源,其中操作由 HTTP 动词表示。这意味着许多操作(例如,对同一逻辑资源的 GET 和 POST)使用相同的 URL。属性路由提供了仔细设计 API 的公共端点布局所需的控制级别。

由于属性路由适用于特定操作,因此很容易将所需的参数作为路由模板定义的一部分。在以下示例中,id需要作为 URL 路径的一部分:

[ApiController]
public class Products2ApiController : ControllerBase
{
    [HttpGet("/products2/{id}", Name = "Products_List")]
    public IActionResult GetProduct(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}

路线名称

以下代码定义了一个路由名称Products_List:

C#复制

 

[ApiController]
public class Products2ApiController : ControllerBase
{
    [HttpGet("/products2/{id}", Name = "Products_List")]
    public IActionResult GetProduct(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}

路由名称可用于根据特定路由生成 URL。路线名称:

  • 对路由的 URL 匹配行为没有影响。
  • 仅用于 URL 生成。

路由名称在应用程序范围内必须是唯一的。

将上述代码与常规默认路由进行对比,默认路由将id参数定义为可选({id?})。精确指定 API 的能力具有优势,例如允许/products和/products/5被分派到不同的操作。

 

组合属性路由

为了减少属性路由的重复性,控制器上的路由属性与各个操作的路由属性相结合。控制器上定义的任何路由模板都会添加到操作上的路由模板之前在控制器上放置一个路由属性使得控制器中的所有动作都使用属性路由

[ApiController]
[Route("products")]
public class ProductsApiController : ControllerBase
{
    [HttpGet]
    public IActionResult ListProducts()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    [HttpGet("{id}")]
    public IActionResult GetProduct(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}

在前面的示例中:

  • URL路径/products可以匹配ProductsApi.ListProducts
  • URL 路径/products/5可以匹配ProductsApi.GetProduct(int)

这两个操作都只匹配 HTTP GET,因为它们都标有[HttpGet]属性。

应用于以/或~/不与应用于控制器的路由模板组合的操作的路由模板。

属性路由顺序

路由构建一棵树并同时匹配所有端点:

  • 路由条目的行为就像以理想的顺序排列一样。
  • 最具体的路线有机会在更一般的路线之前执行。

例如,属性路由 likeblog/search/{topic}比属性路由 like 更具体blog/{*article}。默认情况下blog/search/{topic},路由具有更高的优先级,因为它更具体。使用常规路由,开发人员负责按所需顺序放置路由。

属性路由可以使用Order属性配置订单。框架提供的所有路由属性包括Order. Order根据属性的升序处理路由。默认顺序是0. 使用Order = -1在不设置顺序的路线之前运行来设置路线。Order = 1在默认路线排序后使用运行设置路线。

避免依赖Order。如果应用程序的 URL 空间需要明确的顺序值才能正确路由,那么它也可能会让客户端感到困惑。一般来说,属性路由选择正确的路由与 URL 匹配。如果用于 URL 生成的默认顺序不起作用,则使用路由名称作为替代通常比应用Order属性更简单。

考虑以下两个控制器,它们都定义了路由匹配/home:

C#复制

 

public class HomeController : Controller
{
    [Route("")]
    [Route("Home")]
    [Route("Home/Index")]
    [Route("Home/Index/{id?}")]
    public IActionResult Index(int? id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }

    [Route("Home/About")]
    [Route("Home/About/{id?}")]
    public IActionResult About(int? id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}

C#复制

 

public class MyDemoController : Controller
{
    [Route("")]
    [Route("Home")]
    [Route("Home/Index")]
    [Route("Home/Index/{id?}")]
    public IActionResult MyIndex(int? id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }

    [Route("Home/About")]
    [Route("Home/About/{id?}")]
    public IActionResult MyAbout(int? id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}

使用上述代码进行请求/home会引发类似于以下内容的异常:

文本复制

 

AmbiguousMatchException: The request matched multiple endpoints. Matches:

 WebMvcRouting.Controllers.HomeController.Index
 WebMvcRouting.Controllers.MyDemoController.MyIndex

添加Order到其中一个路由属性可以解决歧义:

C#复制

 

[Route("")]
[Route("Home", Order = 2)]
[Route("Home/MyIndex")]
public IActionResult MyIndex()
{
    return ControllerContext.MyDisplayRouteInfo();
}

使用前面的代码,/home运行HomeController.Index端点。要到达MyDemoController.MyIndex,请求/home/MyIndex。注意

  • 前面的代码是一个示例或糟糕的路由设计。它被用来说明Order财产。
  • 该Order属性仅解决歧义,该模板无法匹配。最好去掉[Route("Home")]模板。

有关使用 Razor Pages 的路线顺序的信息,请参阅Razor Pages 路线和应用程序约定:路线顺序

在某些情况下,会返回带有不明确路由的 HTTP 500 错误。使用日志记录查看哪些端点导致了AmbiguousMatchException.

路由模板 [controller]、[action]、[area] 中的令牌替换

为方便起见,属性路由通过将标记括在方括号 ( , )中来支持标记替换。标记、和被替换为定义路由的操作中的操作名称、区域名称和控制器名称的值:[][action][area][controller]

C#复制

 

[Route("[controller]/[action]")]
public class Products0Controller : Controller
{
    [HttpGet]
    public IActionResult List()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }


    [HttpGet("{id}")]
    public IActionResult Edit(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}

在前面的代码中:

C#复制

 

[HttpGet]
public IActionResult List()
{
    return ControllerContext.MyDisplayRouteInfo();
}
  • 火柴/Products0/List

C#复制

 

[HttpGet("{id}")]
public IActionResult Edit(int id)
{
    return ControllerContext.MyDisplayRouteInfo(id);
}
  • 火柴/Products0/Edit/{id}

令牌替换是构建属性路由的最后一步。上述示例的行为与以下代码相同:

令牌替换是构建属性路由的最后一步。上述示例的行为与以下代码相同:

C#复制

 

public class Products20Controller : Controller
{
    [HttpGet("[controller]/[action]")]  // Matches '/Products20/List'
    public IActionResult List()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    [HttpGet("[controller]/[action]/{id}")]   // Matches '/Products20/Edit/{id}'
    public IActionResult Edit(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}

属性路由也可以与继承相结合。这与令牌替换相结合非常强大。令牌替换也适用于由属性路由定义的路由名称。 [Route("[controller]/[action]", Name="[controller]_[action]")]为每个动作生成一个唯一的路由名称:

C#复制

 

[ApiController]
[Route("api/[controller]/[action]", Name = "[controller]_[action]")]
public abstract class MyBase2Controller : ControllerBase
{
}

public class Products11Controller : MyBase2Controller
{
    [HttpGet]                      // /api/products11/list
    public IActionResult List()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

    [HttpGet("{id}")]             //    /api/products11/edit/3
    public IActionResult Edit(int id)
    {
        return ControllerContext.MyDisplayRouteInfo(id);
    }
}

要匹配文字标记替换分隔符[or ],请通过重复字符 ( [[or ]]) 对其进行转义。

使用参数转换器自定义令牌替换

多属性路由

属性路由支持定义到达同一动作的多个路由。最常见的用法是模仿默认常规路由的行为,如下例所示:

C#复制

 

[Route("[controller]")]
public class Products13Controller : Controller
{
    [Route("")]     // Matches 'Products13'
    [Route("Index")] // Matches 'Products13/Index'
    public IActionResult Index()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }

将多个路由属性放在控制器上意味着每个路由属性都与操作方法上的每个路由属性相结合:

C#复制

 

[Route("Store")]
[Route("[controller]")]
public class Products6Controller : Controller
{
    [HttpPost("Buy")]       // Matches 'Products6/Buy' and 'Store/Buy'
    [HttpPost("Checkout")]  // Matches 'Products6/Checkout' and 'Store/Checkout'
    public IActionResult Buy()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
}

所有HTTP 谓词路由约束都实现IActionConstraint。

当多个实现IActionConstraint的路由属性被放置在一个动作上时:

  • 每个动作约束与应用于控制器的路由模板相结合。

C#复制

 

[Route("api/[controller]")]
public class Products7Controller : ControllerBase
{
    [HttpPut("Buy")]        // Matches PUT 'api/Products7/Buy'
    [HttpPost("Checkout")]  // Matches POST 'api/Products7/Checkout'
    public IActionResult Buy()
    {
        return ControllerContext.MyDisplayRouteInfo();
    }
}

在操作上使用多个路由可能看起来有用且强大,最好保持应用程序的 URL 空间基本且定义明确。在需要时对操作使用多个路由,例如,支持现有客户端。

指定属性路由可选参数、默认值和约束


使用 IRouteTemplateProvider 自定义路由属性

使用应用模型自定义属性路由

混合路由:属性路由与常规路由

ASP.NET Core 应用程序可以混合使用传统路由和属性路由。为浏览器提供 HTML 页面的控制器使用常规路由,为提供 REST API 的控制器使用属性路由是典型的做法。

动作要么是传统路由,要么是属性路由在控制器或操作上放置路由使其属性路由。定义属性路由的动作不能通过常规路由到达,反之亦然控制器上的任何路由属性都会使控制器属性中的所有动作都被路由。

属性路由和常规路由使用相同的路由引擎。

URL 生成和环境值

按操作名称生成 URL

通过路由生成 URL

在 HTML 和 Razor 中生成 URL

操作结果中的 URL 生成

专用常规路线的特殊情况

 

总结:

动作要么是传统路由,要么是属性路由在控制器或操作上放置路由使其属性路由。定义属性路由的动作不能通过常规路由到达,反之亦然控制器上的任何路由属性都会使控制器属性中的所有动作都被路由。

属性路由和常规路由使用相同的路由引擎。

[HttpGet] 是谓词可以与传统路由结合,[HttpGet("")]是维持属性路由可与[Rout("")]结合

[ApiController]属性使得属性路由成为一个需求,既:必须同时使用[Route("")]例如:

[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase

无法通过UseMvcUseMvcWithDefaultRoute定义的常规路由访问操作。UseEndpoints

传统路由和属性路由无法组合使用

 

全部评论



提问