[ASP.NET Core 6] 讓你的 ASP.NET Core Web API + Swashbuckle.AspNetCore 支援多個版本

一旦 Web API 部署並開始使用後,它應該是可靠的,並且不應該因任何原因而中斷。另一方面,隨著需求的變化,我們需要更新 Web API 代碼,但這應該不破壞目前 API 的情況下完成,因此新舊版本的 Web  API 都將處於活動狀態,功能也要正常。這時候就要靠 Web  API 版本控制,我們靠它用於處理不同版本的Web  API。微軟的   Microsoft.AspNetCore.Mvc.Versioning 可以讓我們輕易的完成此項目,但我在整合到 Swagger UI / Swashbuckle.AspNetCore 的時候碰到了一些關卡,所幸順利的解決了,以下是我的實作筆記。

 

開發環境

  • Windows 11
  • .NET 6
  • Rider 2021.3.3
  • Swashbuckle.AspNetCore 6.2.3
  • Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer  5.0.0

安裝及設定

安裝套件

dotnet add package Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer --version 5.0.0

這套件依賴 Microsoft.AspNetCore.Mvc.Versioning

 

Step1. 在 DI Container 註冊 Api Version相關資訊

  • 返回響應標頭中支援的版本資訊:option.ReportApiVersions = true
  • 未提供版本請請時,使用預設版號:option.AssumeDefaultVersionWhenUnspecified = true
  • 預設api版本號,支援時間或數字版本號:option.DefaultApiVersion = new ApiVersion(1, 0)
  • 設定版本號方式,使用 ApiVersionReader.Combine 合併多種方式,目前支援以下幾種方式選擇 API 版本
    MediaTypeApiVersionReader("api-version")
    HeaderApiVersionReader("api-version")
    QueryStringApiVersionReader("api-version")
    UrlSegmentApiVersionReader()

代碼如下:

builder.Services.AddApiVersioning(option =>
{
    //返回響應標頭中支援的版本資訊
    option.ReportApiVersions = true;

    //未提供版本請請時,使用預設版號
    option.AssumeDefaultVersionWhenUnspecified = true;

    //預設api版本號,支援時間或數字版本號 
    option.DefaultApiVersion = new ApiVersion(1, 0);

    //支援MediaType、Header、QueryString 設定版本號;預設為 QueryString、UrlSegment
    option.ApiVersionReader = ApiVersionReader.Combine(
        new MediaTypeApiVersionReader("api-version"),
        new HeaderApiVersionReader("api-version"),
        new QueryStringApiVersionReader("api-version"),
        new UrlSegmentApiVersionReader());
});

 

Step2. 使用 Api Version Middleware

app.UseApiVersioning();

 

Step3. 設定  Controller 

我需要兩個版本,分別是 1.0、1.1,不同 namespace,相同的 class name,代碼如下

namespace Lab.Swashbuckle.AspNetCore6.Controllers.Employee.v1_0;

[ApiVersion("1.0", Deprecated = true)]
[ApiController]
[Route("api/[controller]")]
public class DemoController : ControllerBase
{
    [HttpGet]
    public IActionResult Get()
    {
        return this.Ok(new
        {
            Version = 1.0,
            Name = "1.0"
        });
    }
}
namespace Lab.Swashbuckle.AspNetCore6.Controllers.Employee.v1_1;

[ApiVersion("1.1")]
[ApiController]
[Route("api/[controller]")]
public class DemoController : ControllerBase
{
    [HttpGet]
    public IActionResult Get()
    {
        return this.Ok(new
        {
            Version = 1.1,
            Name = "1.1"
        });
    }
}

目錄結構如下:

選擇版本

QueryStringApiVersionReader

builder.Services.AddApiVersioning(option =>
{
    option.ApiVersionReader = new QueryStringApiVersionReader("api-version");
});

不帶版本的請求,回傳了預設版本,這是我在 DI Container 的設定 option.AssumeDefaultVersionWhenUnspecified = true;

 

假使設定option.AssumeDefaultVersionWhenUnspecified = false則必須要帶入版號,否則將不允許使用

增加版本的 query string,?api-version=1.1 就可以訪問了

https://localhost:7236/api/Demo

 

可以看得出來版本的資訊

 

HeaderApiVersionReader

續上一個 Controller 代碼,這次在 DI 設定 Header 增加 api-version

builder.Services.AddApiVersioning(option =>
{
    option.ApiVersionReader = new HeaderApiVersionReader("api-version");
});

 

執行結果如下:

 

MediaTypeApiVersionReader

續上一個 Controller 代碼,這次在 DI 設定 MediaType 增加 api-version

builder.Services.AddApiVersioning(option =>
{
    option.ApiVersionReader = new MediaTypeApiVersionReader("api-version");
});

 

UrlSegmentApiVersionReader

DI Container 設定如下:

builder.Services.AddApiVersioning(option =>
{
    option.ApiVersionReader = new MediaTypeApiVersionReader("api-version");
})

 

Controller 改一下,讓版號成為 URL [Route("api/v{version:apiVersion}/[controller]")]

[ApiVersion("1.0", Deprecated = true)]
[ApiController]
[Route("api/[controller]")]
[Route("api/v{version:apiVersion}/[controller]")]
public class DemoController : ControllerBase
{
...
}
[ApiVersion("1.1")]
[ApiController]
[Route("api/[controller]")]
[Route("api/v{version:apiVersion}/[controller]")]
public class DemoController : ControllerBase
{
...
}

執行結果如下

MapToApiVersion

允許將單個 API 操作對映到任何版本,下面的例子是在 1.1 增加一個 2.0

[ApiVersion("1.1")]
[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
public class DemoController : ControllerBase
{
    public IActionResult Get()
    {
        return this.Ok(new
        {
            Version = 1.1,
            Name = "1.1"
        });
    }

    [HttpGet, MapToApiVersion("2.0")]
    public IActionResult GetV2()
    {
        return this.Ok(new
        {
            Version = 2.0,
            Name = "2.0"
        });
    }
}

 

執行效果如下:

這樣設定我還不知道要怎麼在 Swagger UI 呈現,不過端點還是可以運作的

 

整合 Swagger UI for Swashbuckle.AspNetCore

到目前為止 Swagger 應該還跑不起來,DI Container 還要增加AddVersionedApiExplorer的設定

Step1. 設定 AddVersionedApiExplorer

builder.Services.AddVersionedApiExplorer(options =>
{
    // add the versioned api explorer, which also adds IApiVersionDescriptionProvider service
    // note: the specified format code will format the version as "'v'major[.minor][-status]"
    options.GroupNameFormat = "'v'VVV";

    // note: this option is only necessary when versioning by url segment. the SubstitutionFormat
    // can also be used to control the format of the API version in route templates
    options.SubstituteApiVersionInUrl = true;
});

 

Step2. 設定 Swagger Doc 文件資訊 

builder.Services.AddSingleton<IConfigureOptions<SwaggerGenOptions>, ConfigureApiVersionSwaggerGenOptions>();

由於 Route 已經帶有版號,我們得讓 SwaggerDoc 的 Name 跟 SwaggerEndpoint 的 Name 對應起來

  • IApiVersionDescriptionProvider.ApiVersionDescriptions:端點的版本號
  • options.SwaggerDoc(description.GroupName, CreateInfoForApiVersion(description)):API 文件資訊
public class ConfigureApiVersionSwaggerGenOptions : IConfigureOptions<SwaggerGenOptions>
{
    private readonly IApiVersionDescriptionProvider _provider;

    public ConfigureApiVersionSwaggerGenOptions(IApiVersionDescriptionProvider provider)
    {
        _provider = provider;
    }

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

    private static OpenApiInfo CreateInfoForApiVersion(ApiVersionDescription description)
    {
    	//產生 API 資訊
        var info = new OpenApiInfo
        {
            Version = description.ApiVersion.ToString(),
            Title = "Employee API",
            Description =
                @"<p>Sample API with versioning including Swagger.</p><p>Partly taken from <a href=""https://github.com/microsoft/aspnet-api-versioning"">this repository</a>.</p>",
            TermsOfService = new Uri("https://example.com/terms"),
            Contact = new OpenApiContact
            {
                Name = "Example Contact",
                Url = new Uri("https://example.com/contact")
            },
            License = new OpenApiLicense
            {
                Name = "Example License",
                Url = new Uri("https://example.com/license")
            }
        };

        if (description.IsDeprecated)
        {
            info.Description +=
                @"<p><strong><span style=""color:white;background-color:red"">VERSION IS DEPRECATED</span></strong></p>";
        }

        return info;
    }
}

 

Step3. 設定 Swagger Enpoint,從 DI Container 取出 IApiVersionDescriptionProvider,組合出 Swagger Json 的 URL

app.UseSwaggerUI(
options =>
{
 var provider = app.Services.GetService<IApiVersionDescriptionProvider>();
 // build a swagger endpoint for each discovered API version
 foreach (var description in provider.ApiVersionDescriptions)
 {
  var url = $"/swagger/{description.GroupName}/swagger.json";
  options.SwaggerEndpoint(url,
        description.GroupName.ToUpperInvariant());
 }
});

參考資料

api-guidelines/Guidelines.md at master · microsoft/api-guidelines (github.com)

API versioning with ASP.NET Core and Swashbuckle | codingfreaks

Creating .NET Core API with versioning - PureSourceCode

Set up Swagger and API versioning in .NET 5 web API (nwb.one)

若有謬誤,煩請告知,新手發帖請多包涵


Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET

Image result for microsoft+mvp+logo