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": "管理您的帳戶" } } diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json index 4a565ad189..1db05cebaf 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 enabled", + "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" } } 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/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 0000000000..ab61217e75 Binary files /dev/null and b/docs/en/Community-Articles/2022-02-06-How-to-Hide-ABP-Related-Endpoints-on-Swagger-UI/swagger-document-filter.png differ 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" } ] }, 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" } ] }, 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/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 b5204ae501..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,7 +8,9 @@ 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; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; @@ -64,6 +66,19 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide } } + 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 @@ public class AspNetCoreApiDescriptionModelProvider : IApiDescriptionModelProvide 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 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..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 @@ -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 @@ 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 => 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 @@ 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..f56e9e893a --- /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 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); +} 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 9526809157..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,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, 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 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, {"); 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