17

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

 3 years ago
source link: https://mp.weixin.qq.com/s?__biz=MjM5MjQwMDUzMw%3D%3D&%3Bmid=2247484175&%3Bidx=1&%3Bsn=5b4c74abf903b4789640640ced6d1bc6
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

一、前言

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

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

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

二、快速的解决办法

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

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

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

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

我们可以写成:

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

或者

[Route("api/[controller]")]
public class DemoController : ControllerBase
{
    [Route("v1/demo")]
    public ActionResult<T> 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(1, 0);
        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<T> 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<SwaggerGenOptions> 和过滤 IOperationFilter

先看 Startup.cs 里:

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

    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<SwaggerGenOptions>
{
    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

喜欢就来个三连,让更多人因你而受益


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK