Dotnet Core多版本API共存的優雅實現

老王Plus發表於2020-12-23

API升級,新舊版本的API共存,怎麼管理呢?

一、前言

最近,單位APP做了升級,同步的,API也做了升級。

升級過程中,出現了一點問題:API升級後,舊API也需要保留,因為有舊的APP還在使用中。

那麼,API端如何作到多個版本共存呢?

    為防止非授權轉發,這兒給出本文的原文連結:https://www.cnblogs.com/tiger-wang/p/14167625.html

二、快速的解決辦法

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

微信公眾號:老王Plus

掃描二維碼,關注個人公眾號,可以第一時間得到最新的個人文章和內容推送

本文版權歸作者所有,轉載請保留此宣告和原文連結

相關文章