在 ASP.Net Core Web Api 新增版本管理

  • 410
  • 0

現在開發上習慣 Api First,久了之後難免會修改原本的 Api 方法或是推出新的版本,這時候如果要維持方法的名稱一樣,透過傳入版號來決定要執行的版本,目前常見的方式是網址會帶版號 (ex: /api/v1/sample) 或是在 Query String 加上版號參數 (ex: /api/sample?v=1.0),而這些要如何實做,找了一下發現到一個方便的套件,可以很快速的完成這一個需求,後面就來簡單介紹一下這個套件的用法。

說明

套件說明

原本套件名稱為 Microsoft.AspNetCore.Mvc.Versioning ,但是作者後來離開微軟 (參考),該套件原本是他自己維護的獨立專案,因此後來就搬到 .NET Foundation 底下,並更改套件名稱 Asp.Versioning.* (根據不同專案類型有不同套件) 繼續維護,舊的套件會停留在 v5,新的目前則到了 v7 了,如果是 .NET 5 以前可以使用舊的套件,之後就建議用新的套件了。

後面我用 .NET 7 的 ASP.Net Core Web Api 專案來介紹用法,因此安裝的套件是 Asp.Versioning.Mvc

dotnet add package Asp.Versioning.Mvc --version 7.0.1

安裝好之後在 Program.cs 新增服務。

builder.Services.AddApiVersioning().AddMvc();

Query String 版號

接下來準備兩個 Controller ,並且設定一樣的 Route。

SampleController

[Route("Sample")]
[ApiVersion(1.0)] // 可不加,預設為 1.0
[ApiController]
public class SampleController : ControllerBase
{
    [HttpGet]
    public IActionResult Get()
    {
        return Ok(new { version = "1.0" });
    }
}

SampleV2Controller

[Route("Sample")]
[ApiVersion(2.0)]
[ApiController]
public class SampleV2Controller : ControllerBase
{
    [HttpGet]
    public IActionResult Get()
    {
        return Ok(new { version = "2.0" });
    }
}

這樣我們就可以透過 Query String 來得到對應的 Api 版本結果。

/sample?api-version=1.0 或 /sample?api-version=2.0

但是也會發現如果不加參數就會找不到結果,這時候就可以加入參數來指定沒有待參數時候會自動連到預設的版本。

builder.Services.AddApiVersioning(options => {
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.DefaultApiVersion = new ApiVersion(1, 0); // 預設是 1.0 可以不加
}).AddMvc();

如果想要自訂 Query String 名稱的話可以透過參數來設定。

options.ApiVersionReader = new QueryStringApiVersionReader("v");

網址版號

那如果想要透過網址有 v1 v2 來分辨版本,不要透過 Query String 的話,可以加上 Route 就可以了。

[Route("api/v{version:apiVersion}/Sample")]

如此就可以透過版號的網址來連到對應的版本。

/api/v1/sample 或 /api/v2/sample

前面我們是把不同版本用不同 Controller 來區分,如果要放在同一個的話也是可以的,接續前面的範例 v2 上面加上 v3,並且新增一個 v3 的 Action 並且透過 MapToApiVersion 來指定版本。

[Route("Sample")]
[Route("api/v{version:apiVersion}/sample")]
[ApiVersion(2.0)]
[ApiVersion(3.0)]
[ApiController]
public class SampleV2Controller : ControllerBase
{
    [HttpGet]
    public IActionResult Get()
    {
        return Ok(new { version = "2.0" });
    }

    [HttpGet]
    [MapToApiVersion(3.0)]
    public IActionResult GetV3()
    {
        return Ok(new { version = "3.0" });
    }
}

Header 版號

除此之外也可以透過 Header 來分別版號,只要在參數內加入底下的方法就可以了。

options.ApiVersionReader = new HeaderApiVersionReader("x-ms-version");

那想混搭的話可以透過 ApiVersionReader.Combine 來指定多種方式。

options.ApiVersionReader = ApiVersionReader.Combine(
    new HeaderApiVersionReader("x-ms-version"),
    new QueryStringApiVersionReader("v"));

無關版本

但是有些方法不會因為版本而有所不同,就可以透過 ApiVersionNeutral 來設定,如此一來未來有新版本也用另外要再去指定版號了。

[ApiVersionNeutral]
[HttpGet]
public IActionResult Get()
{
    return Ok(new { version = "Neutral" });
}

棄用版本

舊的版本如果有規劃在未來棄用的話可以設定 Deprecated 為 true,這樣呼叫 Api 的時候 Header 就會標示棄用來提醒使用的開發者。

[ApiVersion(1.0, Deprecated = true)]

但是要在 Header 看到這個訊息需要加上設定,才會在回傳的 Header 多一些資訊。

options.ReportApiVersions = true;

OpenApi (swagger) 支援

加上版本號之後假設原本有用 swagger 來呈現 Api 文件頁面的話會發現會無法支援,這時候會需要做一些調整才有辦法正確的顯示。

假設原本沒有安裝 swagger 的話就新增底下兩個套件 Microsoft.AspNetCore.OpenApiSwashbuckle.AspNetCore,以及為了讓他支援多版本需要安裝 Asp.Versioning.Mvc.ApiExplorer

然後新增兩個設定的類別。

SwaggerDefaultValues
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 == 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 == null && description.DefaultValue != null )
        {
            var json = JsonSerializer.Serialize(
                description.DefaultValue,
                description.ModelMetadata.ModelType );
            parameter.Schema.Default = OpenApiAnyFactory.CreateFromJson( json );
        }

        parameter.Required |= description.IsRequired;
    }
  }
}
ConfigureSwaggerOptions
public class ConfigureSwaggerOptions : IConfigureOptions<SwaggerGenOptions>
{
    private readonly IApiVersionDescriptionProvider provider;

    public ConfigureSwaggerOptions( IApiVersionDescriptionProvider provider ) => this.provider = provider;

    public void Configure( SwaggerGenOptions options )
    {
        foreach ( var description in provider.ApiVersionDescriptions )
        {
            options.SwaggerDoc(
                description.GroupName,
                new OpenApiInfo()
                {
                    Title = "Example API",
                    Description = "An example API",
                    Version = description.ApiVersion.ToString(),
                } );
        }
    }
}

最後修改 Program.cs。

builder.Services.AddApiVersioning().AddMvc().AddApiExplorer();
builder.Services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();
builder.Services.AddSwaggerGen( options => options.OperationFilter<SwaggerDefaultValues>() );

設定在開發階段才出現文件,如果 Api 有對外的話就要注意安全,避免整個資訊外洩。

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI(
        options => {
            foreach (var description in app.DescribeApiVersions())
            {
                options.SwaggerEndpoint(
                    $"/swagger/{description.GroupName}/swagger.json",
                    description.GroupName);
            }
        });
}

完成之後就可以透過網址 /swagger/index.html 來察看文件,右上角就會有我們多個版本的定義可以選擇,像我範例設定 v1 為棄用也可以明確的顯示出來。

此外如果有在同一個 Controller 有設定多個版本的話,記得每個 Action 要透過 MapToApiVersion 來明確指定版本,不然也會有錯誤發生。

取得版本

如果要在程式裡面取得目前版本資訊的話可以透過底下方法,而在 ToString 的格式化條件上,可以參考官方這個頁面來看每一個參數的定義。

var apiVersion = HttpContext.GetRequestedApiVersion();
return Ok(new { version = apiVersion.ToString("F") });

結論

大致上把基本會用到的功能和 Swagger 整合都介紹了,事實上這套件還有更多可以客制化的地方,大家可以自行參考套件的文件,就不一一介紹了。

範例完整程式碼可以到 GitHub 下載。

參考資料