Browse Source

Merge branch 'dev' into stsrki/dev-blazorise-1.0

pull/11649/head
Enis Necipoglu 5 years ago
committed by GitHub
parent
commit
6b4a45b73e
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 23
      abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json
  2. 7
      abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json
  3. 2
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/ar.json
  4. 2
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/cs.json
  5. 2
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/de-DE.json
  6. 7
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json
  7. 2
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/es.json
  8. 2
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/fi.json
  9. 2
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/fr.json
  10. 2
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/hi.json
  11. 2
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/hu.json
  12. 2
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/is.json
  13. 2
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/it.json
  14. 2
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/nl.json
  15. 2
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/pl-PL.json
  16. 2
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/pt-BR.json
  17. 2
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/ro-RO.json
  18. 2
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/ru.json
  19. 2
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/sk.json
  20. 2
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/sl.json
  21. 2
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/tr.json
  22. 2
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/vi.json
  23. 2
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/zh-Hans.json
  24. 2
      abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/zh-Hant.json
  25. 9
      docs/en/CLI.md
  26. 601
      docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/README.md
  27. BIN
      docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/android-login-demo.gif
  28. BIN
      docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/compiler-select.png
  29. BIN
      docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/identity-users-request-result.png
  30. BIN
      docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/ios-login-demo.gif
  31. BIN
      docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/macos-login-demo.gif
  32. BIN
      docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/openid-configuration.png
  33. BIN
      docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/Cover.png
  34. 529
      docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/README.md
  35. BIN
      docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/android-users-demo.gif
  36. BIN
      docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/ios-users-demo.gif
  37. BIN
      docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/macos-users-demo.gif
  38. BIN
      docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/net-maui-contentpage-template.png
  39. BIN
      docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/uwp-users-demo.gif
  40. BIN
      docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/xaml-intellisense.png
  41. 529
      docs/en/Community-Articles/2022-02-24-Using-HttpApi.Client-in-MAUI-with-OpenID-Connect/README.md
  42. 100
      docs/en/Multi-Lingual-Entities.md
  43. 4
      docs/en/Previews.md
  44. 2
      docs/en/Samples/Index.md
  45. 2
      docs/en/Tutorials/Part-1.md
  46. 2
      docs/en/Tutorials/Part-10.md
  47. 2
      docs/en/Tutorials/Part-3.md
  48. 2
      docs/en/Tutorials/Part-7.md
  49. 16
      docs/en/UI/Blazor/Error-Handling.md
  50. 9
      docs/zh-Hans/CLI.md
  51. 257
      docs/zh-Hans/Emailing.md
  52. 49
      docs/zh-Hans/MailKit.md
  53. 41
      docs/zh-Hans/Previews.md
  54. 4
      docs/zh-Hans/Tutorials/Part-1.md
  55. 2
      docs/zh-Hans/Tutorials/Part-10.md
  56. 2
      docs/zh-Hans/Tutorials/Part-2.md
  57. 4
      docs/zh-Hans/Tutorials/Part-3.md
  58. 2
      docs/zh-Hans/Tutorials/Part-5.md
  59. 2
      docs/zh-Hans/Tutorials/Part-6.md
  60. 2
      docs/zh-Hans/Tutorials/Part-7.md
  61. 2
      docs/zh-Hans/Tutorials/Part-8.md
  62. 6
      docs/zh-Hans/Tutorials/Part-9.md
  63. 60
      docs/zh-Hans/Upgrading.md
  64. 34
      docs/zh-Hans/docs-nav.json
  65. 2
      framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/datatables/datatables-extensions.js
  66. 12
      framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/Auditing/AbpAuditHubFilter.cs
  67. 12
      framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Auditing/AbpAuditingMiddleware.cs
  68. 22
      framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditingInterceptor.cs
  69. 4
      framework/src/Volo.Abp.BlazoriseUI/Components/AbpExtensibleDataGrid.razor
  70. 12
      framework/src/Volo.Abp.BlazoriseUI/Components/AbpExtensibleDataGrid.razor.cs
  71. 2
      framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizationResourceContributorList.cs
  72. 12
      framework/src/Volo.Abp.Swashbuckle/Microsoft/Extensions/DependencyInjection/AbpSwaggerGenOptionsExtensions.cs
  73. 28
      framework/src/Volo.Abp.Swashbuckle/Volo/Abp/Swashbuckle/AbpSwashbuckleDocumentFilter.cs
  74. 4
      modules/account/src/Volo.Abp.Account.Web/Pages/Account/Components/ProfileManagementGroup/PersonalInfo/Default.js
  75. 0
      modules/blogging/app/Volo.BloggingTestApp.MongoDB/Volo.BloggingTestApp.MongoDB.abppkg.json
  76. 2
      modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/BlogPosts/create.js
  77. 19
      modules/docs/src/Volo.Docs.Admin.Application/Volo/Docs/Admin/Projects/ProjectAdminAppService.cs
  78. 1
      modules/docs/src/Volo.Docs.Admin.Web/Pages/Docs/Admin/Projects/Edit.cshtml.cs
  79. 20
      modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/FullSearch/Elastic/ElasticDocumentFullSearch.cs
  80. 2
      modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/FullSearch/Elastic/IDocumentFullSearch.cs
  81. 2
      modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/GithubDocumentSource.cs
  82. 2
      modules/setting-management/src/Volo.Abp.SettingManagement.Blazor/Pages/SettingManagement/EmailSettingGroup/EmailSettingGroupViewComponent.razor.cs
  83. 2
      modules/setting-management/src/Volo.Abp.SettingManagement.Blazor/Pages/SettingManagement/SettingManagement.razor
  84. 4
      npm/ng-packs/packages/theme-basic/src/lib/tests/lazy-style.handler.spec.ts
  85. 8
      npm/ng-packs/packages/theme-shared/extensions/src/lib/adapters/date-time.adapter.ts
  86. 2
      npm/ng-packs/packages/theme-shared/extensions/src/lib/enums/props.enum.ts
  87. 4
      npm/ng-packs/packages/theme-shared/src/lib/components/button/button.component.ts
  88. 4
      templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server.Mongo/Program.cs
  89. 4
      templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/Program.cs
  90. 4
      templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host.Mongo/Program.cs
  91. 4
      templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/Program.cs
  92. 4
      templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc.Mongo/Program.cs
  93. 4
      templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/Program.cs

23
abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json

@ -359,12 +359,31 @@
"PurchaseDate": "Purchase date",
"IsAbpBookDownloaded": "ABP book downloaded",
"IsMasteringAbpBookDownloadEnabled": "ABP Book download enabled",
"Permission:Accounting:CustomPaymentLinkGenerator": "Generate Custom Payment Link",
"Permission:Accounting:CustomPaymentLinkGenerator": "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"
"CopyText": "Copy Text",
"Permission:CommunityEvents": "Events",
"Menu:Events": "Events",
"Events": "Events",
"EventType": "Event Type",
"Number": "Number",
"RegistrationURL": "Registration URL",
"URL": "URL",
"EventDeletionConfirmationMessage": "Are you sure you want to delete this event?",
"Enum:EventType:0": "Community Talks",
"CreateAnEvent": "Create an event",
"Permission:CommunitySpeakers": "Speakers",
"CreateASpeaker": "Create a speaker",
"Speakers": "Speakers",
"Image": "Image",
"GithubURL": "Github URL",
"SpeakerDeletionConfirmationMessage": "Are you sure you want to delete this speaker?",
"Menu:Speakers": "Speakers",
"ChooseSpeakerImage": "Choose a speaker image...",
"SpeakerImage": "Speaker image"
}
}

7
abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json

@ -98,6 +98,11 @@
"EULA": "EULA",
"ABPCommercialIntroductionMessage": "Pre-built application modules, advanced startup templates, rapid application development tooling, professional UI themes and premium support.",
"MasteringAbpFrameworkEBook": "Mastering ABP Framework",
"MasteringTheABPFrameworkExplanation": "Written by the creator of the ABP Framework, this book will help you gain a complete understanding of the framework and modern web application development techniques."
"MasteringTheABPFrameworkExplanation": "Written by the creator of the ABP Framework, this book will help you gain a complete understanding of the framework and modern web application development techniques.",
"Speakers": "Speakers",
"PreviousEvents": "Previous Events",
"WatchTheEvent": "Watch the Event",
"RegisterNow": "Register Now",
"ThereIsNoEvent": "There is no event."
}
}

2
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/ar.json

@ -118,7 +118,7 @@
"CreatePostCoverInfo": "لإنشاء منشور فعال ، أضف صورة غلاف. قم بتحميل صور بنسبة عرض إلى ارتفاع تبلغ 16: 9 للحصول على أفضل عرض. الحد الأقصى لحجم الملف: 1 ميغا بايت.",
"ThankYouForContribution": "شكرًا لك على المساهمة في مجتمع ABP.",
"GithubPost": "المادة جيثب",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\"> 1. </span> اكتب مقالة في أي مستودع GitHub عام بتنسيق Markdown. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Posts/2020-12-04-Event-Organizer/Post.md\"> مثال </a>",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\"> 1. </span> اكتب مقالة في أي مستودع GitHub عام بتنسيق Markdown. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md\"> مثال </a>",
"GithubPostSubmitStepTwo": "<span class=\"font-weight-bold\"> 2. </span> أرسل عنوان URL لمقالتك باستخدام النموذج.",
"GithubPostSubmitStepThree": "<span class=\"font-weight-bold\"> 3. </span> سيتم عرض مقالتك في موقع الويب هذا.",
"YoutubeVideo": "فيديو يوتيوب",

2
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/cs.json

@ -118,7 +118,7 @@
"CreatePostCoverInfo": "Pro vytvoření efektního příspěvku přidejte titulní fotku. Pro nejlepší zobrazení nahrajte obrázky s poměrem stran 16:9. Maximální velikost souboru: 1 MB.",
"ThankYouForContribution": "Děkujeme vám za příspěvek do komunity ABP.",
"GithubPost": "Článek Github",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Napište článek na libovolném veřejném úložišti GitHub ve formátu Markdown. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Posts/2020-12-04-Event-Organizer/Post.md\">příklad </a>",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Napište článek na libovolném veřejném úložišti GitHub ve formátu Markdown. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md\">příklad </a>",
"GithubPostSubmitStepTwo": "<span class=\"font-weight-bold\">2.</span> Odešlete adresu URL svého článku pomocí formuláře.",
"GithubPostSubmitStepThree": "<span class=\"font-weight-bold\">3.</span> Váš článek bude vykreslen na této webové stránce.",
"YoutubeVideo": "Youtube Video",

2
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/de-DE.json

@ -118,7 +118,7 @@
"CreatePostCoverInfo": "Um einen effektiven Beitrag zu erstellen, fügen Sie ein Titelbild hinzu. Laden Sie Bilder mit einem Seitenverhältnis von 16:9 hoch, um die beste Ansicht zu erhalten. Maximale Dateigröße: 1 MB.",
"ThankYouForContribution": "Vielen Dank für Ihren Beitrag zur ABP-Community.",
"GithubPost": "Github-Artikel",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Schreiben Sie einen Artikel in einem öffentlichen GitHub-Repository im Markdown-Format. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Posts/2020-12-04-Event-Organizer/Post.md\">Beispiel </a>",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Schreiben Sie einen Artikel in einem öffentlichen GitHub-Repository im Markdown-Format. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md\">Beispiel </a>",
"GithubPostSubmitStepTwo": "<span class=\"font-weight-bold\">2.</span> Senden Sie Ihre Artikel-URL über das Formular.",
"GithubPostSubmitStepThree": "<span class=\"font-weight-bold\">3.</span> Ihr Artikel wird auf dieser Website wiedergegeben.",
"YoutubeVideo": "Youtube-Video",

7
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json

@ -117,7 +117,7 @@
"ExternalPostUrl": "External Post Url",
"ThankYouForContribution": "Thank you for contributing to the ABP Community.",
"GithubPost": "GitHub Post",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Write a post on any public GitHub repository with the Markdown format. <a target=\"_blank\" class=\"fw-6 text-white text-decoration-underline\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Posts/2020-12-04-Event-Organizer/Post.md\">example</a>",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Write a post on any public GitHub repository with the Markdown format. <a target=\"_blank\" class=\"fw-6 text-white text-decoration-underline\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md\">example</a>",
"GithubPostSubmitStepTwo": "<span class=\"font-weight-bold\">2.</span> Submit your post URL using the form.",
"GithubPostSubmitStepThree": "<span class=\"font-weight-bold\">3.</span> Your post will be rendered in this website.",
"YoutubeVideo": "Youtube Video",
@ -162,6 +162,9 @@
"Previous": "Previous",
"Next": "Next",
"Share": "Share",
"SortBy": "Sort by"
"SortBy": "Sort by",
"NoPublishedEventsYet": "No published events yet.",
"SubscribeYoutubeChannel": "Subscribe to the Youtube Channel",
"Enum:EventType:0": "Talks"
}
}

2
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/es.json

@ -118,7 +118,7 @@
"CreatePostCoverInfo": "Para crear una publicación eficaz, agregue una foto de portada. Cargue imágenes con una relación de aspecto de 16: 9 para obtener la mejor vista. Tamaño máximo de archivo: 1 MB.",
"ThankYouForContribution": "Gracias por contribuir a la comunidad ABP.",
"GithubPost": "Artículo de Github",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\"> 1. </span> Escribe un artículo en cualquier repositorio público de GitHub con el formato Markdown. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Posts/2020-12-04-Event-Organizer/Post.md\"> ejemplo </a>",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\"> 1. </span> Escribe un artículo en cualquier repositorio público de GitHub con el formato Markdown. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md\"> ejemplo </a>",
"GithubPostSubmitStepTwo": "<span class=\"font-weight-bold\"> 2. </span> Envíe la URL de su artículo mediante el formulario.",
"GithubPostSubmitStepThree": "<span class=\"font-weight-bold\"> 3. </span> Su artículo se mostrará en este sitio web.",
"YoutubeVideo": "Video de Youtube",

2
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/fi.json

@ -118,7 +118,7 @@
"CreatePostCoverInfo": "Lisää kansikuva, jotta voit luoda tehokkaan viestin. Lataa 16:9 -kuvasuhteen kuvat parhaan näkymän saamiseksi. Tiedoston enimmäiskoko: 1MB.",
"ThankYouForContribution": "Kiitos osallistumisesta ABP Community.",
"GithubPost": "Github-artikkeli",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Kirjoita artikkeli mistä tahansa julkisesta GitHub-arkistosta Markdown-muodossa.<a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Posts/2020-12-04-Event-Organizer/Post.md\"> esimerkki </a>",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Kirjoita artikkeli mistä tahansa julkisesta GitHub-arkistosta Markdown-muodossa.<a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md\"> esimerkki </a>",
"GithubPostSubmitStepTwo": "<span class=\"font-weight-bold\"> 2.</span> Lähetä artikkelin URL-osoite lomaketta käyttämällä.",
"GithubPostSubmitStepThree": "<span class=\"font-weight-bold\"> 3.</span> Artikkelisi renderöidään tällä verkkosivustolla.",
"YoutubeVideo": "Youtube-video",

2
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/fr.json

@ -118,7 +118,7 @@
"CreatePostCoverInfo": "Pour créer un post efficace, ajoutez une photo de couverture. Téléchargez des images au format 16: 9 pour une meilleure vue. Taille maximale du fichier: 1 Mo.",
"ThankYouForContribution": "Merci de contribuer à la communauté ABP.",
"GithubPost": "Post Github",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\"> 1.</span> Rédigez un post sur n'importe quel référentiel GitHub public au format Markdown. Exemple de <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Posts/2020-12-04-Event-Organizer/Post.md\"></a>",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\"> 1.</span> Rédigez un post sur n'importe quel référentiel GitHub public au format Markdown. Exemple de <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md\"></a>",
"GithubPostSubmitStepTwo": "<span class=\"font-weight-bold\"> 2.</span> Envoyez l'URL de votre post à l'aide du formulaire.",
"GithubPostSubmitStepThree": "<span class=\"font-weight-bold\"> 3.</span> Votre post sera rendu sur ce site Web.",
"YoutubeVideo": "Vidéo Youtube",

2
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/hi.json

@ -118,7 +118,7 @@
"CreatePostCoverInfo": "एक प्रभावी पोस्ट बनाने के लिए, एक कवर फ़ोटो जोड़ें। सर्वश्रेष्ठ दृश्य के लिए 16: 9 पहलू अनुपात चित्र अपलोड करें। अधिकतम फ़ाइल आकार: 1 एमबी।",
"ThankYouForContribution": "ABP समुदाय में योगदान के लिए धन्यवाद।",
"GithubPost": "गीथब लेख",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> मार्कडाउन प्रारूप के साथ किसी भी सार्वजनिक GitHub रिपॉजिटरी पर एक लेख लिखें। <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Posts/2020-12-04-Event-Organizer/Post.md\">उदाहरण </a>",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> मार्कडाउन प्रारूप के साथ किसी भी सार्वजनिक GitHub रिपॉजिटरी पर एक लेख लिखें। <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md\">उदाहरण </a>",
"GithubPostSubmitStepTwo": "<span class=\"font-weight-bold\"> 2। </span> फ़ॉर्म का उपयोग करके अपना लेख URL सबमिट करें।",
"GithubPostSubmitStepThree": "<span class=\"font-weight-bold\"> 3। </span> इस वेब साइट में आपका लेख प्रस्तुत किया जाएगा।",
"YoutubeVideo": "यूट्यूब वीडियो",

2
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/hu.json

@ -118,7 +118,7 @@
"CreatePostCoverInfo": "Hatékony bejegyzés létrehozásához adj hozzá egy borítóképet. Tölts fel 16:9-es képarányú képeket a legjobb nézet érdekében. Maximális fájlméret: 1 MB.",
"ThankYouForContribution": "Köszönjük, hogy hozzájárult az ABP közösséghez.",
"GithubPost": "Github cikk",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Írjon cikket bármely nyilvános GitHub-tárhelyről Markdown formátumban. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Posts/2020-12-04-Event-Organizer/Post.md\">példa </a>",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Írjon cikket bármely nyilvános GitHub-tárhelyről Markdown formátumban. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md\">példa </a>",
"GithubPostSubmitStepTwo": "<span class=\"font-weight-bold\">2.</span> Az űrlap segítségével küldje el cikkének URL-jét.",
"GithubPostSubmitStepThree": "<span class=\"font-weight-bold\">3.</span> A cikk ezen a webhelyen jelenik meg.",
"YoutubeVideo": "Youtube videó",

2
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/is.json

@ -118,7 +118,7 @@
"CreatePostCoverInfo": "Til að búa til áhrifaríka færslu skaltu bæta við forsíðumynd. Hladdu upp 16: 9 myndhlutföllum fyrir besta útsýnið. Hámarks skráarstærð: 1MB.",
"ThankYouForContribution": "Þakka þér fyrir að leggja þitt af mörkum til ABP samfélagsins.",
"GithubPost": "Github Greinar",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold \"> 1. </span> Skrifaðu grein á hvaða opinbera GitHub geymslu sem er með markdown sniðinu. <a target=\"_ blank \" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Posts 2020-12-04-Event-Organizer/Post.md \"> dæmi </a>",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold \"> 1. </span> Skrifaðu grein á hvaða opinbera GitHub geymslu sem er með markdown sniðinu. <a target=\"_ blank \" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles 2020-12-04-Event-Organizer/Post.md \"> dæmi </a>",
"GithubPostSubmitStepTwo": "<span class=\"font-weight-bold\">2.</span> Sendu vefslóð greinarinnar með því að nota formið.",
"GithubPostSubmitStepThree": "<span class=\"font-weight-bold\">3.</span> Grein þín verður birt á þessari vefsíðu.",
"YoutubeVideo": "Youtube Videó",

2
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/it.json

@ -118,7 +118,7 @@
"CreatePostCoverInfo": "Per creare un post efficace, aggiungi una foto di copertina. Carica immagini in formato 16: 9 per una visualizzazione migliore. Dimensione massima del file: 1MB.",
"ThankYouForContribution": "Grazie per aver contribuito alla comunità ABP.",
"GithubPost": "Articolo GitHub",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Scrivi un articolo su qualsiasi repository GitHub pubblico con il formato Markdown.<a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Posts/2020-12-04-Event-Organizer/Post.md\">esempio</a>",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Scrivi un articolo su qualsiasi repository GitHub pubblico con il formato Markdown.<a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md\">esempio</a>",
"GithubPostSubmitStepTwo": "<span class=\"font-weight-bold\">2.</span> Invia l'URL del tuo articolo utilizzando il modulo.",
"GithubPostSubmitStepThree": "<span class=\"font-weight-bold\">3.</span> Il tuo articolo verrà visualizzato in questo sito web.",
"YoutubeVideo": "Video Youtube",

2
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/nl.json

@ -118,7 +118,7 @@
"CreatePostCoverInfo": "Voeg een omslagfoto toe om een effectieve post te maken. Upload afbeeldingen met een beeldverhouding van 16:9 voor de beste weergave. Maximale bestandsgrootte: 1 MB.",
"ThankYouForContribution": "Bedankt voor uw bijdrage aan de ABP-community.",
"GithubPost": "Github-artikel",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Schrijf een artikel op een openbare GitHub-repository met het Markdown-formaat. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Posts/2020-12-04-Event-Organizer/Post.md\">voorbeeld </a>",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Schrijf een artikel op een openbare GitHub-repository met het Markdown-formaat. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md\">voorbeeld </a>",
"GithubPostSubmitStepTwo": "<span class=\"font-weight-bold\">2.</span> Dien uw artikel-URL in via het formulier.",
"GithubPostSubmitStepThree": "<span class=\"font-weight-bold\">3.</span> Uw artikel wordt weergegeven op deze website.",
"YoutubeVideo": "Youtube video",

2
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/pl-PL.json

@ -118,7 +118,7 @@
"CreatePostCoverInfo": "Aby stworzyć efektowny post, dodaj zdjęcie na okładkę. Prześlij zdjęcia o proporcjach 16:9, aby uzyskać najlepszy widok. Maksymalny rozmiar pliku: 1 MB.",
"ThankYouForContribution": "Dziękujemy za udział w społeczności ABP.",
"GithubPost": "Artykuł na Github",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Napisz artykuł w dowolnym publicznym repozytorium GitHub w formacie Markdown. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Posts/2020-12-04-Event-Organizer/Post.md\">przykład </a>",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Napisz artykuł w dowolnym publicznym repozytorium GitHub w formacie Markdown. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md\">przykład </a>",
"GithubPostSubmitStepTwo": "<span class=\"font-weight-bold\">2.</span> Prześlij adres URL artykułu za pomocą formularza.",
"GithubPostSubmitStepThree": "<span class=\"font-weight-bold\">3.</span> Twój artykuł zostanie wyrenderowany w tej witrynie internetowej.",
"YoutubeVideo": "Wideo z YouTube",

2
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/pt-BR.json

@ -118,7 +118,7 @@
"CreatePostCoverInfo": "Para criar uma postagem eficaz, adicione uma foto de capa. Carregue imagens de proporção de 16: 9 para a melhor visualização. Tamanho máximo do arquivo: 1 MB.",
"ThankYouForContribution": "Obrigado por contribuir com a Comunidade ABP.",
"GithubPost": "Artigo Github",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\"> 1. </span> Escreva um artigo em qualquer repositório GitHub público com o formato Markdown. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Posts/2020-12-04-Event-Organizer/Post.md\"> exemplo </a>",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\"> 1. </span> Escreva um artigo em qualquer repositório GitHub público com o formato Markdown. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md\"> exemplo </a>",
"GithubPostSubmitStepTwo": "<span class=\"font-weight-bold\"> 2. </span> Envie o URL do seu artigo usando o formulário.",
"GithubPostSubmitStepThree": "<span class=\"font-weight-bold\"> 3. </span> Seu artigo será processado neste site.",
"YoutubeVideo": "Vídeo do youtube",

2
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/ro-RO.json

@ -118,7 +118,7 @@
"CreatePostCoverInfo": "Pentru crearea unei postări eficace, adăugaţi o poză de copertă. Încărcaţi imagini cu raport de aspect 16:9 pentru cea mai bună vizualizare. Dimensiunea maximă a fişierului: 1MB.",
"ThankYouForContribution": "Vă mulţumim pentru că aţi contribuit la comunitatea ABP.",
"GithubPost": "Articol GitHub",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Scrieţi un articol pe orice repository GitHub folosind formatul Markup. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Posts/2020-12-04-Event-Organizer/Post.md\">exemplu</a>",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Scrieţi un articol pe orice repository GitHub folosind formatul Markup. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md\">exemplu</a>",
"GithubPostSubmitStepTwo": "<span class=\"font-weight-bold\">2.</span> Trimiteţi Url-ul articolului folosind formularul.",
"GithubPostSubmitStepThree": "<span class=\"font-weight-bold\">3.</span> Articolul dumneavoastră va fi redat în acest site web.",
"YoutubeVideo": "Videoclip YouTube",

2
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/ru.json

@ -118,7 +118,7 @@
"CreatePostCoverInfo": "Для создания эффектного поста добавьте фото обложки. Загрузите изображения с соотношением сторон 16: 9 для наилучшего просмотра. Максимальный размер файла: 1 МБ.",
"ThankYouForContribution": "Спасибо за ваш вклад в сообщество ABP.",
"GithubPost": "Статья на Github",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\"> 1. </span> Напишите статью в любом общедоступном репозитории GitHub с форматом Markdown. Пример <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Posts/2020-12-04-Event-Organizer/Post.md\"> </a>",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\"> 1. </span> Напишите статью в любом общедоступном репозитории GitHub с форматом Markdown. Пример <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md\"> </a>",
"GithubPostSubmitStepTwo": "<span class=\"font-weight-bold\"> 2. </span> Отправьте URL своей статьи, используя форму.",
"GithubPostSubmitStepThree": "<span class=\"font-weight-bold\"> 3. </span> Ваша статья будет отображаться на этом веб-сайте.",
"YoutubeVideo": "YouTube видео",

2
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/sk.json

@ -118,7 +118,7 @@
"CreatePostCoverInfo": "Ak chcete vytvoriť efektívny príspevok, pridajte titulnú fotografiu. Pre optimálne zobrazenie nahrávajte obrázky s pomerom strán 16:9. Maximálna veľkosť súboru: 1 MB.",
"ThankYouForContribution": "Ďakujeme, že ste prispeli do ABP komunity.",
"GithubPost": "Článok na Githube",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Napíšte článok v ľubovoľnom verejnom repozitári GitHub vo formáte Markdown. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Posts/2020-12-04-Event-Organizer/Post.md\">príklad</a>",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Napíšte článok v ľubovoľnom verejnom repozitári GitHub vo formáte Markdown. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md\">príklad</a>",
"GithubPostSubmitStepTwo": "<span class=\"font-weight-bold\">2.</span> Odoslať URL adresu článku pomocou formulára.",
"GithubPostSubmitStepThree": "<span class=\"font-weight-bold\">3.</span> Váš článok sa zobrazí na tejto webovej stránke.",
"YoutubeVideo": "Video na YouTube",

2
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/sl.json

@ -118,7 +118,7 @@
"CreatePostCoverInfo": "Če želite ustvariti učinkovito objavo, dodajte naslovno fotografijo. Naložite slike v razmerju stranic 16:9 za najboljši pogled. Največja velikost datoteke: 1 MB.",
"ThankYouForContribution": "Hvala za prispevek k skupnosti ABP.",
"GithubPost": "Članek Github",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Napišite članek v katerem koli javnem repozitoriju GitHub z obliko Markdown. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Posts/2020-12-04-Event-Organizer/Post.md\">primer </a>",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Napišite članek v katerem koli javnem repozitoriju GitHub z obliko Markdown. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md\">primer </a>",
"GithubPostSubmitStepTwo": "<span class=\"font-weight-bold\">2.</span> Pošljite URL svojega članka z uporabo obrazca.",
"GithubPostSubmitStepThree": "<span class=\"font-weight-bold\">3.</span> Vaš članek bo prikazan na tem spletnem mestu.",
"YoutubeVideo": "Youtube video",

2
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/tr.json

@ -118,7 +118,7 @@
"CreatePostCoverInfo": "Etkili bir gönderi oluşturmak için bir kapak fotoğrafı ekleyin. En iyi görünüm için 16:9 en boy oranlı resimler yükleyin. Maksimum dosya boyutu: 1MB.",
"ThankYouForContribution": "ABP Topluluğuna katkıda bulunduğunuz için teşekkür ederiz.",
"GithubPost": "Github Makalesi",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Herhangi bir genel GitHub deposunda Markdown biçimiyle bir makale yazın. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Posts/2020-12-04-Event-Organizer/Post.md\">örnek </a>",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> Herhangi bir genel GitHub deposunda Markdown biçimiyle bir makale yazın. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md\">örnek </a>",
"GithubPostSubmitStepTwo": "<span class=\"font-weight-bold\">2.</span> Formu kullanarak makale URL'nizi gönderin.",
"GithubPostSubmitStepThree": "<span class=\"font-weight-bold\">3.</span> Makaleniz bu web sitesinde işlenecektir.",
"YoutubeVideo": "Youtube videosu",

2
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/vi.json

@ -118,7 +118,7 @@
"CreatePostCoverInfo": "Để tạo một bài đăng hiệu quả, hãy thêm ảnh bìa. Tải lên hình ảnh tỷ lệ khung hình 16: 9 để có chế độ xem tốt nhất. Kích thước tệp tối đa: 1MB.",
"ThankYouForContribution": "Cảm ơn bạn đã đóng góp cho Cộng đồng ABP.",
"GithubPost": "Bài báo trên Github",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\"> 1. </span> Viết một bài báo trên bất kỳ kho lưu trữ GitHub công khai nào với định dạng Markdown. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Posts/2020-12-04-Event-Organizer/Post.md\"> ví dụ </a>",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\"> 1. </span> Viết một bài báo trên bất kỳ kho lưu trữ GitHub công khai nào với định dạng Markdown. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md\"> ví dụ </a>",
"GithubPostSubmitStepTwo": "<span class=\"font-weight-bold\"> 2. </span> Gửi URL bài viết của bạn bằng biểu mẫu.",
"GithubPostSubmitStepThree": "<span class=\"font-weight-bold\"> 3. </span> Bài viết của bạn sẽ được hiển thị trên trang web này.",
"YoutubeVideo": "Video trên Youtube",

2
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/zh-Hans.json

@ -118,7 +118,7 @@
"CreatePostCoverInfo": "要创建一个有效的帖子,需要添加封面图片. 上传16:9的图片获取最佳视觉效果. 文件最大支持: 1MB",
"ThankYouForContribution": "感谢你对ABP社区的贡献",
"GithubPost": "Github文章",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> 用Markdown格式在GitHub的任何公共存储库上写一篇文章. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Posts/2020-12-04-Event-Organizer/Post.md\">示例</a>",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> 用Markdown格式在GitHub的任何公共存储库上写一篇文章. <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md\">示例</a>",
"GithubPostSubmitStepTwo": "<span class=\"font-weight-bold\">2.</span> 使用表单提交你的文章URL.",
"GithubPostSubmitStepThree": "<span class=\"font-weight-bold\">3.</span> 你的文章将在这个网站中呈现.",
"YoutubeVideo": "Youtube视频",

2
abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/zh-Hant.json

@ -118,7 +118,7 @@
"CreatePostCoverInfo": "要創建有效的帖子,請添加封面照片。上傳 16:9 縱橫比的圖片以獲得最佳觀看效果。最大文件大小:1MB。",
"ThankYouForContribution": "感謝您為 ABP 社區做出貢獻。",
"GithubPost": "Github 文章",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> 用 Markdown 格式在任何公共 GitHub 存儲庫上寫一篇文章。 <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Posts/2020-12-04-Event-Organizer/Post.md\">示例</a>",
"GithubPostSubmitStepOne": "<span class=\"font-weight-bold\">1.</span> 用 Markdown 格式在任何公共 GitHub 存儲庫上寫一篇文章。 <a target=\"_blank\" href=\"https://github.com/abpframework/abp/blob/dev/docs/en/Community-Articles/2020-12-04-Event-Organizer/Post.md\">示例</a>",
"GithubPostSubmitStepTwo": "<span class=\"font-weight-bold\">2.</span> 使用表單提交您的文章 URL。",
"GithubPostSubmitStepThree": "<span class=\"font-weight-bold\">3.</span> 您的文章將在此網站中呈現。",
"YoutubeVideo": "優酷視頻",

9
docs/en/CLI.md

@ -106,6 +106,15 @@ For more samples, go to [ABP CLI Create Solution Samples](CLI-New-Command-Sample
* **`module`**: [Module template](Startup-Templates/Module.md). Additional options:
* `--no-ui`: Specifies to not include the UI. This makes possible to create service-only modules (a.k.a. microservices - without UI).
* **`console`**: [Console template](Startup-Templates/Console.md).
* **`app-nolayers`**: Application single layer template.
* `--ui` or `-u`: Specifies the UI framework. Default framework is `mvc`. Available frameworks:
* `mvc`: ASP.NET Core MVC.
* `angular`: Angular UI.
* `blazor-server`: Blazor Server UI.
* `none`: Without UI.
* `--database-provider` or `-d`: Specifies the database provider. Default provider is `ef`. Available providers:
* `ef`: Entity Framework Core.
* `mongodb`: MongoDB.
* `--output-folder` or `-o`: Specifies the output folder. Default value is the current directory.
* `--version` or `-v`: Specifies the ABP & template version. It can be a [release tag](https://github.com/abpframework/abp/releases) or a [branch name](https://github.com/abpframework/abp/branches). Uses the latest release if not specified. Most of the times, you will want to use the latest version.
* `--preview`: Use latest preview version.

601
docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/README.md

@ -0,0 +1,601 @@
# Integrating MAUI Client via using OpenID Connect
This is a demonstration for connecting ABP backend from MAUI app via using openid connect.
In this flow, a web browser will be opened when user tries to log in and user will perform login operation in the browser. Then IdentityServer will redirect user to application with login credentials (state, token etc.) will be handled by application.
> This is by intent. The code flow does not allow the user to log in using a native view in the app. The reason being that this flow ensures that the username and password are never seen by the client (except the browser, which is part of the OS system - aka we trust it). You could enable using a native login view with the Resource Owner Password Credentials (ROPC) flow. But this is also an attack vector. Suppose someone makes a fraud duplicate of your application and tricking users into entering their credentials. The fraudulent app could store those credentials in-between. You just got to enjoy those tin-foil-hat moments when doing security. In other words, using the code flow does not give an attacker that opportunity and therefore is the recommended option for mobile clients.
>
> - [@Mark Allibone](https://mallibone.com/post/xamarin-oidc)
By the way, my motivation for building this sample is presenting just another way for authentication. **Resource Owner Password Credentials** authentication is already provided and it's more common way to do. This is yet another way to authenticate users.
## Source Code
You can also find source code on GitHub in ABP-Samples.
- [abpframework/abp-samples/MAUI-OpenId](https://github.com/abpframework/abp-samples/tree/master/MAUI-OpenId)
## Creating projects
- Create an ABP project without UI
```bash
abp new Acme.BookStore -t app --no-ui -d mongodb --no-random-ports
```
- Create a maui application
```bash
mkdir maui
cd maui
dotnet new maui -n Acme.BookStore.MauiClient
```
There is a long way for configuring scopes and callback urls for both server and clients. We'll use [WebAuthenticator](https://docs.microsoft.com/en-us/xamarin/essentials/web-authenticator?tabs=android) to perform this operation.
## Configuring IdentityServer
- Go to DbMigrator folder and MAUI client in **appsettings.json**. Add following client code in **IdentityServer:Clients** path:
```json
"BookStore_Maui": {
"ClientId": "BookStore_Maui",
"ClientSecret": "1q2w3e*",
"RootUrl": "bookstore://"
}
```
- Go to **IdentityServerDataSeedContributor** in Domain project under IdentityServer folder. Append following code section into **CreateClientsAsync()** method.
```csharp
// Maui Client
var mauiClientId = configurationSection["BookStore_Maui:ClientId"];
if (!mauiClientId.IsNullOrWhiteSpace())
{
var mauiRootUrl = configurationSection["BookStore_Maui:RootUrl"];
await CreateClientAsync(
name: mauiClientId,
scopes: commonScopes,
grantTypes: new[] { "authorization_code" },
secret: configurationSection["BookStore_Maui:ClientSecret"]?.Sha256(),
requireClientSecret: false,
redirectUri: $"{mauiRootUrl}"
);
}
```
- Run DbMigrator
- Then run HttpApi.Host
### Configuring NGROK
Client will check configuration from `/.well-known/openid-configuration` path and it must be a secured connection between client & server. I prefer to use ngrok to open my backend app to entire web.
- Go to [getting started](https://dashboard.ngrok.com/get-started/setup) page of ngrok _(login or register first)_ and download the ngrok tool.
- Don't forget to login from tool:
```bash
ngrok authtoken XXX
```
_A sample command is being displayed at dashboard where you download ngrok from_
- Open your HttpApi.Host with ngrok
```bash
.\ngrok.exe http https://localhost:44350
```
- You'll see a generated xxx.ngrok.io url. Navigate to `/.well-known/openid-configuration` to check if it's working right.
You should see something like that:
![](art/openid-configuration.png)
Issuer must be your URL, not localhost! If you see still localhost, try to disable host header rewrite.
- Also, ValidIssuers must be defined to validate tokens.
- Add **ValidIssuers** section to your `appsettings.json` of HttpApi.Host
```js
"AuthServer": {
"Authority": "https://localhost:44350",
"RequireHttpsMetadata": "false",
"SwaggerClientId": "BookStore_Swagger",
"SwaggerClientSecret": "1q2w3e*",
"ValidIssuers": [
"https://46fd-45-156-29-175.ngrok.io"
]
},
```
- Then define it in **ConfigureAuthentication** method in Module class
```csharp
private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration)
{
context.Services.AddAuthentication()
.AddJwtBearer(options =>
{
// ...
options.TokenValidationParameters.ValidIssuers = configuration.GetSection("AuthServer:ValidIssuers").Get<string[]>();
});
}
```
We're done with backend. Let's continue with MAUI app.
## Developing MAUI App
Before we go, there is something to do like configuring dependency injection to get rid of unnecessary huge class coupling.
### Configuring Dependency Injection
- Go to **MauiApplication** class and add `MainPage` in services.
```csharp
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
});
builder.Services.AddTransient<MainPage>();
return builder.Build();
}
```
- And inject MainPage from constructor in **App.xaml.cs**
```csharp
public App(MainPage mainPage)
{
InitializeComponent();
MainPage = mainPage;
}
```
Now MainPage is ready for injecting dependencies to it.
### Configuring OIDC
- Add `IdentityModel.OidcClient` package to project
```xml
<ItemGroup>
<PackageReference Include="IdentityModel.OidcClient" Version="5.0.0" />
</ItemGroup>
```
- Create **WebAuthenticatorBrowser**
```csharp
internal class WebAuthenticatorBrowser : IBrowser
{
public async Task<BrowserResult> InvokeAsync(BrowserOptions options, CancellationToken cancellationToken = default)
{
try
{
WebAuthenticatorResult authResult =
await WebAuthenticator.AuthenticateAsync(new Uri(options.StartUrl), new Uri(options.EndUrl));
var authorizeResponse = ToRawIdentityUrl(options.EndUrl, authResult);
return new BrowserResult
{
Response = authorizeResponse
};
}
catch (Exception ex)
{
Debug.WriteLine(ex);
return new BrowserResult()
{
ResultType = BrowserResultType.UnknownError,
Error = ex.ToString()
};
}
}
public string ToRawIdentityUrl(string redirectUrl, WebAuthenticatorResult result)
{
IEnumerable<string> parameters = result.Properties.Select(pair => $"{pair.Key}={pair.Value}");
var values = string.Join("&", parameters);
return $"{redirectUrl}#{values}";
}
}
```
- Configure **OidcClient** in **MauiProgram**
```csharp
builder.Services.AddTransient<WebAuthenticatorBrowser>();
builder.Services.AddTransient<OidcClient>(sp =>
new OidcClient(new OidcClientOptions
{
// Use your own ngrok url:
Authority = "https://46fd-45-156-29-175.ngrok.io",
ClientId = "BookStore_Maui",
RedirectUri = "bookstore://",
Scope = "openid email profile role BookStore",
ClientSecret = "1q2w3E*",
Browser = sp.GetRequiredService<WebAuthenticatorBrowser>(),
})
);
```
- Go to **MainPage.xaml**, remove everyting and add a button for login
```xml
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="MauiApp1.MainPage">
<ScrollView>
<Grid RowSpacing="25" RowDefinitions="Auto,Auto,Auto,Auto,*"
Padding="{OnPlatform iOS='30,60,30,30', Default='30'}">
<Button Text="Click to Log In" Clicked="OnLoginClicked" VerticalOptions="CenterAndExpand" HorizontalOptions="Center"/>
</Grid>
</ScrollView>
</ContentPage>
```
- Inject **OidcClient** in **MainPage.xaml.cs** and make login operation.
```csharp
using IdentityModel.OidcClient;
namespace Acme.BookStore.MauiClient;
public partial class MainPage : ContentPage
{
protected OidcClient OidcClient { get; }
public MainPage(OidcClient oidcClient)
{
InitializeComponent();
OidcClient = oidcClient;
}
private async void OnLoginClicked(object sender, EventArgs e)
{
try
{
var loginResult = await OidcClient.LoginAsync(new LoginRequest());
await DisplayAlert("Login Result", "Access Token is:\n\n" + loginResult.AccessToken, "Close");
}
catch (Exception ex)
{
await DisplayAlert("Error", ex.ToString(), "ok");
}
}
}
```
It still won't work because there is something more to do for each platform. Check out the next step and configure the platforms that you're using.
## Platform Specific Configurations
Each platform (UWP, OSX, iOS and Android) requires some configuration to use authentication from browser. In that step, we'll open a browser and user will login on the browser. After that, as you see in IdentityServer configurations, IdentityServer will redirect 'bookstore://' url that only contains scheme and that scheme is not http. Our application will handle that scheme and will be launched with parameters.
### Android
- Start with creating a new Activity named **BookStoreWebAuthenticatorCallbackActivity**
```csharp
using Android.App;
using Android.Content;
using Android.Content.PM;
namespace Acme.BookStore.MauiClient.Platforms.Android;
[Activity(NoHistory = true, LaunchMode = LaunchMode.SingleTop)]
[IntentFilter(new[] { Intent.ActionView },
Categories = new[] { Intent.CategoryDefault, Intent.CategoryBrowsable },
DataScheme = CALLBACK_SCHEME)]
public class BookStoreWebAuthenticatorCallbackActivity : Microsoft.Maui.Essentials.WebAuthenticatorCallbackActivity
{
const string CALLBACK_SCHEME = "bookstore";
}
```
- Add `CustomTabsService` to **AndroidManifest.xml** as below. _(queries tags only.)_
```xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="30" />
<application android:allowBackup="true" android:icon="@mipmap/appicon" android:roundIcon="@mipmap/appicon_round" android:supportsRtl="true"></application>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<queries>
<intent>
<action android:name="android.support.customtabs.action.CustomTabsService" />
</intent>
</queries>
</manifest>
```
> For some reason, an error occurs with my emulator while targeting SDK 31, so I've changed the target SDK to 30.
- Run the Application and perform a login operation.
AccessToken will be retrieved.
![abp-maui-android-openid-connect](art/android-login-demo.gif)
---
### iOS/MacCatalyst
- Add following key to **Info.plist**
```xml
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>mauiessentials</string>
<key>CFBundleURLSchemes</key>
<array>
<string>bookstore</string>
</array>
<key>CFBundleTypeRole</key>
<string>Editor</string>
</dict>
</array>
```
- Open **AppDelegate** class and override `OpenUrl` and `ContinueUserActivity` methods
```csharp
public override bool OpenUrl(UIApplication app, NSUrl url, NSDictionary options)
{
if (Microsoft.Maui.Essentials.Platform.OpenUrl(app, url, options))
return true;
return base.OpenUrl(app, url, options);
}
public override bool ContinueUserActivity(UIApplication application, NSUserActivity userActivity, UIApplicationRestorationHandler completionHandler)
{
if (Microsoft.Maui.Platform.ContinueUserActivity(application, userActivity, completionHandler))
return true;
return base.ContinueUserActivity(application, userActivity, completionHandler);
}
```
- Make all steps for MacCatalyst, too.
> **Tip:** If your IDE struggles while displaying references and namespace suggestions, make sure you're displaying that file with iOS Target Framework.
>
> You'll find it at the top of the editor.
> ![abp-maui-example-compiler-select](art/compiler-select.png)
![abp-maui-ios-openid-connect](art/ios-login-demo.gif)
![abp-maui-MacCatalyst-openid-connect](art/macos-login-demo.gif)
---
### UWP (Windows)
- Add following protocol extension in `Package.appxmanifest` file.
```xml
<Applications>
<Application Id="App"
Executable="$targetnametoken$.exe"
EntryPoint="$targetentrypoint$">
<Extensions>
<uap:Extension Category="windows.protocol">
<uap:Protocol Name="bookstore">
<uap:DisplayName>BookStore</uap:DisplayName>
</uap:Protocol>
</uap:Extension>
</Extensions>
</Application>
</Applications>
```
> Currently UWP has a bug in MAUI Essentials, I believe the MAUI team will solve it as soon as possible. You can track the status of issue:
> https://github.com/dotnet/maui/issues/2702
- That's it on Windows side. Run the application.
## Refreshing the access token
IdentityServer doesn't return a refresh token by default. So we have to add `offline_access` to our scope while sending login request.
- Add `offline_access` to scope in **MauiApplication.cs** that we configured before.
```csharp
builder.Services.AddTransient<OidcClient>(sp =>
new OidcClient(new OidcClientOptions
{
// Use your own ngrok url:
Authority = "https://46fd-45-156-29-175.ngrok.io",
ClientId = "BookStore_Maui",
RedirectUri = "bookstore://",
Scope = "openid email profile role BookStore offline_access", // <-- Final state must be like this.
ClientSecret = "1q2w3E*",
Browser = sp.GetRequiredService<WebAuthenticatorBrowser>(),
})
);
```
- Then check if it's working or not in **MainPage.xaml.cs**. Update OnLoginClicked method as below
```csharp
private async void OnLoginClicked(object sender, EventArgs e)
{
try
{
var loginResult = await OidcClient.LoginAsync(new LoginRequest());
await DisplayAlert("Login Result", "Access Token is:\n\n" + loginResult.AccessToken, "Close");
var refreshResult = await OidcClient.RefreshTokenAsync(loginResult.RefreshToken);
await DisplayAlert("Refresh Result", "New Access Token is: \n\n" + refreshResult.AccessToken, "Close");
}
catch (Exception ex)
{
await DisplayAlert("Error", ex.ToString(), "ok");
}
}
```
## Storing the access token
In this step we have to store access token & refresh token for future requests.
> [Secure Storage](https://docs.microsoft.com/en-us/xamarin/essentials/secure-storage?tabs=android) is highly recommended to store this kind of sensitive data. But it's not topic of this article. You can configure and use SecureStorage on your own. I'll go with `App Properties`.
- Add following class to store key names instead of using magic strings in code.
```csharp
namespace Acme.BookStore.MauiClient;
public static class OidcConsts
{
internal const string AccessTokenKeyName = "__access_token";
internal const string RefreshTokenKeyName = "__refresh_token";
}
```
- Then go back to **MainPage.xaml.cs** and save our tokens after a successfull login.
```csharp
private async void OnLoginClicked(object sender, EventArgs e)
{
try
{
var loginResult = await OidcClient.LoginAsync(new LoginRequest());
App.Current.Properties[OidcConsts.AccessTokenKeyName] = loginResult.AccessToken;
App.Current.Properties[OidcConsts.RefreshTokenKeyName] = loginResult.RefreshToken;
await App.Current.SavePropertiesAsync();
// Navigate to an inner page here.
}
catch (Exception ex)
{
await DisplayAlert("Error", ex.ToString(), "ok");
}
}
```
- Add following **AccessTokenHttpMessageHandler** to append AccessToken to our requests & refresh token when required.
```csharp
public class AccessTokenHttpMessageHandler : DelegatingHandler
{
protected OidcClient OidcClient { get; }
public AccessTokenHttpMessageHandler(OidcClient oidcClient) : base(new HttpClientHandler())
{
OidcClient = oidcClient;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (App.Current.Properties.TryGetValue(OidcConsts.AccessTokenKeyName, out object currentTokenValue) && currentTokenValue != null)
{
request.SetBearerToken(currentTokenValue?.ToString());
request.Headers.Accept.Add(new System.Net.Http.Headers.MediaTypeWithQualityHeaderValue("application/json"));
}
var response = await base.SendAsync(request, cancellationToken);
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
if (App.Current.Properties.TryGetValue(OidcConsts.RefreshTokenKeyName, out object refreshTokenValue) && refreshTokenValue != null)
{
var refreshResult = await OidcClient.RefreshTokenAsync(refreshTokenValue?.ToString());
App.Current.Properties[OidcConsts.AccessTokenKeyName] = refreshResult.AccessToken;
App.Current.Properties[OidcConsts.RefreshTokenKeyName] = refreshResult.RefreshToken;
await App.Current.SavePropertiesAsync();
request.SetBearerToken(refreshResult.AccessToken);
return await base.SendAsync(request, cancellationToken);
}
else
{
var result = await OidcClient.LoginAsync(new LoginRequest());
request.SetBearerToken(result.AccessToken);
App.Current.Properties[OidcConsts.AccessTokenKeyName] = result.AccessToken;
App.Current.Properties[OidcConsts.RefreshTokenKeyName] = result.RefreshToken;
await App.Current.SavePropertiesAsync();
request.SetBearerToken(result.AccessToken);
return await base.SendAsync(request, cancellationToken);
}
}
return response;
}
}
```
- Then register to dependency injection.
```csharp
builder.Services.AddSingleton<AccessTokenHttpMessageHandler>();
builder.Services.AddTransient<HttpClient>(sp =>
new HttpClient(sp.GetRequiredService<AccessTokenHttpMessageHandler>())
{
BaseAddress = new Uri("https://46fd-45-156-29-175.ngrok.io")
});
```
- Now make we can send request to backend with authentication. Go to **MainPage.xaml.cs** and send a request right after authentication.
```csharp
private async void OnLoginClicked(object sender, EventArgs e)
{
try
{
var loginResult = await OidcClient.LoginAsync(new LoginRequest());
App.Current.Properties[OidcConsts.AccessTokenKeyName] = loginResult.AccessToken;
App.Current.Properties[OidcConsts.RefreshTokenKeyName] = loginResult.RefreshToken;
await App.Current.SavePropertiesAsync();
var json = await httpClient.GetStringAsync("/api/identity/users");
await DisplayAlert("/api/identity/users", json, "close");
}
catch (Exception ex)
{
await DisplayAlert("Error", ex.ToString(), "ok");
}
}
```
- Following result will be returned from API.
<img src="art/identity-users-request-result.png" height="480">
---
## Recap
The purpose of this arcitle is connecting to ABP backend with access token and it's working properly.
I'm planning to integrate HttpApi.Client library of backend project instead of making requests manually as a second part of this article. I'll get inspired by [hikalkan/maui-abp-playing](https://github.com/hikalkan/maui-abp-playing) repo to achive that.

BIN
docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/android-login-demo.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/compiler-select.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/identity-users-request-result.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

BIN
docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/ios-login-demo.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 KiB

BIN
docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/macos-login-demo.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 646 KiB

BIN
docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/openid-configuration.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

BIN
docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/Cover.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

529
docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/README.md

@ -0,0 +1,529 @@
# Using ABP Client Proxies in MAUI with OpenID Connect
The purpose of this article is to integrate ABP Core into the MAUI project and initialize it as an **AbpModule** then make able consuming API using ABP IAppServices.
Before we start, I offer my special thanks to [@hikalkan](https://github.com/hikalkan/maui-abp-playing) because this repository ( [hikalkan/maui-abp-playing](https://github.com/hikalkan/maui-abp-playing) ) is a fantastic inspiration for the purpose of this article.
## Getting Started
In this article, we'll work on an application that was built on the previous article: [Integrating MAUI Client via Using OpenID Connect](https://community.abp.io/posts/integrating-maui-client-via-using-openid-connect-aqjjwsdf).
## Source Code
Source code is available on GitHub:
[abpframework/abp-samples/MAUI-OpenId](https://github.com/abpframework/abp-samples/tree/master/MAUI-OpenId)
## Configuring ABP Core
As a first step, Dependency Injection will be changed with module initialization. We have to initialize our application as an ABP Module first.
- Add the following dependencies to MAUI Client.
```xml
<PackageReference Include="Volo.Abp.Http.Client.IdentityModel" Version="5.1.3" />
<PackageReference Include="Volo.Abp.Autofac" Version="5.1.3" />
```
- Add HttpApi.Client project reference
```xml
<ProjectReference Include="..\..\aspnet-core\src\Acme.BookStore.HttpApi.Client\Acme.BookStore.HttpApi.Client.csproj" />
```
And run `abp build` command under MAUI application folder.
> `abp build` command is equivalent of `dotnet build /graphBuild`, it's like a shortcut to graphBuild. The graphBuild finds all dependency tree and build them recursively.
- Create **BookStoreMauiClientModule**.
```csharp
using IdentityModel.OidcClient;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using Volo.Abp.Autofac;
using Volo.Abp.Http.Client.IdentityModel;
using Volo.Abp.Modularity;
namespace Acme.BookStore.MauiClient;
[DependsOn(
typeof(AbpAutofacModule),
typeof(AbpHttpClientIdentityModelModule),
typeof(BookStoreHttpApiClientModule)
)]
public class BookStoreMauiClientModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
Configure<OidcClientOptions>(configuration.GetSection("Oidc:Options"));
context.Services.AddTransient<OidcClient>(sp =>
{
var options = sp.GetRequiredService<IOptions<OidcClientOptions>>().Value;
options.Browser = sp.GetRequiredService<WebAuthenticatorBrowser>();
return new OidcClient(options);
});
context.Services.AddTransient<HttpClient>(sp =>
new HttpClient(sp.GetRequiredService<AccessTokenHttpMessageHandler>())
{
// Temporarily. We'll use ABP's Proxy for sendind requests.
BaseAddress = new Uri(configuration.GetValue<string>("RemoteServices:Default:BaseUrl"))
});
}
}
```
- Mark all dependencies with interfaces for registering as services.
```csharp
internal class WebAuthenticatorBrowser : IBrowser, ITransientDependency
```
```csharp
public partial class MainPage : ContentPage, ITransientDependency
```
```csharp
public class AccessTokenHttpMessageHandler : DelegatingHandler, ISingletonDependency
```
- Add `appsettings.json` file to root path of your application and mark it as **Embedded resource**.
```json
{
"Oidc": {
"Options": {
"Authority": "https://46fd-45-156-29-175.ngrok.io",
"ClientId": "BookStore_Maui",
"RedirectUri": "bookstore://",
"Scope": "openid email profile role BookStore offline_access",
"ClientSecret": "1q2w3E*"
}
},
"RemoteServices": {
"Default": {
"BaseUrl": "https://46fd-45-156-29-175.ngrok.io"
}
}
}
```
- Finally, Go back `MauiApplication.cs` and clear old codes and initialize ABP.
```csharp
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.FileProviders;
using System.Reflection;
using Volo.Abp;
using Volo.Abp.Autofac;
namespace Acme.BookStore.MauiClient;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder.ConfigureContainer(new AbpAutofacServiceProviderFactory(new Autofac.ContainerBuilder()), containerBuilder =>
{
});
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
});
ConfigureConfiguration(builder);
builder.Services.AddApplication<BookStoreMauiClientModule>(options =>
{
options.Services.ReplaceConfiguration(builder.Configuration);
});
var app = builder.Build();
app.Services.GetRequiredService<IAbpApplicationWithExternalServiceProvider>()
.Initialize(app.Services);
return app;
}
private static void ConfigureConfiguration(MauiAppBuilder builder)
{
var assembly = typeof(App).GetTypeInfo().Assembly;
builder.Configuration.AddJsonFile(new EmbeddedFileProvider(assembly), "appsettings.json", optional: false, false);
}
}
```
Now application is runnable and all behaviors are the same with previos state. But it uses power of ABP right now.
### Switching to SecureStorage
.Net MAUI supports a secure storage by default. Before we go further, we need to switch to secure storage instead of using app properties. Just update login method as below at **MainPage.xaml.cs**
```csharp
private async void OnLoginClicked(object sender, EventArgs e)
{
var loginResult = await OidcClient.LoginAsync(new LoginRequest());
if (loginResult.IsError)
{
await DisplayAlert("Error", loginResult.Error, "Close");
return;
}
await SecureStorage.SetAsync(OidcConsts.AccessTokenKeyName, loginResult.AccessToken);
await SecureStorage.SetAsync(OidcConsts.RefreshTokenKeyName, loginResult.RefreshToken);
}
```
> Additionally, please configure each platform according to [Secure Storage documentation](https://docs.microsoft.com/en-us/xamarin/essentials/secure-storage?tabs=android)
## Configuring Client Proxies
ABP Client-Proxies don't use HttpClient directly. They use `IHttpClientFactory` to activate a new HttpClient instead of injecting it directly. So, we won't need **AccessTokenHttpMessageHandler** anymore. But still there is a way needed to set access token in requests. No worries, ABP has `IRemoteServiceHttpClientAuthenticator` to do that operation. Implementing it and registering to container will solve that issue and the client will be able to make authorized request to server.
- Remove **AccessTokenHttpMessageHandler.cs** from the project.
- Add **AccessTokenRemoteServiceHttpClientAuthenticator.cs** instead.
```csharp
using IdentityModel.Client;
using IdentityModel.OidcClient;
using System.IdentityModel.Tokens.Jwt;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Http.Client.Authentication;
using DependencyAttribute = Volo.Abp.DependencyInjection.DependencyAttribute;
namespace Acme.BookStore.MauiClient;
[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(IRemoteServiceHttpClientAuthenticator))]
public class AccessTokenRemoteServiceHttpClientAuthenticator : IRemoteServiceHttpClientAuthenticator, ITransientDependency
{
protected OidcClient OidcClient { get; }
public AccessTokenRemoteServiceHttpClientAuthenticator(OidcClient oidcClient)
{
OidcClient = oidcClient;
}
public async Task Authenticate(RemoteServiceHttpClientAuthenticateContext context)
{
var currentAccessToken = await SecureStorage.GetAsync(OidcConsts.AccessTokenKeyName);
if (!currentAccessToken.IsNullOrEmpty())
{
// TODO: Find better way to find if token is expired instead of parsing it.
var jwtToken = new JwtSecurityTokenHandler().ReadJwtToken(currentAccessToken) as JwtSecurityToken;
if (jwtToken.ValidTo <= DateTime.UtcNow)
{
var refreshToken = await SecureStorage.GetAsync(OidcConsts.RefreshTokenKeyName);
if (!refreshToken.IsNullOrEmpty())
{
var refreshResult = await OidcClient.RefreshTokenAsync(refreshToken);
await SecureStorage.SetAsync(OidcConsts.AccessTokenKeyName, refreshResult.AccessToken);
await SecureStorage.SetAsync(OidcConsts.RefreshTokenKeyName, refreshResult.RefreshToken);
context.Request.SetBearerToken(refreshResult.AccessToken);
}
else
{
var loginResult = await OidcClient.LoginAsync(new LoginRequest());
await SecureStorage.SetAsync(OidcConsts.AccessTokenKeyName, loginResult.AccessToken);
await SecureStorage.SetAsync(OidcConsts.RefreshTokenKeyName, loginResult.RefreshToken);
context.Request.SetBearerToken(loginResult.AccessToken);
}
}
context.Request.SetBearerToken(currentAccessToken);
}
}
}
```
- Now we are ready to inject IAppServices to communicate with backend.
## Displaying Data in UI
- Go back to **Acme.BookStore.Domain** project and add a simple data seed contributor to generate some example data for users.
```csharp
public class UsersDataSeederContributor : IDataSeedContributor, ITransientDependency
{
protected IIdentityUserRepository repository;
protected IGuidGenerator guidGenerator;
public UsersDataSeederContributor(IIdentityUserRepository repository, IGuidGenerator guidGenerator)
{
this.repository = repository;
this.guidGenerator = guidGenerator;
}
public async Task SeedAsync(DataSeedContext context)
{
var count = await repository.GetCountAsync();
if(count <= 1) // Not sure 'admin' user was seeded before or not.
{
// All the names below were generated by https://www.name-generator.org.uk/quick/
// The names does not represent real people.
await repository.InsertManyAsync(new []{
new IdentityUser(guidGenerator.Create(), "john.doe", "john.doe@abp.io"),
new IdentityUser(guidGenerator.Create(), "Zane.Frost", "Zane.Frost@abp.io"),
new IdentityUser(guidGenerator.Create(), "Oscar.Landry", "Oscar.Landry@abp.io"),
new IdentityUser(guidGenerator.Create(), "Yasemin.Roberts", "Yasemin.Roberts@abp.io"),
new IdentityUser(guidGenerator.Create(), "Yasmine.Perez", "Yasmine.Perez@abp.io"),
new IdentityUser(guidGenerator.Create(), "Tobi.Becker", "Tobi.Becker@abp.io"),
new IdentityUser(guidGenerator.Create(), "Fox.Gilmore", "Fox.Gilmore@abp.io"),
new IdentityUser(guidGenerator.Create(), "Benny.Burris", "Benny.Burris@abp.io"),
new IdentityUser(guidGenerator.Create(), "Chad.Camacho", "Chad.Camacho@abp.io"),
});
}
}
}
```
- Run the **Acme.BookStore.DbMigrator** project.
- Turn back to MAUI app, and create a folder named **ViewModels** and add a simple `UsersViewModel.cs` under it.
```csharp
public class UsersViewModel : BindableObject, ITransientDependency
{
protected IIdentityUserAppService IdentityUserAppService { get; }
public GetIdentityUsersInput Input { get; } = new();
public ObservableCollection<IdentityUserDto> Items { get; } = new();
public Command RefreshCommand { get; }
private bool isBusy;
public bool IsBusy { get => isBusy; set => SetProperty(ref isBusy, value); }
public UsersViewModel(IIdentityUserAppService identityUserAppService)
{
IdentityUserAppService = identityUserAppService;
GetUsersAsync();
RefreshCommand = new Command(GetUsersAsync);
}
protected async void GetUsersAsync()
{
if (IsBusy)
{
return; // For preventing parallel request while searching.
}
IsBusy = true;
Items.Clear();
var result = await IdentityUserAppService.GetListAsync(Input);
foreach (var user in result.Items)
{
Items.Add(user);
}
IsBusy = false;
}
protected void SetProperty<T>(ref T backField, T value, [CallerMemberName] string propertyName = null)
{
backField = value;
OnPropertyChanged(propertyName);
}
}
```
- Create a folder named **Pages** and add a content page named `UsersPage`.
_(Make sure you're adding MAUI Content Page)_
![net-maui-abp-contentpage-template](art/net-maui-contentpage-template.png)
- And inject `UsersViewModel` into it.
```csharp
public partial class UsersPage : ContentPage, ITransientDependency
{
public UsersViewModel ViewModel { get; }
public UsersPage(UsersViewModel viewModel)
{
ViewModel = viewModel;
InitializeComponent();
}
}
```
- And use that ViewModel in XAML design page.
```xml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Acme.BookStore.MauiClient.UsersPage"
Title="UsersPage"
x:Name="page"
BindingContext="{Binding ViewModel, Source={x:Reference page}}">
<StackLayout>
<ListView
IsPullToRefreshEnabled="True"
ItemsSource="{Binding Items}"
IsRefreshing="{Binding IsBusy}"
RefreshCommand="{Binding RefreshCommand}">
<ListView.Header>
<SearchBar Text="{Binding Input.Filter}" SearchCommand="{Binding RefreshCommand}" />
</ListView.Header>
<ListView.ItemTemplate>
<DataTemplate>
<TextCell
Text="{Binding UserName, StringFormat='@{0}'}"
Detail="{Binding Email}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
</ContentPage>
```
> I've used binding while setting **BindingContext** as **ViewModel** because of IntelliSense support. With this method, you'll see intellisense will suggest properties from your ViewModel.
>
> ![abp-maui-demo-xaml-intellisense](art/xaml-intellisense.png)
After a couple of try, I realized, only AppShell supports dependency injection while navigating between pages. So, adding a new AppShell will help to build app menus and navigating with route. We can pass parameters with querystring with this way.
- Add `Shell Pagae (MAUI)` to root of your application with name **AppShell.xaml**.
_I've got some help for design of shell page from microsoft's articles._
_Additionally, you might want to put [abp_icon.svg](maui/Acme.BookStore.MauiClient/Resources/Images/abp_logo.svg) file under your **Resources/Images** folder._
```xml
<?xml version="1.0" encoding="utf-8" ?>
<Shell x:Class="Acme.BookStore.MauiClient.AppShell"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:Acme.BookStore.MauiClient">
<Shell.Resources>
<ResourceDictionary>
<Color x:Key="Primary">#512BD4</Color>
<Style x:Key="BaseStyle" TargetType="Element">
<Setter Property="Shell.BackgroundColor" Value="{StaticResource Primary}" />
<Setter Property="Shell.ForegroundColor" Value="White" />
<Setter Property="Shell.TitleColor" Value="White" />
<Setter Property="Shell.DisabledColor" Value="#B4FFFFFF" />
<Setter Property="Shell.UnselectedColor" Value="#95FFFFFF" />
<Setter Property="Shell.TabBarBackgroundColor" Value="{StaticResource Primary}" />
<Setter Property="Shell.TabBarForegroundColor" Value="White"/>
<Setter Property="Shell.TabBarUnselectedColor" Value="#95FFFFFF"/>
<Setter Property="Shell.TabBarTitleColor" Value="White"/>
</Style>
<Style TargetType="TabBar" BasedOn="{StaticResource BaseStyle}" />
<Style TargetType="FlyoutItem" BasedOn="{StaticResource BaseStyle}" />
<Style Class="FlyoutItemLabelStyle" TargetType="Label">
<Setter Property="TextColor" Value="White"></Setter>
<Setter Property="Margin" Value="16"></Setter>
</Style>
<Style Class="FlyoutItemLayoutStyle" TargetType="Layout" ApplyToDerivedTypes="True">
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="{x:OnPlatform UWP=Transparent, iOS=White, Android=White}" />
<Setter TargetName="FlyoutItemLabel" Property="Label.TextColor" Value="{StaticResource Primary}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Selected">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="{StaticResource Primary}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style Class="MenuItemLayoutStyle" TargetType="Layout" ApplyToDerivedTypes="True">
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal">
<VisualState.Setters>
<Setter TargetName="FlyoutItemLabel" Property="Label.TextColor" Value="{StaticResource Primary}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
</ResourceDictionary>
</Shell.Resources>
<FlyoutItem Title="Home">
<ShellContent ContentTemplate="{DataTemplate local:MainPage}" Route="main" />
</FlyoutItem>
<FlyoutItem Title="Users">
<ShellContent ContentTemplate="{DataTemplate local:UsersPage}" Route="UsersPage" />
</FlyoutItem>
<Shell.FlyoutHeader>
<StackLayout>
<Image
Source="abp_logo.svg"
HorizontalOptions="Center"
Margin="25"/>
</StackLayout>
</Shell.FlyoutHeader>
</Shell>
```
- One more step is required. Go to **App.xaml.cs** and replace MainPage with AppShell.
```csharp
public partial class App : Application
{
public App()
{
InitializeComponent();
MainPage = new AppShell();
}
}
```
- Run the application.
- Login once if you haven't done before.
- Navigate to Users page with hamburger menu at the right top.
| Android| iOS|
| --- | --- |
| ![abp-maui-android-appservice](art/android-users-demo.gif) | ![ios-abp-maui](art/ios-users-demo.gif) |
| UWP |
| --- |
| ![abp-maui-uwp-appservice](art/uwp-users-demo.gif) |
| MacCatalyst |
| --- |
| ![abp-maui-MacCatalyst-appservice](art/macos-users-demo.gif) |
## Conclusion
ABP Framework can be implemented any platform that runs on dotnet without suffer. ABP provides reusable abstractions layers and HttpApi Clients. In this article we've used powerful ABP core features such as Dependency Injection, Client Proxies, Validation and more.

BIN
docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/android-users-demo.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 KiB

BIN
docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/ios-users-demo.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 KiB

BIN
docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/macos-users-demo.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 KiB

BIN
docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/net-maui-contentpage-template.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/uwp-users-demo.gif

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

BIN
docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/xaml-intellisense.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

529
docs/en/Community-Articles/2022-02-24-Using-HttpApi.Client-in-MAUI-with-OpenID-Connect/README.md

@ -0,0 +1,529 @@
# Using ABP Client Proxies in MAUI with OpenID Connect
The purpose of this article is to integrate ABP Core into the MAUI project and initialize it as an **AbpModule** then make able consuming API using ABP IAppServices.
Before we start, I offer my special thanks to [@hikalkan](https://github.com/hikalkan/maui-abp-playing) because this repository ( [hikalkan/maui-abp-playing](https://github.com/hikalkan/maui-abp-playing) ) is a fantastic inspiration for the purpose of this article.
## Getting Started
In this article, we'll work on an application that was built on the previous article: [Integrating MAUI Client via Using OpenID Connect](https://community.abp.io/posts/integrating-maui-client-via-using-openid-connect-aqjjwsdf).
## Source Code
Source code is available on GitHub:
[abpframework/abp-samples/MAUI-OpenId](https://github.com/abpframework/abp-samples/tree/master/MAUI-OpenId)
## Configuring ABP Core
As a first step, Dependency Injection will be changed with module initialization. We have to initialize our application as an ABP Module first.
- Add the following dependencies to MAUI Client.
```xml
<PackageReference Include="Volo.Abp.Http.Client.IdentityModel" Version="5.1.3" />
<PackageReference Include="Volo.Abp.Autofac" Version="5.1.3" />
```
- Add HttpApi.Client project reference
```xml
<ProjectReference Include="..\..\aspnet-core\src\Acme.BookStore.HttpApi.Client\Acme.BookStore.HttpApi.Client.csproj" />
```
And run `abp build` command under MAUI application folder.
> `abp build` command is equivalent of `dotnet build /graphBuild`, it's like a shortcut to graphBuild. The graphBuild finds all dependency tree and build them recursively.
- Create **BookStoreMauiClientModule**.
```csharp
using IdentityModel.OidcClient;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;
using Volo.Abp.Autofac;
using Volo.Abp.Http.Client.IdentityModel;
using Volo.Abp.Modularity;
namespace Acme.BookStore.MauiClient;
[DependsOn(
typeof(AbpAutofacModule),
typeof(AbpHttpClientIdentityModelModule),
typeof(BookStoreHttpApiClientModule)
)]
public class BookStoreMauiClientModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
var configuration = context.Services.GetConfiguration();
Configure<OidcClientOptions>(configuration.GetSection("Oidc:Options"));
context.Services.AddTransient<OidcClient>(sp =>
{
var options = sp.GetRequiredService<IOptions<OidcClientOptions>>().Value;
options.Browser = sp.GetRequiredService<WebAuthenticatorBrowser>();
return new OidcClient(options);
});
context.Services.AddTransient<HttpClient>(sp =>
new HttpClient(sp.GetRequiredService<AccessTokenHttpMessageHandler>())
{
// Temporarily. We'll use ABP's Proxy for sendind requests.
BaseAddress = new Uri(configuration.GetValue<string>("RemoteServices:Default:BaseUrl"))
});
}
}
```
- Mark all dependencies with interfaces for registering as services.
```csharp
internal class WebAuthenticatorBrowser : IBrowser, ITransientDependency
```
```csharp
public partial class MainPage : ContentPage, ITransientDependency
```
```csharp
public class AccessTokenHttpMessageHandler : DelegatingHandler, ISingletonDependency
```
- Add `appsettings.json` file to root path of your application and mark it as **Embedded resource**.
```json
{
"Oidc": {
"Options": {
"Authority": "https://46fd-45-156-29-175.ngrok.io",
"ClientId": "BookStore_Maui",
"RedirectUri": "bookstore://",
"Scope": "openid email profile role BookStore offline_access",
"ClientSecret": "1q2w3E*"
}
},
"RemoteServices": {
"Default": {
"BaseUrl": "https://46fd-45-156-29-175.ngrok.io"
}
}
}
```
- Finally, Go back `MauiApplication.cs` and clear old codes and initialize ABP.
```csharp
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.FileProviders;
using System.Reflection;
using Volo.Abp;
using Volo.Abp.Autofac;
namespace Acme.BookStore.MauiClient;
public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();
builder.ConfigureContainer(new AbpAutofacServiceProviderFactory(new Autofac.ContainerBuilder()), containerBuilder =>
{
});
builder
.UseMauiApp<App>()
.ConfigureFonts(fonts =>
{
fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
});
ConfigureConfiguration(builder);
builder.Services.AddApplication<BookStoreMauiClientModule>(options =>
{
options.Services.ReplaceConfiguration(builder.Configuration);
});
var app = builder.Build();
app.Services.GetRequiredService<IAbpApplicationWithExternalServiceProvider>()
.Initialize(app.Services);
return app;
}
private static void ConfigureConfiguration(MauiAppBuilder builder)
{
var assembly = typeof(App).GetTypeInfo().Assembly;
builder.Configuration.AddJsonFile(new EmbeddedFileProvider(assembly), "appsettings.json", optional: false, false);
}
}
```
Now application is runnable and all behaviors are the same with previos state. But it uses power of ABP right now.
### Switching to SecureStorage
.Net MAUI supports a secure storage by default. Before we go further, we need to switch to secure storage instead of using app properties. Just update login method as below at **MainPage.xaml.cs**
```csharp
private async void OnLoginClicked(object sender, EventArgs e)
{
var loginResult = await OidcClient.LoginAsync(new LoginRequest());
if (loginResult.IsError)
{
await DisplayAlert("Error", loginResult.Error, "Close");
return;
}
await SecureStorage.SetAsync(OidcConsts.AccessTokenKeyName, loginResult.AccessToken);
await SecureStorage.SetAsync(OidcConsts.RefreshTokenKeyName, loginResult.RefreshToken);
}
```
> Additionally, please configure each platform according to [Secure Storage documentation](https://docs.microsoft.com/en-us/xamarin/essentials/secure-storage?tabs=android)
## Configuring Client Proxies
ABP Client-Proxies don't use HttpClient directly. They use `IHttpClientFactory` to activate a new HttpClient instead of injecting it directly. So, we won't need **AccessTokenHttpMessageHandler** anymore. But still there is a way needed to set access token in requests. No worries, ABP has `IRemoteServiceHttpClientAuthenticator` to do that operation. Implementing it and registering to container will solve that issue and the client will be able to make authorized request to server.
- Remove **AccessTokenHttpMessageHandler.cs** from the project.
- Add **AccessTokenRemoteServiceHttpClientAuthenticator.cs** instead.
```csharp
using IdentityModel.Client;
using IdentityModel.OidcClient;
using System.IdentityModel.Tokens.Jwt;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Http.Client.Authentication;
using DependencyAttribute = Volo.Abp.DependencyInjection.DependencyAttribute;
namespace Acme.BookStore.MauiClient;
[Dependency(ReplaceServices = true)]
[ExposeServices(typeof(IRemoteServiceHttpClientAuthenticator))]
public class AccessTokenRemoteServiceHttpClientAuthenticator : IRemoteServiceHttpClientAuthenticator, ITransientDependency
{
protected OidcClient OidcClient { get; }
public AccessTokenRemoteServiceHttpClientAuthenticator(OidcClient oidcClient)
{
OidcClient = oidcClient;
}
public async Task Authenticate(RemoteServiceHttpClientAuthenticateContext context)
{
var currentAccessToken = await SecureStorage.GetAsync(OidcConsts.AccessTokenKeyName);
if (!currentAccessToken.IsNullOrEmpty())
{
// TODO: Find better way to find if token is expired instead of parsing it.
var jwtToken = new JwtSecurityTokenHandler().ReadJwtToken(currentAccessToken) as JwtSecurityToken;
if (jwtToken.ValidTo <= DateTime.UtcNow)
{
var refreshToken = await SecureStorage.GetAsync(OidcConsts.RefreshTokenKeyName);
if (!refreshToken.IsNullOrEmpty())
{
var refreshResult = await OidcClient.RefreshTokenAsync(refreshToken);
await SecureStorage.SetAsync(OidcConsts.AccessTokenKeyName, refreshResult.AccessToken);
await SecureStorage.SetAsync(OidcConsts.RefreshTokenKeyName, refreshResult.RefreshToken);
context.Request.SetBearerToken(refreshResult.AccessToken);
}
else
{
var loginResult = await OidcClient.LoginAsync(new LoginRequest());
await SecureStorage.SetAsync(OidcConsts.AccessTokenKeyName, loginResult.AccessToken);
await SecureStorage.SetAsync(OidcConsts.RefreshTokenKeyName, loginResult.RefreshToken);
context.Request.SetBearerToken(loginResult.AccessToken);
}
}
context.Request.SetBearerToken(currentAccessToken);
}
}
}
```
- Now we are ready to inject IAppServices to communicate with backend.
## Displaying Data in UI
- Go back to **Acme.BookStore.Domain** project and add a simple data seed contributor to generate some example data for users.
```csharp
public class UsersDataSeederContributor : IDataSeedContributor, ITransientDependency
{
protected IIdentityUserRepository repository;
protected IGuidGenerator guidGenerator;
public UsersDataSeederContributor(IIdentityUserRepository repository, IGuidGenerator guidGenerator)
{
this.repository = repository;
this.guidGenerator = guidGenerator;
}
public async Task SeedAsync(DataSeedContext context)
{
var count = await repository.GetCountAsync();
if(count <= 1) // Not sure 'admin' user was seeded before or not.
{
// All the names below were generated by https://www.name-generator.org.uk/quick/
// The names does not represent real people.
await repository.InsertManyAsync(new []{
new IdentityUser(guidGenerator.Create(), "john.doe", "john.doe@abp.io"),
new IdentityUser(guidGenerator.Create(), "Zane.Frost", "Zane.Frost@abp.io"),
new IdentityUser(guidGenerator.Create(), "Oscar.Landry", "Oscar.Landry@abp.io"),
new IdentityUser(guidGenerator.Create(), "Yasemin.Roberts", "Yasemin.Roberts@abp.io"),
new IdentityUser(guidGenerator.Create(), "Yasmine.Perez", "Yasmine.Perez@abp.io"),
new IdentityUser(guidGenerator.Create(), "Tobi.Becker", "Tobi.Becker@abp.io"),
new IdentityUser(guidGenerator.Create(), "Fox.Gilmore", "Fox.Gilmore@abp.io"),
new IdentityUser(guidGenerator.Create(), "Benny.Burris", "Benny.Burris@abp.io"),
new IdentityUser(guidGenerator.Create(), "Chad.Camacho", "Chad.Camacho@abp.io"),
});
}
}
}
```
- Run the **Acme.BookStore.DbMigrator** project.
- Turn back to MAUI app, and create a folder named **ViewModels** and add a simple `UsersViewModel.cs` under it.
```csharp
public class UsersViewModel : BindableObject, ITransientDependency
{
protected IIdentityUserAppService IdentityUserAppService { get; }
public GetIdentityUsersInput Input { get; } = new();
public ObservableCollection<IdentityUserDto> Items { get; } = new();
public Command RefreshCommand { get; }
private bool isBusy;
public bool IsBusy { get => isBusy; set => SetProperty(ref isBusy, value); }
public UsersViewModel(IIdentityUserAppService identityUserAppService)
{
IdentityUserAppService = identityUserAppService;
GetUsersAsync();
RefreshCommand = new Command(GetUsersAsync);
}
protected async void GetUsersAsync()
{
if (IsBusy)
{
return; // For preventing parallel request while searching.
}
IsBusy = true;
Items.Clear();
var result = await IdentityUserAppService.GetListAsync(Input);
foreach (var user in result.Items)
{
Items.Add(user);
}
IsBusy = false;
}
protected void SetProperty<T>(ref T backField, T value, [CallerMemberName] string propertyName = null)
{
backField = value;
OnPropertyChanged(propertyName);
}
}
```
- Create a folder named **Pages** and add a content page named `UsersPage`.
_(Make sure you're adding MAUI Content Page)_
![net-maui-abp-contentpage-template](art/net-maui-contentpage-template.png)
- And inject `UsersViewModel` into it.
```csharp
public partial class UsersPage : ContentPage, ITransientDependency
{
public UsersViewModel ViewModel { get; }
public UsersPage(UsersViewModel viewModel)
{
ViewModel = viewModel;
InitializeComponent();
}
}
```
- And use that ViewModel in XAML design page.
```xml
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="Acme.BookStore.MauiClient.UsersPage"
Title="UsersPage"
x:Name="page"
BindingContext="{Binding ViewModel, Source={x:Reference page}}">
<StackLayout>
<ListView
IsPullToRefreshEnabled="True"
ItemsSource="{Binding Items}"
IsRefreshing="{Binding IsBusy}"
RefreshCommand="{Binding RefreshCommand}">
<ListView.Header>
<SearchBar Text="{Binding Input.Filter}" SearchCommand="{Binding RefreshCommand}" />
</ListView.Header>
<ListView.ItemTemplate>
<DataTemplate>
<TextCell
Text="{Binding UserName, StringFormat='@{0}'}"
Detail="{Binding Email}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</StackLayout>
</ContentPage>
```
> I've used binding while setting **BindingContext** as **ViewModel** because of IntelliSense support. With this method, you'll see intellisense will suggest properties from your ViewModel.
>
> ![abp-maui-demo-xaml-intellisense](art/xaml-intellisense.png)
After a couple of try, I realized, only AppShell supports dependency injection while navigating between pages. So, adding a new AppShell will help to build app menus and navigating with route. We can pass parameters with querystring with this way.
- Add `Shell Pagae (MAUI)` to root of your application with name **AppShell.xaml**.
_I've got some help for design of shell page from microsoft's articles._
_Additionally, you might want to put [abp_icon.svg](maui/Acme.BookStore.MauiClient/Resources/Images/abp_logo.svg) file under your **Resources/Images** folder._
```xml
<?xml version="1.0" encoding="utf-8" ?>
<Shell x:Class="Acme.BookStore.MauiClient.AppShell"
xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:Acme.BookStore.MauiClient">
<Shell.Resources>
<ResourceDictionary>
<Color x:Key="Primary">#512BD4</Color>
<Style x:Key="BaseStyle" TargetType="Element">
<Setter Property="Shell.BackgroundColor" Value="{StaticResource Primary}" />
<Setter Property="Shell.ForegroundColor" Value="White" />
<Setter Property="Shell.TitleColor" Value="White" />
<Setter Property="Shell.DisabledColor" Value="#B4FFFFFF" />
<Setter Property="Shell.UnselectedColor" Value="#95FFFFFF" />
<Setter Property="Shell.TabBarBackgroundColor" Value="{StaticResource Primary}" />
<Setter Property="Shell.TabBarForegroundColor" Value="White"/>
<Setter Property="Shell.TabBarUnselectedColor" Value="#95FFFFFF"/>
<Setter Property="Shell.TabBarTitleColor" Value="White"/>
</Style>
<Style TargetType="TabBar" BasedOn="{StaticResource BaseStyle}" />
<Style TargetType="FlyoutItem" BasedOn="{StaticResource BaseStyle}" />
<Style Class="FlyoutItemLabelStyle" TargetType="Label">
<Setter Property="TextColor" Value="White"></Setter>
<Setter Property="Margin" Value="16"></Setter>
</Style>
<Style Class="FlyoutItemLayoutStyle" TargetType="Layout" ApplyToDerivedTypes="True">
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="{x:OnPlatform UWP=Transparent, iOS=White, Android=White}" />
<Setter TargetName="FlyoutItemLabel" Property="Label.TextColor" Value="{StaticResource Primary}" />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Selected">
<VisualState.Setters>
<Setter Property="BackgroundColor" Value="{StaticResource Primary}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
<Style Class="MenuItemLayoutStyle" TargetType="Layout" ApplyToDerivedTypes="True">
<Setter Property="VisualStateManager.VisualStateGroups">
<VisualStateGroupList>
<VisualStateGroup x:Name="CommonStates">
<VisualState x:Name="Normal">
<VisualState.Setters>
<Setter TargetName="FlyoutItemLabel" Property="Label.TextColor" Value="{StaticResource Primary}" />
</VisualState.Setters>
</VisualState>
</VisualStateGroup>
</VisualStateGroupList>
</Setter>
</Style>
</ResourceDictionary>
</Shell.Resources>
<FlyoutItem Title="Home">
<ShellContent ContentTemplate="{DataTemplate local:MainPage}" Route="main" />
</FlyoutItem>
<FlyoutItem Title="Users">
<ShellContent ContentTemplate="{DataTemplate local:UsersPage}" Route="UsersPage" />
</FlyoutItem>
<Shell.FlyoutHeader>
<StackLayout>
<Image
Source="abp_logo.svg"
HorizontalOptions="Center"
Margin="25"/>
</StackLayout>
</Shell.FlyoutHeader>
</Shell>
```
- One more step is required. Go to **App.xaml.cs** and replace MainPage with AppShell.
```csharp
public partial class App : Application
{
public App()
{
InitializeComponent();
MainPage = new AppShell();
}
}
```
- Run the application.
- Login once if you haven't done before.
- Navigate to Users page with hamburger menu at the right top.
| Android| iOS|
| --- | --- |
| ![abp-maui-android-appservice](art/android-users-demo.gif) | ![ios-abp-maui](art/ios-users-demo.gif) |
| UWP |
| --- |
| ![abp-maui-uwp-appservice](art/uwp-users-demo.gif) |
| MacCatalyst |
| --- |
| ![abp-maui-MacCatalyst-appservice](art/macos-users-demo.gif) |
## Conclusion
ABP Framework can be implemented any platform that runs on dotnet without suffer. ABP provides reusable abstractions layers and HttpApi Clients. In this article we've used powerful ABP core features such as Dependency Injection, Client Proxies, Validation and more.

100
docs/en/Multi-Lingual-Entities.md

@ -1,100 +1,6 @@
# Multi Lingual Entities
ABP Framework defines two basic interfaces for Multi-Lingual entity definitions to provide a standard model for translating entities.
This feature is still under development.
Follow the below link to get information about the development status
## IHasMultiLingual
`IHasMultiLingual<TTranslation>` interface is used to mark multi lingual entities. The entities marked with `IHasMultiLingual<TTranslation>` interface must define language-neutral information. The entities marked with `IHasMultiLingual<TTranslation>` contains a collection of Translations which contains language-dependent information.
Example:
```csharp
public class Product : Entity, IMultiLingualEntity<ProductTranslation>
{
public decimal Price { get; set; }
public ICollection<ProductTranslation> Translations { get; set; }
}
```
## IMultiLingualTranslation
`IMultiLingualTranslation` interface is used to mark translation of a Multi-Lingual entity. The entities marked with `IMultiLingualTranslation` interface must define language dependent information. The entities marked with `IMultiLingualTranslation` contains Language field which contains a language code for the translation.
Example:
```csharp
public class ProductTranslation : Entity, IMultiLingualTranslation
{
public string Name { get; set; }
public string Language { get; set; }
}
```
## Map to DTO object
ABP provdies the [Object To Object Mapping](Object-To-Object-Mapping.md) system, you can implement the `IObjectMapper<TSource, TDestination>` interface to map multi lingual entities to DTOs.
Example:
```csharp
public class MultiLingualProductObjectMapper : IObjectMapper<Product, ProductDto>, ITransientDependency
{
private readonly IMultiLingualObjectManager _multiLingualObjectManager;
public MultiLingualProductObjectMapper(IMultiLingualObjectManager multiLingualObjectManager)
{
_multiLingualObjectManager = multiLingualObjectManager;
}
public ProductDto Map(Product source)
{
var translation = _multiLingualObjectManager.GetTranslation<Product, ProductDto>(source);
return new ProductDto
{
Price = source.Price,
Id = source.Id,
Name = translation?.Name
};
}
public ProductDto Map(Product source, ProductDto destination)
{
return default;
}
}
```
### AutoMapper integration
ABP provides the `CreateMultiLingualMap` extension method for mapping multilingual entities to DTOs.
Example:
```csharp
public class ProductProfile : Profile
{
public ProductProfile()
{
var mapResult = this.CreateMultiLingualMap<Product, ProductTranslation, ProductDto>();
}
}
```
`CreateMultiLingualMap` extension method returns an object of type `CreateMultiLingualMapResult` which contains `EntityMap` and `TranslationMap` fields. These fields can be used to customize multi lingual mapping.
Example:
```csharp
this.CreateMultiLingualMap<Order, OrderTranslation, OrderListDto>(context)
.EntityMap.ForMember(dest => dest.ProductCount, opt => opt.MapFrom(src => src.Products.Count));
```
## IMultiLingualObjectManager
`IMultiLingualObjectManager` interface defines `GetTranslation` and `GetTranslationAsync` method to get the translation object of the entity.
The default implementation of the `IMultiLingualObjectManager` interface finds the translation with selected UI language first. If there is no translation with selected UI language, then extension method searches for the default language setting (see [Setting](Settings.md)) and uses the translation in default language. If extension method couldn't find any translation in current UI language or default language, it uses one of the existing translations.
https://github.com/abpframework/abp/issues/1169

4
docs/en/Previews.md

@ -1,6 +1,6 @@
# Preview Releases
The preview versions are released **~2 weeks before** releasing a major or feature version of the ABP Framework. They are released for developers to try and provide feedback to have more stable versions.
The preview versions are released **~4 weeks before** releasing a major or feature version of the ABP Framework. They are released for developers to try and provide feedback to have more stable versions.
Versioning of a preview release is like that:
@ -37,4 +37,4 @@ abp switch-to-stable
## Providing Feedback
You can open an issue on the [GitHub repository](https://github.com/abpframework/abp/issues/new), if you find a bug or want to provide any kind of feedback.
You can open an issue on the [GitHub repository](https://github.com/abpframework/abp/issues/new), if you find a bug or want to provide any kind of feedback.

2
docs/en/Samples/Index.md

@ -82,6 +82,6 @@ While there is no Razor Pages & MongoDB combination, you can check both document
* **Empty ASP.NET Core Application**: The most basic ASP.NET Core application with the ABP Framework installed.
* [Source code](https://github.com/abpframework/abp-samples/tree/master/BasicAspNetCoreApplication)
* [Documentation](../Getting-Started-AspNetCore-Application.md)
* **Using Elsa Workflow with ABP Framework**: Shows how we can use Elsa Core workflow library within our ABP-based application.
* **Using Elsa Workflow with ABP Framework**: Shows how to use the Elsa Core workflow library within an ABP-based application.
* [Source code](https://github.com/abpframework/abp-samples/tree/master/ElsaDemo)
* [Article](https://community.abp.io/articles/using-elsa-workflow-with-the-abp-framework-773siqi9)

2
docs/en/Tutorials/Part-1.md

@ -202,7 +202,7 @@ This will add a new migration class to the project:
![bookstore-efcore-migration](./images/bookstore-efcore-migration.png)
> If you are using Visual Studio, you may want to use the `Add-Migration Created_Book_Entity -c BookStoreMigrationsDbContext` and `Update-Database -c BookStoreMigrationsDbContext` commands in the *Package Manager Console (PMC)*. In this case, ensure that {{if UI=="MVC"}}`Acme.BookStore.Web`{{else if UI=="BlazorServer"}}`Acme.BookStore.Blazor`{{else if UI=="Blazor" || UI=="NG"}}`Acme.BookStore.HttpApi.Host`{{end}} is the startup project and `Acme.BookStore.EntityFrameworkCore` is the *Default Project* in PMC.
> If you are using Visual Studio, you may want to use the `Add-Migration Created_Book_Entity -c BookStoreDbContext` and `Update-Database -Context BookStoreDbContext` commands in the *Package Manager Console (PMC)*. In this case, ensure that {{if UI=="MVC"}}`Acme.BookStore.Web`{{else if UI=="BlazorServer"}}`Acme.BookStore.Blazor`{{else if UI=="Blazor" || UI=="NG"}}`Acme.BookStore.HttpApi.Host`{{end}} is the startup project and `Acme.BookStore.EntityFrameworkCore` is the *Default Project* in PMC.
{{end}}

2
docs/en/Tutorials/Part-10.md

@ -126,7 +126,7 @@ migrationBuilder.AddForeignKey(
* Creates an index on the `AuthorId` field.
* Declares the foreign key to the `AppAuthors` table.
> If you are using Visual Studio, you may want to use `Add-Migration Added_AuthorId_To_Book -c BookStoreMigrationsDbContext` and `Update-Database -c BookStoreMigrationsDbContext` commands in the *Package Manager Console (PMC)*. In this case, ensure that {{if UI=="MVC"}}`Acme.BookStore.Web`{{else if UI=="BlazorServer"}}`Acme.BookStore.Blazor`{{else if UI=="Blazor" || UI=="NG"}}`Acme.BookStore.HttpApi.Host`{{end}} is the startup project and `Acme.BookStore.EntityFrameworkCore` is the *Default Project* in PMC.
> If you are using Visual Studio, you may want to use `Add-Migration Added_AuthorId_To_Book -c BookStoreDbContext` and `Update-Database -Context BookStoreDbContext` commands in the *Package Manager Console (PMC)*. In this case, ensure that {{if UI=="MVC"}}`Acme.BookStore.Web`{{else if UI=="BlazorServer"}}`Acme.BookStore.Blazor`{{else if UI=="Blazor" || UI=="NG"}}`Acme.BookStore.HttpApi.Host`{{end}} is the startup project and `Acme.BookStore.EntityFrameworkCore` is the *Default Project* in PMC.
{{end}}

2
docs/en/Tutorials/Part-3.md

@ -129,7 +129,7 @@ Open the `CreateModal.cshtml` file and paste the code below:
</abp-dynamic-form>
````
* This modal uses `abp-dynamic-form` [tag helper](../UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md) to automatically create the form from the `CreateBookViewModel` model class.
* This modal uses `abp-dynamic-form` [tag helper](../UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md) to automatically create the form from the `CreateUpdateBookDto` model class.
* `abp-model` attribute indicates the model object where it's the `Book` property in this case.
* `abp-form-content` tag helper is a placeholder to render the form controls (it is optional and needed only if you have added some other content in the `abp-dynamic-form` tag, just like in this page).

2
docs/en/Tutorials/Part-7.md

@ -93,7 +93,7 @@ You can apply changes to the database using the following command, in the same c
dotnet ef database update
````
> If you are using Visual Studio, you may want to use `Add-Migration Added_Authors -c BookStoreMigrationsDbContext` and `Update-Database -c BookStoreMigrationsDbContext` commands in the *Package Manager Console (PMC)*. In this case, ensure that {{if UI=="MVC"}}`Acme.BookStore.Web`{{else if UI=="BlazorServer"}}`Acme.BookStore.Blazor`{{else if UI=="Blazor" || UI=="NG"}}`Acme.BookStore.HttpApi.Host`{{end}} is the startup project and `Acme.BookStore.EntityFrameworkCore` is the *Default Project* in PMC.
> If you are using Visual Studio, you may want to use `Add-Migration Added_Authors -c BookStoreDbContext` and `Update-Database -Context BookStoreDbContext` commands in the *Package Manager Console (PMC)*. In this case, ensure that {{if UI=="MVC"}}`Acme.BookStore.Web`{{else if UI=="BlazorServer"}}`Acme.BookStore.Blazor`{{else if UI=="Blazor" || UI=="NG"}}`Acme.BookStore.HttpApi.Host`{{end}} is the startup project and `Acme.BookStore.EntityFrameworkCore` is the *Default Project* in PMC.
{{else if DB=="Mongo"}}

16
docs/en/UI/Blazor/Error-Handling.md

@ -16,6 +16,8 @@ There are different type of `Exception` classes handled differently by the ABP F
`UserFriendlyException` is a special type of exception. You can directly show a error message dialog to the user by throwing such an exception.
> For Blazor Server, exceptions must be handled manually. Otherwise it crashes the whole application. Auto Exception Handling is not possible with Blazor Server right now, please follow [this issue](https://github.com/abpframework/abp/issues/8195) to see progress. In meantime, you can use try-catch blocks and call the `HandleErrorAsync` method of ABP to handle errors manually, which shows an error dialog for you.
**Example**
````csharp
@ -26,10 +28,24 @@ There are different type of `Exception` classes handled differently by the ABP F
@code
{
//for Blazor WASM
private void TestException()
{
throw new UserFriendlyException("A user friendly error message!");
}
//for Blazor Server
private async Task TestException()
{
try
{
throw new UserFriendlyException("A user friendly error message!");
}
catch(UserFriendlyException ex)
{
await HandleErrorAsync(ex);
}
}
}
````

9
docs/zh-Hans/CLI.md

@ -100,6 +100,15 @@ abp new Acme.BookStore
* `module`: [Module template](Startup-Templates/Module.md). 其他选项:
* `--no-ui`: 不包含UI.仅创建服务模块(也称为微服务 - 没有UI).
* **`console`**: [Console template](Startup-Templates/Console.md).
* **`app-nolayers`**: 应用程序单层模板
* `--ui` 或者 `-u`: 指定ui框架.默认`mvc`框架.其他选项:
* `mvc`: ASP.NET Core MVC.
* `angular`: Angular.
* `blazor-server`: Blazor Server.
* `none`: 不包含UI.
* `--database-provider` 或 `-d`: 或者 `-d`: 指定数据库提供程序.默认是 `ef`.其他选项:
* `ef`: Entity Framework Core.
* `mongodb`: MongoDB.
* `--output-folder` 或者 `-o`: 指定输出文件夹,默认是当前目录.
* `--version` 或者 `-v`: 指定ABP和模板的版本.它可以是 [release tag](https://github.com/abpframework/abp/releases) 或者 [branch name](https://github.com/abpframework/abp/branches). 如果没有指定,则使用最新版本.大多数情况下,你会希望使用最新的版本.
* `--preview`: 使用最新的预览版本.

257
docs/zh-Hans/Emailing.md

@ -1,3 +1,256 @@
# Emailing
# 邮件发送系统
待添加
ABP 框架为发送电子邮件提供各种服务、设置和集成;
* 提供用于发送电子邮件的`IEmailSender`服务.
* 定义 [settings](Settings.md)来配置电子邮件发送.
* 集成到[后台作业系统](Background-Jobs.md)以通过后台作业发送电子邮件.
* 提供[MailKit 集成](MailKit.md)包.
## 安装
> 如果你使用的是[应用程序启动模板](Startup-Templates/Application.md),则该软件包已安装。
>
建议使用 [ABP CLI](CLI.md) 安装此包。在项目文件夹(.csproj 文件)中打开命令行窗口并键入以下命令:
````bash
abp add-package Volo.Abp.Emailing
````
如果你还没有做到这一点,你首先需要安装 ABP CLI。有关其他安装选项,请参阅 [包描述页面](https://abp.io/package-detail/Volo.Abp.Emailing)。
## 发送电子邮件
### IEmailSender
[Inject](Dependency-Injection.md) 将 `IEmailSender` 注入任何服务并使用 `SendAsync` 方法发送电子邮件。
**Example**
````csharp
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Emailing;
namespace MyProject
{
public class MyService : ITransientDependency
{
private readonly IEmailSender _emailSender;
public MyService(IEmailSender emailSender)
{
_emailSender = emailSender;
}
public async Task DoItAsync()
{
await _emailSender.SendAsync(
"target@domain.com", // target email address
"Email subject", // subject
"This is email body..." // email body
);
}
}
}
````
`SendAsync` 方法具有重载以提供更多参数,例如;
* **from**: 你可以将其设置为设置发件人电子邮件地址。如果未提供,则使用默认发件人地址(请参阅下面的电子邮件设置)
* **isBodyHtml**: 表示邮件正文是否可以包含HTML标签。**默认:true**。
> `IEmailSender` 是建议的发送邮件的方式,因为它使你的代码提供者独立。
#### 邮件消息
除了原始参数之外,你还可以传递一个**标准的 `MailMessage` 对象**([参见](https://docs.microsoft.com/en-us/dotnet/api/system.net.mail.mailmessage) ) 到 `SendAsync` 方法以设置更多选项,例如添加附件。
### ISmtpEmailSender
默认情况下,发送电子邮件由标准的 `SmtpClient` 类([参见](https://docs.microsoft.com/en-us/dotnet/api/system.net.mail.smtpclient))实现。实现类是`SmtpEmailSender`。此类还公开了 `ISmtpEmailSender` 服务(除了 `IEmailSender`)。
大多数时候你想直接使用`IEmailSender`来让你的代码提供者独立。但是,如果要创建具有相同电子邮件设置的 `SmtpClient` 对象,可以注入 `ISmtpEmailSender` 并使用其 `BuildClientAsync` 方法获取 `SmtpClient` 对象并自己发送电子邮件。
## 发送邮件任务队列/后台作业
`IEmailSender`有一个`QueueAsync`方法,可以用来将邮件添加到后台作业队列中,在后台线程中发送。通过这种方式,你不会因为等待发送邮件而占用用户的时间。`QueueAsync`方法得到的参数与`SendAsync`方法相同。
发送邮件任务队列可以容忍错误,因为后台作业系统具有重试机制来克服临时网络/服务器问题。
有关后台作业系统的更多信息,请参阅[后台作业文档](Background-Jobs.md)。
## 电子邮件设置
电子邮件发送使用 [设置系统](Settings.md) 来定义设置并在运行时获取这些设置的值。 `Volo.Abp.Emailing.EmailSettingNames` 定义了设置名称的常量,如下所示:
* **Abp.Mailing.DefaultFromAddress**: 当你在发送电子邮件时未指定发件人时,用作发件人的电子邮件地址(就像上面的示例一样).
* **Abp.Mailing.DefaultFromDisplayName**: 当你在发送电子邮件时未指定发件人时,用作发件人的显示名称(就像在上面的示例中一样).
* **Abp.Mailing.Smtp.Host**: SMTP 服务器的 IP/域(默认值:127.0.0.1)。
* **Abp.Mailing.Smtp.Port**: SMTP 服务器的端口(默认值:25).
* **Abp.Mailing.Smtp.UserName**: 用户名,如果 SMTP 服务器需要身份验证需要。
* **Abp.Mailing.Smtp.Password**: 密码,如果 SMTP 服务器需要身份验证需要。 **此值已加密**(请参阅下面的部分).
* **Abp.Mailing.Smtp.Domain**: 账号域,如果 SMTP 服务器需要身份验证需要.
* **Abp.Mailing.Smtp.EnableSsl**: 指示 SMTP 服务器是否使用 SSL 的值(“true”或“false”。默认值:“false”).
* **Abp.Mailing.Smtp.UseDefaultCredentials**:如果为 true,则使用默认凭据,而不是提供的用户名和密码(“true”或“false”。默认值:“true”)。.
可以从[设置管理](Modules/Setting-Management.md)模块的*设置页面*管理电子邮件设置:
![email-settings](images/email-settings.png)
>如果你已从 ABP 启动模板创建解决方案,则已安装设置管理模块。
如果你不使用设置管理模块,你可以简单地在 `appsettings.json` 文件中定义设置:
````json
"Settings": {
"Abp.Mailing.Smtp.Host": "127.0.0.1",
"Abp.Mailing.Smtp.Port": "25",
"Abp.Mailing.Smtp.UserName": "",
"Abp.Mailing.Smtp.Password": "",
"Abp.Mailing.Smtp.Domain": "",
"Abp.Mailing.Smtp.EnableSsl": "false",
"Abp.Mailing.Smtp.UseDefaultCredentials": "true",
"Abp.Mailing.DefaultFromAddress": "noreply@abp.io",
"Abp.Mailing.DefaultFromDisplayName": "ABP application"
}
````
You can set/change these settings programmatically using the `ISettingManager` and store values in a database. See the [setting system document](Settings.md) to understand the setting system better.
你可以使用 `ISettingManager` 以编程方式设置/更改这些设置,并将值存储在数据库中。请参阅 [设置系统文档](Settings.md)更好地了解设置系统。
### 加密 SMTP 密码
*Abp.Mailing.Smtp.Password* 必须是一个**加密**值。如果你使用 `ISettingManager` 设置密码,你不必担心。它在内部加密 set 上的值并在 get 上解密。
如果使用 `appsettings.json` 存储密码,则应手动注入 `ISettingEncryptionService` 并使用其 `Encrypt` 方法获取加密值。这可以通过在你的应用程序中创建一个简单的代码来完成。然后你可以删除代码。更好的是,你可以在应用程序中创建一个 UI 来配置电子邮件设置。在这种情况下,你可以直接使用 `ISettingManager` 而不用担心加密。
### ISmtpEmailSenderConfiguration
如果你不想使用设置系统来存储电子邮件发送配置,你可以将 `ISmtpEmailSenderConfiguration` 服务替换为你自己的实现,以从任何其他来源获取配置。 `ISmtpEmailSenderConfiguration` 默认由 `SmtpEmailSenderConfiguration` 实现,如上所述,它从设置系统中获取配置。
## 文本模板集成
ABP 框架提供了一个强大而灵活的[文本模板系统](Text-Templating.md)。你可以使用文本模板系统来创建动态电子邮件内容。注入 `ITemplateRenderer` 并使用 `RenderAsync` 渲染模板。然后将结果用作电子邮件正文。
虽然你可以定义和使用自己的文本模板,但电子邮件发送系统提供了两个简单的内置文本模板。
**示例:使用标准和简单的消息模板发送电子邮件**
````csharp
using System.Threading.Tasks;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Emailing;
using Volo.Abp.Emailing.Templates;
using Volo.Abp.TextTemplating;
namespace Acme.BookStore.Web
{
public class MyService : ITransientDependency
{
private readonly IEmailSender _emailSender;
private readonly ITemplateRenderer _templateRenderer;
public MyService(
IEmailSender emailSender,
ITemplateRenderer templateRenderer)
{
_emailSender = emailSender;
_templateRenderer = templateRenderer;
}
public async Task DoItAsync()
{
var body = await _templateRenderer.RenderAsync(
StandardEmailTemplates.Message,
new
{
message = "This is email body..."
}
);
await _emailSender.SendAsync(
"target-address@domain.com",
"Email subject",
body
);
}
}
}
````
生成的电子邮件正文将如下所示:
````html
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
</head>
<body>
This is email body...
</body>
</html>
````
电子邮件系统定义了具有给定名称的内置文本模板:
"**Abp.StandardEmailTemplates.Message**" 是最简单的带有文本消息的模板:
````html
{%{{{model.message}}}%}
````
此模板使用“Abp.StandardEmailTemplates.Layout”作为其布局.
"**Abp.StandardEmailTemplates.Layout**" 是一个提供 HTML 文档布局的简单模板:
````html
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8" />
</head>
<body>
{%{{{content}}}%}
</body>
</html>
````
最终呈现的消息如上所示。
> 这些模板名称是在 `Volo.Abp.Emailing.Templates.StandardEmailTemplates` 类中定义的。
### 覆盖/替换标准模板
You typically want to replace the standard templates with your own ones, so you can prepare a branded email messages. To do that, you can use the power of the [virtual file system](Virtual-File-System.md) (VFS) or replace them in your own template definition provider.
你希望用自己的模板替换标准模板,这样你就可以准备电子邮件模板文件。你可以使用 [虚拟文件系统](Virtual-File-System.md) (VFS),或在你自己的模板定义提供程序中替换它们。
虚拟文件系统中模板的路径如下图所示:
* `/Volo/Abp/Emailing/Templates/Layout.tpl`
* `/Volo/Abp/Emailing/Templates/Message.tpl`
如果你将文件添加到虚拟文件系统中的相同位置,你的文件将覆盖它们。
模板是内联本地化的,这意味着你可以利用 [本地化系统](Localization.md) 的强大功能使你的模板具有多元文化。
详见[文本模板系统](Text-Templating.md) 文档。
> 请注意,你可以为应用程序定义和使用自己的模板,而不是使用标准的简单模板。这些标准模板主要用于可重用的模块,它们不定义自己的模板,而是依赖内置的模板。只需覆盖标准的电子邮件布局模板,就可以轻松自定义使用的模块发送的电子邮件。
## NullEmailSender
`NullEmailSender` 是实现 `IEmailSender` 的内置类,但将电子邮件内容写入 [标准日志系统](Logging.md),而不是实际发送电子邮件。
这个类特别有用,尤其是在你开发是不想发送真实电子邮件。 [应用启动模板](Startup-Templates/Application.md)已经在**DEBUG模式**中使用了这个类,在领域层配置如下:
````csharp
#if DEBUG
context.Services.Replace(ServiceDescriptor.Singleton<IEmailSender, NullEmailSender>());
#endif
````
因此,你在 DEBUG 模式下不会收到电子邮件。电子邮件将在生产时按预期发送(RELEASE 模式)。如果你也想在 DEBUG 上发送电子邮件,请删除这些行。
## 其他
* [用于发送电子邮件的MailKit集成](MailKit.md)

49
docs/zh-Hans/MailKit.md

@ -1 +1,48 @@
TODO...
# MailKit 集成
[MailKit](http://www.mimekit.net/) 是一个用于 .net 的跨平台、流行的开源邮件客户端库.ABP 框架提供了一个集成包来使用 MailKit 作为[邮件发送系统](Emailing.md)的收发组件.
## 安装
建议使用 [ABP CLI](CLI.md) 安装包.在项目文件夹(.csproj 文件)中打开命令行窗口并键入以下命令:
````bash
abp add-package Volo.Abp.MailKit
````
如果执行失败,你首先需要安装 ABP CLI.有关其他安装选项,请参阅 [包描述页面](https://abp.io/package-detail/Volo.Abp.MailKit).
## 发送电子邮件
### IEmailSender
[注入](Dependency-Injection.md) 标准的 `IEmailSender` 到任何服务并使用 `SendAsync` 方法发送电子邮件.详见 [邮件发送文档](Emailing.md).
> `IEmailSender` 是建议的发送邮件的方式,即使你使用MailKit,因为它使你的代码独立.
### IMailKitSmtpEmailSender
`BuildClientAsync()` 方法扩展了`IEmailSender`.此方法可用于获取可用于执行 MailKit 特定操作的`MailKit.Net.Smtp.SmtpClient`对象.
## 配置
MailKit 集成包使用电子邮件发送系统相同配置选项.请参阅[电子邮件发送文档](Emailing.md) 进行配置.
除了标准设置之外,这个包还定义了 `AbpMailKitOptions` 作为一个简单的 [选项](Options.md) 类.此类仅定义一个选项:
* **SecureSocketOption**:用于设置“SecureSocketOptions” . Default:`null`(使用默认值)
**示例: 使用 *SecureSocketOptions.SslOnConnect***
````csharp
Configure<AbpMailKitOptions>(options =>
{
options.SecureSocketOption = SecureSocketOptions.SslOnConnect;
});
````
请参阅 [MailKit 文档](http://www.mimekit.net/) 了解更多信息.
## 也可以看看
* [电子邮件发送系统](Emailing.md)

41
docs/zh-Hans/Previews.md

@ -1 +1,40 @@
TODO...
# 预览版
预览版在发布ABP 框架的主要版本或功能版本之前约 4 周发布。它们是为开发人员发布的,以尝试提供反馈以获得更稳定的版本。
预览版的版本控制是类似这样的:
* 3.1.0-rc.1
* 4.0.0-rc.1
在稳定版本(如 3.1.0)之前,可能会发布多个预览版本(如 3.1.0-rc.2 和 3.1.0-rc.3)。
## 使用预览版
### 新解决方案
要创建用于测试预览版的项目,您可以选择[下载页面](https://abp.io/get-started)上的“预览”选项或带有 **--preview** 参数的[ABP CLI](CLI.md) **abp new** 命令:
````bash
abp new Acme.BookStore --preview
````
此命令将使用最新的预览版 NuGet 包、NPM 包和解决方案模板创建一个新项目。每当发布稳定版本时,您都可以使用解决方案abp switch-to-stable根文件夹中的命令切换到解决方案的稳定版本。
### 现有解决方案
如果您已有解决方案并希望使用/测试最新的预览版,请在解决方案的根文件夹中使用以下 [ABP CLI](CLI.md) 命令.
````bash
abp switch-to-preview
````
您可以稍后使用 `abp switch-to-stable ` 命令返回最新的稳定版.
````bash
abp switch-to-stable
````
## 提供反馈
如果您发现错误或想要提供任何类型的反馈,您可以在[GitHub 存储库](https://github.com/abpframework/abp/issues/new)上打开一个问题。

4
docs/zh-Hans/Tutorials/Part-1.md

@ -202,7 +202,7 @@ dotnet ef migrations add Created_Book_Entity
![bookstore-efcore-migration](./images/bookstore-efcore-migration.png)
> 如果你使用Visual Studio, 你也许想要在*包管理控制台(PMC)*中使用 `Add-Migration Created_Book_Entity -c BookStoreMigrationsDbContext` 和 `Update-Database -c BookStoreMigrationsDbContext` 命令. 确保 {{if UI=="MVC"}}`Acme.BookStore.Web`{{else if UI=="BlazorServer"}}`Acme.BookStore.Blazor`{{else if UI=="Blazor" || UI=="NG"}}`Acme.BookStore.HttpApi.Host`{{end}} 是启动项目并且 `Acme.BookStore.EntityFrameworkCore.DbMigrations` 是 PMC 的*默认项目*.
> 如果你使用Visual Studio, 你也许想要在*包管理控制台(PMC)*中使用 `Add-Migration Created_Book_Entity -c BookStoreDbContext` 和 `Update-Database -Context BookStoreDbContext` 命令. 确保 {{if UI=="MVC"}}`Acme.BookStore.Web`{{else if UI=="BlazorServer"}}`Acme.BookStore.Blazor`{{else if UI=="Blazor" || UI=="NG"}}`Acme.BookStore.HttpApi.Host`{{end}} 是启动项目并且 `Acme.BookStore.EntityFrameworkCore.DbMigrations` 是 PMC 的*默认项目*.
{{end}}
@ -290,7 +290,7 @@ namespace Acme.BookStore
using System;
using Volo.Abp.Application.Dtos;
namespace Acme.BookStore
namespace Acme.BookStore.Books
{
public class BookDto : AuditedEntityDto<Guid>
{

2
docs/zh-Hans/Tutorials/Part-10.md

@ -126,7 +126,7 @@ migrationBuilder.AddForeignKey(
* 根据 `AuthorId` 字段新建一个索引.
* 声明到 `AppAuthors` 表的外键.
> 如果你使用 Visual Studio, 可能希望在 *Package Manager Console (PMC)* 使用 `Add-Migration Added_AuthorId_To_Book -c BookStoreMigrationsDbContext` 和 `Update-Database -c BookStoreMigrationsDbContext` 命令. 如果这样, 保证 {{if UI=="MVC"}}`Acme.BookStore.Web`{{else if UI=="BlazorServer"}}`Acme.BookStore.Blazor`{{else if UI=="Blazor" || UI=="NG"}}`Acme.BookStore.HttpApi.Host`{{end}} 是启动项目并且在PMC中 `Acme.BookStore.EntityFrameworkCore` 是 *默认项目* .
> 如果你使用 Visual Studio, 可能希望在 *Package Manager Console (PMC)* 使用 `Add-Migration Added_AuthorId_To_Book -c BookStoreDbContext` 和 `Update-Database -Context BookStoreDbContext` 命令. 如果这样, 保证 {{if UI=="MVC"}}`Acme.BookStore.Web`{{else if UI=="BlazorServer"}}`Acme.BookStore.Blazor`{{else if UI=="Blazor" || UI=="NG"}}`Acme.BookStore.HttpApi.Host`{{end}} 是启动项目并且在PMC中 `Acme.BookStore.EntityFrameworkCore` 是 *默认项目* .
{{end}}

2
docs/zh-Hans/Tutorials/Part-2.md

@ -148,6 +148,8 @@ successfully created the book with id: 439b0ea8-923e-8e1e-5d97-39f2c7ac4246
}
````
> 简体中文翻译请打开`zh-Hans.json`文件 ,并将"Texts"对象中对应的值替换为中文.
* 本地化关键字名称是任意的. 你可以设置任何名称. 对于特定的文本类型,我们更喜欢遵循一些约定:
* 为按钮项添加 `Menu:` 前缀.
* 使用 `Enum:<enum-type>:<enum-value>` 命名约定来本地化枚举成员. 当您这样做时ABP可以在某些适当的情况下自动将枚举本地化.

4
docs/zh-Hans/Tutorials/Part-3.md

@ -129,7 +129,7 @@ namespace Acme.BookStore.Web.Pages.Books
</abp-dynamic-form>
````
* 这个 modal 使用 `abp-dynamic-form` [tag Helper](../UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md) 根据 `CreateBookViewModel` 类自动构建了表单.
* 这个 modal 使用 `abp-dynamic-form` [tag Helper](../UI/AspNetCore/Tag-Helpers/Dynamic-Forms.md) 根据 `CreateUpdateBookDto` 类自动构建了表单.
* `abp-model` 指定了 `Book` 属性为模型对象.
* `abp-form-content` tag helper 作为表单控件渲染位置的占位符 (这是可选的,只有你在 `abp-dynamic-form` 中像本示例这样添加了其他内容才需要).
@ -523,6 +523,8 @@ $(function () {
"SuccessfullyDeleted": "Successfully deleted!"
````
> 简体中文翻译请打开`zh-Hans.json`文件 ,并将"Texts"对象中对应的值替换为中文.
`Index.js` 的内容最终如下所示:
````js

2
docs/zh-Hans/Tutorials/Part-5.md

@ -122,7 +122,7 @@ namespace Acme.BookStore.Permissions
"Permission:Books.Delete": "Deleting the books"
````
> 本地化键名可以是任意的, 并没有强制的规则. 但我们推荐上面使用的约定.
> 本地化键名可以是任意的, 并没有强制的规则. 但我们推荐上面使用的约定. 简体中文翻译请打开`zh-Hans.json`文件 ,并将"Texts"对象中对应的值替换为中文.
### 权限管理界面

2
docs/zh-Hans/Tutorials/Part-6.md

@ -231,6 +231,8 @@ namespace Acme.BookStore
"BookStore:00001": "There is already an author with the same name: {name}"
````
> 简体中文翻译请打开`zh-Hans.json`文件 ,并将"Texts"对象中对应的值替换为中文.
当 `AuthorAlreadyExistsException` 被抛出, 终端用户将会在UI上看到组织好的错误消息.
## IAuthorRepository

2
docs/zh-Hans/Tutorials/Part-7.md

@ -93,7 +93,7 @@ dotnet ef migrations add Added_Authors
dotnet ef database update
````
> 如果你使用 Visual Studio, 可能希望在 *Package Manager Console (PMC)* 使用 `Add-Migration Added_Authors -c BookStoreMigrationsDbContext` 和 `Update-Database -c BookStoreMigrationsDbContext` 命令. 如果这样, 保证 {{if UI=="MVC"}}`Acme.BookStore.Web`{{else if UI=="BlazorServer"}}`Acme.BookStore.Blazor`{{else if UI=="Blazor" || UI=="NG"}}`Acme.BookStore.HttpApi.Host`{{end}} 是启动项目并且在PMC中 `Acme.BookStore.EntityFrameworkCore` 是 *默认项目* .
> 如果你使用 Visual Studio, 可能希望在 *Package Manager Console (PMC)* 使用 `Add-Migration Added_Authors -c BookStoreDbContext` 和 `Update-Database -Context BookStoreDbContext` 命令. 如果这样, 保证 {{if UI=="MVC"}}`Acme.BookStore.Web`{{else if UI=="BlazorServer"}}`Acme.BookStore.Blazor`{{else if UI=="Blazor" || UI=="NG"}}`Acme.BookStore.HttpApi.Host`{{end}} 是启动项目并且在PMC中 `Acme.BookStore.EntityFrameworkCore` 是 *默认项目* .
{{else if DB=="Mongo"}}

2
docs/zh-Hans/Tutorials/Part-8.md

@ -381,6 +381,8 @@ authorsPermission.AddChild(
"Permission:Authors.Delete": "Deleting the authors"
````
> 简体中文翻译请打开`zh-Hans.json`文件 ,并将"Texts"对象中对应的值替换为中文.
## 对象到对象映射
`AuthorAppService` 使用 `ObjectMapper` 将 `Author` 对象 转换为 `AuthorDto` 对象. 所以, 我们需要在 AutoMapper 配置中定义映射.

6
docs/zh-Hans/Tutorials/Part-9.md

@ -219,6 +219,8 @@ $(function () {
"NewAuthor": "New author"
````
> 简体中文翻译请打开`zh-Hans.json`文件 ,并将"Texts"对象中对应的值替换为中文.
注意我们加入了额外的键. 它们会在下面的小节中被使用.
### 加入主菜单
@ -821,6 +823,8 @@ export class AuthorComponent implements OnInit {
"NewAuthor": "New author"
````
> 简体中文翻译请打开`zh-Hans.json`文件 ,并将"Texts"对象中对应的值替换为中文.
### 运行应用程序
运行并登录应用程序. **因为你还没有权限, 所以不能看见菜单项**. 转到 `Identity/Roles` 页面, 点击 *操作* 按钮并选择**管理员角色**的*权限*操作:
@ -1228,6 +1232,8 @@ if (await context.IsGrantedAsync(BookStorePermissions.Authors.Default))
"NewAuthor": "New author"
````
> 简体中文翻译请打开`zh-Hans.json`文件 ,并将"Texts"对象中对应的值替换为中文.
### 运行应用程序
运行并登录应用程序. **因为你还没有权限, 所以不能看见菜单项**. 转到 `Identity/Roles` 页面, 点击 *操作* 按钮并选择**管理员角色**的*权限*操作:

60
docs/zh-Hans/Upgrading.md

@ -1 +1,59 @@
TODO...
# 升级 ABP 框架
本文档解释当ABP框架发布一个新版本时,怎样升级你已存在的解决方案.
## ABP UPDATE 命令
ABP 框架 & 模块生态系统由数百个 NuGet 和 NPM 包组成. 手动更新所有这些软件包以升级您的应用程序会很麻烦。
[ABP CLI](CLI.md) 提供了一个方便的命令,可以使用单个命令更新解决方案中所有与 ABP 相关的 NuGet 和 NPM 包:
````bash
abp update
````
在终端里,解决方案的根文件夹中运行此命令。
> 如果您的解决方案有 Angular UI,您的解决方案中可能有 `aspnet-core` 和 `angular` 文件夹。在这两个文件夹的父文件夹中运行此命令。
### 数据库迁移
> 警告:迁移数据库时要小心,因为在某些情况下可能会丢失数据。在执行之前仔细检查生成的迁移代码。建议备份您当前的数据库。
当您升级到新版本时,如果您的数据库提供者是 **Entity Framework Core**,最好检查是否有数据库架构更改并升级您的数据库架构;
* 在包管理器控制台 (PMC) 中使用 `Add-Migration "Upgraded_To_Abp_4_1"` 或类似命令创建新迁移(在 Visual Studio 中将 `EntityFrameworkCore` 设置为 PMC 中的默认项目,并将 `.DbMigrator` 在解决方案资源管理器中设置为启动项目)。
* 运行 `.DbMigrator` 应用程序以升级数据库并播种初始数据。
如果 `Add-Migration` 生成一个空迁移,您可以在执行 `.DbMigrator` 之前使用 `Remove-Migration` 将其删除。
## 博客文章和指南
每当您升级您的解决方案时,强烈建议您查看 [ABP BLOG](https://blog.abp.io/) 以了解新版本的新功能和更改。我们会定期发布帖子并编写此类更改。
### 迁移指南
如果新版本为现有应用程序带来重大更改,我们会准备迁移指南。所有指南请参见 [迁移指南](Migration-Guides/Index.md) 页面。
### 升级 Startup 模板
有时我们会引入需要**在启动模板中进行更改**的新功能/更改。我们已经在新应用程序的启动模板中实现了更改。但是,在某些情况下,您需要手动对现有解决方案进行一些小的更改。
逐行记录必要的更改是不切实际的。在这种情况下,我们建议您创建一个示例解决方案,一个是现有版本,一个是新版本,并使用差异工具进行比较。您可以 [查看本指南](Migration-Guides/Upgrading-Startup-Template.md) 了解如何使用 WinMerge 应用程序进行操作。
## 语义版本控制和重大更改
我们正在努力保持语义版本控制规则,因此您不会对 3.1、3.2、3.3 等次要(功能)版本进行重大更改......
但是,在某些情况下,我们也可能会在功能版本中引入重大更改;
* ABP 有许多集成包,有时集成库/框架会发布主要版本并进行重大更改。在这种情况下,我们会仔细检查这些更改并决定是否升级集成包。如果变更的影响比较小,我们会更新集成包,并在发布博文中说明变更。在这种情况下,如果您使用过此集成包,则应按照博文中的说明进行操作。如果更改可能会破坏许多应用程序并且不容易修复,我们决定等到下一个主要的 ABP 框架版本发布时升级。
* 有时我们必须进行重大更改以修复主要错误或使用问题。在这种情况下,我们认为开发人员已经无法正确使用该功能,因此通过重大更改修复它没有问题。在这种情况下,该功能通常是很少使用的功能。同样,我们尝试将影响保持在最低限度。
## 预览版和每日构建
预览版和夜间构建可以帮助您在新的稳定版本之前尝试新功能并调整您的解决方案。
* [预览版](Previews.md) 通常在次要(功能)版本之前约 2 周发布(我们的次要版本开发周期约为 4 周)。
* [每日构建](Nightly-Builds.md) 每天晚上(周末除外)从开发分支发布。这意味着您可以尝试前一天的开发。
请参阅他们的文档以了解有关此类版本的详细信息。

34
docs/zh-Hans/docs-nav.json

@ -71,16 +71,44 @@
"text": "应用开发",
"items": [
{
"text": "第一章: 创建新解决方案和列表页",
"text": "1: 创建应用服务端",
"path": "Tutorials/Part-1.md"
},
{
"text": "第二章: 增删改查操作",
"text": "2: 图书列表页面",
"path": "Tutorials/Part-2.md"
},
{
"text": "第三章: 集成测试",
"text": "3: 创建,更新和删除书籍",
"path": "Tutorials/Part-3.md"
},
{
"text": "4: 集成测试",
"path": "Tutorials/Part-4.md"
},
{
"text": "5: 授权",
"path": "Tutorials/Part-5.md"
},
{
"text": "6: 作者:领域层",
"path": "Tutorials/Part-6.md"
},
{
"text": "7: 作者:数据库集成",
"path": "Tutorials/Part-7.md"
},
{
"text": "8: 作者:应用服务层",
"path": "Tutorials/Part-8.md"
},
{
"text": "9: 作者:用户界面",
"path": "Tutorials/Part-9.md"
},
{
"text": "10: 图书-作者关系",
"path": "Tutorials/Part-10.md"
}
]
},

2
framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/datatables/datatables-extensions.js

@ -366,7 +366,7 @@ var abp = abp || {};
}
//Text filter
if(settings.oInit.searching){
if(settings.oInit.searching !== false){
if (requestData.search && requestData.search.value !== "") {
input.filter = requestData.search.value;
} else {

12
framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/Auditing/AbpAuditHubFilter.cs

@ -55,7 +55,17 @@ public class AbpAuditHubFilter : IHubFilter
var unitOfWorkManager = invocationContext.ServiceProvider.GetRequiredService<IUnitOfWorkManager>();
if (unitOfWorkManager.Current != null)
{
await unitOfWorkManager.Current.SaveChangesAsync();
try
{
await unitOfWorkManager.Current.SaveChangesAsync();
}
catch (Exception ex)
{
if (!auditingManager.Current.Log.Exceptions.Contains(ex))
{
auditingManager.Current.Log.Exceptions.Add(ex);
}
}
}
await saveHandle.SaveAsync();

12
framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Auditing/AbpAuditingMiddleware.cs

@ -73,7 +73,17 @@ public class AbpAuditingMiddleware : IMiddleware, ITransientDependency
{
if (UnitOfWorkManager.Current != null)
{
await UnitOfWorkManager.Current.SaveChangesAsync();
try
{
await UnitOfWorkManager.Current.SaveChangesAsync();
}
catch (Exception ex)
{
if (!_auditingManager.Current.Log.Exceptions.Contains(ex))
{
_auditingManager.Current.Log.Exceptions.Add(ex);
}
}
}
await saveHandle.SaveAsync();

22
framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditingInterceptor.cs

@ -7,6 +7,7 @@ using Microsoft.Extensions.Options;
using Volo.Abp.Aspects;
using Volo.Abp.DependencyInjection;
using Volo.Abp.DynamicProxy;
using Volo.Abp.Uow;
using Volo.Abp.Users;
namespace Volo.Abp.Auditing;
@ -41,7 +42,8 @@ public class AuditingInterceptor : AbpInterceptor, ITransientDependency
else
{
var currentUser = serviceScope.ServiceProvider.GetRequiredService<ICurrentUser>();
await ProcessWithNewAuditingScopeAsync(invocation, auditingOptions, currentUser, auditingManager, auditingHelper);
var unitOfWorkManager = serviceScope.ServiceProvider.GetRequiredService<IUnitOfWorkManager>();
await ProcessWithNewAuditingScopeAsync(invocation, auditingOptions, currentUser, auditingManager, auditingHelper, unitOfWorkManager);
}
}
}
@ -105,7 +107,8 @@ public class AuditingInterceptor : AbpInterceptor, ITransientDependency
AbpAuditingOptions options,
ICurrentUser currentUser,
IAuditingManager auditingManager,
IAuditingHelper auditingHelper)
IAuditingHelper auditingHelper,
IUnitOfWorkManager unitOfWorkManager)
{
var hasError = false;
using (var saveHandle = auditingManager.BeginScope())
@ -129,6 +132,21 @@ public class AuditingInterceptor : AbpInterceptor, ITransientDependency
{
if (ShouldWriteAuditLog(invocation, options, currentUser, hasError))
{
if (unitOfWorkManager.Current != null)
{
try
{
await unitOfWorkManager.Current.SaveChangesAsync();
}
catch (Exception ex)
{
if (!auditingManager.Current.Log.Exceptions.Contains(ex))
{
auditingManager.Current.Log.Exceptions.Add(ex);
}
}
}
await saveHandle.SaveAsync();
}
}

4
framework/src/Volo.Abp.BlazoriseUI/Components/AbpExtensibleDataGrid.razor

@ -9,7 +9,9 @@
TotalItems="@TotalItems"
ShowPager="@ShowPager"
CurrentPage="@CurrentPage"
PageSize="@PageSize">
PageSize="@PageSize"
Responsive="@Responsive"
Class="@Class">
<LoadingTemplate>
<Row Class="w-100 align-items-center" Style="height: 150px;">
<Column>

12
framework/src/Volo.Abp.BlazoriseUI/Components/AbpExtensibleDataGrid.razor.cs

@ -1,12 +1,10 @@
using System;
using Blazorise.Extensions;
using Blazorise.DataGrid;
using Microsoft.AspNetCore.Components;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using Blazorise.DataGrid;
using Blazorise.Extensions;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Localization;
using System.Threading.Tasks;
using Volo.Abp.AspNetCore.Components.Web.Extensibility.EntityActions;
using Volo.Abp.AspNetCore.Components.Web.Extensibility.TableColumns;
namespace Volo.Abp.BlazoriseUI.Components;
@ -34,6 +32,10 @@ public partial class AbpExtensibleDataGrid<TItem> : ComponentBase
[Parameter] public int CurrentPage { get; set; } = 1;
[Parameter] public string Class { get; set; }
[Parameter] public bool Responsive { get; set; }
[Inject]
public IStringLocalizerFactory StringLocalizerFactory { get; set; }

2
framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizationResourceContributorList.cs

@ -8,7 +8,7 @@ public class LocalizationResourceContributorList : List<ILocalizationResourceCon
{
public LocalizedString GetOrNull(string cultureName, string name)
{
foreach (var contributor in this.AsQueryable().Reverse())
foreach (var contributor in this.Select(x => x).Reverse())
{
var localString = contributor.GetOrNull(cultureName, name);
if (localString != null)

12
framework/src/Volo.Abp.Swashbuckle/Microsoft/Extensions/DependencyInjection/AbpSwaggerGenOptionsExtensions.cs

@ -0,0 +1,12 @@
using Swashbuckle.AspNetCore.SwaggerGen;
using Volo.Abp.Swashbuckle;
namespace Microsoft.Extensions.DependencyInjection;
public static class AbpSwaggerGenOptionsExtensions
{
public static void HideAbpEndpoints(this SwaggerGenOptions swaggerGenOptions)
{
swaggerGenOptions.DocumentFilter<AbpSwashbuckleDocumentFilter>();
}
}

28
framework/src/Volo.Abp.Swashbuckle/Volo/Abp/Swashbuckle/AbpSwashbuckleDocumentFilter.cs

@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace Volo.Abp.Swashbuckle;
public class AbpSwashbuckleDocumentFilter : IDocumentFilter
{
protected string[] ActionUrlPrefixes = new[] {"Volo.Abp"};
public virtual void Apply(OpenApiDocument swaggerDoc, DocumentFilterContext context)
{
var actionUrls = context.ApiDescriptions
.Select(apiDescription => apiDescription.ActionDescriptor)
.Where(actionDescriptor => !string.IsNullOrEmpty(actionDescriptor.DisplayName) &&
ActionUrlPrefixes.Any(actionUrlPrefix => !actionDescriptor.DisplayName.Contains(actionUrlPrefix)))
.DistinctBy(actionDescriptor => actionDescriptor.AttributeRouteInfo?.Template)
.Select(actionDescriptor => actionDescriptor.AttributeRouteInfo?.Template.EnsureStartsWith('/'))
.Where(actionUrl => !string.IsNullOrEmpty(actionUrl))
.ToList();
swaggerDoc
.Paths
.RemoveAll(path => !actionUrls.Contains(path.Key));
}
}

4
modules/account/src/Volo.Abp.Account.Web/Pages/Account/Components/ProfileManagementGroup/PersonalInfo/Default.js

@ -13,6 +13,10 @@
volo.abp.account.profile.update(input).then(function (result) {
abp.notify.success(l('PersonalSettingsSaved'));
volo.abp.account.profile.get().then(function(profile){
$("#ConcurrencyStamp").val(profile.concurrencyStamp);
});
});
});
});

0
modules/blogging/app/Volo.BloggingTestApp.MongoDb/Volo.BloggingTestApp.MongoDB.abppkg.json → modules/blogging/app/Volo.BloggingTestApp.MongoDB/Volo.BloggingTestApp.MongoDB.abppkg.json

2
modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/BlogPosts/create.js

@ -242,7 +242,7 @@ $(function () {
change: function (_val) {
$editorInput.val(editor.getMarkdown());
$editorInput.trigger("change");
reflectContentChanges(editor.getHtml());
reflectContentChanges(editor.getHTML());
}
}
});

19
modules/docs/src/Volo.Docs.Admin.Application/Volo/Docs/Admin/Projects/ProjectAdminAppService.cs

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Volo.Abp.Application.Dtos;
@ -134,22 +135,14 @@ namespace Volo.Docs.Admin.Projects
throw new Exception("Cannot find the project with the Id " + projectId);
}
var docs = await _documentRepository.GetListByProjectId(project.Id);
var docs = (await _documentRepository.GetListByProjectId(project.Id))
.Where(doc => doc.FileName != project.NavigationDocumentName && doc.FileName != project.ParametersDocumentName)
.ToList();
await _elasticSearchService.DeleteAllByProjectIdAsync(project.Id);
foreach (var doc in docs)
if(docs.Any())
{
if (doc.FileName == project.NavigationDocumentName)
{
continue;
}
if (doc.FileName == project.ParametersDocumentName)
{
continue;
}
await _elasticSearchService.AddOrUpdateAsync(doc);
await _elasticSearchService.AddOrUpdateManyAsync(docs);
}
}

1
modules/docs/src/Volo.Docs.Admin.Web/Pages/Docs/Admin/Projects/Edit.cshtml.cs

@ -121,6 +121,7 @@ namespace Volo.Docs.Admin.Pages.Docs.Admin.Projects
[DynamicStringLength(typeof(ProjectConsts), nameof(ProjectConsts.MaxLatestVersionBranchNameLength))]
public string LatestVersionBranchName { get; set; }
[HiddenInput]
public string ConcurrencyStamp { get; set; }
}

20
modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/FullSearch/Elastic/ElasticDocumentFullSearch.cs

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Elasticsearch.Net;
@ -69,7 +70,24 @@ namespace Volo.Docs.Documents.FullSearch.Elastic
Version = NormalizeField(document.Version)
};
await client.IndexAsync(esDocument, x => x.Index(_options.IndexName), cancellationToken);
HandleError(await client.IndexAsync(esDocument, x => x.Index(_options.IndexName), cancellationToken));
}
public virtual async Task AddOrUpdateManyAsync(IEnumerable<Document> documents, CancellationToken cancellationToken = default)
{
var client = _clientProvider.GetClient();
var esDocuments = documents.Select(x => new EsDocument {
Id = NormalizeField(x.Id),
ProjectId = NormalizeField(x.ProjectId),
Name = x.Name,
FileName = x.FileName,
Content = x.Content,
LanguageCode = NormalizeField(x.LanguageCode),
Version = NormalizeField(x.Version)
});
HandleError(await client.IndexManyAsync(esDocuments, _options.IndexName, cancellationToken));
}
public virtual async Task DeleteAsync(Guid id, CancellationToken cancellationToken = default)

2
modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/FullSearch/Elastic/IDocumentFullSearch.cs

@ -11,6 +11,8 @@ namespace Volo.Docs.Documents.FullSearch.Elastic
Task AddOrUpdateAsync(Document document, CancellationToken cancellationToken = default);
Task AddOrUpdateManyAsync(IEnumerable<Document> documents, CancellationToken cancellationToken = default);
Task DeleteAsync(Guid id, CancellationToken cancellationToken = default);
Task DeleteAllAsync(CancellationToken cancellationToken = default);

2
modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/GithubDocumentSource.cs

@ -383,7 +383,7 @@ namespace Volo.Docs.GitHub.Documents
catch (Exception ex)
{
//TODO: Only handle when document is really not available
Logger.LogWarning(ex.Message, ex);
Logger.LogWarning($"{ex.Message}: {rawUrl}", ex);
throw new DocumentNotFoundException(rawUrl);
}
}

2
modules/setting-management/src/Volo.Abp.SettingManagement.Blazor/Pages/SettingManagement/EmailSettingGroup/EmailSettingGroupViewComponent.razor.cs

@ -29,7 +29,7 @@ public partial class EmailSettingGroupViewComponent
LocalizationResource = typeof(AbpSettingManagementResource);
}
protected override async Task OnInitializedAsync()
protected async override Task OnInitializedAsync()
{
try
{

2
modules/setting-management/src/Volo.Abp.SettingManagement.Blazor/Pages/SettingManagement/SettingManagement.razor

@ -11,7 +11,7 @@
<Card>
<CardBody>
<Tabs @bind-SelectedTab="@SelectedGroup" TabPosition="TabPosition.Start" Pills="true">
<Tabs @bind-SelectedTab="@SelectedGroup" TabPosition="TabPosition.Start" Pills="true" Mode="TabsMode.LazyReload">
<Items>
@foreach (var group in SettingComponentCreationContext.Groups)
{

4
npm/ng-packs/packages/theme-basic/src/lib/tests/lazy-style.handler.spec.ts

@ -4,7 +4,7 @@ import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest';
import { of } from 'rxjs';
import { BOOTSTRAP, createLazyStyleHref, LazyStyleHandler } from '../handlers';
const languageChange$ = of({ payload: 'en' });
const currentLang$ = of({ payload: 'en' });
describe('LazyStyleHandler', () => {
let spectator: SpectatorService<LazyStyleHandler>;
@ -17,7 +17,7 @@ describe('LazyStyleHandler', () => {
DocumentDirHandlerService,
{
provide: LocalizationService,
useValue: { currentLang: 'en', languageChange$ },
useValue: { currentLang: 'en', currentLang$ },
},
],
});

8
npm/ng-packs/packages/theme-shared/extensions/src/lib/adapters/date-time.adapter.ts

@ -27,16 +27,16 @@ export class DateTimeAdapter {
toModel(value: NgbDateTimeStruct | null): string {
if (!value) return '';
//const now = new Date();
const now = new Date();
value = {
// TODO
/* year: now.getUTCFullYear(),
// TODO look for strict mode errors
year: now.getUTCFullYear(),
month: now.getMonth() + 1,
day: now.getDate(),
hour: 0,
minute: 0,
second: 0, */
second: 0,
...this.value,
...value,
};

2
npm/ng-packs/packages/theme-shared/extensions/src/lib/enums/props.enum.ts

@ -1,7 +1,7 @@
export const enum ePropType {
Boolean = 'boolean',
Date = 'date',
DateTime = 'dateTime',
DateTime = 'datetime',
Email = 'email',
Enum = 'enum',
Hidden = 'hidden',

4
npm/ng-packs/packages/theme-shared/src/lib/components/button/button.component.ts

@ -18,6 +18,7 @@ import {
#button
[id]="buttonId"
[attr.type]="buttonType"
[attr.form]="formName"
[ngClass]="buttonClass"
[disabled]="loading || disabled"
(click.stop)="click.next($event); abpClick.next($event)"
@ -37,6 +38,9 @@ export class ButtonComponent implements OnInit {
@Input()
buttonType = 'button';
@Input()
formName?: string = undefined;
@Input()
iconClass?: string;

4
templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server.Mongo/Program.cs

@ -18,9 +18,11 @@ public class Program
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
.MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Warning)
.Enrich.FromLogContext()
.WriteTo.Async(c => c.File("Logs/logs.txt"))
#if DEBUG
.WriteTo.Async(c => c.File("Logs/logs.txt"))
.WriteTo.Async(c => c.Console());
#else
.WriteTo.Async(c => c.File("Logs/logs.txt"));
#endif
if (IsMigrateDatabase(args))
{

4
templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/Program.cs

@ -18,9 +18,11 @@ public class Program
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
.MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Warning)
.Enrich.FromLogContext()
.WriteTo.Async(c => c.File("Logs/logs.txt"))
#if DEBUG
.WriteTo.Async(c => c.File("Logs/logs.txt"))
.WriteTo.Async(c => c.Console());
#else
.WriteTo.Async(c => c.File("Logs/logs.txt"));
#endif
if (IsMigrateDatabase(args))
{

4
templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host.Mongo/Program.cs

@ -17,9 +17,11 @@ public class Program
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
.MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Warning)
.Enrich.FromLogContext()
.WriteTo.Async(c => c.File("Logs/logs.txt"))
#if DEBUG
.WriteTo.Async(c => c.File("Logs/logs.txt"))
.WriteTo.Async(c => c.Console());
#else
.WriteTo.Async(c => c.File("Logs/logs.txt"));
#endif
if (IsMigrateDatabase(args))
{

4
templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/Program.cs

@ -17,9 +17,11 @@ public class Program
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
.MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Warning)
.Enrich.FromLogContext()
.WriteTo.Async(c => c.File("Logs/logs.txt"))
#if DEBUG
.WriteTo.Async(c => c.File("Logs/logs.txt"))
.WriteTo.Async(c => c.Console());
#else
.WriteTo.Async(c => c.File("Logs/logs.txt"));
#endif
if (IsMigrateDatabase(args))
{

4
templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc.Mongo/Program.cs

@ -18,9 +18,11 @@ public class Program
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
.MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Warning)
.Enrich.FromLogContext()
.WriteTo.Async(c => c.File("Logs/logs.txt"))
#if DEBUG
.WriteTo.Async(c => c.File("Logs/logs.txt"))
.WriteTo.Async(c => c.Console());
#else
.WriteTo.Async(c => c.File("Logs/logs.txt"));
#endif
if (IsMigrateDatabase(args))
{

4
templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/Program.cs

@ -18,9 +18,11 @@ public class Program
.MinimumLevel.Override("Microsoft", LogEventLevel.Information)
.MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Warning)
.Enrich.FromLogContext()
.WriteTo.Async(c => c.File("Logs/logs.txt"))
#if DEBUG
.WriteTo.Async(c => c.File("Logs/logs.txt"))
.WriteTo.Async(c => c.Console());
#else
.WriteTo.Async(c => c.File("Logs/logs.txt"));
#endif
if (IsMigrateDatabase(args))
{

Loading…
Cancel
Save