Dotnet Core多版本API共存的优雅实现

dotNET全栈开发

共 12084字,需浏览 25分钟

 ·

2020-12-23 22:39

API升级,新旧版本的API共存,怎么管理呢?

一、前言

最近,单位APP做了升级,同步的,API也做了升级。

升级过程中,出现了一点问题:API升级后,旧API也需要保留,因为有旧的APP还在使用中。

那么,API端如何作到多个版本共存呢?

二、快速的解决办法

API的露出,是在API的Route定义中实现的。看下面的例子:

[Route("api/[controller]")]
public class DemoController : ControllerBase
{
    [Route("demo")]
    public ActionResult  DemoFunc()
    {
    }
}

那我们知道,这个API的终结点是:/api/demo/demo。代码中[controller]是个可替换变量,编译时会替换为当前控制器的名称。

这个Route,里面的参数是个字符串,也就是说是可以随便换的。所以,对于多版本API,有个快速的办法,就是在里面做文章。

我们可以写成:

[Route("api/v1/[controller]")]
public class DemoController : ControllerBase
{
    [Route("demo")]
    public ActionResult  DemoFunc()
    {
    }
}

或者

[Route("api/[controller]")]
public class DemoController : ControllerBase
{
    [Route("v1/demo")]
    public ActionResult  DemoFunc()
    {
    }
}

这样就区分出了版本号。

当然,这样做比较LOW,因为版本号是硬编码在代码中的。而且,这个改动会影响到API的终结点,例如上面两个变化,会让终结点变为:/api/v1/demo/demo/api/demo/v1/demo。如果前端可以方便修改,也算是一个方法。但对于我们APP已经上线运行来说,这个改动无法接受。

三、优雅的解决办法

这个方案,才是今天要说的核心内容。

首先,我们需要从Nuget上引入两个库:

% dotnet add package Microsoft.AspNetCore.Mvc.Versioning
% dotnet add package Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer

这两个库,Versioning用来实现API的版本控制,Versioning.ApiExplorer用来实现元数据的发现工作。

引入完成后,修改Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddApiVersioning(options =>
    {
        options.DefaultApiVersion = new ApiVersion(10);
        options.AssumeDefaultVersionWhenUnspecified = true;
        options.ReportApiVersions = true;
    });

    services.AddVersionedApiExplorer(options =>
    {
        options.GroupNameFormat = "'v'VVV";
        options.SubstituteApiVersionInUrl = true;
    });

    services.AddControllers();
}

就可以了。

这里面用了两个配置:AddApiVersioning,主要用来配置向前兼容,定义了如果没有带版本号的访问,会默认访问v1.0的接口。AddVersionedApiExplorer用来添加API的版本管理,并定义了版本号的格式化方式,以及兼容终结点上带版本号的方式。

到这儿,引入版本管理的工作就完成了。

使用时,就直接在控制器或方法上定义版本号:

[ApiVersion("1")]
[Route("api/[controller]")]
public class DemoController : ControllerBase
{
    [MapToApiVersion("2")]
    [Route("demo")]
    public ActionResult  DemoFunc()
    {
    }
}

这里面,又是两个属性:ApiVersion定义控制器提供哪个版本的API。这个属性可以定义多个。例如,我们控制器里既有v1的API,也有v2的API,我们可以写成:

[ApiVersion("1")]
[ApiVersion("2")]
[Route("api/[controller]")]
public class DemoController : ControllerBase
{
}

MapToApiVersion是API的版本定义,定义我们这个API是哪一个版本。

方法就这么简单。其它的,微软都帮我们做好了。

那,通常我们会用Swagger来做API文档。这个方法如何跟Swagger配合呢?

四、与Swagger的配合

Swagger也来自于Nuget的引用:

% dotnet add package swashbuckle.aspnetcore

引用后,通常我们Startup.cs里的配置是这样的:

public void ConfigureServices(IServiceCollection services)
{
    services.AddSwaggerGen(option =>
    {
        option.SwaggerDoc("v1"new OpenApiInfo { Title = "Demo", Version = "V1" });
    });

    services.AddControllers();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseSwagger();
    app.UseSwaggerUI(option =>
    {
        option.SwaggerEndpoint("/swagger/v1/swagger.json""Demo");
    });

}

API多版本管理与Swagger配合,也有一个快速但比较LOW的方法:

public void ConfigureServices(IServiceCollection services)
{
    services.AddSwaggerGen(option =>
    {
        option.SwaggerDoc("v1"new OpenApiInfo { Title = "Demo", Version = "V1" });
        option.SwaggerDoc("v1"new OpenApiInfo { Title = "Demo", Version = "V2" });

    });

    services.AddControllers();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseSwagger();
    app.UseSwaggerUI(option =>
    {
        option.SwaggerEndpoint("/swagger/v1/swagger.json""Demo V1");
        option.SwaggerEndpoint("/swagger/v2/swagger.json""Demo V2");
    });
}

这个方法也可以快速实现,不过跟上边的情况一样,版本号是硬编码的。

其实,也有另一个比较优雅的方式,就是手动实现IConfigureOptions 和过滤IOperationFilter

先看Startup.cs里:

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient , ConfigureSwaggerOptions>();
    services.AddSwaggerGen(options => options.OperationFilter ());

    services.AddControllers();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseSwagger();
    app.UseSwaggerUI(option =>
    {
        foreach (var description in provider.ApiVersionDescriptions)
        {
            c.SwaggerEndpoint($ "/swagger/{description.GroupName}/swagger.json", description.GroupName.ToUpperInvariant());
        }
    });
}

这里加了两个类,第一个ConfigureSwaggerOptions

internal class ConfigureSwaggerOptions : IConfigureOptions
     
      
{
     private readonly IApiVersionDescriptionProvider _provider;
     public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider) => _provider = provider;

     public void Configure(SwaggerGenOptions options)
    
{
        foreach (var description in _provider.ApiVersionDescriptions)
        {
            options.SwaggerDoc(description.GroupName, CreateInfoForApiVersion(description));
        }
    }

     private OpenApiInfo CreateInfoForApiVersion(ApiVersionDescription description)
    
{
        var info =  new OpenApiInfo()
        {
            Title =  "Demo API",
            Version = description.ApiVersion.ToString(),
        };

         if (description.IsDeprecated)
        {
            info.Description +=  " 方法被弃用.";
        }

         return info;
    }
}

第二个SwaggerDefaultValues

internal class SwaggerDefaultValues : IOperationFilter
{
    public void Apply(OpenApiOperation operation, OperationFilterContext context)
    
{
            var apiDescription = context.ApiDescription;
            operation.Deprecated |= apiDescription.IsDeprecated();

            if (operation.Parameters == null)
            return;

            foreach (var parameter in operation.Parameters)
            {
            var description = apiDescription.ParameterDescriptions.First(p => p.Name == parameter.Name);
            if (parameter.Description == null)
            {
                parameter.Description = description.ModelMetadata?.Description;
            }

            if (parameter.Schema.Default == null && description.DefaultValue != null)
            {
                parameter.Schema.Default = new OpenApiString(description.DefaultValue.ToString());
            }

            parameter.Required |= description.IsRequired;
        }
    }
}

代码不一行行解释了,都是比较简单的。

运行,进入Swagger界面,右上角Select a definition,可以选择我们定义的版本号。

今天的配套代码已上传到Github,位置在:https://github.com/humornif/Demo-Code/tree/master/0035/demo

回复 【关闭】广
回复 【实战】获取20套实战源码
回复 【被删】
回复 【访客】访
回复 【小程序】学获取15套【入门+实战+赚钱】小程序源码
回复 【python】学微获取全套0基础Python知识手册
回复 【2019】获取2019 .NET 开发者峰会资料PPT
回复 【加群】加入dotnet微信交流群

再见,VIP,卧槽又来一个看片神器!


你应该知道.NET Core的8点总结!



浏览 78
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报