使用ASP.NET Web Api构建基于REST风格的服务实战系列教程【九】——API变了,客户端怎么办?

系列导航地址http://www.cnblogs.com/fzrain/p/3490137.html

前言

一旦我们将API发布之后,消费者就会开始使用并和其他的一些数据混在一起。然而,当新的需求出现时变化是不可避免的,你也许会庆幸API变了对现有客户端没受到影响,但是这种情况不会一直发生。

因此,在具体实现之前仔细考虑一下ASP.NET Web Api的版本策略就变得很有必要了。在我们的案例中,需求发生了变化而且我们通过创建不同版本的API来解决变化,同时不影响已经在使用API的客户端。我们把新的API版本和旧的API版本一起返回给客户端,让它有足够的时间迁移到最新版本的API,有时候多版本共存也是有可能的。

实现版本控制的方式有好多,本文主要介绍URI,query string,自定义Header和接收Header

API变了

简单起见,我们让“StudentsController”中的Get方法发生变化——在响应报文的body中,我们用“CoursesDuration”和“FullName”属性替换原来的“FirstName”和“LastName”属性。

最简单的做法就是创建一个与“StudentsController”一样的Controller并命名为“StudentsV2Controller”,我们将根据不同的API版本选择合适的Controller。在新的Controller中我们实现上述变化并使用相同的Http方法,同时不做任何介绍

现在我们请求“StudentsController”的Get方法是,会返回如下数据:

  1. [{
  2. "id": 2,
  3. "url": "http://localhost:8323/api/students/HasanAhmad",
  4. "firstName": "Hasan",
  5. "lastName": "Ahmad",
  6. "gender": 0,
  7. "enrollmentsCount": 4
  8. },
  9. {
  10. "id": 3,
  11. "url": "http://localhost:8323/api/students/MoatasemAhmad",
  12. "firstName": "Moatasem",
  13. "lastName": "Ahmad",
  14. "gender": 0,
  15. "enrollmentsCount": 4
  16. }]

我们期待访问“StudentsV2Controller”的Get方法后应该的到:

  1. [{
  2. "id": 2,
  3. "url": "http://localhost:8323/api/students/HasanAhmad",
  4. "fullName": "Hasan Ahmad",
  5. "gender": 0,
  6. "enrollmentsCount": 4,
  7. "coursesDuration": 13
  8. },
  9. {
  10. "id": 3,
  11. "url": "http://localhost:8323/api/students/MoatasemAhmad",
  12. "fullName": "Moatasem Ahmad",
  13. "gender": 0,
  14. "enrollmentsCount": 4,
  15. "coursesDuration": 16
  16. }]

ok,下面来实现,复制粘贴”StudnetsController”并重命名为“StudnetsV2Controller”,更改Get方法的实现:

  1. public IEnumerable<StudentV2BaseModel> Get(int page = 0, int pageSize = 10)
  2. {
  3. IQueryable<Student> query;
  4.  
  5. query = TheRepository.GetAllStudentsWithEnrollments().OrderBy(c => c.LastName);
  6.  
  7. var totalCount = query.Count();
  8. var totalPages = Math.Ceiling((double)totalCount / pageSize);
  9.  
  10. var urlHelper = new UrlHelper(Request);
  11. var prevLink = page > 0 ? urlHelper.Link("Students", new { page = page - 1, pageSize = pageSize }) : "";
  12. var nextLink = page < totalPages - 1 ? urlHelper.Link("Students", new { page = page + 1, pageSize = pageSize }) : "";
  13.  
  14. var paginationHeader = new
  15. {
  16. TotalCount = totalCount,
  17. TotalPages = totalPages,
  18. PrevPageLink = prevLink,
  19. NextPageLink = nextLink
  20. };
  21.  
  22. System.Web.HttpContext.Current.Response.Headers.Add("X-Pagination",
  23. Newtonsoft.Json.JsonConvert.SerializeObject(paginationHeader));
  24.  
  25. var results = query
  26. .Skip(pageSize * page)
  27. .Take(pageSize)
  28. .ToList()
  29. .Select(s => TheModelFactory.CreateV2Summary(s));
  30.  
  31. return results;
  32. }

可以看到,这里我们改的很少,返回的类型变成了“StudentV2BaseModel”,而这个类型是由ModelFactory的CreateV2Summary方法创建的。因此我们需要添加StudentV2BaseModel类和CreateV2Summary方法:

  1. public class StudentV2BaseModel
  2. {
  3. public int Id { get; set; }
  4. public string Url { get; set; }
  5. public string FullName { get; set; }
  6. public Data.Enums.Gender Gender { get; set; }
  7. public int EnrollmentsCount { get; set; }
  8. public double CoursesDuration { get; set; }
  9. }
  10.  
  11. public class ModelFactory
  12. {
  13. public StudentV2BaseModel CreateV2Summary(Student student)
  14. {
  15. return new StudentV2BaseModel()
  16. {
  17. Url = _UrlHelper.Link("Students", new { userName = student.UserName }),
  18. Id = student.Id,
  19. FullName = string.Format("{0} {1}", student.FirstName, student.LastName),
  20. Gender = student.Gender,
  21. EnrollmentsCount = student.Enrollments.Count(),
  22. CoursesDuration = Math.Round(student.Enrollments.Sum(c => c.Course.Duration))
  23. };
  24. }
  25. }

到目前为止,我们的准备工作就算做完了,下面介绍四种方式实现版本变化

使用URI控制Web Api的版本

在URI中包含版本号是最常见的做法,如果想用V1版本的api(使用http://localhost:{your_port}/api/v1/students/),同理,如果想用V2版本的api(使用http://localhost:{your_port}/api/v2/students/

这种做法的好处就是客户端知道自己用的是哪一版本的api,实现方法就是在“WebApiConfig”中添加2条路由:

  1. config.Routes.MapHttpRoute(
  2. name: "Students",
  3. routeTemplate: "api/v1/students/{userName}",
  4. defaults: new { controller = "students", userName = RouteParameter.Optional }
  5. );
  6.  
  7. config.Routes.MapHttpRoute(
  8. name: "Students2",
  9. routeTemplate: "api/v2/students/{userName}",
  10. defaults: new { controller = "studentsV2", userName = RouteParameter.Optional }
  11. );

在上面代码中,我们添加了2条路由规则,它们彼此对应了相应的Controller。如果以后我们打算添加V3,那么就得再加一条。这里就会变得越来越混乱。

这种技术的主要缺点就是不符合REST规范因为URI一直会变,换句话说一旦我们发布一个新版本,就得添加一条新路由。

在我们讲解另外3种实现模式之前,我们先来看一下在web api框架是怎么根据我们的请求来选择相应的Controller的:在web api中有一个“DefaultHttpControllerSelector”类,其中有一个方法“SelectController()”,这个方法接收一个“HttpRequestMessage”类型的参数。这个对象包含一个含key/value键值对的route data,其中就包括在“WebApiConfig”中配置的controller的名字。根据这一条信息,通过反射获取所有实现“ApiController”的类,web api就会匹配到这个Controller,如果匹配结果不等于1(等于0或大于等于2),那么就会抛出一个异常。

我们自定义一个类“LearningControllerSelector”继承自“Http.Dispatcher.DefaultHttpControllerSelector”,重写“SelectController()”方法,具体代码如下:

  1. public class LearningControllerSelector : DefaultHttpControllerSelector
  2. {
  3. private HttpConfiguration _config;
  4. public LearningControllerSelector(HttpConfiguration config)
  5. : base(config)
  6. {
  7. _config = config;
  8. }
  9.  
  10. public override HttpControllerDescriptor SelectController(HttpRequestMessage request)
  11. {
  12. var controllers = GetControllerMapping(); //Will ignore any controls in same name even if they are in different namepsace
  13.  
  14. var routeData = request.GetRouteData();
  15.  
  16. var controllerName = routeData.Values["controller"].ToString();
  17.  
  18. HttpControllerDescriptor controllerDescriptor;
  19.  
  20. if (controllers.TryGetValue(controllerName, out controllerDescriptor))
  21. {
  22.  
  23. var version = "2";
  24.  
  25. var versionedControllerName = string.Concat(controllerName, "V", version);
  26.  
  27. HttpControllerDescriptor versionedControllerDescriptor;
  28. if (controllers.TryGetValue(versionedControllerName, out versionedControllerDescriptor))
  29. {
  30. return versionedControllerDescriptor;
  31. }
  32.  
  33. return controllerDescriptor;
  34. }
  35.  
  36. return null;
  37.  
  38. }
  39. }

上述代码主要意思如下:

1.调用父类方法GetControllerMapping()获取所有实现了ApiController的类。

2.通过request对象获取routeData ,然后进一步获得Controller的name

3.根据我们刚刚得到的Controller,名字创建“HttpControllerDescriptor”对象,这个对象包含了描述Controller的信息

.4.接着,在我们找到的Controller的名字后面加上“V”和版本号,重复上面步骤即可。关于如何获得版本号,我们一会儿讨论,这里暂时写死成“2”。

为了使我们自定义的“Controller Selector”生效,因此需要在“WebApiConfig”中做如下配置:

  1. config.Services.Replace(typeof(IHttpControllerSelector), new LearningControllerSelector((config)));

接下来我们就来实现请求如何发送版本号

使用Query String设置版本

使用query string设置版本顾名思义,就是在请求URI后面加上”?v=2“,例如这个URI:http://localhost:{your_port}/api/students/?v=2

我们可以认为客户端没有提供query string的版本号,那么版本号默认为“1”。

实现起来也不复杂,在我们的“LearningControllerSelector”类中添加一个“GetVersionFromQueryString()”方法,该方法接收一个HttpRequestMessage参数,并从这个请求对象中获取客户端所需要的版本:

  1. private string GetVersionFromQueryString(HttpRequestMessage request)
  2. {
  3. var query = HttpUtility.ParseQueryString(request.RequestUri.Query);
  4.  
  5. var version = query["v"];
  6.  
  7. if (version != null)
  8. {
  9. return version;
  10. }
  11.  
  12. return "1";
  13.  
  14. }

我们只需要在SelectController方法中调用这个方法即可,唯一的缺点依然是URI会变,不符合REST规范。

通过自定义请求头设置版本

现在我们使用另一种方式来发生版本号——自定义请求头,它不是URI的一部分,添加一个头“X-Learning-Version”并把版本号设置在里面,当客户端没有这条头信息是我们可以认为它需要V1版本。

实现这个技术,我们在“LearningControllerSelector”中添加一个“GetVersionFromHeader”方法,代码如下:

  1. private string GetVersionFromHeader(HttpRequestMessage request)
  2. {
  3. const string HEADER_NAME = "X-Learning-Version";
  4.  
  5. if (request.Headers.Contains(HEADER_NAME))
  6. {
  7. var versionHeader = request.Headers.GetValues(HEADER_NAME).FirstOrDefault();
  8. if (versionHeader != null)
  9. {
  10. return versionHeader;
  11. }
  12. }
  13.  
  14. return "1";
  15. }

这里做法很简单,我们先确定好请求头的名字,然后去request的Header中找,如果有数据,就获得。

客户端发送的请求如下:

这么做也有缺点,就是添加了一个请求头(注:这个缺点不是很理解),下面介绍第四种方式

使用Accept Header设置版本

这种方法是直接使用Accept Header, 请求的时候将它设置为“Accept:application/json; version=2”,我们依旧这么认为:如果客户端不提供版本号,我们就给他V1的数据。

在“LearningControllerSelector”类中添加“GetVersionFromAcceptHeaderVersion”方法,具体实现如下:

  1. private string GetVersionFromAcceptHeaderVersion(HttpRequestMessage request)
  2. {
  3. var acceptHeader = request.Headers.Accept;
  4.  
  5. foreach (var mime in acceptHeader)
  6. {
  7. if (mime.MediaType == "application/json")
  8. {
  9. var version = mime.Parameters
  10. .Where(v => v.Name.Equals("version", StringComparison.OrdinalIgnoreCase)).FirstOrDefault();
  11.  
  12. if (version != null)
  13. {
  14. return version.Value;
  15. }
  16. return "1";
  17. }
  18. }
  19. return "1";
  20. }

这个实现看上去比上面更标准,更专业了。

源码地址:https://github.com/fzrain/WebApi.eLearning

 

API变了,客户端怎么办?的更多相关文章

  1. 使用ASP.NET Web Api构建基于REST风格的服务实战系列教程【九】——API变了,客户端怎么办?

    系列导航地址http://www.cnblogs.com/fzrain/p/3490137.html 前言 一旦我们将API发布之后,消费者就会开始使用并和其他的一些数据混在一起.然而,当新的需求出现 ...

  2. api接口对于客户端的身份认证方式以及安全措施

    转载 基于http协议的api接口对于客户端的身份认证方式以及安全措施 由于http是无状态的,所以正常情况下在浏览器浏览网页,服务器都是通过访问者的cookie(cookie中存储的jsession ...

  3. Identity4实现服务端+api资源控制+客户端请求

    准备写一些关于Identity4相关的东西,最近也比较对这方面感兴趣.所有做个开篇笔记记录一下,以便督促自己下一个技术方案方向 已经写好的入门级别Identity4的服务+api资源访问控制和简单的客 ...

  4. zookeeper原生API做java客户端

    简介 本文是使用apache提供的原生api做zookeeper客户端 jar包 zookeeper-3.4.5.jar   Demo package bjsxt.zookeeper.base; im ...

  5. IdentityServer4[3]:使用客户端认证控制API访问(客户端授权模式)

    使用客户端认证控制API访问(客户端授权模式) 场景描述 使用IdentityServer保护API的最基本场景. 我们定义一个API和要访问API的客户端.客户端从IdentityServer请求A ...

  6. 基于http协议的api接口对于客户端的身份认证方式以及安全措施

    由于http是无状态的,所以正常情况下在浏览器浏览网页,服务器都是通过访问者的cookie(cookie中存储的jsessionid)来辨别客户端的身份的,当客户端进行登录服务器也会将登录信息存放在服 ...

  7. IdentityServer4(7)- 使用客户端认证控制API访问(客户端授权模式)

    一.前言 本文已更新到 .NET Core 2.2 本文包括后续的Demo都会放在github:https://github.com/stulzq/IdentityServer4.Samples (Q ...

  8. (转)基于http协议的api接口对于客户端的身份认证方式以及安全措施

    由于http是无状态的,所以正常情况下在浏览器浏览网页,服务器都是通过访问者的cookie(cookie中存储的 jsessionid)来辨别客户端的身份的,当客户端进行登录服务器也会将登录信息存放在 ...

  9. 【翻译】Flink Table Api & SQL — SQL客户端Beta 版

    本文翻译自官网:SQL Client Beta  https://ci.apache.org/projects/flink/flink-docs-release-1.9/dev/table/sqlCl ...

随机推荐

  1. hdu 4975 最大流问题解决队伍和矩阵,利用矩阵dp优化

    //刚開始乱搞. //网络流求解,假设最大流=全部元素的和则有解:利用残留网络推断是否唯一, //方法有两种,第一种是深搜看看是否存在正边权的环.见上一篇4888 //至少四个点构成的环,另外一种是用 ...

  2. Oracle语句优化1

    Oracle语句优化1 优化就是选择最有效的方法来执行SQL语句.Oracle优化器选择它认为最有效的     方法来执行SQL语句.         1. IS   NULL和IS   NOT   ...

  3. asp.net学习之SqlDataSource

    原文:asp.net学习之SqlDataSource 通过 SqlDataSource 控件,可以使用 Web 服务器控件访问位于关系数据库中的数据.其中可以包括 Microsoft SQL Serv ...

  4. JavaEE(6) - JMS消息选择和查看

    1. JMS消息的类型.消息头和消息属性 消息类型: StreamMessage MapMessage TextMessage ObjectMessage BytesMessage JMS消息中的消息 ...

  5. 记得12306货运系统“抢购空”编写插件--chrome交互式插件的各个部分

    --chrome交互式插件的各个部分 Chrome插件的基础知识就不多说了.随便找个新手教程就能够上手了,比如官方提供的Overview与Getting Started教程足够入门了:笔者也是现学现卖 ...

  6. 允许Android随着屏幕转动的控制自由转移到任何地方(附demo)

    在本文中,Android ViewGroup/View流程,及经常使用的自己定义ViewGroup的方法.在此基础上介绍动态控制View的位置的三种方法,并给出最佳的一种方法. 一.ViewGroup ...

  7. Spring4 SpringMVC Hibernate4 Freemaker 集成示例

    变更更正(2014-05-30 13:47:22):一些IDE在web.xml我们会报告这个错误: cvc-complex-type.2.4.a: Invalid content was found ...

  8. 大数据系列修炼-Scala课程04

    Scala中继承实现:超类的构造.字段重写.方法重写 关于超类的构建:超类可以在子类没有位置的限制,可以在子类中调用父类的方法 类中字段重写:在重写字段前面加一个override就可以重新赋值 类中方 ...

  9. python_基础学习_01_按行读取文件的最优方法

    python 按行读取文件 ,网上搜集有N种方法,效率有区别,先mark最优答案,下次补充测试数据 with open('filename') as file: for line in file: d ...

  10. F4107Usart数据处理程序

    解决:Cortex-M4上,usart自己主动发送数据计划. 1. usart快速突破.数据还没有被处理.usart中断会把盖掉的数据不被处理. 数据丢失. 2.此过程需要main处理4一个usart ...