系列導航地址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方法是,會返回如下數據:
[{ "id": 2, "url": "http://localhost:8323/api/students/HasanAhmad", "firstName": "Hasan", "lastName": "Ahmad", "gender": 0, "enrollmentsCount": 4 }, { "id": 3, "url": "http://localhost:8323/api/students/MoatasemAhmad", "firstName": "Moatasem", "lastName": "Ahmad", "gender": 0, "enrollmentsCount": 4 }]
我們期待訪問“StudentsV2Controller”的Get方法后應該的到:
[{ "id": 2, "url": "http://localhost:8323/api/students/HasanAhmad", "fullName": "Hasan Ahmad", "gender": 0, "enrollmentsCount": 4, "coursesDuration": 13 }, { "id": 3, "url": "http://localhost:8323/api/students/MoatasemAhmad", "fullName": "Moatasem Ahmad", "gender": 0, "enrollmentsCount": 4, "coursesDuration": 16 }]
ok,下面來實現,復制粘貼”StudnetsController”並重命名為“StudnetsV2Controller”,更改Get方法的實現:
public IEnumerable<StudentV2BaseModel> Get(int page = 0, int pageSize = 10) { IQueryable<Student> query; query = TheRepository.GetAllStudentsWithEnrollments().OrderBy(c => c.LastName); var totalCount = query.Count(); var totalPages = Math.Ceiling((double)totalCount / pageSize); var urlHelper = new UrlHelper(Request); var prevLink = page > 0 ? urlHelper.Link("Students", new { page = page - 1, pageSize = pageSize }) : ""; var nextLink = page < totalPages - 1 ? urlHelper.Link("Students", new { page = page + 1, pageSize = pageSize }) : ""; var paginationHeader = new { TotalCount = totalCount, TotalPages = totalPages, PrevPageLink = prevLink, NextPageLink = nextLink }; System.Web.HttpContext.Current.Response.Headers.Add("X-Pagination", Newtonsoft.Json.JsonConvert.SerializeObject(paginationHeader)); var results = query .Skip(pageSize * page) .Take(pageSize) .ToList() .Select(s => TheModelFactory.CreateV2Summary(s)); return results; }
可以看到,這里我們改的很少,返回的類型變成了“StudentV2BaseModel”,而這個類型是由ModelFactory的CreateV2Summary方法創建的。因此我們需要添加StudentV2BaseModel類和CreateV2Summary方法:
public class StudentV2BaseModel { public int Id { get; set; } public string Url { get; set; } public string FullName { get; set; } public Data.Enums.Gender Gender { get; set; } public int EnrollmentsCount { get; set; } public double CoursesDuration { get; set; } } public class ModelFactory { public StudentV2BaseModel CreateV2Summary(Student student) { return new StudentV2BaseModel() { Url = _UrlHelper.Link("Students", new { userName = student.UserName }), Id = student.Id, FullName = string.Format("{0} {1}", student.FirstName, student.LastName), Gender = student.Gender, EnrollmentsCount = student.Enrollments.Count(), CoursesDuration = Math.Round(student.Enrollments.Sum(c => c.Course.Duration)) }; } }
到目前為止,我們的准備工作就算做完了,下面介紹四種方式實現版本變化
使用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條路由:
config.Routes.MapHttpRoute( name: "Students", routeTemplate: "api/v1/students/{userName}", defaults: new { controller = "students", userName = RouteParameter.Optional } ); config.Routes.MapHttpRoute( name: "Students2", routeTemplate: "api/v2/students/{userName}", defaults: new { controller = "studentsV2", userName = RouteParameter.Optional } );
在上面代碼中,我們添加了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()”方法,具體代碼如下:
public class LearningControllerSelector : DefaultHttpControllerSelector { private HttpConfiguration _config; public LearningControllerSelector(HttpConfiguration config) : base(config) { _config = config; } public override HttpControllerDescriptor SelectController(HttpRequestMessage request) { var controllers = GetControllerMapping(); //Will ignore any controls in same name even if they are in different namepsace var routeData = request.GetRouteData(); var controllerName = routeData.Values["controller"].ToString(); HttpControllerDescriptor controllerDescriptor; if (controllers.TryGetValue(controllerName, out controllerDescriptor)) { var version = "2"; var versionedControllerName = string.Concat(controllerName, "V", version); HttpControllerDescriptor versionedControllerDescriptor; if (controllers.TryGetValue(versionedControllerName, out versionedControllerDescriptor)) { return versionedControllerDescriptor; } return controllerDescriptor; } return null; } }
上述代碼主要意思如下:
1.調用父類方法GetControllerMapping()獲取所有實現了ApiController的類。
2.通過request對象獲取routeData ,然后進一步獲得Controller的name
3.根據我們剛剛得到的Controller,名字創建“HttpControllerDescriptor”對象,這個對象包含了描述Controller的信息
.4.接着,在我們找到的Controller的名字后面加上“V”和版本號,重復上面步驟即可。關於如何獲得版本號,我們一會兒討論,這里暫時寫死成“2”。
為了使我們自定義的“Controller Selector”生效,因此需要在“WebApiConfig”中做如下配置:
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參數,並從這個請求對象中獲取客戶端所需要的版本:
private string GetVersionFromQueryString(HttpRequestMessage request) { var query = HttpUtility.ParseQueryString(request.RequestUri.Query); var version = query["v"]; if (version != null) { return version; } return "1"; }
我們只需要在SelectController方法中調用這個方法即可,唯一的缺點依然是URI會變,不符合REST規范。
通過自定義請求頭設置版本
現在我們使用另一種方式來發生版本號——自定義請求頭,它不是URI的一部分,添加一個頭“X-Learning-Version”並把版本號設置在里面,當客戶端沒有這條頭信息是我們可以認為它需要V1版本。
實現這個技術,我們在“LearningControllerSelector”中添加一個“GetVersionFromHeader”方法,代碼如下:
private string GetVersionFromHeader(HttpRequestMessage request) { const string HEADER_NAME = "X-Learning-Version"; if (request.Headers.Contains(HEADER_NAME)) { var versionHeader = request.Headers.GetValues(HEADER_NAME).FirstOrDefault(); if (versionHeader != null) { return versionHeader; } } return "1"; }
這里做法很簡單,我們先確定好請求頭的名字,然后去request的Header中找,如果有數據,就獲得。
客戶端發送的請求如下:
這么做也有缺點,就是添加了一個請求頭(注:這個缺點不是很理解),下面介紹第四種方式
使用Accept Header設置版本
這種方法是直接使用Accept Header, 請求的時候將它設置為“Accept:application/json; version=2”,我們依舊這么認為:如果客戶端不提供版本號,我們就給他V1的數據。
在“LearningControllerSelector”類中添加“GetVersionFromAcceptHeaderVersion”方法,具體實現如下:
private string GetVersionFromAcceptHeaderVersion(HttpRequestMessage request) { var acceptHeader = request.Headers.Accept; foreach (var mime in acceptHeader) { if (mime.MediaType == "application/json") { var version = mime.Parameters .Where(v => v.Name.Equals("version", StringComparison.OrdinalIgnoreCase)).FirstOrDefault(); if (version != null) { return version.Value; } return "1"; } } return "1"; }
這個實現看上去比上面更標准,更專業了。