系列導航地址http://www.cnblogs.com/fzrain/p/3490137.htmlhtml
一旦咱們將API發佈以後,消費者就會開始使用並和其餘的一些數據混在一塊兒。然而,當新的需求出現時變化是不可避免的,你也許會慶幸API變了對現有客戶端沒受到影響,可是這種狀況不會一直髮生。git
所以,在具體實現以前仔細考慮一下ASP.NET Web Api的版本策略就變得頗有必要了。在咱們的案例中,需求發生了變化並且咱們經過建立不一樣版本的API來解決變化,同時不影響已經在使用API的客戶端。咱們把新的API版本和舊的API版本一塊兒返回給客戶端,讓它有足夠的時間遷移到最新版本的API,有時候多版本共存也是有可能的。github
實現版本控制的方式有好多,本文主要介紹URI,query string,自定義Header和接收Headerweb
簡單起見,咱們讓「StudentsController」中的Get方法發生變化——在響應報文的body中,咱們用「CoursesDuration」和「FullName」屬性替換原來的「FirstName」和「LastName」屬性。json
最簡單的作法就是建立一個與「StudentsController」同樣的Controller並命名爲「StudentsV2Controller」,咱們將根據不一樣的API版本選擇合適的Controller。在新的Controller中咱們實現上述變化並使用相同的Http方法,同時不作任何介紹api
如今咱們請求「StudentsController」的Get方法是,會返回以下數據:app
[{ "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方法的實現:ide
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方法:url
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中包含版本號是最多見的作法,若是想用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設置版本顧名思義,就是在請求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: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"; }
這個實現看上去比上面更標準,更專業了。