使用ASP.NET Web Api構建基於REST風格的服務實戰系列教程【九】——API變了,客戶端怎麼辦?

系列導航地址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

API變了

簡單起見,咱們讓「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控制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中找,若是有數據,就得到。

客戶端發送的請求以下:

image

這麼作也有缺點,就是添加了一個請求頭(注:這個缺點不是很理解),下面介紹第四種方式

使用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";
    }

這個實現看上去比上面更標準,更專業了。

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

相關文章
相關標籤/搜索