路由注册方式

路由的核心作用就是:URL和应用程序Controller的对应关系的一种映射
映射关系实际上有两种:

  1. 把URL映射到我们对应的Controller的action上面去
  2. 根据Controller和action的名字来生产URL

.NET Core 提供了两种路由注册的方式:

  • 路由模板的方式
  • RouteAttribute方式

这两种方式分别适用于的场景是不一样的
路由模板的方式是之前传统的方式,可以用来作为 MVC 的页面 Web 配置
现在用的比较多的前后端分离的架构,定义 Web API 的时候使用 RouteAttribute 方式去做
在定义路由,注册路由的过程中间,有一个重要的特性就是路由约束,是指路由如何匹配

路由约束

在定义路由,注册路由的过程中间,有一个重要的特性就是路由约束,是指路由如何匹配

  • 类型约束
  • 范围约束
  • 正则表达式
  • 是否必选
  • 自定义IRouteConstraint

URL生成

另外路由系统提供了两个关键的类,用来反向根据路由的信息生产 URL地址

  • LinkGenerator
    • LinkGenerator 是全新提供的一个链接生成的对象,可以从容器里面,在任意的位置都可以获取到这个对象,然后根据需要生成 URL 地址
  • IUrlHelper
    • IUrlHelper 与 MVC 框架里面的 MVCHelper 很像

示例

新建Web程序👉选择API模板
为了方便演示,这里先注册了一组 Swagger的代码,将Web API通过 Swagger的可视化界面输出出来
引入 Swagger 对应 ASP.NET Core 的包
image.png
将代码文档 XML文档注入给 Swagger

  1. services.AddSwaggerGen(c =>
  2. {
  3. c.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" });
  4. var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
  5. var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
  6. c.IncludeXmlComments(xmlPath);
  7. });

在中间件里面注册Swagger

  1. app.UseSwagger();
  2. app.UseSwaggerUI(c =>
  3. {
  4. c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
  5. });

这样子就可以在界面上看到 Swagger的界面,并且浏览我们定义的 API

Controllers文件夹👉新建OrderController

  1. using Microsoft.AspNetCore.Mvc;
  2. namespace RoutingDemo.Controllers
  3. {
  4. [Route("api/[controller]/[action]")]
  5. [ApiController]
  6. public class OrderController : ControllerBase
  7. {
  8. /// <summary>
  9. ///
  10. /// </summary>
  11. /// <param name="id">必须可以转为long</param>
  12. /// <returns></returns>
  13. [HttpGet("{id:MyRouteConstraint}")]// 这里使用了自定义的约束
  14. public bool OrderExist(object id)
  15. {
  16. return true;
  17. }
  18. /// <summary>
  19. ///
  20. /// </summary>
  21. /// <param name="id">最大20</param>
  22. /// <returns></returns>
  23. [HttpGet("{id:max(20)}")]// 这里使用了 Max 的约束
  24. public bool Max(long id)
  25. {
  26. return true;
  27. }
  28. /// <summary>
  29. ///
  30. /// </summary>
  31. /// <param name="ss">必填</param>
  32. /// <returns></returns>
  33. [HttpGet("{name:required}")]// 必填约束
  34. public bool Reque(string name)
  35. {
  36. return true;
  37. }
  38. /// <summary>
  39. ///
  40. /// </summary>
  41. /// <param name="number">以三个数字开始</param>
  42. /// <returns></returns>
  43. [HttpGet("{number:regex(^\\d{{3}}$)}")]// 正则表达式约束
  44. public bool Number(string number)
  45. {
  46. return true;
  47. }
  48. }
  49. }

上面用到了自定义约束 MyRouteConstraint

  1. using Microsoft.AspNetCore.Http;
  2. using Microsoft.AspNetCore.Routing;
  3. namespace RoutingDemo.Constraints
  4. {
  5. public class MyRouteConstraint : IRouteConstraint
  6. {
  7. public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
  8. {
  9. if (RouteDirection.IncomingRequest == routeDirection)
  10. {
  11. var v = values[routeKey];
  12. if (long.TryParse(v.ToString(), out var value))
  13. {
  14. return true;
  15. }
  16. }
  17. return false;
  18. }
  19. }
  20. }

注册 MyRouteConstraint

  1. services.AddRouting(options =>
  2. {
  3. options.ConstraintMap.Add("MyRouteConstraint", typeof(MyRouteConstraint));
  4. });

让它生效之前,需要在中间件注册的位置注入 UseEndpoints,然后对 UseEndpoints使用 MapControllers

  1. app.UseEndpoints(endpoints =>
  2. {
  3. // 使用RouteAttribute
  4. endpoints.MapControllers();
  5. });

通过这样子的方式把 OrderController的路由注入进来

image.png
第一个接口是我们实现的自定义约束,点击 try it out 后输入参数
25 | 路由与终结点:如何规划好你的Web API - 图3
第二个接口约束最大为20
输入5,执行
25 | 路由与终结点:如何规划好你的Web API - 图4
可以看到响应码是 200
输入25,执行
25 | 路由与终结点:如何规划好你的Web API - 图5
可以看到响应码是 404,也就说路由匹配失败了
第三个接口因为参数是必须的,所以没办法输入空值,有一个前端的验证
第四个接口以三个数字开始,输入 234,符合正则表达式,响应码 200

自定义约束实现了路由约束接口,它只有一个 Match方法,这个方法传入了 Http当前的 httpContextrouterouteKey
这个 routeKey就是我们要验证的 key
后面两个参数 RouteValueDictionary就是当前可以获取到的这个 routeKey对应的传入的值是什么值,这样就可以验证我们传入的信息
routeDirection这个枚举的作用是当前验证是用来验证 URL请求进来,验证是否路由匹配,还是用来生成 URL,是进还是出的这样一个定义,在不同的场景下面可能响应的逻辑是不一样的
下面的逻辑是如果路由是进来的,也就是通过 URL配置 action的情况,就做一个判断,根据 routeKey取到当前输入的这个值,然后判断它是否可以转成 long,这个其实模拟了类型验证,比如说 long型验证的方式

可以给我们的约束起一个名字 isLong,这个名字就是用来 Attribute上面标识约束的

  1. services.AddRouting(options =>
  2. {
  3. //options.ConstraintMap.Add("MyRouteConstraint", typeof(MyRouteConstraint));
  4. options.ConstraintMap.Add("isLong", typeof(MyRouteConstraint));
  5. });

OrderController 里面也修改为 isLong

  1. /// <summary>
  2. ///
  3. /// </summary>
  4. /// <param name="id">必须可以转为long</param>
  5. /// <returns></returns>
  6. //[HttpGet("{id:MyRouteConstraint}")]// 这里使用了自定义的约束
  7. [HttpGet("{id:isLong}")]
  8. //public bool OrderExist(object id)
  9. public bool OrderExist([FromRoute] string id)
  10. {
  11. return true;
  12. }

启动程序,输入34,返回响应码200,输入abc,返回响应码404,也就是自定义约束生效了
接下来讲一下链接生成的过程

  1. /// <summary>
  2. ///
  3. /// </summary>
  4. /// <param name="id">最大20</param>
  5. /// <param name="linkGenerator"></param>
  6. /// <returns></returns>
  7. [HttpGet("{id:max(20)}")]// 这里使用了 Max 的约束
  8. //public bool Max(long id)
  9. public bool Max([FromRoute]long id, [FromServices]LinkGenerator linkGenerator)
  10. {
  11. // 这两行就是分别获取完整 Uri 和 path 的代码
  12. // 它还有不同的重载,可以根据需要传入不同的路由的值
  13. var path = linkGenerator.GetPathByAction(HttpContext,
  14. action: "Reque",
  15. controller: "Order",
  16. values: new { name = "abc" });// 因为下面对 name 有一个必填的约束,所以这里需要传值
  17. var uri = linkGenerator.GetUriByAction(HttpContext,
  18. action: "Reque",
  19. controller: "Order",
  20. values: new { name = "abc" });
  21. return true;
  22. }
  23. /// <summary>
  24. ///
  25. /// </summary>
  26. /// <param name="ss">必填</param>
  27. /// <returns></returns>
  28. [HttpGet("{name:required}")]// 必填约束
  29. public bool Reque(string name)
  30. {
  31. return true;
  32. }

启动程序,端点调试,输入1,点击执行,可以看到
path的值为/api/Order/Reque/abc
uri的值为https://localhost:5001/api/Order/Reque/abc
在定义 Controller的时候,实际上还会做一些接口废弃的过程,通过 [Obsolete]

  1. /// <summary>
  2. ///
  3. /// </summary>
  4. /// <param name="ss">必填</param>
  5. /// <returns></returns>
  6. [HttpGet("{name:required}")]// 必填约束
  7. [Obsolete]
  8. public bool Reque(string name)
  9. {
  10. return true;
  11. }

我们不必直接删除我们的接口,它还可以正常工作,但是我们可以把它标记为已废弃,在 Swagger上面会有体现
25 | 路由与终结点:如何规划好你的Web API - 图6
可以看到这个接口已经被标记为废弃的,但是它的调用还是可以工作的

总结一下

  • Restful不是必须的,只要约束好 Http方法以及 URL地址,还有 Http响应码,响应的 Json格式,这些约定只要适合团队的协作习惯就可以了,也就是说需要定义好 API的表达契约
  • 建议是把 API 都约束在特定的目录下面,与其他功能性页面进行隔离,比如说 /api加版本号这样子的方式
  • 在废弃 API的过程中间,应该是间隔版本的方式废弃,也就是说先将即将废弃的 API标记为已废弃,但是它还是可以工作,间隔几个版本之后将代码删除掉