ABP 適用性改造 - 添加 API 版本化支持


Overview

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

完整的項目模板如下所示

模板源碼地址:https://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 = "danvic.wang@outlook.com",
						Url = new Uri("https://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 接口


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM