• .Net接口版本管理与OpenApi


    前言

    作为开发人员,我们经常向应用程序添加新功能并修改当前的 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来对 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");
        todoGroup.MapGet("sayhello", (string name) => $"hello {name}").IsApiVersionNeutral();
    }
    

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

    Run 项目 测试一下


    访问 api/Todo,Options 配置了默认版本为 2.0
    https://localhost:7141/api/todo 返回 Version 2.0 符合预期
    image


    测试 V1 版本
    https://localhost:7141/api/todo?api-version=1.0
    返回 Version 1.0 符合预期且 ResponseHeader 标记了过期版本和受支持的版本
    image

    image


    测试 V2 版本
    https://localhost:7141/api/todo?api-version=2.0
    可以看到 返回 Version 2.0 符合预期
    image


    测试 V3 版本
    https://localhost:7141/api/todo?api-version=3.0
    可以看到 返回 Version 3.0 符合预期
    image


    测试 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
      image

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

    版本管理对接 OpenApi

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

    创建 ConfigureSwaggerOptions 添加多个 SwaggerDoc

    public class ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider) : IConfigureOptions
    {
        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, 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());
    

    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 查看项目

    image

    左上角可以成功切换版本,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 官网学习文档

  • 相关阅读:
    (动手学习深度学习)第13章 计算机视觉---微调
    java毕业设计采购系统mybatis+源码+调试部署+系统+数据库+lw
    ソイラ / 索伊拉
    jextract的使用
    软考-系统架构师-计算机与网络基础知识-数据库系统基础知识
    从libc-2.27.so[7fd68b298000+1e7000]崩溃回溯程序段错误segfault
    string类(一)
    基于nodejs+vue 中小学课程辅导系统
    区块链中的数据可用性层是什么?
    代码随想录day38|DP动态规划登场|理论基础|509. 斐波那契数|70. 爬楼梯|746. 使用最小花费爬楼梯|Golang
  • 原文地址:https://www.cnblogs.com/ruipeng/p/18072151