diff --git a/docs/en/API/API-Versioning.md b/docs/en/API/API-Versioning.md new file mode 100644 index 00000000000..5eefb129c31 --- /dev/null +++ b/docs/en/API/API-Versioning.md @@ -0,0 +1,349 @@ +# API Versioning System + +ABP Framework integrates the [ASPNET-API-Versioning](https://github.com/dotnet/aspnet-api-versioning/wiki) feature and adapts to C# and JavaScript Static Client Proxies and [Auto API Controller](API/Auto-API-Controllers.md). + + +## Enable API Versioning + +```cs +public override void ConfigureServices(ServiceConfigurationContext context) +{ + context.Services.AddAbpApiVersioning(options => + { + // Show neutral/versionless APIs. + options.UseApiBehavior = false; + + options.ReportApiVersions = true; + options.AssumeDefaultVersionWhenUnspecified = true; + }); + + Configure(options => + { + options.ChangeControllerModelApiExplorerGroupName = false; + }); +} +``` + +## C# and JavaScript Static Client Proxies + +This feature does not compatible with [URL Path Versioning](https://github.com/dotnet/aspnet-api-versioning/wiki/Versioning-via-the-URL-Path), I suggest you always use [Versioning-via-the-Query-String](https://github.com/dotnet/aspnet-api-versioning/wiki/Versioning-via-the-Query-String). + +### Example + +**Application Services:** +```cs +public interface IBookAppService : IApplicationService +{ + Task GetAsync(); +} + +public interface IBookV2AppService : IApplicationService +{ + Task GetAsync(); + + Task GetAsync(string isbn); +} +``` + +**HttpApi Controillers:** +```cs +[Area(BookStoreRemoteServiceConsts.ModuleName)] +[RemoteService(Name = BookStoreRemoteServiceConsts.RemoteServiceName)] +[ApiVersion("1.0", Deprecated = true)] +[ApiController] +[ControllerName("Book")] +[Route("api/BookStore/Book")] +public class BookController : BookStoreController, IBookAppService +{ + private readonly IBookAppService _bookAppService; + + public BookController(IBookAppService bookAppService) + { + _bookAppService = bookAppService; + } + + [HttpGet] + public async Task GetAsync() + { + return await _bookAppService.GetAsync(); + } +} + +[Area(BookStoreRemoteServiceConsts.ModuleName)] +[RemoteService(Name = BookStoreRemoteServiceConsts.RemoteServiceName)] +[ApiVersion("2.0")] +[ApiController] +[ControllerName("Book")] +[Route("api/BookStore/Book")] +public class BookV2Controller : BookStoreController, IBookV2AppService +{ + private readonly IBookV2AppService _bookAppService; + + public BookV2Controller(IBookV2AppService bookAppService) + { + _bookAppService = bookAppService; + } + + [HttpGet] + public async Task GetAsync() + { + return await _bookAppService.GetAsync(); + } + + [HttpGet] + [Route("{isbn}")] + public async Task GetAsync(string isbn) + { + return await _bookAppService.GetAsync(isbn); + } +} +``` + +**Generated CS and JS proxies:** + +```cs +[Dependency(ReplaceServices = true)] +[ExposeServices(typeof(IBookAppService), typeof(BookClientProxy))] +public partial class BookClientProxy : ClientProxyBase, IBookAppService +{ + public virtual async Task GetAsync() + { + return await RequestAsync(nameof(GetAsync)); + } +} + +[Dependency(ReplaceServices = true)] +[ExposeServices(typeof(IBookV2AppService), typeof(BookV2ClientProxy))] +public partial class BookV2ClientProxy : ClientProxyBase, IBookV2AppService +{ + public virtual async Task GetAsync() + { + return await RequestAsync(nameof(GetAsync)); + } + + public virtual async Task GetAsync(string isbn) + { + return await RequestAsync(nameof(GetAsync), new ClientProxyRequestTypeValue + { + { typeof(string), isbn } + }); + } +} +``` + + +```js +// controller bookStore.books.book + +(function(){ + +abp.utils.createNamespace(window, 'bookStore.books.book'); + +bookStore.books.book.get = function(api_version, ajaxParams) { + var api_version = api_version ? api_version : '1.0'; + return abp.ajax($.extend(true, { + url: abp.appPath + 'api/BookStore/Book' + abp.utils.buildQueryString([{ name: 'api-version', value: api_version }]) + '', + type: 'GET' + }, ajaxParams)); +}; + +})(); + +// controller bookStore.books.bookV2 + +(function(){ + +abp.utils.createNamespace(window, 'bookStore.books.bookV2'); + +bookStore.books.bookV2.get = function(api_version, ajaxParams) { + var api_version = api_version ? api_version : '2.0'; + return abp.ajax($.extend(true, { + url: abp.appPath + 'api/BookStore/Book' + abp.utils.buildQueryString([{ name: 'api-version', value: api_version }]) + '', + type: 'GET' + }, ajaxParams)); +}; + +bookStore.books.bookV2.getAsyncByIsbn = function(isbn, api_version, ajaxParams) { + var api_version = api_version ? api_version : '2.0'; + return abp.ajax($.extend(true, { + url: abp.appPath + 'api/BookStore/Book/' + isbn + '' + abp.utils.buildQueryString([{ name: 'api-version', value: api_version }]) + '', + type: 'GET' + }, ajaxParams)); +}; + +})(); +``` + + +## Manually change version + +If an application service class supports multiple versions. You can inject `ICurrentApiVersionInfo` to switch versions in C#. + +```cs +var currentApiVersionInfo = _abpApplication.ServiceProvider.GetRequiredService(); +var bookV4AppService = _abpApplication.ServiceProvider.GetRequiredService(); +using (currentApiVersionInfo.Change(new ApiVersionInfo(ParameterBindingSources.Query, "4.0"))) +{ + book = await bookV4AppService.GetAsync(); + logger.LogWarning(book.Title); + logger.LogWarning(book.ISBN); +} + +using (currentApiVersionInfo.Change(new ApiVersionInfo(ParameterBindingSources.Query, "4.1"))) +{ + book = await bookV4AppService.GetAsync(); + logger.LogWarning(book.Title); + logger.LogWarning(book.ISBN); +} +``` + +We have made a default version in the JS proxy. Of course, you can also manually change the version. + +```js + +bookStore.books.bookV4.get("4.0") // Manually change the version. +//Title: Mastering ABP Framework V4.0 + +bookStore.books.bookV4.get() // The latest supported version is used by default. +//Title: Mastering ABP Framework V4.1 +``` + +## Auto API Controller + +```cs +public override void PreConfigureServices(ServiceConfigurationContext context) +{ + PreConfigure(options => + { + //2.0 Version + options.ConventionalControllers.Create(typeof(BookStoreWebAppModule).Assembly, opts => + { + opts.TypePredicate = t => t.Namespace == typeof(BookStore.Controllers.ConventionalControllers.v2.TodoAppService).Namespace; + opts.ApiVersions.Add(new ApiVersion(2, 0)); + }); + + //1.0 Compatibility version + options.ConventionalControllers.Create(typeof(BookStoreWebAppModule).Assembly, opts => + { + opts.TypePredicate = t => t.Namespace == typeof(BookStore.Controllers.ConventionalControllers.v1.TodoAppService).Namespace; + opts.ApiVersions.Add(new ApiVersion(1, 0)); + }); + }); +} + +public override void ConfigureServices(ServiceConfigurationContext context) +{ + var preActions = context.Services.GetPreConfigureActions(); + Configure(options => + { + preActions.Configure(options); + }); + + context.Services.AddAbpApiVersioning(options => + { + // Show neutral/versionless APIs. + options.UseApiBehavior = false; + + options.ReportApiVersions = true; + options.AssumeDefaultVersionWhenUnspecified = true; + + options.ConfigureAbp(preActions.Configure()); + }); + + Configure(options => + { + options.ChangeControllerModelApiExplorerGroupName = false; + }); +} +``` + +## Swagger/VersionedApiExplorer + +```cs + +public override void ConfigureServices(ServiceConfigurationContext context) +{ + context.Services.AddAbpApiVersioning(options => + { + // Show neutral/versionless APIs. + options.UseApiBehavior = false; + + options.ReportApiVersions = true; + options.AssumeDefaultVersionWhenUnspecified = true; + }); + + context.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; + }); + + context.Services.AddTransient, ConfigureSwaggerOptions>(); + + context.Services.AddAbpSwaggerGen( + options => + { + // add a custom operation filter which sets default values + options.OperationFilter(); + + options.CustomSchemaIds(type => type.FullName); + }); + + Configure(options => + { + options.ChangeControllerModelApiExplorerGroupName = false; + }); +} + +public override void OnApplicationInitialization(ApplicationInitializationContext context) +{ + var app = context.GetApplicationBuilder(); + var env = context.GetEnvironment(); + + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + else + { + app.UseErrorPage(); + app.UseHsts(); + } + + app.UseHttpsRedirection(); + app.UseStaticFiles(); + app.UseRouting(); + app.UseAbpRequestLocalization(); + + app.UseSwagger(); + app.UseSwaggerUI( + options => + { + var provider = app.ApplicationServices.GetRequiredService(); + // build a swagger endpoint for each discovered API version + foreach (var description in provider.ApiVersionDescriptions) + { + options.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", description.GroupName.ToUpperInvariant()); + } + }); + + app.UseConfiguredEndpoints(); +} +``` + +## Custom multi-version API controller. + +ABP Framework will not affect to your APIs, you can freely implement your APIs according to Microsoft's documentation. + +Get more from https://github.com/dotnet/aspnet-api-versioning/wiki + + +## Sample source code + +You can get the complete sample source code in https://github.com/abpframework/abp-samples/tree/master/Api-Versioning \ No newline at end of file diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index 943408d70ab..6df98cb05a2 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -590,6 +590,10 @@ { "text": "Swagger Integration", "path": "API/Swagger-Integration.md" + }, + { + "text": "API Versioning", + "path": "API/API-Versioning.md" } ] }, diff --git a/docs/zh-Hans/API/API-Versioning.md b/docs/zh-Hans/API/API-Versioning.md new file mode 100644 index 00000000000..81bb6574c64 --- /dev/null +++ b/docs/zh-Hans/API/API-Versioning.md @@ -0,0 +1,347 @@ +# ABP版本控制系统 + +ABP框架集成了[ASPNET-API-版本控制](https://github.com/dotnet/aspnet-api-versioning/wiki)功能并适配C#和JavaScript静态代理和[自动API控制器](API/Auto-API-Controllers.md). + +## 启用API版本控制 + +```cs +public override void ConfigureServices(ServiceConfigurationContext context) +{ + context.Services.AddAbpApiVersioning(options => + { + // Show neutral/versionless APIs. + options.UseApiBehavior = false; + + options.ReportApiVersions = true; + options.AssumeDefaultVersionWhenUnspecified = true; + }); + + Configure(options => + { + options.ChangeControllerModelApiExplorerGroupName = false; + }); +} +``` + +## C# 和 JavaScript 静态客户端代理 + +这个功能不兼容[URL路径版本控制](https://github.com/dotnet/aspnet-api-versioning/wiki/Versioning-via-the-URL-Path), 建议你始终使用[Query-String版本控制](https://github.com/dotnet/aspnet-api-versioning/wiki/Versioning-via-the-Query-String) + +### 示例 + +**Application Services:** + +```cs +public interface IBookAppService : IApplicationService +{ + Task GetAsync(); +} + +public interface IBookV2AppService : IApplicationService +{ + Task GetAsync(); + + Task GetAsync(string isbn); +} +``` + +**HttpApi Controillers:** + +```cs +[Area(BookStoreRemoteServiceConsts.ModuleName)] +[RemoteService(Name = BookStoreRemoteServiceConsts.RemoteServiceName)] +[ApiVersion("1.0", Deprecated = true)] +[ApiController] +[ControllerName("Book")] +[Route("api/BookStore/Book")] +public class BookController : BookStoreController, IBookAppService +{ + private readonly IBookAppService _bookAppService; + + public BookController(IBookAppService bookAppService) + { + _bookAppService = bookAppService; + } + + [HttpGet] + public async Task GetAsync() + { + return await _bookAppService.GetAsync(); + } +} + +[Area(BookStoreRemoteServiceConsts.ModuleName)] +[RemoteService(Name = BookStoreRemoteServiceConsts.RemoteServiceName)] +[ApiVersion("2.0")] +[ApiController] +[ControllerName("Book")] +[Route("api/BookStore/Book")] +public class BookV2Controller : BookStoreController, IBookV2AppService +{ + private readonly IBookV2AppService _bookAppService; + + public BookV2Controller(IBookV2AppService bookAppService) + { + _bookAppService = bookAppService; + } + + [HttpGet] + public async Task GetAsync() + { + return await _bookAppService.GetAsync(); + } + + [HttpGet] + [Route("{isbn}")] + public async Task GetAsync(string isbn) + { + return await _bookAppService.GetAsync(isbn); + } +} +``` + +**生成 CS 和 JS 代理:** + +```cs +[Dependency(ReplaceServices = true)] +[ExposeServices(typeof(IBookAppService), typeof(BookClientProxy))] +public partial class BookClientProxy : ClientProxyBase, IBookAppService +{ + public virtual async Task GetAsync() + { + return await RequestAsync(nameof(GetAsync)); + } +} + +[Dependency(ReplaceServices = true)] +[ExposeServices(typeof(IBookV2AppService), typeof(BookV2ClientProxy))] +public partial class BookV2ClientProxy : ClientProxyBase, IBookV2AppService +{ + public virtual async Task GetAsync() + { + return await RequestAsync(nameof(GetAsync)); + } + + public virtual async Task GetAsync(string isbn) + { + return await RequestAsync(nameof(GetAsync), new ClientProxyRequestTypeValue + { + { typeof(string), isbn } + }); + } +} +``` + +```js +// controller bookStore.books.book + +(function(){ + +abp.utils.createNamespace(window, 'bookStore.books.book'); + +bookStore.books.book.get = function(api_version, ajaxParams) { + var api_version = api_version ? api_version : '1.0'; + return abp.ajax($.extend(true, { + url: abp.appPath + 'api/BookStore/Book' + abp.utils.buildQueryString([{ name: 'api-version', value: api_version }]) + '', + type: 'GET' + }, ajaxParams)); +}; + +})(); + +// controller bookStore.books.bookV2 + +(function(){ + +abp.utils.createNamespace(window, 'bookStore.books.bookV2'); + +bookStore.books.bookV2.get = function(api_version, ajaxParams) { + var api_version = api_version ? api_version : '2.0'; + return abp.ajax($.extend(true, { + url: abp.appPath + 'api/BookStore/Book' + abp.utils.buildQueryString([{ name: 'api-version', value: api_version }]) + '', + type: 'GET' + }, ajaxParams)); +}; + +bookStore.books.bookV2.getAsyncByIsbn = function(isbn, api_version, ajaxParams) { + var api_version = api_version ? api_version : '2.0'; + return abp.ajax($.extend(true, { + url: abp.appPath + 'api/BookStore/Book/' + isbn + '' + abp.utils.buildQueryString([{ name: 'api-version', value: api_version }]) + '', + type: 'GET' + }, ajaxParams)); +}; + +})(); +``` + +## 手动更改版本 + +如果应用服务支持多版本, 你可以注入 `ICurrentApiVersionInfo` 来切换版本. + +```cs +var currentApiVersionInfo = _abpApplication.ServiceProvider.GetRequiredService(); +var bookV4AppService = _abpApplication.ServiceProvider.GetRequiredService(); +using (currentApiVersionInfo.Change(new ApiVersionInfo(ParameterBindingSources.Query, "4.0"))) +{ + book = await bookV4AppService.GetAsync(); + logger.LogWarning(book.Title); + logger.LogWarning(book.ISBN); +} + +using (currentApiVersionInfo.Change(new ApiVersionInfo(ParameterBindingSources.Query, "4.1"))) +{ + book = await bookV4AppService.GetAsync(); + logger.LogWarning(book.Title); + logger.LogWarning(book.ISBN); +} +``` + +在JS代理中有一个默认版本, 当然你也可以手动更改. + +```js + +bookStore.books.bookV4.get("4.0") // Manually change the version. +//Title: Mastering ABP Framework V4.0 + +bookStore.books.bookV4.get() // The latest supported version is used by default. +//Title: Mastering ABP Framework V4.1 +``` + +## 自动API控制器 + +```cs +public override void PreConfigureServices(ServiceConfigurationContext context) +{ + PreConfigure(options => + { + //2.0 Version + options.ConventionalControllers.Create(typeof(BookStoreWebAppModule).Assembly, opts => + { + opts.TypePredicate = t => t.Namespace == typeof(BookStore.Controllers.ConventionalControllers.v2.TodoAppService).Namespace; + opts.ApiVersions.Add(new ApiVersion(2, 0)); + }); + + //1.0 Compatibility version + options.ConventionalControllers.Create(typeof(BookStoreWebAppModule).Assembly, opts => + { + opts.TypePredicate = t => t.Namespace == typeof(BookStore.Controllers.ConventionalControllers.v1.TodoAppService).Namespace; + opts.ApiVersions.Add(new ApiVersion(1, 0)); + }); + }); +} + +public override void ConfigureServices(ServiceConfigurationContext context) +{ + var preActions = context.Services.GetPreConfigureActions(); + Configure(options => + { + preActions.Configure(options); + }); + + context.Services.AddAbpApiVersioning(options => + { + // Show neutral/versionless APIs. + options.UseApiBehavior = false; + + options.ReportApiVersions = true; + options.AssumeDefaultVersionWhenUnspecified = true; + + options.ConfigureAbp(preActions.Configure()); + }); + + Configure(options => + { + options.ChangeControllerModelApiExplorerGroupName = false; + }); +} +``` + +## Swagger/VersionedApiExplorer + +```cs + +public override void ConfigureServices(ServiceConfigurationContext context) +{ + context.Services.AddAbpApiVersioning(options => + { + // Show neutral/versionless APIs. + options.UseApiBehavior = false; + + options.ReportApiVersions = true; + options.AssumeDefaultVersionWhenUnspecified = true; + }); + + context.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; + }); + + context.Services.AddTransient, ConfigureSwaggerOptions>(); + + context.Services.AddAbpSwaggerGen( + options => + { + // add a custom operation filter which sets default values + options.OperationFilter(); + + options.CustomSchemaIds(type => type.FullName); + }); + + Configure(options => + { + options.ChangeControllerModelApiExplorerGroupName = false; + }); +} + +public override void OnApplicationInitialization(ApplicationInitializationContext context) +{ + var app = context.GetApplicationBuilder(); + var env = context.GetEnvironment(); + + if (env.IsDevelopment()) + { + app.UseDeveloperExceptionPage(); + } + else + { + app.UseErrorPage(); + app.UseHsts(); + } + + app.UseHttpsRedirection(); + app.UseStaticFiles(); + app.UseRouting(); + app.UseAbpRequestLocalization(); + + app.UseSwagger(); + app.UseSwaggerUI( + options => + { + var provider = app.ApplicationServices.GetRequiredService(); + // build a swagger endpoint for each discovered API version + foreach (var description in provider.ApiVersionDescriptions) + { + options.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", description.GroupName.ToUpperInvariant()); + } + }); + + app.UseConfiguredEndpoints(); +} +``` + +## 自定义多版本API控制器 + +ABP框架不会影响你的API, 你可以根据微软文档自由的实现你的API. + +参阅: https://github.com/dotnet/aspnet-api-versioning/wiki + +## 示例源码 + +你可以在这里得到完整的示例源码: https://github.com/abpframework/abp-samples/tree/master/Api-Versioning \ No newline at end of file diff --git a/docs/zh-Hans/docs-nav.json b/docs/zh-Hans/docs-nav.json index ebe562a97e8..e5f736088e7 100644 --- a/docs/zh-Hans/docs-nav.json +++ b/docs/zh-Hans/docs-nav.json @@ -444,6 +444,10 @@ "path": "API/Application-Configuration.md" } ] + }, + { + "text": "API版本控制", + "path": "API/API-Versioning.md" } ] }, diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcOptions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcOptions.cs index e615be5ecbe..364bb76c1f6 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcOptions.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AbpAspNetCoreMvcOptions.cs @@ -16,11 +16,14 @@ public class AbpAspNetCoreMvcOptions public bool EnableRazorRuntimeCompilationOnDevelopment { get; set; } + public bool ChangeControllerModelApiExplorerGroupName { get; set; } + public AbpAspNetCoreMvcOptions() { ConventionalControllers = new AbpConventionalControllerOptions(); IgnoredControllersOnModelExclusion = new HashSet(); AutoModelValidation = true; EnableRazorRuntimeCompilationOnDevelopment = true; + ChangeControllerModelApiExplorerGroupName = true; } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApiDescriptionExtensions.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApiDescriptionExtensions.cs new file mode 100644 index 00000000000..fdf33e788dd --- /dev/null +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApiDescriptionExtensions.cs @@ -0,0 +1,34 @@ +using Microsoft.AspNetCore.Mvc.Abstractions; +using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.AspNetCore.Mvc.Controllers; +using Volo.Abp.Reflection; + +namespace Volo.Abp.AspNetCore.Mvc; + +public static class ApiDescriptionExtensions +{ + public static bool IsRemoteService(this ApiDescription actionDescriptor) + { + if (actionDescriptor.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor) + { + var remoteServiceAttr = ReflectionHelper.GetSingleAttributeOfMemberOrDeclaringTypeOrDefault(controllerActionDescriptor.MethodInfo); + if (remoteServiceAttr != null && remoteServiceAttr.IsEnabled) + { + return true; + } + + remoteServiceAttr = ReflectionHelper.GetSingleAttributeOfMemberOrDeclaringTypeOrDefault(controllerActionDescriptor.ControllerTypeInfo); + if (remoteServiceAttr != null && remoteServiceAttr.IsEnabled) + { + return true; + } + + if (typeof(IRemoteService).IsAssignableFrom(controllerActionDescriptor.ControllerTypeInfo)) + { + return true; + } + } + + return false; + } +} diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpRemoteServiceApiDescriptionProvider.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpRemoteServiceApiDescriptionProvider.cs index ff01af02861..23cc2e155d1 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpRemoteServiceApiDescriptionProvider.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApiExploring/AbpRemoteServiceApiDescriptionProvider.cs @@ -42,7 +42,7 @@ public void OnProvidersExecuting(ApiDescriptionProviderContext context) { foreach (var apiResponseType in GetApiResponseTypes()) { - foreach (var result in context.Results.Where(IsRemoteService)) + foreach (var result in context.Results.Where(x => x.IsRemoteService())) { var actionProducesResponseTypeAttributes = ReflectionHelper.GetAttributesOfMemberOrDeclaringType( @@ -85,29 +85,4 @@ protected virtual IEnumerable GetApiResponseTypes() return _options.SupportedResponseTypes; } - - protected virtual bool IsRemoteService(ApiDescription actionDescriptor) - { - if (actionDescriptor.ActionDescriptor is ControllerActionDescriptor controllerActionDescriptor) - { - var remoteServiceAttr = ReflectionHelper.GetSingleAttributeOfMemberOrDeclaringTypeOrDefault(controllerActionDescriptor.MethodInfo); - if (remoteServiceAttr != null && remoteServiceAttr.IsEnabled) - { - return true; - } - - remoteServiceAttr = ReflectionHelper.GetSingleAttributeOfMemberOrDeclaringTypeOrDefault(controllerActionDescriptor.ControllerTypeInfo); - if (remoteServiceAttr != null && remoteServiceAttr.IsEnabled) - { - return true; - } - - if (typeof(IRemoteService).IsAssignableFrom(controllerActionDescriptor.ControllerTypeInfo)) - { - return true; - } - } - - return false; - } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProvider.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProvider.cs index b5204ae501c..dbc7532aab4 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProvider.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProvider.cs @@ -8,7 +8,9 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ApiExplorer; +using Microsoft.AspNetCore.Mvc.Controllers; using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.Versioning; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; @@ -64,6 +66,19 @@ public ApplicationApiDescriptionModel CreateApiModel(ApplicationApiDescriptionMo } } + foreach (var (_, module) in model.Modules) + { + var controllers = module.Controllers.GroupBy(x => x.Value.Type).ToList(); + foreach (var controller in controllers.Where(x => x.Count() > 1)) + { + var removedController = module.Controllers.RemoveAll(x => x.Value.IsRemoteService && controller.OrderBy(c => c.Value.ControllerGroupName).Skip(1).Contains(x)); + foreach (var removed in removedController) + { + Logger.LogInformation($"The controller named '{removed.Value.Type}' was removed from ApplicationApiDescriptionModel because it same with other controller."); + } + } + } + return model; } @@ -87,6 +102,8 @@ private void AddApiDescriptionToModel( var controllerModel = moduleModel.GetOrAddController( _options.ControllerNameGenerator(controllerType, setting), FindGroupName(controllerType) ?? apiDescription.GroupName, + apiDescription.IsRemoteService(), + apiDescription.GetProperty()?.ToString(), controllerType, _modelOptions.IgnoredInterfaces ); diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpServiceConvention.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpServiceConvention.cs index e96ae0df379..9a78479b9c5 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpServiceConvention.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpServiceConvention.cs @@ -207,7 +207,7 @@ protected virtual bool CanUseFormBodyBinding(ActionModel action, ParameterModel protected virtual void ConfigureApiExplorer(ControllerModel controller) { - if (controller.ApiExplorer.GroupName.IsNullOrEmpty()) + if (Options.ChangeControllerModelApiExplorerGroupName && controller.ApiExplorer.GroupName.IsNullOrEmpty()) { controller.ApiExplorer.GroupName = controller.ControllerName; } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Versioning/HttpContextRequestedApiVersion.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Versioning/HttpContextRequestedApiVersion.cs index c6537ac98d6..09db4c3ce2e 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Versioning/HttpContextRequestedApiVersion.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Versioning/HttpContextRequestedApiVersion.cs @@ -6,7 +6,7 @@ namespace Volo.Abp.AspNetCore.Mvc.Versioning; public class HttpContextRequestedApiVersion : IRequestedApiVersion { - public string Current => _httpContextAccessor.HttpContext?.GetRequestedApiVersion().ToString(); + public string Current => _httpContextAccessor.HttpContext?.GetRequestedApiVersion()?.ToString(); private readonly IHttpContextAccessor _httpContextAccessor; diff --git a/framework/src/Volo.Abp.Http.Client.Web/Volo/Abp/Http/Client/Web/Conventions/AbpHttpClientProxyServiceConvention.cs b/framework/src/Volo.Abp.Http.Client.Web/Volo/Abp/Http/Client/Web/Conventions/AbpHttpClientProxyServiceConvention.cs index 2cbe02c95e6..96503775809 100644 --- a/framework/src/Volo.Abp.Http.Client.Web/Volo/Abp/Http/Client/Web/Conventions/AbpHttpClientProxyServiceConvention.cs +++ b/framework/src/Volo.Abp.Http.Client.Web/Volo/Abp/Http/Client/Web/Conventions/AbpHttpClientProxyServiceConvention.cs @@ -170,7 +170,7 @@ protected virtual void ConfigureClientProxyApiExplorer(ControllerModel controlle { if (ControllerWithAttributeRoute.Contains(controller)) { - if (controller.ApiExplorer.GroupName.IsNullOrEmpty()) + if (Options.ChangeControllerModelApiExplorerGroupName && controller.ApiExplorer.GroupName.IsNullOrEmpty()) { controller.ApiExplorer.GroupName = controller.ControllerName; } diff --git a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyBase.cs b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyBase.cs index 85c4848f023..ec6abb1daba 100644 --- a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyBase.cs +++ b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ClientProxyBase.cs @@ -38,6 +38,7 @@ public class ClientProxyBase : ITransientDependency protected IRemoteServiceHttpClientAuthenticator ClientAuthenticator => LazyServiceProvider.LazyGetRequiredService(); protected ClientProxyRequestPayloadBuilder ClientProxyRequestPayloadBuilder => LazyServiceProvider.LazyGetRequiredService(); protected ClientProxyUrlBuilder ClientProxyUrlBuilder => LazyServiceProvider.LazyGetRequiredService(); + protected ICurrentApiVersionInfo CurrentApiVersionInfo => LazyServiceProvider.LazyGetRequiredService(); protected virtual async Task RequestAsync(string methodName, ClientProxyRequestTypeValue arguments = null) { @@ -62,10 +63,14 @@ protected virtual ClientProxyRequestContext BuildHttpProxyClientProxyContext(str { throw new AbpException($"The API description of the {typeof(TService).FullName}.{methodName} method was not found!"); } + + return new ClientProxyRequestContext( action, action.Parameters .GroupBy(x => x.NameOnMethod) + //TODO: make names configurable + .Where(x => action.SupportedVersions.Any() && x.Key != "api-version" && x.Key !="apiVersion") .Select((x, i) => new KeyValuePair(x.Key, arguments.Values[i].Value)) .ToDictionary(x => x.Key, x => x.Value), typeof(TService)); @@ -156,6 +161,11 @@ await ClientAuthenticator.Authenticate( protected virtual async Task GetApiVersionInfoAsync(ClientProxyRequestContext requestContext) { + if (CurrentApiVersionInfo.ApiVersionInfo != null) + { + return CurrentApiVersionInfo.ApiVersionInfo; + } + var apiVersion = await FindBestApiVersionAsync(requestContext); //TODO: Make names configurable? diff --git a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/CurrentApiVersionInfo.cs b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/CurrentApiVersionInfo.cs new file mode 100644 index 00000000000..f56e9e893a9 --- /dev/null +++ b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/CurrentApiVersionInfo.cs @@ -0,0 +1,22 @@ +using System; +using System.Threading; +using Volo.Abp.DependencyInjection; + +namespace Volo.Abp.Http.Client.ClientProxying; + +public class CurrentApiVersionInfo : ICurrentApiVersionInfo, ISingletonDependency +{ + public ApiVersionInfo ApiVersionInfo => _currentApiVersionInfo.Value; + + private readonly AsyncLocal _currentApiVersionInfo = new AsyncLocal(); + + public virtual IDisposable Change(ApiVersionInfo apiVersionInfo) + { + var parent = ApiVersionInfo; + _currentApiVersionInfo.Value = apiVersionInfo; + return new DisposeAction(() => + { + _currentApiVersionInfo.Value = parent; + }); + } +} diff --git a/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ICurrentApiVersionInfo.cs b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ICurrentApiVersionInfo.cs new file mode 100644 index 00000000000..9a8fe264bf6 --- /dev/null +++ b/framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ICurrentApiVersionInfo.cs @@ -0,0 +1,10 @@ +using System; + +namespace Volo.Abp.Http.Client.ClientProxying; + +public interface ICurrentApiVersionInfo +{ + ApiVersionInfo ApiVersionInfo { get; } + + IDisposable Change(ApiVersionInfo apiVersionInfo); +} diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ControllerApiDescriptionModel.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ControllerApiDescriptionModel.cs index 5f93e0d863b..05021a91156 100644 --- a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ControllerApiDescriptionModel.cs +++ b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ControllerApiDescriptionModel.cs @@ -12,6 +12,10 @@ public class ControllerApiDescriptionModel public string ControllerGroupName { get; set; } + public bool IsRemoteService { get; set; } + + public string ApiVersion { get; set; } + public string Type { get; set; } public List Interfaces { get; set; } @@ -23,12 +27,14 @@ public ControllerApiDescriptionModel() } - public static ControllerApiDescriptionModel Create(string controllerName, string groupName, Type type, [CanBeNull] HashSet ignoredInterfaces = null) + public static ControllerApiDescriptionModel Create(string controllerName, string groupName, bool isRemoteService, string apiVersion, Type type, [CanBeNull] HashSet ignoredInterfaces = null) { return new ControllerApiDescriptionModel { ControllerName = controllerName, ControllerGroupName = groupName, + IsRemoteService = isRemoteService, + ApiVersion = apiVersion, Type = type.FullName, Actions = new Dictionary(), Interfaces = type diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ModuleApiDescriptionModel.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ModuleApiDescriptionModel.cs index 95268091578..48b6cd3cda6 100644 --- a/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ModuleApiDescriptionModel.cs +++ b/framework/src/Volo.Abp.Http/Volo/Abp/Http/Modeling/ModuleApiDescriptionModel.cs @@ -49,9 +49,10 @@ public ControllerApiDescriptionModel AddController(ControllerApiDescriptionModel return Controllers[controller.Type] = controller; } - public ControllerApiDescriptionModel GetOrAddController(string name, string groupName, Type type, [CanBeNull] HashSet ignoredInterfaces = null) + public ControllerApiDescriptionModel GetOrAddController(string name, string groupName, bool isRemoteService, string apiVersion, Type type, [CanBeNull] HashSet ignoredInterfaces = null) { - return Controllers.GetOrAdd(type.FullName, () => ControllerApiDescriptionModel.Create(name, groupName, type, ignoredInterfaces)); + var key = apiVersion.IsNullOrWhiteSpace() ? type.FullName : $"{apiVersion + "."}{type.FullName}"; + return Controllers.GetOrAdd(key, () => ControllerApiDescriptionModel.Create(name, groupName, isRemoteService, apiVersion, type, ignoredInterfaces)); } public ModuleApiDescriptionModel CreateSubModel(string[] controllers, string[] actions) diff --git a/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/Generators/JQuery/JQueryProxyScriptGenerator.cs b/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/Generators/JQuery/JQueryProxyScriptGenerator.cs index 9917713d34e..d51ff5fe654 100644 --- a/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/Generators/JQuery/JQueryProxyScriptGenerator.cs +++ b/framework/src/Volo.Abp.Http/Volo/Abp/Http/ProxyScripting/Generators/JQuery/JQueryProxyScriptGenerator.cs @@ -114,7 +114,7 @@ private static void AddActionScript(StringBuilder script, string controllerName, if (versionParam != null) { var version = FindBestApiVersion(action); - script.AppendLine($" var {ProxyScriptingJsFuncHelper.NormalizeJsVariableName(versionParam.Name)} = '{version}';"); + script.AppendLine($" var {ProxyScriptingJsFuncHelper.NormalizeJsVariableName(versionParam.Name)} = api_version ? api_version : '{version}';"); } script.AppendLine(" return abp.ajax($.extend(true, {");