一. 背景和方案
1. 多版本管理的概念
Android 、IOS等 App 存在着多版本客户端共存的问题:App 最新版已经升级到了5.0 了,但是有的用户手机上还运行着 4.8、3.9 甚至2.2 版本的 App,由于早期没有内置升级机制、用户不会升级、用户拒绝升级等原因,造成这些旧版本 App 也在运行。开发新版本 App 的时候,要给接口增加新的功能或者修改以前接口的规范,会造成旧版本App 无法使用,因此在一定情况下会“保留旧接口的运行、新功能用新接口”,这样就会存在多版本接口共存的问题。
通常的做法是:旧版接口做一个代码分支,除了进行 bug 修改外,旧版本接口不再做改动,新接口代码继续演化升级。在客户端请求的时候带着要请求的接口版本号,在服务器端选择合适的版本代码进行处理。
2. 解决方案
(1). 不同的版本使用不同的域名:v1.api.ypf.com、v2.api.ypf.com、v3…… (最佳方案)
(2). 在Url,报文头等中带不同的版本信息,用Nginx等做反向代理服务,然后将 http://api.ypf.com/api/v1/User/1和http://api.ypf.com/api/v2/User/1 转到不同的服务器处理。
(3). 多个版本的 Controller共处在一个项目中,然 后使 用 [RoutePrefix] 特性来进行区分,这种方案Controller的名字不能一样,如下:
(4). 如果我想在Controller文件夹中新建多个版本文件夹,如:v1、v2、v3,每个文件夹中存放的控制器名称相同,比如都叫PersonController,不同文件夹下代表不同版本,这个时候会有一个很尴尬的问题,没法请求,识别不了,这个时候就需要重写系统默认的机制,IHttpControllerSelector 根据 “报文头”或者“请求路径”等选择不同的 Controller 执行。
(该方案的实现,详见下面的实战测试)
二. 实战测试
1. 在Controller文件下新建v1和v2文件夹,分别存放不同版本的Person控制器,每个Person控制器新建一个GetName方法,如下图:
2. 重写系统默认的控制器选择机制,可以直接实现IHttpControllerSelector接口,也可以继承DefaultHttpControllerSelector类,从而间接实现IHttpControllerSelector接口,在重写的SelectController方法中,实现了两种区分机制,分别是根据请求路径区分 和 根据报文头中的ApiVersion参数区分。
1 /// <summary>2 /// 自己实现IHttpControllerSelector接口来替换系统默认的IHttpControllerSelector3 /// </summary>4 public class VersionConstrollerSelector : DefaultHttpControllerSelector5 {6 7 private HttpConfiguration _config;8 /// <summary>9 /// 构造函数
10 /// </summary>
11 /// <param name="config"></param>
12 public VersionConstrollerSelector(HttpConfiguration config) : base(config)
13 {
14 _config = config;
15 }
16
17
18 /// <summary>
19 /// 获取所有的Controller
20 /// </summary>
21 /// <returns></returns>
22 public override IDictionary<string, HttpControllerDescriptor> GetControllerMapping()
23 {
24 Dictionary<string, HttpControllerDescriptor> dict = new Dictionary<string, HttpControllerDescriptor>();
25 //循环所有的程序集
26 foreach (var asm in _config.Services.GetAssembliesResolver().GetAssemblies())
27 {
28 //获取所有继承ApiController的非抽象类
29 var controllerTypes = asm.GetTypes().Where(t => !t.IsAbstract && typeof(ApiController).IsAssignableFrom(t)).ToArray();
30 //循环上述获取的非抽象类
31 foreach (var ctrlType in controllerTypes)
32 {
33 //从namespace中提取版本号
34 var match = Regex.Match(ctrlType.Namespace, @"_05_WebApiExtend.Controllers.v(\d+)");
35 if (match.Success)
36 {
37 //获取版本号
38 string verNum = match.Groups[1].Value;
39 //获取控制器名称(eg:PersonController中获取Person)
40 string controllerName = Regex.Match(ctrlType.Name, "(.+)Controller").Groups[1].Value;
41 //声明集合中的键(形式:Personv1 、Personv2)
42 string key = controllerName + "v" + verNum;
43 dict[key] = new HttpControllerDescriptor(_config, controllerName, ctrlType);
44 }
45
46 }
47
48 }
49 return dict;
50 }
51
52 /// <summary>
53 /// 进行Controller的匹配
54 /// </summary>
55 /// <param name="request"></param>
56 /// <returns>匹配成功返回控制器信息,匹配失败返回null</returns>
57
58 public override HttpControllerDescriptor SelectController(HttpRequestMessage request)
59 {
60 //获取所有的controller键值集合
61 var controllers = GetControllerMapping();
62 //获取路由数据
63 var routeData = request.GetRouteData();
64 //从路由中获取当前controller的名称
65 var controllerName = (string)routeData.Values["controller"];
66
67 //下面是两种方式获取版本号
68 string verNum = "";
69 try
70 {
71 //从报文头中获取版本号(当没有这个参数的时候走catch)
72 verNum = request.Headers.GetValues("ApiVersion").Single();
73 }
74 catch (Exception)
75 {
76 //从url中获取版本号
77 verNum = Regex.Match(request.RequestUri.PathAndQuery, @"api/v(\d+)").Groups[1].Value;
78 }
79 //拼接key值
80 string key = controllerName + "v" + verNum;
81 if (controllers.ContainsKey(key))
82 {
83 return controllers[key];
84 }
85 else
86 {
87 return null;
88 }
89 }
90 }
3. 在WebConfig.cs类中,加上 config.Services.Replace(typeof(IHttpControllerSelector), new VersionConstrollerSelector(config)); ,即用重写IHttpControllerSelector的替换原先的IHttpControllerSelector,如下图:
4. 注释掉原先的路由请求模式,新增下面两个路由规则
1 public static class WebApiConfig2 {3 public static void Register(HttpConfiguration config)4 {5 // Web API 配置和服务6 //用重写IHttpControllerSelector的替换原先的IHttpControllerSelector7 config.Services.Replace(typeof(IHttpControllerSelector), new VersionConstrollerSelector(config));8 // Web API 路由9 config.MapHttpAttributeRoutes();
10 //config.Routes.MapHttpRoute(
11 // name: "DefaultApi",
12 // routeTemplate: "api/{controller}/{action}/{id}",
13 // defaults: new { id = RouteParameter.Optional }
14 //);
15
16 //多版本控制的路由改造
17 config.Routes.MapHttpRoute(
18 name: "DefaultApiV1",
19 routeTemplate: "api/v1/{controller}/{action}/{id}",
20 defaults: new { id = RouteParameter.Optional }
21 );
22
23 config.Routes.MapHttpRoute(
24 name: "DefaultApiV2",
25 routeTemplate: "api/v2/{controller}/{action}/{id}",
26 defaults: new { id = RouteParameter.Optional }
27 );
28 }
29 }
5. 用PostMan测试
(1). 分别请求 http://localhost:2182/api/v1/Person/GetName?Name=2 和 http://localhost:2182/api/v2/Person/GetName?Name=2, 返回不同的版本的信息,证明可以根据Url中的v1和v2进行版本区分。
(2). 请求 http://localhost:2182/api/v1/Person/GetName?Name=2 地址两次 ,表头中分别带有ApiVersion=1 和 ApiVersion=2,返回不同版本的信息,证明可以报文头中的参数进行版本区分。