第25课:路由与终结点:如何规划好你的Web API

学习分享 丨作者 / 郑 子 铭 丨公众号 / DotNet NB / CloudNative NB

路由系统在 ASP.NET MVC 框架里面就已经存在了,在 ASP.NET Core 框架里面进行了改进

路由系统的核心作用是指 URL 和 应用程序 Controller 的对应关系的一种映射

这个映射关系实际上有两种作用:

1、把 URL 映射到对应的 Controller 对应的 action 上面去

2、根据 Controller 和 action 的名字来生产 URL

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

1、路由模板的方式

2、RouteAttribute 方式

这两种方式分别适用于的场景是不一样的

路由模板的方式是之前传统的方式,可以用来作为 MVC 的页面 Web 配置

现在用的比较多的前后端分离的架构,定义 Web API 的时候使用 RouteAttribute 方式去做

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

有以下简单的几种约束:

1、类型约束

2、范围约束

3、正则表达式

4、是否必选

5、自定义 IRouteConstraint

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

1、LinkGenerator

2、IUrlHelper

IUrlHelper 与 MVC 框架里面的 MVCHelper 很像

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

接下来看一下代码

为了方便演示,这里先注册了一组 Swagger 的代码,将 Web API 通过 Swagger 的可视化界面输出出来

引入 Swagger 对应 ASP.NET Core 的包

Swashbuckle.AspNetCore

将代码文档 XML 文档注入给 Swagger

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

在中间件里面注册 Swagger

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

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

接着是路由的定义 OrderController

namespace RoutingDemo.Controllers
{
    [Route("api/[controller]/[action]")]// RouteAttribute 的方式
    [ApiController]
    public class OrderController : ControllerBase
    {
        /// <summary>
        /// 
        /// </summary>
        /// <param name="id">必须可以转为long</param>
        /// <returns></returns>
        [HttpGet("{id:MyRouteConstraint}")]// 这里使用了自定义的约束
        public bool OrderExist(object id)
        {
            return true;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="id">最大20</param>
        /// <returns></returns>
        [HttpGet("{id:max(20)}")]// 这里使用了 Max 的约束
        public bool Max(long id)
        {
            return true;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="ss">必填</param>
        /// <returns></returns>
        [HttpGet("{name:required}")]// 必填约束
        public bool Reque(string name)
        {
            return true;
        }


        /// <summary>
        /// 
        /// </summary>
        /// <param name="number">以三个数字开始</param>
        /// <returns></returns>
        [HttpGet("{number:regex(^\\d{{3}}$)}")]// 正则表达式约束
        public bool Number(string number)
        {
            return true;
        }
    }
}

上面用到了自定义约束 MyRouteConstraint

namespace RoutingDemo.Constraints
{
    public class MyRouteConstraint : IRouteConstraint
    {
        public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
        {
            if (RouteDirection.IncomingRequest == routeDirection)
            {
                var v = values[routeKey];
                if (long.TryParse(v.ToString(), out var value))
                {
                    return true;
                }
            }
            return false;
        }
    }
}

注册 MyRouteConstraint

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

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

app.UseEndpoints(endpoints =>
{
    // 使用 RouteAttribute
    endpoints.MapControllers();
});

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

项目右键属性,生成,输出,XML文档文件勾选

启动程序,可以看到一共有五个接口

第一个接口是我们实现的自定义约束,点击 try it out 后输入参数

第二个接口约束最大为20

输入5,执行

可以看到响应码是 200

输入25,执行

可以看到响应码是 404,也就说路由匹配失败了

第三个接口因为参数是必须的,所以没办法输入空值,有一个前端的验证

第四个接口以三个数字开始,输入 234,符合正则表达式,响应码 200

自定义约束实现了路由约束接口,它只有一个 Match 方法,这个方法传入了 Http 当前的 httpContext,route,routeKey

这个 routeKey 就是我们要验证的 key 值

后面两个参数 RouteValueDictionary 就是当前可以获取到的这个 routeKey 对应的传入的值是什么值,这样就可以验证我们传入的信息

routeDirection 这个枚举的作用是当前验证是用来验证 URL 请求进来,验证是否路由匹配,还是用来生成 URL,是进还是出的这样一个定义,在不同的场景下面可能响应的逻辑是不一样的

下面的逻辑是如果路由是进来的,也就是通过 URL 配置 action 的情况,就做一个判断,根据 routeKey 取到当前输入的这个值,然后判断它是否可以转成 long,这个其实模拟了类型验证,比如说 long 型验证的方式

namespace RoutingDemo.Constraints
{
    public class MyRouteConstraint : IRouteConstraint
    {
        public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection)
        {
            if (RouteDirection.IncomingRequest == routeDirection)
            {
                var v = values[routeKey];
                if (long.TryParse(v.ToString(), out var value))
                {
                    return true;
                }
            }
            return false;
        }
    }
}

RouteDirection

namespace Microsoft.AspNetCore.Routing
{
    public enum RouteDirection
    {
        IncomingRequest = 0,
        UrlGeneration = 1
    }
}

接下来看一下约束是如何注入到我们系统里生效的

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

services.AddRouting(options =>
{
    //options.ConstraintMap.Add("MyRouteConstraint", typeof(MyRouteConstraint));
    options.ConstraintMap.Add("isLong", typeof(MyRouteConstraint));
});

OrderController 里面也修改为 isLong

/// <summary>
/// 
/// </summary>
/// <param name="id">必须可以转为long</param>
/// <returns></returns>
//[HttpGet("{id:MyRouteConstraint}")]// 这里使用了自定义的约束
[HttpGet("{id:isLong}")]
//public bool OrderExist(object id)
public bool OrderExist([FromRoute] string id)
{
    return true;
}

启动程序,输入34,返回响应码200,输入abc,返回响应码404,也就是自定义约束生效了

接下来讲一下链接生成的过程

/// <summary>
/// 
/// </summary>
/// <param name="id">最大20</param>
/// <param name="linkGenerator"></param>
/// <returns></returns>
[HttpGet("{id:max(20)}")]// 这里使用了 Max 的约束
//public bool Max(long id)
public bool Max([FromRoute]long id, [FromServices]LinkGenerator linkGenerator)
{
    // 这两行就是分别获取完整 Uri 和 path 的代码
    // 它还有不同的重载,可以根据需要传入不同的路由的值
    var path = linkGenerator.GetPathByAction(HttpContext,
        action: "Reque",
        controller: "Order",
        values: new { name = "abc" });// 因为下面对 name 有一个必填的约束,所以这里需要传值

    var uri = linkGenerator.GetUriByAction(HttpContext,
        action: "Reque",
        controller: "Order",
        values: new { name = "abc" });
    return true;
}

/// <summary>
/// 
/// </summary>
/// <param name="ss">必填</param>
/// <returns></returns>
[HttpGet("{name:required}")]// 必填约束
public bool Reque(string name)
{
    return true;
}

启动程序,端点调试,输入1,点击执行,可以看到

path 的值为

/api/Order/Reque/abc

uri 的值为

https://localhost:5001/api/Order/Reque/abc

在定义 Controller 的时候,实际上还会做一些接口废弃的过程,通过 [Obsolete]

/// <summary>
/// 
/// </summary>
/// <param name="ss">必填</param>
/// <returns></returns>
[HttpGet("{name:required}")]// 必填约束
[Obsolete]
public bool Reque(string name)
{
    return true;
}

我们不必直接删除我们的接口,它还可以正常工作,但是我们可以把它标记为已废弃,在 Swagger 上面会有体现

可以看到这个接口已经被标记为废弃的,但是它的调用还是可以工作的

总结一下

1、Restful 不是必须的,只要约束好 Http 方法以及 URL 地址,还有 Http 响应码,响应的 Json 格式,这些约定只要适合团队的协作习惯就可以了,也就是说需要定义好 API 的表达契约

2、建议是把 API 都约束在特定的目录下面,与其他功能性页面进行隔离,比如说 /api /api 加版本号这样子的方式

3、在废弃 API 的过程中间,应该是间隔版本的方式废弃,也就是说先将即将废弃的 API 标记为已废弃,但是它还是可以工作,间隔几个版本之后将代码删除掉

到目前为止,讲解了依赖注入,配置日志,中间件等必要的内容,下一节开始将进入微服务实战的部分

GitHub源码链接:

https://github.com/MingsonZheng/DotNetCoreDevelopmentActualCombat/tree/main/RoutingDemo

Last updated