ABP 適用性改造 – 添加 API 版本化支援

Overview

在前面的文章里有針對 abp 的項目模板進行簡化,構建了一個精簡的項目模板,在使用過程中,因為我們暴露的 api 需要包含版本資訊,我們採取的方式是將 api 的版本號包含在資源的 URI 中。因為 abp 默認的 api 是沒有版本的概念的,所以這裡為了實現 api 版本化需要針對 abp 項目的 api 路由進行改造,從而滿足我們的需求。本篇文章則是實現這一改造過程的演示說明,希望可以對你有所幫助

完整的項目模板如下所示

模板源碼地址://github.com/danvic712/ingos-abp-api-template

Step by Step

在 abp 項目中,可以通過如下的兩種方式實現 api 介面的定義

  1. 傳統的 web api 實現方式,通過定義 controller 來完成資源 api 構建
  2. 通過 abp 框架內置的 Auto API Controller 功能,將項目中定義的應用服務(application service),自動暴露成 api 介面

因為這裡的兩種方式在項目開發中我們都會使用到,所以這裡需要針對這兩種不同的方式都實現 api 版本化的支援

對於第一種方式的 api 版本化支援,我在之前的文章中有提到過,如果你有需要的話,可以點擊此處進行查閱,這裡就不再贅述了,本篇文章主要關注點在如何對 abp 自動生成的 api 介面進行改造,實現將 api 版本資訊添加到路由中

因為這裡我使用的是精簡後的 abp 模板,與默認的 abp 項目中的程式集名稱存在差異,程式集之間的對應關係如下所示,你可以對照默認的項目進行修改

  • xxx.API => xxx.HttpApi.Host
  • xxx.Application => xxx.Application

2.1、添加程式集

對於 api 版本化的實現,這裡也是基於下面的兩個類庫來的,因此,在使用之前我們需要先在項目中通過 nuget 添加對於這兩個程式集的引用

## 添加 API 多版本支援
Install-Package Microsoft.AspNetCore.Mvc.Versioning

## 添加 Swagger 文檔的 API 版本顯示支援
Install-Package Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer

因為在 xxx.API 這個項目中已經使用到的 abp 的程式集中已經間接引用了 *.Versioning 這個程式集,所以這裡就可以選擇不添加,只需要將 *.Versioning.ApiExplorer 添加引用到項目即可

對於 xxx.Application 這個類庫,因為不會關聯到 Swagger 的相關設置,所以這裡只需要在項目中添加 *.Versioning 的引用

2.2、路由改造

當所需的程式集引用添加完成之後,就可以針對 abp 生成的路由格式進行改造,從而實現我們想要添加 api 版本資訊到路由地址中的目的

對於通過創建 controller 來暴露 api 服務的介面,我們可以直接在 controller or action 上添加 ApiVersion 特性,然後修改特性路由即可,示例程式碼如下所示

[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiController]
public class VaulesController : ControllerBase
{
	// action ...
}

而對於 abp 基於 application service 自動生成的 api,在默認的項目模板中,你可以在 *HttpApiHostModule 類中找到如下的配置,最終可以生成下圖中的 api 路由格式

public override void ConfigureServices(ServiceConfigurationContext context)
{
	var configuration = context.Services.GetConfiguration();
	var hostingEnvironment = context.Services.GetHostingEnvironment();
	
	ConfigureConventionalControllers(context);
}

private void ConfigureConventionalControllers()
{
	Configure<AbpAspNetCoreMvcOptions>(options =>
	{
		options.ConventionalControllers.Create(typeof(XXXApplicationModule).Assembly);
	});
}

默認路由

從 abp 的文檔中可知,基於約定俗成的定義,所有根據 application service 自動生成的 api 全部會以 /api 開頭,而路由路徑中的的 */app/* 我們則可以通過修改 RootPath 變數值的方式進行調整,例如,你可以將 app 修改成 your-api-path-define

private void ConfigureConventionalControllers()
{
	Configure<AbpAspNetCoreMvcOptions>(options =>
	{
		options.ConventionalControllers.Create(typeof(XXXApplicationModule).Assembly, opts =>
            {
                opts.RootPath = "your-api-path-define";
            });
	});
}

這裡調整之後的 api 路由就會變成 /api/your-api-path-define/*,因此這裡我們就可以通過修改變數值的方式來實現路由中包含 api 的版本資訊,eg. /api/v1/*

找到能夠調整的地方後,我們就需要思考具體的改造方式了,如果這裡我們寫死變數值為 v1 or v2 的話,意味著整個 XXXApplicationModule 程式集中的 application service 生成的 api 版本就限制死了,後續的可擴展性就太差了,所以這裡需要實現一個動態的配置

因此這裡同樣是藉助了上面引用的組件包,選擇通過添加 ApiVersion 特性的方式來標明應用服務所映射的 api 版本資訊,例如下面對應生成的 api 版本為 1.0

[ApiVersion("1.0")]
public class BookAppService :
	CrudAppService<
		Book, // The Book entity
		BookDto, // Used to show books
		Guid, // Primary key of the book entity
		PagedAndSortedResultRequestDto, // Used for paging/sorting
		CreateUpdateBookDto>, // Used to create/update a book
	IBookAppService // implement the IBookAppService
{
	public BookAppService(IRepository<Book, Guid> repository)
		: base(repository)
	{

	}
}

定義了服務對應的 api 版本之後,這裡就可以通過路由模板變數值的方式來替換 RootPath 參數值,因為這裡的路由相對於原來的方式來說是一種不確定的,所以這裡我們將配置路由的方法放在 abp 的 PreConfigureServices 生命周期函數中,位於該函數中的程式碼會在整個項目所有模組的 ConfigureServices 方法執行之前執行,調整後的程式碼如下

public override void PreConfigureServices(ServiceConfigurationContext context)
{
	PreConfigure<AbpAspNetCoreMvcOptions>(options =>
	{
		// 依據 api 版本資訊動態設置路由資訊
		options.ConventionalControllers.Create(typeof(IngosAbpTemplateApplicationModule).Assembly,
			opts => { opts.RootPath = "v{version:apiVersion}"; });
	});
}

public override void ConfigureServices(ServiceConfigurationContext context)
{
	var configuration = context.Services.GetConfiguration();
	var hostingEnvironment = context.Services.GetHostingEnvironment();
    
    ConfigureConventionalControllers(context);
}

private void ConfigureConventionalControllers(ServiceConfigurationContext context)
{
    // 基於 PreConfigureServices 中的配置進行
	Configure<AbpAspNetCoreMvcOptions>(options => { context.Services.ExecutePreConfiguredActions(options); });
}

當然,這裡只是針對我們自己編寫的應用服務進行的版本設定,對於 abp 框架所包含的一些 api 介面,可以直接在 PreConfigureServices 函數中通過直接指定 api 版本的方式來實現,例如這裡我將許可權相關的 api 介面版本設置為 1.0

PS,這裡針對框架內置 api 的版本設定,並不會改變介面的路由地址,僅僅是為了下面將要實現的 swagger 依據 api 版本號進行分組顯示時可以將內置的 api 暴露出來

public override void PreConfigureServices(ServiceConfigurationContext context)
{
	PreConfigure<AbpAspNetCoreMvcOptions>(options =>
	{
		// 依據 api 版本資訊動態設置路由資訊
		options.ConventionalControllers.Create(typeof(IngosAbpTemplateApplicationModule).Assembly,
			opts => { opts.RootPath = "v{version:apiVersion}"; });

		// 指定內置許可權相關 api 版本為 1.0
		options.ConventionalControllers.Create(typeof(AbpPermissionManagementHttpApiModule).Assembly,
			opts => { opts.ApiVersions.Add(new ApiVersion(1, 0)); });
	});
}

配置好路由之後,就可以將 api 版本服務以及給到 swagger 使用的 api explorer 服務注入到 IServiceCollection 中,這裡的配置項和之前的方式一樣就不做解釋了,完善後的方法程式碼如下所示

private void ConfigureConventionalControllers(ServiceConfigurationContext context)
{
	Configure<AbpAspNetCoreMvcOptions>(options => { context.Services.ExecutePreConfiguredActions(options); });

	context.Services.AddAbpApiVersioning(options =>
	{
		options.ReportApiVersions = true;

		options.AssumeDefaultVersionWhenUnspecified = true;

		options.DefaultApiVersion = new ApiVersion(1, 0);

		options.ApiVersionReader = new UrlSegmentApiVersionReader();

		var mvcOptions = context.Services.ExecutePreConfiguredActions<AbpAspNetCoreMvcOptions>();
		options.ConfigureAbp(mvcOptions);
	});

	context.Services.AddVersionedApiExplorer(option =>
	{
		option.GroupNameFormat = "'v'VVV";

		option.AssumeDefaultVersionWhenUnspecified = true;
	});
}

2.3、Swagger 改造

因為改造前的項目是不存在 api 版本的概念的,所以默認的 swagger 是會顯示出所有的介面,而當項目可以支援 api 版本化之後,這裡就應該基於 api 版本生成不同的 json 文件,達到 swagger 可以基於 api 的版本來分組顯示的目的

因為在上面的程式碼中已經將 api explorer 服務注入到了 IServiceCollection 中,所以這裡可以直接使用 IApiVersionDescriptionProvider 獲取到 api 的版本資訊,從而據此生成不同的 swagger json 文件,swagger 相關的配置程式碼如下

public override void ConfigureServices(ServiceConfigurationContext context)
{
	var configuration = context.Services.GetConfiguration();
	var hostingEnvironment = context.Services.GetHostingEnvironment();
    
    ConfigureSwaggerServices(context);
}

public override void OnApplicationInitialization(ApplicationInitializationContext context)
{
	var app = context.GetApplicationBuilder();

	app.UseSwagger();
	app.UseAbpSwaggerUI(options =>
	{
		options.DocumentTitle = "IngosAbpTemplate API";

		// 默認顯示最新版本的 api 
		//
		var provider = context.ServiceProvider.GetRequiredService<IApiVersionDescriptionProvider>();
		var apiVersionList = provider.ApiVersionDescriptions
			.Select(i => $"v{i.ApiVersion.MajorVersion}")
			.Distinct().Reverse();
		foreach (var apiVersion in apiVersionList)
			options.SwaggerEndpoint($"/swagger/{apiVersion}/swagger.json",
				$"IngosAbpTemplate API {apiVersion?.ToUpperInvariant()}");
	});
}

private static void ConfigureSwaggerServices(ServiceConfigurationContext context, IConfiguration configuration)
{
	context.Services.AddAbpSwaggerGenWithOAuth(
		configuration["AuthServer:Authority"],
		options =>
		{
			// 獲取 api 版本資訊
			var provider = context.Services.BuildServiceProvider()
				.GetRequiredService<IApiVersionDescriptionProvider>();

			// 基於大版本生成 swagger 
			foreach (var description in provider.ApiVersionDescriptions)
				options.SwaggerDoc(description.GroupName, new OpenApiInfo
				{
					Contact = new OpenApiContact
					{
						Name = "Danvic Wang",
						Email = "[email protected]",
						Url = new Uri("//yuiter.com")
					},
					Description = "IngosAbpTemplate API",
					Title = "IngosAbpTemplate API",
					Version = $"v{description.ApiVersion.MajorVersion}"
				});

			options.DocInclusionPredicate((docName, description) =>
			{
				// 獲取主要版本,如果不是該版本的 api 就不顯示
				var apiVersion = $"v{description.GetApiVersion().MajorVersion}";

				if (!docName.Equals(apiVersion))
					return false;

				// 替換路由參數
				var values = description.RelativePath
					.Split('/')
					.Select(v => v.Replace("v{version}", apiVersion));

				description.RelativePath = string.Join("/", values);

				return true;
			});

			// 取消 API 文檔需要輸入版本資訊
			options.OperationFilter<RemoveVersionFromParameter>();
		});
}

自此,整個關於 api 版本化的調整就已經完成了,完整的程式碼可以點擊此處跳轉到 github 上進行查看,最終實現效果如下所示

版本化 api 介面