前言

作为开发人员,我们经常向应用程序添加新功能并修改当前的 Api。版本控制使我们能够安全地添加新功能而不会造成中断性变更。一个良好的 Api 版本控制策略可以清晰地传达所做的更改,并允许使用现有 REST Api 的客户端在准备好时才迁移或更新他们的应用程序到最新版本。

哪些行为可能会造成 Api 的中断性变更呢?

  • 删除或重命名 Api
  • 修改 Api 参数(类型,名称,可选参数变成非可选参数,删除必需参数等)
  • 更改现有 Api 的行为
  • 更改 Api 响应
  • 更改 Api 错误代码
  • More

我们在做开发的过程中迟早会面对 Api 版本控制需求,在 Api 开发的过程中学习如何进行版本控制是至关重要的。

本文主要介绍在 MinimalApis 进行版本控制,官网文档在文末

借助aspnet-api-versioning 帮助 Minimalapis 实现版本控制

开始之前在项目中安装两个 nuget 包:

Install-Package Asp.Versioning.Http
Install-Package Asp.Versioning.Mvc.ApiExplorer
  • Asp.Versioning.Http 用于在 MinimalApis 中提供版本控制支持
  • Asp.Versioning.Mvc.ApiExplorer 用于 OpenApi,格式化路由版本参数等

配置详情

builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(2, 0);//默认版本
options.ReportApiVersions = true;//Response Header 指定可用版本
options.AssumeDefaultVersionWhenUnspecified = true;//如果没有指定版本用默认配置
options.ApiVersionReader = ApiVersionReader.Combine(
new QueryStringApiVersionReader("api-version"),//QueryString
new HeaderApiVersionReader("X-Version"),//Header
new MediaTypeApiVersionReader("ver"),//Accept MediaType
new UrlSegmentApiVersionReader());//Route Path
}).AddApiExplorer(options =>
{
options.GroupNameFormat = "'v'VVV";
options.SubstituteApiVersionInUrl = true;
});

AddApiVersioning 提供了一个委托参数 Action<ApiVersioningOptions>来对 Api 版本控制配置,下面看主要参数的配置解释

  • DefaultApiVersion

    options.DefaultApiVersion = new ApiVersion(2,0);

    指定 Api 的默认版本以上设置为 2.0 版本,默认是 1.0

  • ReportApiVersions

     options.ReportApiVersions = true;//Response Header 指定可用版本

    在 ResponseHeader 中指定当前 Api 可用的版本 默认不开启

  • AssumeDefaultVersionWhenUnspecified

    options.AssumeDefaultVersionWhenUnspecified = true;

    开启后 如果 Api 不指定版本默认 DefaultApiVersion 设置版本,适合已经存在的服务开启版本控制,帮助在不破坏现有客户端的情况下改进现有服务并集成正式的 Api

  • ApiVersionReader


    options.ApiVersionReader = ApiVersionReader.Combine(
    new QueryStringApiVersionReader("api-version"),//QueryString 默认参数 api-vesion
    new HeaderApiVersionReader("X-Version"),//Header 默认v
    new MediaTypeApiVersionReader("ver"),//Accept MediaType 默认参数v
    new UrlSegmentApiVersionReader());//Route Path 参数v

    配置如何读取客户端指定的 Api 版本,默认为 QueryStringApiVersionReader 即使用名为 api-version 的查询字符串参数。

    从上面可以看出有四种开箱即用的 Api 服务版本定义方式

    • QueryStringApiVersionReader: https://localhost:7196/api/Todo?api-version=1
    • HeaderApiVersionReader: https://localhost:7196/api/Todo -H 'X-Version: 1'
    • MediaTypeApiVersionReader:
      GET api/helloworld HTTP/2
      Host: localhost
      Accept: application/json;ver=1.0
    • UrlSegmentApiVersionReader: https://localhost:7196/api/workouts?api-version=1

    可以通过ApiVersionReader.Combine联合使用。

虽然aspnet-api-versioning提供了多种版本控制的方式,但是在我们实际项目开发的过程中,我们尽可能只采用一种方案,只用一种标准可以让我们版本开发更加的容易维护,而且多种方案配置默认策略 对 OpenApi 的集成和版本控制的默认行为都互有影响。

以上四种方案只有QueryStringApiVersionReaderUrlSegmentApiVersionReader符合 Microsoft REST Guidelines 的规范,所以我们只需要上面选一个即可.

MinimalApis 版本控制

我们采用其中的一种 来做演示看看 ApiVesioning 是如何实现的,就按默认行为 QueryStringApiVersionReader 来做一个简单的 Demo。

创建一个 MinimalApi 的项目

VS 创建新项目->输入项目名字然后点击下一步-> 使用控制器的 CheckBox 确定取消勾选

.Net Cli 安装 nuget 或者 VS 包管理器

dotnet add package Asp.Versioning.Http
dotnet add package Asp.Versioning.Mvc.ApiExplorer

Program.cs 添加默认配置

builder.Services.AddProblemDetails();
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(2, 0);//默认版本
options.ReportApiVersions = true;//Response Header 指定可用版本
options.AssumeDefaultVersionWhenUnspecified = true;//如果没有指定版本用默认配置
}).AddApiExplorer(options =>
{
options.GroupNameFormat = "'v'VVV";
options.SubstituteApiVersionInUrl = true;
});

aspnet-api-versioning的异常处理机制依赖ProblemDetails,

所以builder.Services.AddProblemDetails();必须要注册到 IOC 容器。

AddApiVersioning 没有注册任何的ApiVersionReader,所以会用默认的QueryStringApiVersionReader的模式。

AddApiExplorerOpenApi 对接口格式化的策略配置

认识 几个核心方法

  • NewVersionedApi :创建一个路由组建造者,用于定义 Api 中所有版本化端点。
  • HasApiVersion :表示 ApiVersionSet 支持指定的 ApiVersion。

  • HasDeprecatedApiVersion :配置废弃指定的 Api 版本。

  • MapToApiVersion : 将指定的 Api 版本映射到配置的端点。

  • IsApiVersionNeutral : 版本无关 也可以说任何的版本都可以访问到这个终结点

添加 Api EndPoint


{
var todoV1 = app.NewVersionedApi("Todo")
.HasDeprecatedApiVersion(new ApiVersion(1, 0));//过期版本
var todoGroup = todoV1.MapGroup("/api/Todo");
todoGroup.MapGet("/", () => "Version 1.0").WithSummary("请用V2版本代替");
todoGroup.MapGet("sayhello", (string name) => $"hello {name}").
}
{
var todoV2 = app.NewVersionedApi("Todo")
.HasApiVersion(new ApiVersion(2, 0));
var todoGroup = todoV2.MapGroup("/api/Todo");
todoGroup.MapGet("/", () => "Version 2.0").MapToApiVersion(new ApiVersion(2, 0)).WithSummary("Version2");
}
{
var todoV3 = app.NewVersionedApi("Todo")
.HasApiVersion(new ApiVersion(3, 0));
var todoGroup = todoV3.MapGroup("/api/Todo");
todoGroup.MapGet("/", () => "Version 3.0").WithSummary("Version3");
IsApiVersionNeutral();
}

上面定义 Todo 的相关业务,当前有三个版本,V1 已经过期不推荐使用,V2 是主要版本,V3 是预览开发版本,IsApiVersionNeutral标注了一个sayHello接口是跟版本无关的

Run 项目 测试一下


访问 api/Todo,Options 配置了默认版本为 2.0

https://localhost:7141/api/todo 返回 Version 1.0 符合预期


测试 V1 版本

https://localhost:7141/api/todo?api-version=1.0

返回 Version 1.0 符合预期且 ResponseHeader 标记了过期版本和受支持的版本


测试 V2 版本

https://localhost:7141/api/todo?api-version=2.0

可以看到 返回 Version 2.0 符合预期


测试 V3 版本

https://localhost:7141/api/todo?api-version=3.0

可以看到 返回 Version 3.0 符合预期


测试 sayHello (版本无关)

  • https://localhost:7141/api/Todo/sayhello
  • https://localhost:7141/api/Todo/sayhello?name=ruipeng& api-vesion=1.0
  • https://localhost:7141/api/Todo/sayhello?name=ruipeng&api-vesion=2.0
  • https://localhost:7141/api/Todo/sayhello?name=ruipeng&api-vesion=3.0


到这儿基本可以实现我们的需求了,在aspnet-api-versioning中还提供了NewApiVersionSet的方法配置添加实现 Api 的管理,大家也可以尝试下。

版本管理对接 OpenApi

刚才我们的项目 Run 起来之后 Swagger 首页看到只有 V1 版本的界面,我们来设置一下让他支持 Swagger 界面版本切换

创建 ConfigureSwaggerOptions 添加多个 SwaggerDoc

public class ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider) : IConfigureOptions<SwaggerGenOptions>
{
public void Configure(SwaggerGenOptions options)
{
foreach (var description in provider.ApiVersionDescriptions)
{
options.SwaggerDoc(description.GroupName, CreateInfoForApiVersion(description));
}
} private static OpenApiInfo CreateInfoForApiVersion(ApiVersionDescription description)
{
var text = new StringBuilder("An example application with OpenAPI, Swashbuckle, and API versioning.");
var info = new OpenApiInfo()
{
Title = "MinimalApis With OpenApi ",
Version = description.ApiVersion.ToString(),
Contact = new OpenApiContact() { Name = "Ruipeng", Email = "478083649@qq.com" },
License = new OpenApiLicense() { Name = "MIT", Url = new Uri("https://opensource.org/licenses/MIT") }
}; if (description.IsDeprecated)
{
text.Append(" This API version has been deprecated.");
} if (description.SunsetPolicy is SunsetPolicy policy)
{
if (policy.Date is DateTimeOffset when)
{
text.Append(" The API will be sunset on ")
.Append(when.Date.ToShortDateString())
.Append('.');
} if (policy.HasLinks)
{
text.AppendLine(); for (var i = 0; i < policy.Links.Count; i++)
{
var link = policy.Links[i]; if (link.Type == "text/html")
{
text.AppendLine(); if (link.Title.HasValue)
{
text.Append(link.Title.Value).Append(": ");
} text.Append(link.LinkTarget.OriginalString);
}
}
}
} info.Description = text.ToString(); return info;
}
}

依赖注入

builder.Services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();

创建拦截器

public class SwaggerDefaultValues : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var apiDescription = context.ApiDescription; operation.Deprecated |= apiDescription.IsDeprecated(); foreach (var responseType in context.ApiDescription.SupportedResponseTypes)
{
var responseKey = responseType.IsDefaultResponse ? "default" : responseType.StatusCode.ToString();
var response = operation.Responses[responseKey]; foreach (var contentType in response.Content.Keys)
{
if (!responseType.ApiResponseFormats.Any(x => x.MediaType == contentType))
{
response.Content.Remove(contentType);
}
}
} if (operation.Parameters is null)
{
return;
} foreach (var parameter in operation.Parameters)
{
var description = apiDescription.ParameterDescriptions.First(p => p.Name == parameter.Name); parameter.Description ??= description.ModelMetadata?.Description; if (parameter.Schema.Default is null &&
description.DefaultValue is not null &&
description.DefaultValue is not DBNull &&
description.ModelMetadata is ModelMetadata modelMetadata)
{ var json = JsonSerializer.Serialize(description.DefaultValue, modelMetadata.ModelType);
parameter.Schema.Default = OpenApiAnyFactory.CreateFromJson(json);
} parameter.Required |= description.IsRequired;
}
}
}

Swagger 依赖注入

builder.Services.AddSwaggerGen(options => options.OperationFilter<SwaggerDefaultValues>());

UseSwaggerUI 添加 Swagger 终结点

    app.UseSwaggerUI(options =>
{
var descriptions = app.DescribeApiVersions();
// build a swagger endpoint for each discovered API version
foreach (var description in descriptions)
{
var url = $"/swagger/{description.GroupName}/swagger.json";
var name = description.GroupName.ToUpperInvariant();
options.SwaggerEndpoint(url, name);
}
});

Run Swagger 查看项目

左上角可以成功切换版本,OpenApi 版本管理成功

最后

本文的 demo 用了aspnet-api-versioning版本控制的一种方式来做的演示,WebApi Controller 配置好 Options 之后只需要用aspnet-api-versioning提供的 Attribute 就可以实现版本管理,Route PathhttpHeader 等传参数的方式只需要微调就可以实现,更多高级功能请浏览aspnet-api-versioning官网(文末有官网地址)。

Api 版本控制是设计现代 Api 的最佳实践之一。从第一个版本开始实现 Api 版本控制,这样可以更容易地让客户端支持未来的 Api 版本,同时也让您的团队习惯于管理破坏性变化和对 Api 进行版本控制。

以下是本文的完整 源代码

aspnet-api-versioning 官网学习文档

.Net接口版本管理与OpenApi的更多相关文章

  1. 使用Swagger和OpenAPI 3规范定义API接口并集成到SpringBoot

    1. OpenAPI 3 规范介绍及属性定义 参考官方定义:https://swagger.io/specification/ 2. 使用OpenAPI 3规范定义API接口 官方样例参考:https ...

  2. 【API】获取优酷视频信息接口

    序:        自己的网站中需要接入一个视频模块,虚拟主机的空间小所以只能引用第三方的链接.感觉国内优酷好不错,所以查了一下优酷的接口. 0x00:        先去优酷API开放中心申请一个开 ...

  3. 公交线路免费api接口代码

    描写叙述:本接口主要是依据城市名称 +  线路名称 模糊查找城市公交线路信息. 开源api接口:http://openapi.aibang.com/bus/lines?app_key=keyvalue ...

  4. 详解功能版本管理之使用eoLinker

    先看一个对话: "这里,你改一下,这里返回一个object." "好...好......" "还有这里,返回个String." ...... ...

  5. 图灵机器人API接口

    调用图灵API接口实现人机交互 流程一: 注册 图灵机器人官网: http://www.tuling123.com/ 第一步: 先注册, 然后创建机器人, 拿到一个32位的key 编码方式 UTF-8 ...

  6. REST接口设计规范

    URI格式规范 URI(Uniform Resource Identifiers) 统一资源标示符 URL(Uniform Resource Locator) 统一资源定位符 URI的格式定义如下: ...

  7. 开源的api文档管理系统

    api文档 php 在项目中,需要协同开发,所以会写许多API文档给其他同事,以前都是写一个简单的TXT文本或Word文档,口口相传,这种方式比较老土了,所以,需要有个api管理系统专门来管理这些ap ...

  8. AI 基础

    what AI ? 人工智能(Artificial Intelligence),英文缩写为AI. 人工智能是计算机科学的一个分支,它企图了解智能的实质,并生产出一种新的能以人类智能相似的方式做出反应的 ...

  9. 利用python itchat给女朋友定时发信息

    利用itchat给女朋友定时发信息 涉及到的技术有itchat,redis,mysql,最主要的还是mysql咯,当然咯,这么多东西,我就只介绍我代码需要用到的,其他的,如果需要了解的话,就需要看参考 ...

  10. python 全栈开发,Day123(图灵机器人,web录音实现自动化交互问答)

    昨日内容回顾 . 百度ai开放平台 . AipSpeech技术,语言合成,语言识别 . Nlp技术,短文本相似度 . 实现一个简单的问答机器人 . 语言识别 ffmpeg (目前所有音乐,视频领域,这 ...

随机推荐

  1. PE格式:新建节并插入代码

    经过了前一章的学习相信你已经能够独立完成FOA与VA之间的互转了,接下来我们将实现在程序中插入新节区,并向新节区内插入一段能够反向连接的ShellCode代码,并保证插入后门的程序依旧能够正常运行不被 ...

  2. 不同网段之间实现GDB远程调试功能

    前言 在开发过程中,使用gdb远程调试时,会碰到 Linux 服务器的网段和板载设备的网段不是一样的,不能正常使用 gbd 远程调试功能. 板载设备和电脑连接路由器,属于同一个网段,如192.168. ...

  3. 将实体光盘制作成光盘映像iso文件

    春节假期整理历史物件时发现一些书籍的光盘,虽然买了多年但一直没有看过,因为自己在用的电脑都没有光驱.正好老爸的电脑是带光驱的,想着趁过节把这些光盘的内容读取出来存在NAS上方便后续使用. 使用Ultr ...

  4. 2024年,我又开始用Linux桌面作为主力系统了~

    前言 19年的时候我买了一个 matebook14 笔记本,配置是8代i5和8G内存,在当时看来作为轻薄本是够用的,但是现在已经2024年了,这内存让我想起来去年苹果的新款 mac mini ,丐版三 ...

  5. 浅谈 2-SAT

    SAT 是适定性(Satisfiability)问题的简称.一般形式为 k - 适定性问题,简称 k-SAT.而当 \(k>2\) 时该问题为 NP 完全的.所以我们只研究 \(k=2\) 的情 ...

  6. JS 这一次彻底理解插入排序

    壹 ❀ 引 在前两篇排序文章中,我们分别介绍了冒泡排序与选择排序,趁热打铁,我们接着聊插入排序.老实说,在分析排序过程中头脑很清楚,过后再尝试写出排序代码还有点坎坷...可能是我脑瓜子不太机灵的问题, ...

  7. 对称加密算法汇总:AES DES 3DES SM4 java 实现入门

    密码的世界 如果你是黑帮老大,平时和手下沟通,如何保证自己的信息安全呢? 在神探夏洛克的第一季中,就讲述了一个如何侦破黑帮的加密交流的故事. 这种密码利用的是密码字典. 密码本身可以是一本书,比如常见 ...

  8. Atom N2600, N2800 安装 Ubuntu22.04 卡住的问题处理

    问题描述 Atom N2600, N2800 的某些旧型号机器, 安装 Ubuntu 时在安装界面选择安装后, 启动过程中会卡住, 或者数秒即黑屏, 再无反应. 这个问题对于Debian系的其他发行版 ...

  9. db2伪表sysibm.sysdummy1

    $ db2 describe table sysibm.sysdummy1 Column                         Type      Type name             ...

  10. Redhat6更改yum源

    最近虚拟机中安装了redhat6.3企业版,自带的yum用不起来,软件都找不到. 网上搜了一下说是没付钱...,需要改下yum源.操作步骤如下: 1.切换到yum源存放目录 [root@rhel6 ~ ...