From de4699f0991f640478b5977779c86690af97d63b Mon Sep 17 00:00:00 2001 From: Engincan VESKE Date: Tue, 8 Feb 2022 18:20:45 +0300 Subject: [PATCH 01/18] Update en.json --- .../Admin/Localization/Resources/en.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json index 4a565ad189..c9d7c0a474 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json @@ -358,6 +358,13 @@ "LicensePrice": "License price", "PurchaseDate": "Purchase date", "IsAbpBookDownloaded": "ABP book downloaded", - "IsMasteringAbpBookDownloadEnabled": "Abp book download enable" + "IsMasteringAbpBookDownloadEnabled": "Abp book download enable", + "Permission:Accounting:CustomPaymentLinkGenerator": "Generate Custom Payment Link", + "CustomPaymentLink": "Custom Payment Link", + "Menu:CustomPaymentLink": "Custom Payment Link", + "Amount": "Amount", + "GenerateCustomPaymentLink": "Generate Custom Payment Link", + "GeneratedPaymentLink": "Generated Payment Link", + "CopyText": "Copy Text" } } From 393ba91c039787a47d01f910b3186300d5ae2b8e Mon Sep 17 00:00:00 2001 From: maliming Date: Thu, 10 Feb 2022 10:13:53 +0800 Subject: [PATCH 02/18] Compatible with API Versioning. --- .../AspNetCore/Mvc/AbpAspNetCoreMvcOptions.cs | 3 +++ .../Mvc/Conventions/AbpServiceConvention.cs | 2 +- .../HttpContextRequestedApiVersion.cs | 2 +- .../AbpHttpClientProxyServiceConvention.cs | 2 +- .../Client/ClientProxying/ClientProxyBase.cs | 6 +++++ .../ClientProxying/CurrentApiVersionInfo.cs | 22 +++++++++++++++++++ .../ClientProxying/ICurrentApiVersionInfo.cs | 10 +++++++++ 7 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/CurrentApiVersionInfo.cs create mode 100644 framework/src/Volo.Abp.Http.Client/Volo/Abp/Http/Client/ClientProxying/ICurrentApiVersionInfo.cs 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 e615be5ecb..364bb76c1f 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/Conventions/AbpServiceConvention.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/Conventions/AbpServiceConvention.cs index e96ae0df37..9a78479b9c 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 @@ public class AbpServiceConvention : IAbpServiceConvention, ITransientDependency 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 c6537ac98d..09db4c3ce2 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 2cbe02c95e..9650377580 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 @@ public class AbpHttpClientProxyServiceConvention : AbpServiceConvention { 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 85c4848f02..b24b7a6b5d 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) { @@ -156,6 +157,11 @@ public class ClientProxyBase : ITransientDependency 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 0000000000..d2040e692c --- /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, ITransientDependency +{ + public ApiVersionInfo ApiVersionInfo => _currentPrincipal.Value; + + private readonly AsyncLocal _currentPrincipal = new AsyncLocal(); + + public virtual IDisposable Change(ApiVersionInfo apiVersionInfo) + { + var parent = ApiVersionInfo; + _currentPrincipal.Value = apiVersionInfo; + return new DisposeAction(() => + { + _currentPrincipal.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 0000000000..9a8fe264bf --- /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); +} From cc60aea2c767e5c7078b4995da705ec29084c15c Mon Sep 17 00:00:00 2001 From: maliming Date: Thu, 10 Feb 2022 14:14:51 +0800 Subject: [PATCH 03/18] Add version to `Controllers` --- .../Mvc/AspNetCoreApiDescriptionModelProvider.cs | 1 + .../Http/Client/ClientProxying/CurrentApiVersionInfo.cs | 8 ++++---- .../Volo/Abp/Http/Modeling/ModuleApiDescriptionModel.cs | 5 +++-- 3 files changed, 8 insertions(+), 6 deletions(-) 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 b5204ae501..793ad22282 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 @@ -88,6 +88,7 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide _options.ControllerNameGenerator(controllerType, setting), FindGroupName(controllerType) ?? apiDescription.GroupName, controllerType, + apiDescription.GroupName, _modelOptions.IgnoredInterfaces ); 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 index d2040e692c..bd3ea89abf 100644 --- 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 @@ -6,17 +6,17 @@ namespace Volo.Abp.Http.Client.ClientProxying; public class CurrentApiVersionInfo : ICurrentApiVersionInfo, ITransientDependency { - public ApiVersionInfo ApiVersionInfo => _currentPrincipal.Value; + public ApiVersionInfo ApiVersionInfo => _currentApiVersionInfo.Value; - private readonly AsyncLocal _currentPrincipal = new AsyncLocal(); + private readonly AsyncLocal _currentApiVersionInfo = new AsyncLocal(); public virtual IDisposable Change(ApiVersionInfo apiVersionInfo) { var parent = ApiVersionInfo; - _currentPrincipal.Value = apiVersionInfo; + _currentApiVersionInfo.Value = apiVersionInfo; return new DisposeAction(() => { - _currentPrincipal.Value = parent; + _currentApiVersionInfo.Value = parent; }); } } 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 9526809157..241d128dfb 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 class ModuleApiDescriptionModel 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, Type type, [CanBeNull]string apiVersion, [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, type, ignoredInterfaces)); } public ModuleApiDescriptionModel CreateSubModel(string[] controllers, string[] actions) From 5405171804def7565994d30ca09df153c7b22091 Mon Sep 17 00:00:00 2001 From: maliming Date: Thu, 10 Feb 2022 16:10:35 +0800 Subject: [PATCH 04/18] Remove duplicate controller in `api-definition`. --- .../Mvc/AspNetCoreApiDescriptionModelProvider.cs | 9 +++++++++ 1 file changed, 9 insertions(+) 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 793ad22282..2d561fd484 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 @@ -64,6 +64,15 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide } } + foreach (var (moduleName, module) in model.Modules) + { + var controllers = module.Controllers.GroupBy(x => x.Value.Type).ToList(); + foreach (var controller in controllers.Where(x => x.Count() > 1)) + { + module.Controllers.RemoveAll(x => controller.OrderBy(c => c.Value.Type).Skip(1).Contains(x)); + } + } + return model; } From 2bbc920a77f4560c88ecd1bdcbcd65ed6a295bd6 Mon Sep 17 00:00:00 2001 From: maliming Date: Thu, 10 Feb 2022 17:03:53 +0800 Subject: [PATCH 05/18] Update AspNetCoreApiDescriptionModelProvider.cs --- .../Abp/AspNetCore/Mvc/AspNetCoreApiDescriptionModelProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 2d561fd484..a16b22ccc8 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 @@ -69,7 +69,7 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide var controllers = module.Controllers.GroupBy(x => x.Value.Type).ToList(); foreach (var controller in controllers.Where(x => x.Count() > 1)) { - module.Controllers.RemoveAll(x => controller.OrderBy(c => c.Value.Type).Skip(1).Contains(x)); + module.Controllers.RemoveAll(x => controller.OrderBy(c => c.Value.ControllerGroupName).Skip(1).Contains(x)); } } From 16fe907362027a5f0e9e4e658975359467753e43 Mon Sep 17 00:00:00 2001 From: liangshiwei Date: Thu, 10 Feb 2022 17:54:02 +0800 Subject: [PATCH 06/18] Update ClientProxyBase.cs --- .../Volo/Abp/Http/Client/ClientProxying/ClientProxyBase.cs | 2 ++ 1 file changed, 2 insertions(+) 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 b24b7a6b5d..01593f2d76 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 @@ -67,6 +67,8 @@ public class ClientProxyBase : ITransientDependency action, action.Parameters .GroupBy(x => x.NameOnMethod) + //TODO: make namen configurable + .Where(x=> 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)); From 9785cb761d6ae362a203b0445970ec140db6788f Mon Sep 17 00:00:00 2001 From: liangshiwei Date: Thu, 10 Feb 2022 17:57:59 +0800 Subject: [PATCH 07/18] Update ClientProxyBase.cs --- .../Volo/Abp/Http/Client/ClientProxying/ClientProxyBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 01593f2d76..2489cbf58c 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 @@ -67,7 +67,7 @@ public class ClientProxyBase : ITransientDependency action, action.Parameters .GroupBy(x => x.NameOnMethod) - //TODO: make namen configurable + //TODO: make names configurable .Where(x=> 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), From c50d29bdda7a651cb2964c607f2ce6cefda4949a Mon Sep 17 00:00:00 2001 From: liangshiwei Date: Thu, 10 Feb 2022 18:04:52 +0800 Subject: [PATCH 08/18] Update ClientProxyBase.cs --- .../Volo/Abp/Http/Client/ClientProxying/ClientProxyBase.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 2489cbf58c..ec6abb1dab 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 @@ -63,12 +63,14 @@ public class ClientProxyBase : ITransientDependency { 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=> x.Key != "api-version" && x.Key !="apiVersion") + .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)); From e91b8bd539809f1d552c9e8c6d34daa243096903 Mon Sep 17 00:00:00 2001 From: maliming Date: Thu, 10 Feb 2022 18:19:08 +0800 Subject: [PATCH 09/18] Update CurrentApiVersionInfo.cs --- .../Abp/Http/Client/ClientProxying/CurrentApiVersionInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index bd3ea89abf..f56e9e893a 100644 --- 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 @@ -4,7 +4,7 @@ using Volo.Abp.DependencyInjection; namespace Volo.Abp.Http.Client.ClientProxying; -public class CurrentApiVersionInfo : ICurrentApiVersionInfo, ITransientDependency +public class CurrentApiVersionInfo : ICurrentApiVersionInfo, ISingletonDependency { public ApiVersionInfo ApiVersionInfo => _currentApiVersionInfo.Value; From d3e918bc3f1a67bd3d21fec6936f227da1af478d Mon Sep 17 00:00:00 2001 From: maliming Date: Thu, 10 Feb 2022 19:02:13 +0800 Subject: [PATCH 10/18] Update JQueryProxyScriptGenerator.cs --- .../Generators/JQuery/JQueryProxyScriptGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 9917713d34..d51ff5fe65 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 @@ public class JQueryProxyScriptGenerator : IProxyScriptGenerator, ITransientDepen 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, {"); From bab5947a7baf720e4d88995e9670adb72734704f Mon Sep 17 00:00:00 2001 From: maliming Date: Fri, 11 Feb 2022 09:53:54 +0800 Subject: [PATCH 11/18] `GetProperty` --- .../AspNetCore/Mvc/AspNetCoreApiDescriptionModelProvider.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 a16b22ccc8..28e45abfd1 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 @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Mvc.ModelBinding; +using Microsoft.AspNetCore.Mvc.Versioning; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; @@ -97,7 +98,7 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide _options.ControllerNameGenerator(controllerType, setting), FindGroupName(controllerType) ?? apiDescription.GroupName, controllerType, - apiDescription.GroupName, + apiDescription.GetProperty()?.ToString(), _modelOptions.IgnoredInterfaces ); From e75f0a29ecbd341f25f5d704f89d90a2b1cbcc30 Mon Sep 17 00:00:00 2001 From: maliming Date: Fri, 11 Feb 2022 10:54:59 +0800 Subject: [PATCH 12/18] Add `IsRemoteService` and `ApiVersion` to `ControllerApiDescriptionModel`. --- .../Mvc/ApiDescriptionExtensions.cs | 34 +++++++++++++++++++ .../AbpRemoteServiceApiDescriptionProvider.cs | 27 +-------------- .../AspNetCoreApiDescriptionModelProvider.cs | 12 +++++-- .../Modeling/ControllerApiDescriptionModel.cs | 8 ++++- .../Modeling/ModuleApiDescriptionModel.cs | 4 +-- 5 files changed, 53 insertions(+), 32 deletions(-) create mode 100644 framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApiDescriptionExtensions.cs 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 0000000000..fdf33e788d --- /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 ff01af0286..23cc2e155d 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 class AbpRemoteServiceApiDescriptionProvider : IApiDescriptionProvider, I { 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 @@ public class AbpRemoteServiceApiDescriptionProvider : IApiDescriptionProvider, I 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 28e45abfd1..dbc7532aab 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,6 +8,7 @@ using Microsoft.AspNetCore.Authorization; 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; @@ -65,12 +66,16 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide } } - foreach (var (moduleName, module) in model.Modules) + 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)) { - module.Controllers.RemoveAll(x => controller.OrderBy(c => c.Value.ControllerGroupName).Skip(1).Contains(x)); + 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."); + } } } @@ -97,8 +102,9 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide var controllerModel = moduleModel.GetOrAddController( _options.ControllerNameGenerator(controllerType, setting), FindGroupName(controllerType) ?? apiDescription.GroupName, - controllerType, + apiDescription.IsRemoteService(), apiDescription.GetProperty()?.ToString(), + controllerType, _modelOptions.IgnoredInterfaces ); 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 5f93e0d863..05021a9115 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 class 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 241d128dfb..48b6cd3cda 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,10 +49,10 @@ public class ModuleApiDescriptionModel return Controllers[controller.Type] = controller; } - public ControllerApiDescriptionModel GetOrAddController(string name, string groupName, Type type, [CanBeNull]string apiVersion, [CanBeNull] HashSet ignoredInterfaces = null) + public ControllerApiDescriptionModel GetOrAddController(string name, string groupName, bool isRemoteService, string apiVersion, Type type, [CanBeNull] HashSet ignoredInterfaces = null) { var key = apiVersion.IsNullOrWhiteSpace() ? type.FullName : $"{apiVersion + "."}{type.FullName}"; - return Controllers.GetOrAdd(key, () => ControllerApiDescriptionModel.Create(name, groupName, type, ignoredInterfaces)); + return Controllers.GetOrAdd(key, () => ControllerApiDescriptionModel.Create(name, groupName, isRemoteService, apiVersion, type, ignoredInterfaces)); } public ModuleApiDescriptionModel CreateSubModel(string[] controllers, string[] actions) From 3d3923c37b3db52939a12d5bc67ce0af720ee2e2 Mon Sep 17 00:00:00 2001 From: maliming Date: Fri, 11 Feb 2022 13:48:39 +0800 Subject: [PATCH 13/18] Add `API Versioning System` document. --- docs/en/API/API-Versioning.md | 349 ++++++++++++++++++++++++++++++++++ docs/en/docs-nav.json | 4 + 2 files changed, 353 insertions(+) create mode 100644 docs/en/API/API-Versioning.md diff --git a/docs/en/API/API-Versioning.md b/docs/en/API/API-Versioning.md new file mode 100644 index 0000000000..5eefb129c3 --- /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 943408d70a..6df98cb05a 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" } ] }, From 4b4f73a668e57d3e3f66bacfebe068ffb323ab57 Mon Sep 17 00:00:00 2001 From: liangshiwei Date: Fri, 11 Feb 2022 16:03:33 +0800 Subject: [PATCH 14/18] Add document --- docs/zh-Hans/API/API-Versioning.md | 347 +++++++++++++++++++++++++++++ docs/zh-Hans/docs-nav.json | 4 + 2 files changed, 351 insertions(+) create mode 100644 docs/zh-Hans/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 0000000000..81bb6574c6 --- /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 ebe562a97e..e5f736088e 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" } ] }, From ad63a201325c563e20664429a1a0a0005b828e0a Mon Sep 17 00:00:00 2001 From: Engincan VESKE Date: Mon, 14 Feb 2022 11:35:11 +0300 Subject: [PATCH 15/18] Add new localizations for account.abp.io --- .../AbpIoLocalization/Account/Localization/Resources/ar.json | 3 ++- .../AbpIoLocalization/Account/Localization/Resources/cs.json | 3 ++- .../Account/Localization/Resources/de-DE.json | 3 ++- .../Account/Localization/Resources/en-GB.json | 3 ++- .../AbpIoLocalization/Account/Localization/Resources/en.json | 3 ++- .../AbpIoLocalization/Account/Localization/Resources/es.json | 3 ++- .../AbpIoLocalization/Account/Localization/Resources/fi.json | 3 ++- .../AbpIoLocalization/Account/Localization/Resources/fr.json | 3 ++- .../AbpIoLocalization/Account/Localization/Resources/hi.json | 3 ++- .../AbpIoLocalization/Account/Localization/Resources/hu.json | 3 ++- .../AbpIoLocalization/Account/Localization/Resources/is.json | 3 ++- .../AbpIoLocalization/Account/Localization/Resources/it.json | 3 ++- .../AbpIoLocalization/Account/Localization/Resources/nl.json | 3 ++- .../Account/Localization/Resources/pl-PL.json | 3 ++- .../Account/Localization/Resources/pt-BR.json | 3 ++- .../Account/Localization/Resources/ro-RO.json | 3 ++- .../AbpIoLocalization/Account/Localization/Resources/ru.json | 3 ++- .../AbpIoLocalization/Account/Localization/Resources/sk.json | 3 ++- .../AbpIoLocalization/Account/Localization/Resources/sl.json | 3 ++- .../AbpIoLocalization/Account/Localization/Resources/tr.json | 3 ++- .../AbpIoLocalization/Account/Localization/Resources/vi.json | 3 ++- .../Account/Localization/Resources/zh-Hans.json | 3 ++- .../Account/Localization/Resources/zh-Hant.json | 3 ++- 23 files changed, 46 insertions(+), 23 deletions(-) diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/ar.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/ar.json index a60af9f218..b71f392b54 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/ar.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/ar.json @@ -10,6 +10,7 @@ "CommercialHomePage": "الصفحة الرئيسية للنظام التجاري", "CommercialSupportWebSite": "موقع الدعم الفني للنظام التجاري", "CommunityWebSite": "موقع مجتمع ABP", - "ManageAccount": "حسابي | ABP.IO" + "ManageAccount": "حسابي | ABP.IO", + "ManageYourAccount": "إدارة حسابك" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/cs.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/cs.json index c66f711c04..6ccaea4234 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/cs.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/cs.json @@ -10,6 +10,7 @@ "CommercialHomePage": "Obchodní domovská stránka", "CommercialSupportWebSite": "Webová stránka komerční podpory", "CommunityWebSite": "Web komunity ABP", - "ManageAccount": "Můj účet | ABP.IO" + "ManageAccount": "Můj účet | ABP.IO", + "ManageYourAccount": "Správa vášho účtu" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/de-DE.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/de-DE.json index dd4b48c417..7358fc36e2 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/de-DE.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/de-DE.json @@ -10,6 +10,7 @@ "CommercialHomePage": "Commercial Website", "CommercialSupportWebSite": "Commercial Support-Website", "CommunityWebSite": "ABP Community-Website", - "ManageAccount": "Mein Konto | ABP.IO" + "ManageAccount": "Mein Konto | ABP.IO", + "ManageYourAccount": "Verwalten Sie Ihr Konto" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/en-GB.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/en-GB.json index c20405a8d3..9b8dd444bc 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/en-GB.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/en-GB.json @@ -10,6 +10,7 @@ "CommercialHomePage": "Commercial home page", "CommercialSupportWebSite": "Commercial support web site", "CommunityWebSite": "ABP community web site", - "ManageAccount": "My Account | ABP.IO" + "ManageAccount": "My Account | ABP.IO", + "ManageYourProfile": "Manage your profile" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/en.json index c4581a5fd8..eebaf72246 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/en.json @@ -10,6 +10,7 @@ "CommercialHomePage": "Commercial home page", "CommercialSupportWebSite": "Commercial support web site", "CommunityWebSite": "ABP community web site", - "ManageAccount": "My Account | ABP.IO" + "ManageAccount": "My Account | ABP.IO", + "ManageYourProfile": "Manage your profile" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/es.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/es.json index eca4dbd13b..131af96b99 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/es.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/es.json @@ -10,6 +10,7 @@ "CommercialHomePage": "Página de inicio comercial", "CommercialSupportWebSite": "Sitio web de soporte comercial", "CommunityWebSite": "Sitio web comunidad ABP", - "ManageAccount": "Mi cuenta | ABP.IO" + "ManageAccount": "Mi cuenta | ABP.IO", + "ManageYourAccount": "Administra tu cuenta" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/fi.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/fi.json index 9dbf9bed65..a7a52d3811 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/fi.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/fi.json @@ -10,6 +10,7 @@ "CommercialHomePage": "Kaupallinen kotisivu", "CommercialSupportWebSite": "Kaupallisen tuen verkkosivusto", "CommunityWebSite": "ABP-yhteisön verkkosivusto", - "ManageAccount": "Oma tili | ABP.IO" + "ManageAccount": "Oma tili | ABP.IO", + "ManageYourAccount": "Hallitse tiliäsi" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/fr.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/fr.json index 8457c19723..189dea7b8e 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/fr.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/fr.json @@ -10,6 +10,7 @@ "CommercialHomePage": "Page d'accueil commerciale", "CommercialSupportWebSite": "Site Web de support commercial", "CommunityWebSite": "Site Web de la communauté ABP", - "ManageAccount": "Mon compte | ABP.IO" + "ManageAccount": "Mon compte | ABP.IO", + "ManageYourAccount": "Gérer votre compte" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/hi.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/hi.json index 7df9df3ff3..461acf6af3 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/hi.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/hi.json @@ -10,6 +10,7 @@ "CommercialHomePage": "वाणिज्यिक मुख पृष्ठ", "CommercialSupportWebSite": "वाणिज्यिक समर्थन वेब साइट", "CommunityWebSite": "एबीपी सामुदायिक वेब साइट", - "ManageAccount": "मेरा खाता | एबीपी.आईओ" + "ManageAccount": "मेरा खाता | एबीपी.आईओ", + "ManageYourAccount": "अपने खाते को प्रबंधित करें" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/hu.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/hu.json index 97a0dbe336..4738183af4 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/hu.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/hu.json @@ -10,6 +10,7 @@ "CommercialHomePage": "Kereskedelmi honlap", "CommercialSupportWebSite": "Kereskedelmi támogatási webhely", "CommunityWebSite": "ABP közösségi webhely", - "ManageAccount": "Saját fiók | ABP.IO" + "ManageAccount": "Saját fiók | ABP.IO", + "ManageYourAccount": "Fiók kezelése" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/is.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/is.json index a2293778f8..24b9ebe9f3 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/is.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/is.json @@ -10,6 +10,7 @@ "CommercialHomePage": "Heimasíða Commercial útgáfunnarinnar", "CommercialSupportWebSite": "Þjónustu síða Commercial útgáfunnarinnar", "CommunityWebSite": "Samfélagssíða ABP", - "ManageAccount": "Minn aðgangur | ABP.IO" + "ManageAccount": "Minn aðgangur | ABP.IO", + "ManageYourAccount": "Stjórnaðu aðganginn þinn" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/it.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/it.json index 4670298064..bad754febb 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/it.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/it.json @@ -10,6 +10,7 @@ "CommercialHomePage": "Home page di Commercial", "CommercialSupportWebSite": "Sito web supporto di Commercial", "CommunityWebSite": "Sito web della comunità ABP", - "ManageAccount": "Il mio account | ABP.IO" + "ManageAccount": "Il mio account | ABP.IO", + "ManageYourAccount": "Gestisci il tuo account" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/nl.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/nl.json index 8a34d40c11..71b4c45b56 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/nl.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/nl.json @@ -10,6 +10,7 @@ "CommercialHomePage": "Commerciële startpagina", "CommercialSupportWebSite": "Commerciële ondersteuningswebsite", "CommunityWebSite": "ABP-communitywebsite", - "ManageAccount": "Mijn account | ABP.IO" + "ManageAccount": "Mijn account | ABP.IO", + "ManageYourAccount": "Beheer uw account" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/pl-PL.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/pl-PL.json index 565a77b915..4cbdbb60af 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/pl-PL.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/pl-PL.json @@ -10,6 +10,7 @@ "CommercialHomePage": "Komercyjna strona główna", "CommercialSupportWebSite": "Witryna wsparcia komercyjnego", "CommunityWebSite": "Strona społeczności ABP", - "ManageAccount": "Moje konto | ABP.IO" + "ManageAccount": "Moje konto | ABP.IO", + "ManageYourAccount": "Zarządzaj swoim kontem" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/pt-BR.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/pt-BR.json index 6d218da0fd..32627455fe 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/pt-BR.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/pt-BR.json @@ -10,6 +10,7 @@ "CommercialHomePage": "Página inicial comercial", "CommercialSupportWebSite": "Site de suporte comercial", "CommunityWebSite": "Site da comunidade ABP", - "ManageAccount": "Minha conta | ABP.IO" + "ManageAccount": "Minha conta | ABP.IO", + "ManageYourAccount": "Gerenciar sua conta" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/ro-RO.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/ro-RO.json index 578778d6d1..c5ba3acd6c 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/ro-RO.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/ro-RO.json @@ -10,6 +10,7 @@ "CommercialHomePage": "Pagina principală comercială", "CommercialSupportWebSite": "Site-ul web al suportului comercial", "CommunityWebSite": "Site-ul web al comunităţii ABP", - "ManageAccount": "Contul meu | ABP.IO" + "ManageAccount": "Contul meu | ABP.IO", + "ManageYourAccount": "Gestionaţi contul dvs." } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/ru.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/ru.json index e79da05322..bb33355d2e 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/ru.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/ru.json @@ -10,6 +10,7 @@ "CommercialHomePage": "Главная страница коммерческой версии", "CommercialSupportWebSite": "Сайт коммерческой поддержки", "CommunityWebSite": "Веб-сайт сообщества ABP", - "ManageAccount": "Моя учетная запись | ABP.IO" + "ManageAccount": "Моя учетная запись | ABP.IO", + "ManageYourAccount": "Управление вашей учетной записью" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/sk.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/sk.json index d1bbeb8678..c5791c5742 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/sk.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/sk.json @@ -10,6 +10,7 @@ "CommercialHomePage": "Komerčná domovská stránka", "CommercialSupportWebSite": "Webová stránka komerčnej podpory", "CommunityWebSite": "Webová stránka ABP komunity", - "ManageAccount": "Môj účet | ABP.IO" + "ManageAccount": "Môj účet | ABP.IO", + "ManageYourAccount": "Môj účet" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/sl.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/sl.json index 7527e3c878..3eb2e10ae9 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/sl.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/sl.json @@ -10,6 +10,7 @@ "CommercialHomePage": "Domača stran različice Commercial", "CommercialSupportWebSite": "Spletna stran za komercialno podporo", "CommunityWebSite": "Spletno mesto skupnosti ABP", - "ManageAccount": "Moj račun | ABP.IO" + "ManageAccount": "Moj račun | ABP.IO", + "ManageYourAccount": "Upravljanje svojega računa" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/tr.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/tr.json index a7fec38074..132b746a9e 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/tr.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/tr.json @@ -10,6 +10,7 @@ "CommercialHomePage": "Kurumsal ana sayfa", "CommercialSupportWebSite": "Kurumsal destek web sitesi", "CommunityWebSite": "ABP topluluk web sitesi", - "ManageAccount": "Hesabım | ABP.IO" + "ManageAccount": "Hesabım | ABP.IO", + "ManageYourProfile": "Hesabımı yönet" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/vi.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/vi.json index 7b4932dcab..a8bcbee2eb 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/vi.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/vi.json @@ -10,6 +10,7 @@ "CommercialHomePage": "Trang chủ thương mại", "CommercialSupportWebSite": "Trang web hỗ trợ thương mại", "CommunityWebSite": "Trang web cộng đồng ABP", - "ManageAccount": "Tài khoản của tôi | ABP.IO" + "ManageAccount": "Tài khoản của tôi | ABP.IO", + "ManageYourAccount": "Quản lý tài khoản của bạn" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/zh-Hans.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/zh-Hans.json index 15086ddc81..e790ad80af 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/zh-Hans.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/zh-Hans.json @@ -10,6 +10,7 @@ "CommercialHomePage": "商业版首页", "CommercialSupportWebSite": "商业版支持网站", "CommunityWebSite": "ABP社区网站", - "ManageAccount": "我的帐户 | ABP.IO" + "ManageAccount": "我的帐户 | ABP.IO", + "ManageYourAccount": "管理您的帐户" } } diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/zh-Hant.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/zh-Hant.json index fd1a0fbb8a..3056265dca 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/zh-Hant.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Account/Localization/Resources/zh-Hant.json @@ -10,6 +10,7 @@ "CommercialHomePage": "商業版首頁", "CommercialSupportWebSite": "商業版支援網站", "CommunityWebSite": "ABP社區網站", - "ManageAccount": "我的帳戶 | ABP.IO" + "ManageAccount": "我的帳戶 | ABP.IO", + "ManageYourAccount": "管理您的帳戶" } } From afd5f64cc48ad45d084350b0b3715b11f16a215e Mon Sep 17 00:00:00 2001 From: Engincan VESKE Date: Mon, 14 Feb 2022 14:35:54 +0300 Subject: [PATCH 16/18] Update the "How to Hide ABP Related Endpoints on Swagger UI" article --- .../POST.md | 98 +++++++++++++----- .../swagger-document-filter.png | Bin 0 -> 72070 bytes 2 files changed, 74 insertions(+), 24 deletions(-) create mode 100644 docs/en/Community-Articles/2022-02-06-How-to-Hide-ABP-Related-Endpoints-on-Swagger-UI/swagger-document-filter.png diff --git a/docs/en/Community-Articles/2022-02-06-How-to-Hide-ABP-Related-Endpoints-on-Swagger-UI/POST.md b/docs/en/Community-Articles/2022-02-06-How-to-Hide-ABP-Related-Endpoints-on-Swagger-UI/POST.md index 77b98d6ff0..5c9c576e9c 100644 --- a/docs/en/Community-Articles/2022-02-06-How-to-Hide-ABP-Related-Endpoints-on-Swagger-UI/POST.md +++ b/docs/en/Community-Articles/2022-02-06-How-to-Hide-ABP-Related-Endpoints-on-Swagger-UI/POST.md @@ -36,7 +36,64 @@ After we've run the application and signed in, we can navigate to **/swagger** t In our scenario, we will hide endpoints that start with "/api/abp". So let's start to do this. -> In this sample project, we only hide our endpoints that start with the "/api/abp" prefix by definining it in our [CustomSwaggerFilter](https://github.com/EngincanV/ABP-Hide-Swagger-Endpoint-Demo/blob/main/src/SwaggerSettingsDemo.Web/Filters/CustomSwaggerFilter.cs#L29). If you want to hide some other endpoints, you can update the class. +### Creating a Document Filter To Hide ABP Related Endpoints + +> In this sample project, we'll only hide our endpoints that start with the "/api/abp" prefix by defining it in our [CustomSwaggerFilter](https://github.com/EngincanV/ABP-Hide-Swagger-Endpoint-Demo/blob/main/src/SwaggerSettingsDemo.Web/Filters/CustomSwaggerFilter.cs#L28) class. If you want to hide some other endpoints, you can update the class. + +To hide/show ABP related endpoints on Swagger UI, we can create a [`DocumentFilter`](https://github.com/domaindrivendev/Swashbuckle.AspNetCore#document-filters). By creating a document filter, we can have full control over the endpoints that need to be shown. + +* Create a document filter class named `CustomSwaggerFilter`(/Filters/CustomSwaggerFilter.cs) in the `.Web` layer: + +> For now, we'll implement this class as hard-coded without checking any settings. Then we'll define a setting for this purpose, and add a new setting management section to manage it on runtime (Enable/Disable). + +```csharp +using System.Linq; +using Microsoft.OpenApi.Models; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace SwaggerSettingsDemo.Web.Filters; + +public class CustomSwaggerFilter : IDocumentFilter +{ + public void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context) + { + //remove paths those start with /api/abp prefix + swaggerDoc.Paths + .Where(x => x.Key.ToLowerInvariant().StartsWith("/api/abp")) + .ToList() + .ForEach(x => swaggerDoc.Paths.Remove(x.Key)); + } +} +``` + +We've created a simple class that implements `IDocumentFilter.Apply` method. In that method, we simply remove paths those start with "/api/abp" prefix. You can customize this expression for your needs, I just wanted to hide endpoints those start with "/api/abp" prefix as a sample. + +* Then, we need to add this document filter as a swagger document filter. To do this we need to open the `ConfigureSwaggerServices` method in the `*WebModule.cs` class and update the content as below: + +```csharp +private void ConfigureSwaggerServices(IServiceCollection services) + { + services.AddAbpSwaggerGen( + options => + { + //... + + //add a new document filter + options.DocumentFilter(); + } + ); + } +``` + +Now we can run the application again and navigate to **/swagger** endpoint to see endpoints. + +![](./swagger-document-filter.png) + +As you can see in the above image, two endpoints are hidden now (**AbpApiDefinition** and **AbpApplicationConfiguration**). + +Let's make it more dynamic and add a setting management section to our **/Settings** page. By doing that, we can dynamically show/hide ABP-related endpoints by checking setting values and applying filtering to Swagger UI on runtime. + +### Defining Settings * Firstly, create a class named `SwaggerSettingConsts` (under `*.Domain.Shared` project): @@ -83,10 +140,12 @@ public class SwaggerSettingsDemoSettingDefinitionProvider : SettingDefinitionPro } ``` -Here we've defined a setting to use in our application. +Here we've defined a setting to use it in our application. > ABP automatically discovers this class and registers the setting definitions. +### Creating Application Service Methods for Swagger Setting Management + After defining a setting, now we can create an application service interface and add two methods to simply get or update the value of our setting. * Create an application service interface named `ISwaggerSettingAppService` (or any name you want): @@ -137,12 +196,16 @@ public class SwaggerSettingAppService : SwaggerSettingsDemoAppService, ISwaggerS } ``` +> Here, I didn't make any permission check to keep the article as short as possible. So, if you want to use it on your system, you should define permissions and make permission checks, in these application service methods. + We've injected two interfaces for the application service implementation: `ISettingProvider` and `ISettingManager` > **ISettingProvider**: Used for getting the value of a setting or getting the values of all settings. It's recommended to use it to read the setting values because it implements caching. > **ISettingManager**: Used for getting and setting the values of the settings. +### Creating a New Setting Management Section + After implementing our application service, now we can add a new group to our "Setting Management UI". * Open the `*.Web` project and create a file named `SwaggerHideEndpointsViewComponent`(/Components/SwaggerHideEndpoints/SwaggerHideEndpointsViewComponent.cs): @@ -307,9 +370,13 @@ After all these steps, if we run our application and navigate to **/SettingManag ![](./swagger-settings.png) -To hide/show ABP related endpoints on Swagger UI, we can create a [`DocumentFilter`](https://github.com/domaindrivendev/Swashbuckle.AspNetCore#document-filters). By creating a document filter, we can have full control over the endpoints that need to be shown. +### Update the Document Filter -* Create a document filter class named `CustomSwaggerFilter`(/Filters/CustomSwaggerFilter.cs) in the `.Web` layer: +We've already defined a document filter but it is hard-coded, in other words, it hides ABP-related endpoints in every case without checking the setting value. + +So, we need to update the `CustomSwaggerFilter` class and apply filtering only if the setting value is true. + +* To do that, update the `CustomSwaggerFilter` method as below: ```csharp using System.Linq; @@ -346,32 +413,15 @@ public class CustomSwaggerFilter : IDocumentFilter } ``` -We've created a simple class that implements `IDocumentFilter.Apply` method. And in that method, we simply need to get our setting value to see whether should we enable hiding ABP related endpoints or not. To do this, we've used the `ISwaggerSettingAppService.GetSettingByNameAsync` method to get the setting value, but as you can see we've wrapped it with the `AsyncHelper.RunSync` method because `IDocumentFilter.Apply` is not an async method so we need to run this method synchronously. +Here we simply need to get our setting value to see whether should we enable hiding ABP related endpoints or not. We've used the `ISwaggerSettingAppService.GetSettingByNameAsync` method to get the setting value, but as you can see we've wrapped it with the `AsyncHelper.RunSync` method because `IDocumentFilter.Apply` is not an async method so we need to run this method synchronously. After getting the setting value, we need to ensure that it's both a valid and true setting value, otherwise we don't need to filter the endpoints and show all of them. -If the setting value is true, we can simply remove the paths that start with "/api/abp" prefix. +If the setting value is true, we can simply remove the paths that start with the "/api/abp" prefix as before. > ABP provides us a class named AsyncHelper and this class provides some helper methods to work with async methods. E.g. RunSync method in here runs the async method synchronously. - -* Finally, we can add this document filter as a swagger document filter. To do this we need to open the `ConfigureSwaggerServices` method in the `*WebModule.cs` class and update the content as below: - -```csharp -private void ConfigureSwaggerServices(IServiceCollection services) - { - services.AddAbpSwaggerGen( - options => - { - //... - - options.DocumentFilter(); //add a new document filter - } - ); - } -``` - -That's it. Now we can open the Setting Management page and enable/disable the swagger option and see its effect. +That's it. Now we can open the Setting Management page and enable/disable the swagger option by selecting the checkbox and show/hide ABP-related endpoints on runtime. ![](./swagger-hide-endpoints.gif) diff --git a/docs/en/Community-Articles/2022-02-06-How-to-Hide-ABP-Related-Endpoints-on-Swagger-UI/swagger-document-filter.png b/docs/en/Community-Articles/2022-02-06-How-to-Hide-ABP-Related-Endpoints-on-Swagger-UI/swagger-document-filter.png new file mode 100644 index 0000000000000000000000000000000000000000..ab61217e75893f9c8c05d4e308e2d103411aa2ca GIT binary patch literal 72070 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 z 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 literal 0 HcmV?d00001 From 554f05067f5379b228d8504e64df27130acb7313 Mon Sep 17 00:00:00 2001 From: albert <9526587+ebicoglu@users.noreply.github.com> Date: Mon, 14 Feb 2022 17:06:29 +0300 Subject: [PATCH 17/18] Update en.json --- .../AbpIoLocalization/Admin/Localization/Resources/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json index c9d7c0a474..1db05cebaf 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json @@ -358,7 +358,7 @@ "LicensePrice": "License price", "PurchaseDate": "Purchase date", "IsAbpBookDownloaded": "ABP book downloaded", - "IsMasteringAbpBookDownloadEnabled": "Abp book download enable", + "IsMasteringAbpBookDownloadEnabled": "ABP Book download enabled", "Permission:Accounting:CustomPaymentLinkGenerator": "Generate Custom Payment Link", "CustomPaymentLink": "Custom Payment Link", "Menu:CustomPaymentLink": "Custom Payment Link", From bafced3667f38039138c62b8cf308cffca313d83 Mon Sep 17 00:00:00 2001 From: liangshiwei Date: Tue, 15 Feb 2022 16:51:23 +0800 Subject: [PATCH 18/18] Enhanced abp.js --- npm/packs/core/src/abp.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/npm/packs/core/src/abp.js b/npm/packs/core/src/abp.js index c267fc151f..7cb0782f1a 100644 --- a/npm/packs/core/src/abp.js +++ b/npm/packs/core/src/abp.js @@ -73,7 +73,7 @@ var abp = abp || {}; abp.localization = abp.localization || {}; - abp.localization.values = {}; + abp.localization.values = abp.localization.values || {}; abp.localization.localize = function (key, sourceName) { if (sourceName === '_') { //A convention to suppress the localization