diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json
index 1db05cebaf..3af6d451e4 100644
--- a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json
+++ b/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"
}
}
diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json
index 8b84c3512e..3bb6320874 100644
--- a/abp_io/AbpIoLocalization/AbpIoLocalization/Base/Localization/Resources/en.json
+++ b/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."
}
}
diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/ar.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/ar.json
index a4487da9b4..6408da0f12 100644
--- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/ar.json
+++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/ar.json
@@ -118,7 +118,7 @@
"CreatePostCoverInfo": "لإنشاء منشور فعال ، أضف صورة غلاف. قم بتحميل صور بنسبة عرض إلى ارتفاع تبلغ 16: 9 للحصول على أفضل عرض. الحد الأقصى لحجم الملف: 1 ميغا بايت.",
"ThankYouForContribution": "شكرًا لك على المساهمة في مجتمع ABP.",
"GithubPost": "المادة جيثب",
- "GithubPostSubmitStepOne": " 1. اكتب مقالة في أي مستودع GitHub عام بتنسيق Markdown. مثال ",
+ "GithubPostSubmitStepOne": " 1. اكتب مقالة في أي مستودع GitHub عام بتنسيق Markdown. مثال ",
"GithubPostSubmitStepTwo": " 2. أرسل عنوان URL لمقالتك باستخدام النموذج.",
"GithubPostSubmitStepThree": " 3. سيتم عرض مقالتك في موقع الويب هذا.",
"YoutubeVideo": "فيديو يوتيوب",
diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/cs.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/cs.json
index 1f057b3c1a..ebd1349383 100644
--- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/cs.json
+++ b/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": "1. Napište článek na libovolném veřejném úložišti GitHub ve formátu Markdown. příklad ",
+ "GithubPostSubmitStepOne": "1. Napište článek na libovolném veřejném úložišti GitHub ve formátu Markdown. příklad ",
"GithubPostSubmitStepTwo": "2. Odešlete adresu URL svého článku pomocí formuláře.",
"GithubPostSubmitStepThree": "3. Váš článek bude vykreslen na této webové stránce.",
"YoutubeVideo": "Youtube Video",
diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/de-DE.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/de-DE.json
index b930e55323..b9cf0e7055 100644
--- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/de-DE.json
+++ b/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": "1. Schreiben Sie einen Artikel in einem öffentlichen GitHub-Repository im Markdown-Format. Beispiel ",
+ "GithubPostSubmitStepOne": "1. Schreiben Sie einen Artikel in einem öffentlichen GitHub-Repository im Markdown-Format. Beispiel ",
"GithubPostSubmitStepTwo": "2. Senden Sie Ihre Artikel-URL über das Formular.",
"GithubPostSubmitStepThree": "3. Ihr Artikel wird auf dieser Website wiedergegeben.",
"YoutubeVideo": "Youtube-Video",
diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json
index 1919f23d0a..167792ef3f 100644
--- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json
+++ b/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": "1. Write a post on any public GitHub repository with the Markdown format. example",
+ "GithubPostSubmitStepOne": "1. Write a post on any public GitHub repository with the Markdown format. example",
"GithubPostSubmitStepTwo": "2. Submit your post URL using the form.",
"GithubPostSubmitStepThree": "3. 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"
}
}
diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/es.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/es.json
index d714e91854..cd5e27df29 100644
--- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/es.json
+++ b/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": " 1. Escribe un artículo en cualquier repositorio público de GitHub con el formato Markdown. ejemplo ",
+ "GithubPostSubmitStepOne": " 1. Escribe un artículo en cualquier repositorio público de GitHub con el formato Markdown. ejemplo ",
"GithubPostSubmitStepTwo": " 2. Envíe la URL de su artículo mediante el formulario.",
"GithubPostSubmitStepThree": " 3. Su artículo se mostrará en este sitio web.",
"YoutubeVideo": "Video de Youtube",
diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/fi.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/fi.json
index 486ffb3916..b686f44775 100644
--- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/fi.json
+++ b/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": "1. Kirjoita artikkeli mistä tahansa julkisesta GitHub-arkistosta Markdown-muodossa. esimerkki ",
+ "GithubPostSubmitStepOne": "1. Kirjoita artikkeli mistä tahansa julkisesta GitHub-arkistosta Markdown-muodossa. esimerkki ",
"GithubPostSubmitStepTwo": " 2. Lähetä artikkelin URL-osoite lomaketta käyttämällä.",
"GithubPostSubmitStepThree": " 3. Artikkelisi renderöidään tällä verkkosivustolla.",
"YoutubeVideo": "Youtube-video",
diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/fr.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/fr.json
index 0db4e72504..c51623e2a5 100644
--- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/fr.json
+++ b/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": " 1. Rédigez un post sur n'importe quel référentiel GitHub public au format Markdown. Exemple de ",
+ "GithubPostSubmitStepOne": " 1. Rédigez un post sur n'importe quel référentiel GitHub public au format Markdown. Exemple de ",
"GithubPostSubmitStepTwo": " 2. Envoyez l'URL de votre post à l'aide du formulaire.",
"GithubPostSubmitStepThree": " 3. Votre post sera rendu sur ce site Web.",
"YoutubeVideo": "Vidéo Youtube",
diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/hi.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/hi.json
index 592ac856b1..f9746dba44 100644
--- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/hi.json
+++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/hi.json
@@ -118,7 +118,7 @@
"CreatePostCoverInfo": "एक प्रभावी पोस्ट बनाने के लिए, एक कवर फ़ोटो जोड़ें। सर्वश्रेष्ठ दृश्य के लिए 16: 9 पहलू अनुपात चित्र अपलोड करें। अधिकतम फ़ाइल आकार: 1 एमबी।",
"ThankYouForContribution": "ABP समुदाय में योगदान के लिए धन्यवाद।",
"GithubPost": "गीथब लेख",
- "GithubPostSubmitStepOne": "1. मार्कडाउन प्रारूप के साथ किसी भी सार्वजनिक GitHub रिपॉजिटरी पर एक लेख लिखें। उदाहरण ",
+ "GithubPostSubmitStepOne": "1. मार्कडाउन प्रारूप के साथ किसी भी सार्वजनिक GitHub रिपॉजिटरी पर एक लेख लिखें। उदाहरण ",
"GithubPostSubmitStepTwo": " 2। फ़ॉर्म का उपयोग करके अपना लेख URL सबमिट करें।",
"GithubPostSubmitStepThree": " 3। इस वेब साइट में आपका लेख प्रस्तुत किया जाएगा।",
"YoutubeVideo": "यूट्यूब वीडियो",
diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/hu.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/hu.json
index a4b76ecc52..68d70f8615 100644
--- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/hu.json
+++ b/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": "1. Írjon cikket bármely nyilvános GitHub-tárhelyről Markdown formátumban. példa ",
+ "GithubPostSubmitStepOne": "1. Írjon cikket bármely nyilvános GitHub-tárhelyről Markdown formátumban. példa ",
"GithubPostSubmitStepTwo": "2. Az űrlap segítségével küldje el cikkének URL-jét.",
"GithubPostSubmitStepThree": "3. A cikk ezen a webhelyen jelenik meg.",
"YoutubeVideo": "Youtube videó",
diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/is.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/is.json
index daeaa3cc13..85c4a134a3 100644
--- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/is.json
+++ b/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": " 1. Skrifaðu grein á hvaða opinbera GitHub geymslu sem er með markdown sniðinu. dæmi ",
+ "GithubPostSubmitStepOne": " 1. Skrifaðu grein á hvaða opinbera GitHub geymslu sem er með markdown sniðinu. dæmi ",
"GithubPostSubmitStepTwo": "2. Sendu vefslóð greinarinnar með því að nota formið.",
"GithubPostSubmitStepThree": "3. Grein þín verður birt á þessari vefsíðu.",
"YoutubeVideo": "Youtube Videó",
diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/it.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/it.json
index 645f5af5a3..d14c9716e4 100644
--- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/it.json
+++ b/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": "1. Scrivi un articolo su qualsiasi repository GitHub pubblico con il formato Markdown.esempio",
+ "GithubPostSubmitStepOne": "1. Scrivi un articolo su qualsiasi repository GitHub pubblico con il formato Markdown.esempio",
"GithubPostSubmitStepTwo": "2. Invia l'URL del tuo articolo utilizzando il modulo.",
"GithubPostSubmitStepThree": "3. Il tuo articolo verrà visualizzato in questo sito web.",
"YoutubeVideo": "Video Youtube",
diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/nl.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/nl.json
index b760ffa034..c0844f722a 100644
--- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/nl.json
+++ b/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": "1. Schrijf een artikel op een openbare GitHub-repository met het Markdown-formaat. voorbeeld ",
+ "GithubPostSubmitStepOne": "1. Schrijf een artikel op een openbare GitHub-repository met het Markdown-formaat. voorbeeld ",
"GithubPostSubmitStepTwo": "2. Dien uw artikel-URL in via het formulier.",
"GithubPostSubmitStepThree": "3. Uw artikel wordt weergegeven op deze website.",
"YoutubeVideo": "Youtube video",
diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/pl-PL.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/pl-PL.json
index e9cc65ca63..c33e9eeccb 100644
--- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/pl-PL.json
+++ b/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": "1. Napisz artykuł w dowolnym publicznym repozytorium GitHub w formacie Markdown. przykład ",
+ "GithubPostSubmitStepOne": "1. Napisz artykuł w dowolnym publicznym repozytorium GitHub w formacie Markdown. przykład ",
"GithubPostSubmitStepTwo": "2. Prześlij adres URL artykułu za pomocą formularza.",
"GithubPostSubmitStepThree": "3. Twój artykuł zostanie wyrenderowany w tej witrynie internetowej.",
"YoutubeVideo": "Wideo z YouTube",
diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/pt-BR.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/pt-BR.json
index 518e954db7..5d5e7b946c 100644
--- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/pt-BR.json
+++ b/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": " 1. Escreva um artigo em qualquer repositório GitHub público com o formato Markdown. exemplo ",
+ "GithubPostSubmitStepOne": " 1. Escreva um artigo em qualquer repositório GitHub público com o formato Markdown. exemplo ",
"GithubPostSubmitStepTwo": " 2. Envie o URL do seu artigo usando o formulário.",
"GithubPostSubmitStepThree": " 3. Seu artigo será processado neste site.",
"YoutubeVideo": "Vídeo do youtube",
diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/ro-RO.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/ro-RO.json
index e1050c9d97..5c0afaa19a 100644
--- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/ro-RO.json
+++ b/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": "1. Scrieţi un articol pe orice repository GitHub folosind formatul Markup. exemplu",
+ "GithubPostSubmitStepOne": "1. Scrieţi un articol pe orice repository GitHub folosind formatul Markup. exemplu",
"GithubPostSubmitStepTwo": "2. Trimiteţi Url-ul articolului folosind formularul.",
"GithubPostSubmitStepThree": "3. Articolul dumneavoastră va fi redat în acest site web.",
"YoutubeVideo": "Videoclip YouTube",
diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/ru.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/ru.json
index 1995cf742d..e47a59691e 100644
--- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/ru.json
+++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/ru.json
@@ -118,7 +118,7 @@
"CreatePostCoverInfo": "Для создания эффектного поста добавьте фото обложки. Загрузите изображения с соотношением сторон 16: 9 для наилучшего просмотра. Максимальный размер файла: 1 МБ.",
"ThankYouForContribution": "Спасибо за ваш вклад в сообщество ABP.",
"GithubPost": "Статья на Github",
- "GithubPostSubmitStepOne": " 1. Напишите статью в любом общедоступном репозитории GitHub с форматом Markdown. Пример ",
+ "GithubPostSubmitStepOne": " 1. Напишите статью в любом общедоступном репозитории GitHub с форматом Markdown. Пример ",
"GithubPostSubmitStepTwo": " 2. Отправьте URL своей статьи, используя форму.",
"GithubPostSubmitStepThree": " 3. Ваша статья будет отображаться на этом веб-сайте.",
"YoutubeVideo": "YouTube видео",
diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/sk.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/sk.json
index df15618753..5abf7e8f07 100644
--- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/sk.json
+++ b/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": "1. Napíšte článok v ľubovoľnom verejnom repozitári GitHub vo formáte Markdown. príklad",
+ "GithubPostSubmitStepOne": "1. Napíšte článok v ľubovoľnom verejnom repozitári GitHub vo formáte Markdown. príklad",
"GithubPostSubmitStepTwo": "2. Odoslať URL adresu článku pomocou formulára.",
"GithubPostSubmitStepThree": "3. Váš článok sa zobrazí na tejto webovej stránke.",
"YoutubeVideo": "Video na YouTube",
diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/sl.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/sl.json
index 8a689eb931..54a49aa5c9 100644
--- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/sl.json
+++ b/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": "1. Napišite članek v katerem koli javnem repozitoriju GitHub z obliko Markdown. primer ",
+ "GithubPostSubmitStepOne": "1. Napišite članek v katerem koli javnem repozitoriju GitHub z obliko Markdown. primer ",
"GithubPostSubmitStepTwo": "2. Pošljite URL svojega članka z uporabo obrazca.",
"GithubPostSubmitStepThree": "3. Vaš članek bo prikazan na tem spletnem mestu.",
"YoutubeVideo": "Youtube video",
diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/tr.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/tr.json
index 062e57fab4..72d6c5e28d 100644
--- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/tr.json
+++ b/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": "1. Herhangi bir genel GitHub deposunda Markdown biçimiyle bir makale yazın. örnek ",
+ "GithubPostSubmitStepOne": "1. Herhangi bir genel GitHub deposunda Markdown biçimiyle bir makale yazın. örnek ",
"GithubPostSubmitStepTwo": "2. Formu kullanarak makale URL'nizi gönderin.",
"GithubPostSubmitStepThree": "3. Makaleniz bu web sitesinde işlenecektir.",
"YoutubeVideo": "Youtube videosu",
diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/vi.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/vi.json
index 9966fd1000..91794dd10a 100644
--- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/vi.json
+++ b/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": " 1. 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. ví dụ ",
+ "GithubPostSubmitStepOne": " 1. 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. ví dụ ",
"GithubPostSubmitStepTwo": " 2. Gửi URL bài viết của bạn bằng biểu mẫu.",
"GithubPostSubmitStepThree": " 3. Bài viết của bạn sẽ được hiển thị trên trang web này.",
"YoutubeVideo": "Video trên Youtube",
diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/zh-Hans.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/zh-Hans.json
index 57fb1ec11c..b5d9d65c82 100644
--- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/zh-Hans.json
+++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/zh-Hans.json
@@ -118,7 +118,7 @@
"CreatePostCoverInfo": "要创建一个有效的帖子,需要添加封面图片. 上传16:9的图片获取最佳视觉效果. 文件最大支持: 1MB",
"ThankYouForContribution": "感谢你对ABP社区的贡献",
"GithubPost": "Github文章",
- "GithubPostSubmitStepOne": "1. 用Markdown格式在GitHub的任何公共存储库上写一篇文章. 示例",
+ "GithubPostSubmitStepOne": "1. 用Markdown格式在GitHub的任何公共存储库上写一篇文章. 示例",
"GithubPostSubmitStepTwo": "2. 使用表单提交你的文章URL.",
"GithubPostSubmitStepThree": "3. 你的文章将在这个网站中呈现.",
"YoutubeVideo": "Youtube视频",
diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/zh-Hant.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/zh-Hant.json
index b79004153e..4ac910d98a 100644
--- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/zh-Hant.json
+++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/zh-Hant.json
@@ -118,7 +118,7 @@
"CreatePostCoverInfo": "要創建有效的帖子,請添加封面照片。上傳 16:9 縱橫比的圖片以獲得最佳觀看效果。最大文件大小:1MB。",
"ThankYouForContribution": "感謝您為 ABP 社區做出貢獻。",
"GithubPost": "Github 文章",
- "GithubPostSubmitStepOne": "1. 用 Markdown 格式在任何公共 GitHub 存儲庫上寫一篇文章。 示例",
+ "GithubPostSubmitStepOne": "1. 用 Markdown 格式在任何公共 GitHub 存儲庫上寫一篇文章。 示例",
"GithubPostSubmitStepTwo": "2. 使用表單提交您的文章 URL。",
"GithubPostSubmitStepThree": "3. 您的文章將在此網站中呈現。",
"YoutubeVideo": "優酷視頻",
diff --git a/docs/en/CLI.md b/docs/en/CLI.md
index 7db562a66e..d381290508 100644
--- a/docs/en/CLI.md
+++ b/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.
diff --git a/docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/README.md b/docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/README.md
new file mode 100644
index 0000000000..0d86c98a1d
--- /dev/null
+++ b/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:
+ 
+ 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();
+ });
+ }
+ ```
+
+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()
+ .ConfigureFonts(fonts =>
+ {
+ fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
+ });
+
+ builder.Services.AddTransient();
+
+ 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
+
+
+
+ ```
+
+- Create **WebAuthenticatorBrowser**
+
+ ```csharp
+ internal class WebAuthenticatorBrowser : IBrowser
+ {
+ public async Task 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 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();
+
+ builder.Services.AddTransient(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(),
+ })
+ );
+ ```
+
+- Go to **MainPage.xaml**, remove everyting and add a button for login
+
+ ```xml
+
+
+
+
+
+
+
+
+
+
+ ```
+
+- 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
+
+
+
+
+
+
+
+
+
+
+
+ ```
+
+ > 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.
+
+ 
+
+---
+
+### iOS/MacCatalyst
+
+- Add following key to **Info.plist**
+
+ ```xml
+ CFBundleURLTypes
+
+
+ CFBundleURLName
+ mauiessentials
+ CFBundleURLSchemes
+
+ bookstore
+
+ CFBundleTypeRole
+ Editor
+
+
+ ```
+
+- 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.
+ > 
+
+
+
+
+
+
+
+---
+
+### UWP (Windows)
+
+- Add following protocol extension in `Package.appxmanifest` file.
+
+ ```xml
+
+
+
+
+
+ BookStore
+
+
+
+
+
+ ```
+
+ > 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(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(),
+ })
+ );
+ ```
+
+- 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 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();
+ builder.Services.AddTransient(sp =>
+ new HttpClient(sp.GetRequiredService())
+ {
+ 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.
+
+
+
+---
+
+## 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.
+
diff --git a/docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/android-login-demo.gif b/docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/android-login-demo.gif
new file mode 100644
index 0000000000..8fd46103dd
Binary files /dev/null and b/docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/android-login-demo.gif differ
diff --git a/docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/compiler-select.png b/docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/compiler-select.png
new file mode 100644
index 0000000000..793c30de2a
Binary files /dev/null and b/docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/compiler-select.png differ
diff --git a/docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/identity-users-request-result.png b/docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/identity-users-request-result.png
new file mode 100644
index 0000000000..7b12b0bbbe
Binary files /dev/null and b/docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/identity-users-request-result.png differ
diff --git a/docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/ios-login-demo.gif b/docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/ios-login-demo.gif
new file mode 100644
index 0000000000..aa7b0b7d8b
Binary files /dev/null and b/docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/ios-login-demo.gif differ
diff --git a/docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/macos-login-demo.gif b/docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/macos-login-demo.gif
new file mode 100644
index 0000000000..3841b438af
Binary files /dev/null and b/docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/macos-login-demo.gif differ
diff --git a/docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/openid-configuration.png b/docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/openid-configuration.png
new file mode 100644
index 0000000000..d12fe536a9
Binary files /dev/null and b/docs/en/Community-Articles/2022-02-22-Integrating-MAUI-Client-via-using-OpenID-Connect/art/openid-configuration.png differ
diff --git a/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/Cover.png b/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/Cover.png
new file mode 100644
index 0000000000..b61bdb6ddb
Binary files /dev/null and b/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/Cover.png differ
diff --git a/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/README.md b/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/README.md
new file mode 100644
index 0000000000..75bc1a9124
--- /dev/null
+++ b/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
+
+
+ ```
+
+- Add HttpApi.Client project reference
+
+ ```xml
+
+ ```
+ 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(configuration.GetSection("Oidc:Options"));
+
+ context.Services.AddTransient(sp =>
+ {
+ var options = sp.GetRequiredService>().Value;
+ options.Browser = sp.GetRequiredService();
+ return new OidcClient(options);
+ });
+
+ context.Services.AddTransient(sp =>
+ new HttpClient(sp.GetRequiredService())
+ {
+ // Temporarily. We'll use ABP's Proxy for sendind requests.
+ BaseAddress = new Uri(configuration.GetValue("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()
+ .ConfigureFonts(fonts =>
+ {
+ fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
+ });
+
+ ConfigureConfiguration(builder);
+
+ builder.Services.AddApplication(options =>
+ {
+ options.Services.ReplaceConfiguration(builder.Configuration);
+ });
+
+ var app = builder.Build();
+
+ app.Services.GetRequiredService()
+ .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 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(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)_
+
+ 
+
+- 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ```
+
+ > 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.
+ >
+ > 
+
+
+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
+
+
+
+
+ #512BD4
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ```
+
+- 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|
+| --- | --- |
+|  |  |
+
+| UWP |
+| --- |
+|  |
+
+| MacCatalyst |
+| --- |
+|  |
+
+
+## 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.
+
diff --git a/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/android-users-demo.gif b/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/android-users-demo.gif
new file mode 100644
index 0000000000..75d0333dec
Binary files /dev/null and b/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/android-users-demo.gif differ
diff --git a/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/ios-users-demo.gif b/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/ios-users-demo.gif
new file mode 100644
index 0000000000..a61d09ede4
Binary files /dev/null and b/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/ios-users-demo.gif differ
diff --git a/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/macos-users-demo.gif b/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/macos-users-demo.gif
new file mode 100644
index 0000000000..6085e216d0
Binary files /dev/null and b/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/macos-users-demo.gif differ
diff --git a/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/net-maui-contentpage-template.png b/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/net-maui-contentpage-template.png
new file mode 100644
index 0000000000..9b0539c1ff
Binary files /dev/null and b/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/net-maui-contentpage-template.png differ
diff --git a/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/uwp-users-demo.gif b/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/uwp-users-demo.gif
new file mode 100644
index 0000000000..3f895e8c52
Binary files /dev/null and b/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/uwp-users-demo.gif differ
diff --git a/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/xaml-intellisense.png b/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/xaml-intellisense.png
new file mode 100644
index 0000000000..521675d53c
Binary files /dev/null and b/docs/en/Community-Articles/2022-02-24-Using-ABP-Client-Proxies-in-MAUI-with-OpenID-Connect/art/xaml-intellisense.png differ
diff --git a/docs/en/Community-Articles/2022-02-24-Using-HttpApi.Client-in-MAUI-with-OpenID-Connect/README.md b/docs/en/Community-Articles/2022-02-24-Using-HttpApi.Client-in-MAUI-with-OpenID-Connect/README.md
new file mode 100644
index 0000000000..75bc1a9124
--- /dev/null
+++ b/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
+
+
+ ```
+
+- Add HttpApi.Client project reference
+
+ ```xml
+
+ ```
+ 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(configuration.GetSection("Oidc:Options"));
+
+ context.Services.AddTransient(sp =>
+ {
+ var options = sp.GetRequiredService>().Value;
+ options.Browser = sp.GetRequiredService();
+ return new OidcClient(options);
+ });
+
+ context.Services.AddTransient(sp =>
+ new HttpClient(sp.GetRequiredService())
+ {
+ // Temporarily. We'll use ABP's Proxy for sendind requests.
+ BaseAddress = new Uri(configuration.GetValue("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()
+ .ConfigureFonts(fonts =>
+ {
+ fonts.AddFont("OpenSans-Regular.ttf", "OpenSansRegular");
+ });
+
+ ConfigureConfiguration(builder);
+
+ builder.Services.AddApplication(options =>
+ {
+ options.Services.ReplaceConfiguration(builder.Configuration);
+ });
+
+ var app = builder.Build();
+
+ app.Services.GetRequiredService()
+ .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 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(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)_
+
+ 
+
+- 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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ```
+
+ > 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.
+ >
+ > 
+
+
+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
+
+
+
+
+ #512BD4
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ```
+
+- 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|
+| --- | --- |
+|  |  |
+
+| UWP |
+| --- |
+|  |
+
+| MacCatalyst |
+| --- |
+|  |
+
+
+## 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.
+
diff --git a/docs/en/Multi-Lingual-Entities.md b/docs/en/Multi-Lingual-Entities.md
index 85746a3fea..8a707b24d0 100644
--- a/docs/en/Multi-Lingual-Entities.md
+++ b/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` interface is used to mark multi lingual entities. The entities marked with `IHasMultiLingual` interface must define language-neutral information. The entities marked with `IHasMultiLingual` contains a collection of Translations which contains language-dependent information.
-
-Example:
-
-```csharp
-public class Product : Entity, IMultiLingualEntity
-{
- public decimal Price { get; set; }
-
- public ICollection 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` interface to map multi lingual entities to DTOs.
-
-Example:
-
-```csharp
-public class MultiLingualProductObjectMapper : IObjectMapper, ITransientDependency
-{
- private readonly IMultiLingualObjectManager _multiLingualObjectManager;
-
- public MultiLingualProductObjectMapper(IMultiLingualObjectManager multiLingualObjectManager)
- {
- _multiLingualObjectManager = multiLingualObjectManager;
- }
-
- public ProductDto Map(Product source)
- {
- var translation = _multiLingualObjectManager.GetTranslation(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();
- }
-}
-```
-
-`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(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.
\ No newline at end of file
+https://github.com/abpframework/abp/issues/1169
diff --git a/docs/en/Previews.md b/docs/en/Previews.md
index 5d52a4ad69..61cf1c9226 100644
--- a/docs/en/Previews.md
+++ b/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.
\ No newline at end of file
+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.
diff --git a/docs/en/Samples/Index.md b/docs/en/Samples/Index.md
index 7388516ed0..58543ba380 100644
--- a/docs/en/Samples/Index.md
+++ b/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)
diff --git a/docs/en/Tutorials/Part-1.md b/docs/en/Tutorials/Part-1.md
index 93bf3d4a0b..eb00a94e0c 100644
--- a/docs/en/Tutorials/Part-1.md
+++ b/docs/en/Tutorials/Part-1.md
@@ -202,7 +202,7 @@ This will add a new migration class to the project:

-> 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}}
diff --git a/docs/en/Tutorials/Part-10.md b/docs/en/Tutorials/Part-10.md
index f0c182b661..6418db7e71 100644
--- a/docs/en/Tutorials/Part-10.md
+++ b/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}}
diff --git a/docs/en/Tutorials/Part-3.md b/docs/en/Tutorials/Part-3.md
index f6a59f1108..182ffa4005 100644
--- a/docs/en/Tutorials/Part-3.md
+++ b/docs/en/Tutorials/Part-3.md
@@ -129,7 +129,7 @@ Open the `CreateModal.cshtml` file and paste the code below:
````
-* 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).
diff --git a/docs/en/Tutorials/Part-7.md b/docs/en/Tutorials/Part-7.md
index 3284ae98d4..27d46bd4ed 100644
--- a/docs/en/Tutorials/Part-7.md
+++ b/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"}}
diff --git a/docs/en/UI/Blazor/Error-Handling.md b/docs/en/UI/Blazor/Error-Handling.md
index f43456e3a7..35aff041af 100644
--- a/docs/en/UI/Blazor/Error-Handling.md
+++ b/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);
+ }
+ }
}
````
diff --git a/docs/zh-Hans/CLI.md b/docs/zh-Hans/CLI.md
index b2f3825c64..5ff00c5501 100644
--- a/docs/zh-Hans/CLI.md
+++ b/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`: 使用最新的预览版本.
diff --git a/docs/zh-Hans/Emailing.md b/docs/zh-Hans/Emailing.md
index d285e5bde2..22edc50301 100644
--- a/docs/zh-Hans/Emailing.md
+++ b/docs/zh-Hans/Emailing.md
@@ -1,3 +1,256 @@
-# Emailing
+# 邮件发送系统
-待添加
\ No newline at end of file
+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)模块的*设置页面*管理电子邮件设置:
+
+
+
+>如果你已从 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
+
+
+
+
+
+
+ This is email body...
+
+
+````
+
+电子邮件系统定义了具有给定名称的内置文本模板:
+
+"**Abp.StandardEmailTemplates.Message**" 是最简单的带有文本消息的模板:
+
+````html
+{%{{{model.message}}}%}
+````
+
+此模板使用“Abp.StandardEmailTemplates.Layout”作为其布局.
+
+"**Abp.StandardEmailTemplates.Layout**" 是一个提供 HTML 文档布局的简单模板:
+
+````html
+
+
+
+
+
+
+ {%{{{content}}}%}
+
+
+````
+
+最终呈现的消息如上所示。
+
+> 这些模板名称是在 `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());
+#endif
+````
+
+因此,你在 DEBUG 模式下不会收到电子邮件。电子邮件将在生产时按预期发送(RELEASE 模式)。如果你也想在 DEBUG 上发送电子邮件,请删除这些行。
+
+## 其他
+
+* [用于发送电子邮件的MailKit集成](MailKit.md)
diff --git a/docs/zh-Hans/MailKit.md b/docs/zh-Hans/MailKit.md
index cb82a640d5..0adb88c85f 100644
--- a/docs/zh-Hans/MailKit.md
+++ b/docs/zh-Hans/MailKit.md
@@ -1 +1,48 @@
-TODO...
\ No newline at end of file
+# 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(options =>
+{
+ options.SecureSocketOption = SecureSocketOptions.SslOnConnect;
+});
+````
+
+请参阅 [MailKit 文档](http://www.mimekit.net/) 了解更多信息.
+
+## 也可以看看
+
+* [电子邮件发送系统](Emailing.md)
diff --git a/docs/zh-Hans/Previews.md b/docs/zh-Hans/Previews.md
index cb82a640d5..cc7c939bab 100644
--- a/docs/zh-Hans/Previews.md
+++ b/docs/zh-Hans/Previews.md
@@ -1 +1,40 @@
-TODO...
\ No newline at end of file
+# 预览版
+
+预览版在发布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)上打开一个问题。
diff --git a/docs/zh-Hans/Tutorials/Part-1.md b/docs/zh-Hans/Tutorials/Part-1.md
index 927463e100..b06643c7c0 100644
--- a/docs/zh-Hans/Tutorials/Part-1.md
+++ b/docs/zh-Hans/Tutorials/Part-1.md
@@ -202,7 +202,7 @@ dotnet ef migrations add Created_Book_Entity

-> 如果你使用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
{
diff --git a/docs/zh-Hans/Tutorials/Part-10.md b/docs/zh-Hans/Tutorials/Part-10.md
index e6170dba9e..170e3106a1 100644
--- a/docs/zh-Hans/Tutorials/Part-10.md
+++ b/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}}
diff --git a/docs/zh-Hans/Tutorials/Part-2.md b/docs/zh-Hans/Tutorials/Part-2.md
index 6e7630719a..4e46380387 100644
--- a/docs/zh-Hans/Tutorials/Part-2.md
+++ b/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::` 命名约定来本地化枚举成员. 当您这样做时ABP可以在某些适当的情况下自动将枚举本地化.
diff --git a/docs/zh-Hans/Tutorials/Part-3.md b/docs/zh-Hans/Tutorials/Part-3.md
index 2fe5a8f477..4d71ade5a5 100644
--- a/docs/zh-Hans/Tutorials/Part-3.md
+++ b/docs/zh-Hans/Tutorials/Part-3.md
@@ -129,7 +129,7 @@ namespace Acme.BookStore.Web.Pages.Books
````
-* 这个 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
diff --git a/docs/zh-Hans/Tutorials/Part-5.md b/docs/zh-Hans/Tutorials/Part-5.md
index a1419d2c4b..d185c4891e 100644
--- a/docs/zh-Hans/Tutorials/Part-5.md
+++ b/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"对象中对应的值替换为中文.
### 权限管理界面
diff --git a/docs/zh-Hans/Tutorials/Part-6.md b/docs/zh-Hans/Tutorials/Part-6.md
index 937aee9b51..93dbc9a0a6 100644
--- a/docs/zh-Hans/Tutorials/Part-6.md
+++ b/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
diff --git a/docs/zh-Hans/Tutorials/Part-7.md b/docs/zh-Hans/Tutorials/Part-7.md
index 95b016e451..d799b9ab4f 100644
--- a/docs/zh-Hans/Tutorials/Part-7.md
+++ b/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"}}
diff --git a/docs/zh-Hans/Tutorials/Part-8.md b/docs/zh-Hans/Tutorials/Part-8.md
index 3622b1af38..aac70389e8 100644
--- a/docs/zh-Hans/Tutorials/Part-8.md
+++ b/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 配置中定义映射.
diff --git a/docs/zh-Hans/Tutorials/Part-9.md b/docs/zh-Hans/Tutorials/Part-9.md
index 10c3a457bc..fd0e9540e0 100644
--- a/docs/zh-Hans/Tutorials/Part-9.md
+++ b/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` 页面, 点击 *操作* 按钮并选择**管理员角色**的*权限*操作:
diff --git a/docs/zh-Hans/Upgrading.md b/docs/zh-Hans/Upgrading.md
index cb82a640d5..dd5bec7b19 100644
--- a/docs/zh-Hans/Upgrading.md
+++ b/docs/zh-Hans/Upgrading.md
@@ -1 +1,59 @@
-TODO...
\ No newline at end of file
+# 升级 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) 每天晚上(周末除外)从开发分支发布。这意味着您可以尝试前一天的开发。
+
+请参阅他们的文档以了解有关此类版本的详细信息。
diff --git a/docs/zh-Hans/docs-nav.json b/docs/zh-Hans/docs-nav.json
index e5f736088e..db0fc10aed 100644
--- a/docs/zh-Hans/docs-nav.json
+++ b/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"
}
]
},
diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/datatables/datatables-extensions.js b/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/datatables/datatables-extensions.js
index de5abe4ed4..5813db7599 100644
--- a/framework/src/Volo.Abp.AspNetCore.Mvc.UI.Theme.Shared/wwwroot/libs/abp/aspnetcore-mvc-ui-theme-shared/datatables/datatables-extensions.js
+++ b/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 {
diff --git a/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/Auditing/AbpAuditHubFilter.cs b/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/Auditing/AbpAuditHubFilter.cs
index d622ba535a..fd5567396b 100644
--- a/framework/src/Volo.Abp.AspNetCore.SignalR/Volo/Abp/AspNetCore/SignalR/Auditing/AbpAuditHubFilter.cs
+++ b/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();
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();
diff --git a/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Auditing/AbpAuditingMiddleware.cs b/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Auditing/AbpAuditingMiddleware.cs
index 5d0462bc5f..4ff49012cf 100644
--- a/framework/src/Volo.Abp.AspNetCore/Volo/Abp/AspNetCore/Auditing/AbpAuditingMiddleware.cs
+++ b/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();
diff --git a/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditingInterceptor.cs b/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditingInterceptor.cs
index 4f4d3727bb..5521c28d1e 100644
--- a/framework/src/Volo.Abp.Auditing/Volo/Abp/Auditing/AuditingInterceptor.cs
+++ b/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();
- await ProcessWithNewAuditingScopeAsync(invocation, auditingOptions, currentUser, auditingManager, auditingHelper);
+ var unitOfWorkManager = serviceScope.ServiceProvider.GetRequiredService();
+ 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();
}
}
diff --git a/framework/src/Volo.Abp.BlazoriseUI/Components/AbpExtensibleDataGrid.razor b/framework/src/Volo.Abp.BlazoriseUI/Components/AbpExtensibleDataGrid.razor
index 9ec408cc41..e7ea795eee 100644
--- a/framework/src/Volo.Abp.BlazoriseUI/Components/AbpExtensibleDataGrid.razor
+++ b/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">
diff --git a/framework/src/Volo.Abp.BlazoriseUI/Components/AbpExtensibleDataGrid.razor.cs b/framework/src/Volo.Abp.BlazoriseUI/Components/AbpExtensibleDataGrid.razor.cs
index 568a9eff3a..f9baf631f5 100644
--- a/framework/src/Volo.Abp.BlazoriseUI/Components/AbpExtensibleDataGrid.razor.cs
+++ b/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 : 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; }
diff --git a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizationResourceContributorList.cs b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizationResourceContributorList.cs
index 126729853d..d52fe78943 100644
--- a/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizationResourceContributorList.cs
+++ b/framework/src/Volo.Abp.Localization/Volo/Abp/Localization/LocalizationResourceContributorList.cs
@@ -8,7 +8,7 @@ public class LocalizationResourceContributorList : List x).Reverse())
{
var localString = contributor.GetOrNull(cultureName, name);
if (localString != null)
diff --git a/framework/src/Volo.Abp.Swashbuckle/Microsoft/Extensions/DependencyInjection/AbpSwaggerGenOptionsExtensions.cs b/framework/src/Volo.Abp.Swashbuckle/Microsoft/Extensions/DependencyInjection/AbpSwaggerGenOptionsExtensions.cs
new file mode 100644
index 0000000000..4313041064
--- /dev/null
+++ b/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();
+ }
+}
\ No newline at end of file
diff --git a/framework/src/Volo.Abp.Swashbuckle/Volo/Abp/Swashbuckle/AbpSwashbuckleDocumentFilter.cs b/framework/src/Volo.Abp.Swashbuckle/Volo/Abp/Swashbuckle/AbpSwashbuckleDocumentFilter.cs
new file mode 100644
index 0000000000..5fd05a1460
--- /dev/null
+++ b/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));
+ }
+}
\ No newline at end of file
diff --git a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Components/ProfileManagementGroup/PersonalInfo/Default.js b/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Components/ProfileManagementGroup/PersonalInfo/Default.js
index 3422b36d63..e4f5f82620 100644
--- a/modules/account/src/Volo.Abp.Account.Web/Pages/Account/Components/ProfileManagementGroup/PersonalInfo/Default.js
+++ b/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);
+ });
});
});
});
diff --git a/modules/blogging/app/Volo.BloggingTestApp.MongoDb/Volo.BloggingTestApp.MongoDB.abppkg.json b/modules/blogging/app/Volo.BloggingTestApp.MongoDB/Volo.BloggingTestApp.MongoDB.abppkg.json
similarity index 100%
rename from modules/blogging/app/Volo.BloggingTestApp.MongoDb/Volo.BloggingTestApp.MongoDB.abppkg.json
rename to modules/blogging/app/Volo.BloggingTestApp.MongoDB/Volo.BloggingTestApp.MongoDB.abppkg.json
diff --git a/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/BlogPosts/create.js b/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/BlogPosts/create.js
index dc4fd10e09..eba7f51654 100644
--- a/modules/cms-kit/src/Volo.CmsKit.Admin.Web/Pages/CmsKit/BlogPosts/create.js
+++ b/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());
}
}
});
diff --git a/modules/docs/src/Volo.Docs.Admin.Application/Volo/Docs/Admin/Projects/ProjectAdminAppService.cs b/modules/docs/src/Volo.Docs.Admin.Application/Volo/Docs/Admin/Projects/ProjectAdminAppService.cs
index 078288572b..14833cd3e4 100644
--- a/modules/docs/src/Volo.Docs.Admin.Application/Volo/Docs/Admin/Projects/ProjectAdminAppService.cs
+++ b/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);
}
}
diff --git a/modules/docs/src/Volo.Docs.Admin.Web/Pages/Docs/Admin/Projects/Edit.cshtml.cs b/modules/docs/src/Volo.Docs.Admin.Web/Pages/Docs/Admin/Projects/Edit.cshtml.cs
index 7f44ae396b..ad9482578e 100644
--- a/modules/docs/src/Volo.Docs.Admin.Web/Pages/Docs/Admin/Projects/Edit.cshtml.cs
+++ b/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; }
}
diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/FullSearch/Elastic/ElasticDocumentFullSearch.cs b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/FullSearch/Elastic/ElasticDocumentFullSearch.cs
index 7a47cd7b45..6a6c8cc7e4 100644
--- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/FullSearch/Elastic/ElasticDocumentFullSearch.cs
+++ b/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 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)
diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/FullSearch/Elastic/IDocumentFullSearch.cs b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/FullSearch/Elastic/IDocumentFullSearch.cs
index c84df577b7..2c7cf1f3e6 100644
--- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/Documents/FullSearch/Elastic/IDocumentFullSearch.cs
+++ b/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 documents, CancellationToken cancellationToken = default);
+
Task DeleteAsync(Guid id, CancellationToken cancellationToken = default);
Task DeleteAllAsync(CancellationToken cancellationToken = default);
diff --git a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/GithubDocumentSource.cs b/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/GithubDocumentSource.cs
index d03d8078e4..060c6dcf08 100644
--- a/modules/docs/src/Volo.Docs.Domain/Volo/Docs/GitHub/Documents/GithubDocumentSource.cs
+++ b/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);
}
}
diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor/Pages/SettingManagement/EmailSettingGroup/EmailSettingGroupViewComponent.razor.cs b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor/Pages/SettingManagement/EmailSettingGroup/EmailSettingGroupViewComponent.razor.cs
index 74f809fdfa..9e5638bfd9 100644
--- a/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor/Pages/SettingManagement/EmailSettingGroup/EmailSettingGroupViewComponent.razor.cs
+++ b/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
{
diff --git a/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor/Pages/SettingManagement/SettingManagement.razor b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor/Pages/SettingManagement/SettingManagement.razor
index b37dbe6b4e..15911c1bb7 100644
--- a/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor/Pages/SettingManagement/SettingManagement.razor
+++ b/modules/setting-management/src/Volo.Abp.SettingManagement.Blazor/Pages/SettingManagement/SettingManagement.razor
@@ -11,7 +11,7 @@
-
+
@foreach (var group in SettingComponentCreationContext.Groups)
{
diff --git a/npm/ng-packs/packages/theme-basic/src/lib/tests/lazy-style.handler.spec.ts b/npm/ng-packs/packages/theme-basic/src/lib/tests/lazy-style.handler.spec.ts
index dec4744a6c..dd0591f9fd 100644
--- a/npm/ng-packs/packages/theme-basic/src/lib/tests/lazy-style.handler.spec.ts
+++ b/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;
@@ -17,7 +17,7 @@ describe('LazyStyleHandler', () => {
DocumentDirHandlerService,
{
provide: LocalizationService,
- useValue: { currentLang: 'en', languageChange$ },
+ useValue: { currentLang: 'en', currentLang$ },
},
],
});
diff --git a/npm/ng-packs/packages/theme-shared/extensions/src/lib/adapters/date-time.adapter.ts b/npm/ng-packs/packages/theme-shared/extensions/src/lib/adapters/date-time.adapter.ts
index 9e74c1eb39..cff2d41597 100644
--- a/npm/ng-packs/packages/theme-shared/extensions/src/lib/adapters/date-time.adapter.ts
+++ b/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,
};
diff --git a/npm/ng-packs/packages/theme-shared/extensions/src/lib/enums/props.enum.ts b/npm/ng-packs/packages/theme-shared/extensions/src/lib/enums/props.enum.ts
index 27cb4990a6..12d4a3571a 100644
--- a/npm/ng-packs/packages/theme-shared/extensions/src/lib/enums/props.enum.ts
+++ b/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',
diff --git a/npm/ng-packs/packages/theme-shared/src/lib/components/button/button.component.ts b/npm/ng-packs/packages/theme-shared/src/lib/components/button/button.component.ts
index 3d874bb7d0..77a00be6bc 100644
--- a/npm/ng-packs/packages/theme-shared/src/lib/components/button/button.component.ts
+++ b/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;
diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server.Mongo/Program.cs b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server.Mongo/Program.cs
index 7e4da829ef..051587bb5c 100644
--- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server.Mongo/Program.cs
+++ b/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))
{
diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/Program.cs b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/Program.cs
index 7e4da829ef..051587bb5c 100644
--- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/Program.cs
+++ b/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))
{
diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host.Mongo/Program.cs b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host.Mongo/Program.cs
index 042101b6db..a9366b95fe 100644
--- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host.Mongo/Program.cs
+++ b/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))
{
diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/Program.cs b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/Program.cs
index 042101b6db..a9366b95fe 100644
--- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/Program.cs
+++ b/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))
{
diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc.Mongo/Program.cs b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc.Mongo/Program.cs
index 75962c0169..e3b1aeed74 100644
--- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc.Mongo/Program.cs
+++ b/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))
{
diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/Program.cs b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/Program.cs
index 75962c0169..e3b1aeed74 100644
--- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/Program.cs
+++ b/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))
{