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

简介: 原文:使用ASP.NET Web Api构建基于REST风格的服务实战系列教程【九】——API变了,客户端怎么办?系列导航地址http://www.cnblogs.com/fzrain/p/3490137.html 前言 一旦我们将API发布之后,消费者就会开始使用并和其他的一些数据混在一起。
原文: 使用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方法是,会返回如下数据:

[{
    "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中找,如果有数据,就获得。

客户端发送的请求如下:

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

目录
相关文章
|
9月前
|
中间件 Go
Golang | Gin:net/http与Gin启动web服务的简单比较
总的来说,`net/http`和 `Gin`都是优秀的库,它们各有优缺点。你应该根据你的需求和经验来选择最适合你的工具。希望这个比较可以帮助你做出决策。
473 35
|
7月前
|
存储 缓存
.NET 6中Startup.cs文件注入本地缓存策略与服务生命周期管理实践:AddTransient, AddScoped, AddSingleton。
记住,选择正确的服务生命周期并妥善管理它们是至关重要的,因为它们直接影响你的应用程序的性能和行为。就像一个成功的建筑工地,工具箱如果整理得当,工具选择和使用得当,工地的整体效率将会大大提高。
291 0
|
运维 前端开发 C#
一套以用户体验出发的.NET8 Web开源框架
一套以用户体验出发的.NET8 Web开源框架
344 7
一套以用户体验出发的.NET8 Web开源框架
|
开发框架 数据可视化 .NET
.NET 中管理 Web API 文档的两种方式
.NET 中管理 Web API 文档的两种方式
241 14
|
JavaScript 前端开发 API
Hello.js – Web 服务授权的 JavaScript SDK
  Hello.js 是一个客户端的 Javascript SDK,用于实现 OAuth2 认证(或者基于 OAuth 代理实现的 OAuth1)的 Web 服务和查询 REST API。 HelloJS 标准化路径和对通用的 API 响应 ,如谷歌的数据服务, Facebook 的图形和 Windows Live 连接。
1201 0
|
3月前
|
算法 Java Go
【GoGin】(1)上手Go Gin 基于Go语言开发的Web框架,本文介绍了各种路由的配置信息;包含各场景下请求参数的基本传入接收
gin 框架中采用的路优酷是基于httprouter做的是一个高性能的 HTTP 请求路由器,适用于 Go 语言。它的设计目标是提供高效的路由匹配和低内存占用,特别适合需要高性能和简单路由的应用场景。
322 4
|
7月前
|
缓存 JavaScript 前端开发
鸿蒙5开发宝藏案例分享---Web开发优化案例分享
本文深入解读鸿蒙官方文档中的 `ArkWeb` 性能优化技巧,从预启动进程到预渲染,涵盖预下载、预连接、预取POST等八大优化策略。通过代码示例详解如何提升Web页面加载速度,助你打造流畅的HarmonyOS应用体验。内容实用,按需选用,让H5页面快到飞起!
|
7月前
|
JavaScript 前端开发 API
鸿蒙5开发宝藏案例分享---Web加载时延优化解析
本文深入解析了鸿蒙开发中Web加载完成时延的优化技巧,结合官方案例与实际代码,助你提升性能。核心内容包括:使用DevEco Profiler和DevTools定位瓶颈、四大优化方向(资源合并、接口预取、图片懒加载、任务拆解)及高频手段总结。同时提供性能优化黄金准则,如首屏资源控制在300KB内、关键接口响应≤200ms等,帮助开发者实现丝般流畅体验。
|
前端开发 JavaScript Shell
鸿蒙5开发宝藏案例分享---Web页面内点击响应时延分析
本文为鸿蒙开发者整理了Web性能优化的实战案例解析,结合官方文档深度扩展。内容涵盖点击响应时延核心指标(≤100ms)、性能分析工具链(如DevTools时间线、ArkUI Trace抓取)以及高频优化场景,包括递归函数优化、网络请求阻塞解决方案和setTimeout滥用问题等。同时提供进阶技巧,如首帧加速、透明动画陷阱规避及Web组件初始化加速,并通过优化前后Trace对比展示成果。最后总结了快速定位问题的方法与开发建议,助力开发者提升Web应用性能。
|
7月前
|
JSON 开发框架 自然语言处理
【HarmonyOS Next之旅】基于ArkTS开发(三) -> 兼容JS的类Web开发(三)
本文主要介绍了应用开发中的三大核心内容:生命周期管理、资源限定与访问以及多语言支持。在生命周期部分,详细说明了应用和页面的生命周期函数及其触发时机,帮助开发者更好地掌控应用状态变化。资源限定与访问章节,则聚焦于资源限定词的定义、命名规则及匹配逻辑,并阐述了如何通过 `$r` 引用 JS 模块内的资源。最后,多语言支持部分讲解了如何通过 JSON 文件定义多语言资源,使用 `$t` 和 `$tc` 方法实现简单格式化与单复数格式化,为全球化应用提供便利。
299 104