diff --git a/Directory.Packages.props b/Directory.Packages.props index 145c809f3e..3a2a7b5aaa 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,9 +6,9 @@ - - - + + + @@ -16,10 +16,10 @@ - - - - + + + + @@ -114,11 +114,11 @@ - - - - - + + + + + diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json index 082438272d..e5d8c15ac2 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Commercial/Localization/Resources/en.json @@ -1129,6 +1129,7 @@ "NameOnCard": "Name on Card", "BillingDetails": "Billing Details", "ThereIsNoDeveloper": "There is no developer.", - "CardDetails": "Card Details" + "CardDetails": "Card Details", + "NoActiveLicence": "You are not eligible for this action! You have no active license." } } diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/ar.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/ar.json index 5cb4529fac..963546d9a3 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/ar.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/ar.json @@ -223,7 +223,7 @@ "AllAttendees": "جميع الحضور", "SeeRaffleDetail": "انظر تفاصيل السحب", "SeeRaffle": "انظر السحب", - "ParticipationIsComplete!": "المشاركة كاملة!", + "ParticipationIsComplete": "المشاركة كامل.", "ABPCoreDevelopmentTeam": "فريق التطوير الأساسي لبرنامج ABP", "RegisterTheEvent": "سجل الحدث", "GoToConferencePage": "انتقل إلى صفحة المؤتمر", diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/cs.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/cs.json index 146808545a..418cf5aef1 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/cs.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/cs.json @@ -223,7 +223,7 @@ "AllAttendees": "Všichni účastníci", "SeeRaffleDetail": "Viz Detail tomboly", "SeeRaffle": "Viz Tombola", - "ParticipationIsComplete!": "Účast je dokončena!", + "ParticipationIsComplete": "Účast je dokončena.", "ABPCoreDevelopmentTeam": "Základní vývojový tým ABP", "RegisterTheEvent": "Zaregistrujte událost", "GoToConferencePage": "Přejděte na stránku konference", diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/de.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/de.json index 3a9dab08a6..021c19b3ff 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/de.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/de.json @@ -223,7 +223,7 @@ "AllAttendees": "Alle Teilnehmer", "SeeRaffleDetail": "Siehe Details zur Verlosung", "SeeRaffle": "Siehe Tombola", - "ParticipationIsComplete!": "Die Teilnahme ist abgeschlossen!", + "ParticipationIsComplete": "Die Teilnahme ist abgeschlossen.", "ABPCoreDevelopmentTeam": "ABP-Kernentwicklungsteam", "RegisterTheEvent": "Registrieren Sie die Veranstaltung", "GoToConferencePage": "Gehen Sie zur Konferenzseite", diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json index 124e987ffb..fa10d9c408 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/en.json @@ -115,7 +115,7 @@ "VideoUrl": "Video Url", "GithubPostUrl": "GitHub Post Url", "ExternalPostUrl": "External Post Url", - "ThankYouForContribution": "Thank you for contributing to the ABP Community.", + "ThankYouForContribution": "Thank you for contributing to the ABP Community. We accept articles and video tutorials on ABP Framework, .NET, ASP.NET Core and general software development topics.", "GithubPost": "GitHub Post", "GithubPostSubmitStepOne": "1. Write a post on any public GitHub repository with the Markdown format. example", "GithubPostSubmitStepTwo": "2. Submit your post URL using the form.", @@ -219,11 +219,11 @@ "RaffleLoginAndRegisterMessage": "You must sign in to join this raffle! If you haven't registered yet, create an account for free now.", "Ok": "Ok", "SeeDetails": "See Details", - "WaitingForTheDraw": "Waiting for the draw", + "WaitingForTheDraw": "Wait for the draw!", "AllAttendees": "All Attendees", "SeeRaffleDetail": "See Raffle Detail", "SeeRaffle": "See Raffle", - "ParticipationIsComplete!": "Participation is complete!", + "ParticipationIsComplete": "Participation is complete.", "ABPCoreDevelopmentTeam": "ABP Core Development Team", "RegisterTheEvent": "Register the Event", "GoToConferencePage": "Go to Conference Page", diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/es.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/es.json index e6199fb7fb..c4e178bbd9 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/es.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/es.json @@ -223,7 +223,7 @@ "AllAttendees": "Todos los asistentes", "SeeRaffleDetail": "Ver Detalle del Sorteo", "SeeRaffle": "Ver Sorteo", - "ParticipationIsComplete!": "¡La participación está completa!", + "ParticipationIsComplete": "¡La participación está completa.", "ABPCoreDevelopmentTeam": "Equipo de desarrollo central de ABP", "RegisterTheEvent": "Registra el evento", "GoToConferencePage": "Ir a la página de la conferencia", diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/fi.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/fi.json index d7a9108249..ac509875d6 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/fi.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/fi.json @@ -223,7 +223,7 @@ "AllAttendees": "Kaikki osallistujat", "SeeRaffleDetail": "Katso arpajaiset", "SeeRaffle": "Katso Arpajaiset", - "ParticipationIsComplete!": "Osallistuminen on valmis!", + "ParticipationIsComplete": "Osallistuminen on valmis.", "ABPCoreDevelopmentTeam": "ABP:n ydinkehitystiimi", "RegisterTheEvent": "Ilmoittaudu tapahtumaan", "GoToConferencePage": "Siirry konferenssisivulle", diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/fr.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/fr.json index 2142ccb0d1..c98a48d93e 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/fr.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/fr.json @@ -223,7 +223,7 @@ "AllAttendees": "Tous les participants", "SeeRaffleDetail": "Voir les détails du tirage au sort", "SeeRaffle": "Voir le tirage au sort", - "ParticipationIsComplete!": "La participation est terminée !", + "ParticipationIsComplete": "La participation est terminée.", "ABPCoreDevelopmentTeam": "Équipe de développement de base ABP", "RegisterTheEvent": "Enregistrez l'événement", "GoToConferencePage": "Aller à la page de la conférence", diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/hi.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/hi.json index 0f5964fe23..f3d300e8a2 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/hi.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/hi.json @@ -223,7 +223,7 @@ "AllAttendees": "सभी उपस्थितगण", "SeeRaffleDetail": "रैफ़ल विवरण देखें", "SeeRaffle": "रैफ़ल देखें", - "ParticipationIsComplete!": "भागीदारी पूरी हो गई है!", + "ParticipationIsComplete": "भागीदारी पूरी हो गई है.", "ABPCoreDevelopmentTeam": "एबीपी कोर डेवलपमेंट टीम", "RegisterTheEvent": "ईवेंट पंजीकृत करें", "GoToConferencePage": "कॉन्फ़्रेंस पृष्ठ पर जाएँ", diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/hr.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/hr.json index 521cc0ebb0..5b3546a78b 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/hr.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/hr.json @@ -223,7 +223,7 @@ "AllAttendees": "Svi Prisutni", "SeeRaffleDetail": "Pogledajte detalje nagradne igre", "SeeRaffle": "Vidi Raffle", - "ParticipationIsComplete!": "Sudjelovanje je završeno!", + "ParticipationIsComplete": "Sudjelovanje je završeno.", "ABPCoreDevelopmentTeam": "ABP glavni razvojni tim", "RegisterTheEvent": "Prijavite događaj", "GoToConferencePage": "Idite na stranicu konferencije", diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/hu.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/hu.json index 6829656470..1f74f9e7a9 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/hu.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/hu.json @@ -223,7 +223,7 @@ "AllAttendees": "Minden résztvevő", "SeeRaffleDetail": "Lásd a nyereményjáték részleteit", "SeeRaffle": "Lásd: Tombola", - "ParticipationIsComplete!": "A részvétel teljes!", + "ParticipationIsComplete": "A részvétel teljes.", "ABPCoreDevelopmentTeam": "ABP Core Development Team", "RegisterTheEvent": "Regisztrálja az Eseményt", "GoToConferencePage": "Menjen a konferencia oldalára", diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/is.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/is.json index c7ade090af..a991c387d4 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/is.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/is.json @@ -223,7 +223,7 @@ "AllAttendees": "Allir fundarmenn", "SeeRaffleDetail": "Sjá upplýsingar um happdrætti", "SeeRaffle": "Sjá Happdrætti", - "ParticipationIsComplete!": "Þátttöku er lokið!", + "ParticipationIsComplete": "Þátttöku er lokið.", "ABPCoreDevelopmentTeam": "ABP Core Development Team", "RegisterTheEvent": "Skráðu viðburðinn", "GoToConferencePage": "Farðu á ráðstefnusíðu", diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/it.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/it.json index 0c2d9f4648..901c356c9a 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/it.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/it.json @@ -223,7 +223,7 @@ "AllAttendees": "Tutti i partecipanti", "SeeRaffleDetail": "Vedi i dettagli della lotteria", "SeeRaffle": "Vedi Lotteria", - "ParticipationIsComplete!": "La partecipazione è completa!", + "ParticipationIsComplete": "La partecipazione è completa.", "ABPCoreDevelopmentTeam": "Team di sviluppo principale dell'ABP", "RegisterTheEvent": "Registra l'Evento", "GoToConferencePage": "Vai alla pagina della conferenza", diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/nl.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/nl.json index f8ba6cf5f0..4ee8e44a09 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/nl.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/nl.json @@ -223,7 +223,7 @@ "AllAttendees": "Alle aanwezigen", "SeeRaffleDetail": "Zie loterijdetails", "SeeRaffle": "Zie loterij", - "ParticipationIsComplete!": "Deelname is voltooid!", + "ParticipationIsComplete": "Deelname is voltooid.", "ABPCoreDevelopmentTeam": "Kernontwikkelingsteam van ABP", "RegisterTheEvent": "Registreer het evenement", "GoToConferencePage": "Ga naar de conferentiepagina", 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 7704c8f5f0..2df6780f03 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/pl-PL.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/pl-PL.json @@ -223,7 +223,7 @@ "AllAttendees": "Wszyscy Uczestnicy", "SeeRaffleDetail": "Zobacz szczegóły loterii", "SeeRaffle": "Zobacz Loterię", - "ParticipationIsComplete!": "Udział jest zakończony!", + "ParticipationIsComplete": "Udział jest zakończony.", "ABPCoreDevelopmentTeam": "Główny zespół programistów ABP", "RegisterTheEvent": "Zarejestruj wydarzenie", "GoToConferencePage": "Przejdź do strony konferencji", 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 88689b5c1a..9f2ce433d9 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/pt-BR.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/pt-BR.json @@ -223,7 +223,7 @@ "AllAttendees": "Todos os participantes", "SeeRaffleDetail": "Veja detalhes do sorteio", "SeeRaffle": "Ver sorteio", - "ParticipationIsComplete!": "A participação está completa!", + "ParticipationIsComplete": "A participação está completa.", "ABPCoreDevelopmentTeam": "Equipe principal de desenvolvimento da ABP", "RegisterTheEvent": "Registre o Evento", "GoToConferencePage": "Ir para a página da conferência", 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 f9c4e99bf1..c2862d1fd7 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/ro-RO.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/ro-RO.json @@ -223,7 +223,7 @@ "AllAttendees": "Toți participanții", "SeeRaffleDetail": "Vezi Detaliul Tombolei", "SeeRaffle": "Vezi tombola", - "ParticipationIsComplete!": "Participarea este completă!", + "ParticipationIsComplete": "Participarea este completă.", "ABPCoreDevelopmentTeam": "Echipa de dezvoltare principală ABP", "RegisterTheEvent": "Înregistrați evenimentul", "GoToConferencePage": "Accesați pagina conferinței", diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/ru.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/ru.json index a2c4f59c87..27f2b1203a 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/ru.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/ru.json @@ -226,7 +226,7 @@ "AllAttendees": "Все участники", "SeeRaffleDetail": "См. подробную информацию о розыгрыше", "SeeRaffle": "Посмотреть лотерею", - "ParticipationIsComplete!": "Участие завершено!", + "ParticipationIsComplete": "Участие завершено.", "ABPCoreDevelopmentTeam": "Основная группа разработчиков ABP", "RegisterTheEvent": "Зарегистрировать мероприятие", "GoToConferencePage": "Перейти на страницу конференции", diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/sk.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/sk.json index b869378681..6ab3c9d074 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/sk.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/sk.json @@ -223,7 +223,7 @@ "AllAttendees": "Všetci účastníci", "SeeRaffleDetail": "Pozrite si detail tomboly", "SeeRaffle": "Pozri Tombola", - "ParticipationIsComplete!": "Účasť je dokončená!", + "ParticipationIsComplete": "Účasť je dokončená.", "ABPCoreDevelopmentTeam": "ABP Core Development Team", "RegisterTheEvent": "Zaregistrujte udalosť", "GoToConferencePage": "Prejdite na stránku konferencie", diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/sl.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/sl.json index 924c92d5cc..23763c6f78 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/sl.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/sl.json @@ -223,7 +223,7 @@ "AllAttendees": "Vsi udeleženci", "SeeRaffleDetail": "Oglejte si podrobnosti žrebanja", "SeeRaffle": "Glej nagradna igra", - "ParticipationIsComplete!": "Sodelovanje je zaključeno!", + "ParticipationIsComplete": "Sodelovanje je zaključeno.", "ABPCoreDevelopmentTeam": "ABP Core Development Team", "RegisterTheEvent": "Prijavite dogodek", "GoToConferencePage": "Pojdite na stran konference", diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/vi.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/vi.json index f6f794223e..a36ec9b0e8 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/vi.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/vi.json @@ -223,7 +223,7 @@ "AllAttendees": "Tất cả người tham dự", "SeeRaffleDetail": "Xem chi tiết xổ số", "SeeRaffle": "Xem xổ số", - "ParticipationIsComplete!": "Việc tham gia đã hoàn tất!", + "ParticipationIsComplete": "Việc tham gia đã hoàn tất.", "ABPCoreDevelopmentTeam": "Nhóm phát triển cốt lõi của ABP", "RegisterTheEvent": "Đăng ký sự kiện", "GoToConferencePage": "Tới trang hội nghị", 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 48f6d386ee..e899a94872 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/zh-Hans.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/zh-Hans.json @@ -223,7 +223,7 @@ "AllAttendees": "所有与会者", "SeeRaffleDetail": "查看抽奖详情", "SeeRaffle": "参见抽奖", - "ParticipationIsComplete!": "参与已完成!", + "ParticipationIsComplete.": "参与已完成.", "ABPCoreDevelopmentTeam": "ABP 核心开发团队", "RegisterTheEvent": "注册活动", "GoToConferencePage": "转至会议页面", 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 62159a4d21..ebec42dd8a 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/zh-Hant.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Community/Localization/Resources/zh-Hant.json @@ -223,7 +223,7 @@ "AllAttendees": "所有與會者", "SeeRaffleDetail": "看抽獎詳情", "SeeRaffle": "查看抽獎活動", - "ParticipationIsComplete!": "參與完畢!", + "ParticipationIsComplete": "參與完畢.", "ABPCoreDevelopmentTeam": "ABP核心開發團隊", "RegisterTheEvent": "註冊活動", "GoToConferencePage": "進入會議頁面", diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/ar.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/ar.json index 4cc00b6f2d..3e0789ce36 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/ar.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/ar.json @@ -466,6 +466,7 @@ "TestimonialSend": "شكرًا لك! لقد تلقينا شهادتك. سنقوم بمراجعتها واتخاذ الخطوة التالية قريبًا.", "Title": "العنوان", "TitlePlaceholder": "مطور برامج، مدير تكنولوجيا المعلومات، إلخ.", - "Characters": "الأحرف" + "Characters": "الأحرف", + "ABPTestimonialDescription": "ABP Framework: دعونا نسمع شهادتك" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/cs.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/cs.json index 8ff0f3d0c9..8e6d88a6f9 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/cs.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/cs.json @@ -466,6 +466,7 @@ "TestimonialSend": "Děkujeme! Obdrželi jsme vaše svědectví. Brzy to zkontrolujeme a přijmeme další kroky.", "Title": "Titul", "TitlePlaceholder": "Vývojář softwaru, CTO atd.", - "Characters": "znaky" + "Characters": "znaky", + "ABPTestimonialDescription": "ABP Framework: Pojďme slyšet vaši referenci" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/de.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/de.json index fdeefe8878..8d2e9aa519 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/de.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/de.json @@ -466,6 +466,7 @@ "TestimonialSend": "Vielen Dank! Wir haben Ihre Bewertung erhalten. Wir werden sie bald überprüfen und den nächsten Schritt unternehmen.", "Title": "Titel", "TitlePlaceholder": "Softwareentwickler, CTO usw.", - "Characters": "zeichen" + "Characters": "zeichen", + "ABPTestimonialDescription": "ABP Framework: Lass uns deine Bewertung hören" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json index aa829b0319..d1bc23b308 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json @@ -466,7 +466,8 @@ "Title": "Title", "TitlePlaceholder": "Software Developer, CTO etc.", "Characters": "characters", - "Testimonial_YourProfilePicture": "Your profile picture (only {0})" + "Testimonial_YourProfilePicture": "Your profile picture (only {0})", + "ABPTestimonialDescription": "ABP Framework: Let's hear your testimonial" } } diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/es.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/es.json index 132a55f0ad..2473197573 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/es.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/es.json @@ -466,6 +466,7 @@ "TestimonialSend": "¡Gracias! Hemos recibido tu testimonio. Lo revisaremos y daremos el próximo paso pronto.", "Title": "Título", "TitlePlaceholder": "Desarrollador de software, CTO, etc.", - "Characters": "caracteres" + "Characters": "caracteres", + "ABPTestimonialDescription": "ABP Framework: Escuchemos tu testimonio" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/fi.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/fi.json index 2332329155..2f25c77ce8 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/fi.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/fi.json @@ -466,6 +466,7 @@ "TestimonialSend": "Kiitos! Olemme vastaanottaneet arvostelusi. Käymme sen läpi ja otamme seuraavan askeleen pian.", "Title": "Titteli", "TitlePlaceholder": "Ohjelmistokehittäjä, CTO, jne.", - "Characters": "hahmot" + "Characters": "hahmot", + "ABPTestimonialDescription": "ABP Framework: Kuunnellaanpa sinun suosittelua" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/fr.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/fr.json index 6d4378ea0b..fa01f12ecc 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/fr.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/fr.json @@ -466,6 +466,7 @@ "TestimonialSend": "Merci ! Nous avons bien reçu votre témoignage. Nous le passerons en revue et prendrons la prochaine étape bientôt.", "Title": "Titre", "TitlePlaceholder": "Développeur de logiciels, CTO, etc.", - "Characters": "caractères" + "Characters": "caractères", + "ABPTestimonialDescription": "ABP Framework : Écoutons votre témoignage" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/hi.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/hi.json index 4cef58db12..b1df00ef53 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/hi.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/hi.json @@ -466,6 +466,7 @@ "TestimonialSend": "धन्यवाद! हमने आपका साक्षात्कार प्राप्त किया है। हम इसे समीक्षा करेंगे और शीघ्र अगला कदम उठाएंगे।", "Title": "शीर्षक", "TitlePlaceholder": "सॉफ़्टवेयर डेवेलपर, सीटीओ, आदि।", - "Characters": "अक्षर" + "Characters": "अक्षर", + "ABPTestimonialDescription": "ABP Framework: आइए आपका प्रशंसापत्र सुनें" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/hr.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/hr.json index a7ac8ad080..fe90b4dc7e 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/hr.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/hr.json @@ -466,6 +466,7 @@ "TestimonialSend": "Hvala vam! Primili smo vašu preporuku. Provjerit ćemo je i uskoro poduzeti sljedeći korak.", "Title": "Naslov", "TitlePlaceholder": "Razvojni inženjer, CTO, itd.", - "Characters": "znakovi" + "Characters": "znakovi", + "ABPTestimonialDescription": "ABP Framework: Hajde da čujemo tvoje iskustvo" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/hu.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/hu.json index 87d0f0c2fd..41655fe1dd 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/hu.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/hu.json @@ -466,6 +466,7 @@ "TestimonialSend": "Köszönjük! Megkaptuk az ajánlásod. Hamarosan átnézzük és következő lépéseket teszünk.", "Title": "Cím", "TitlePlaceholder": "Szoftvertervező, CTO, stb.", - "Characters": "karakterek" + "Characters": "karakterek", + "ABPTestimonialDescription": "ABP Framework: Halljuk az Ön beszámolóját" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/is.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/is.json index 8f9d3ead04..f9c6f8b179 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/is.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/is.json @@ -466,6 +466,7 @@ "TestimonialSend": "Þakka þér! Við höfum fengið viðmiðuna þína. Við munum fara yfir hana fljótt og taka næsta skref.", "Title": "Titill", "TitlePlaceholder": "Hugbúnaðarþróunarmiðill, CTO o.s.frv.", - "Characters": "stafir" + "Characters": "stafir", + "ABPTestimonialDescription": "ABP Framework: Við skulum heyra vitnisburð þinn" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/it.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/it.json index d1396d59fc..c9e082259b 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/it.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/it.json @@ -466,6 +466,7 @@ "TestimonialSend": "Grazie! ", "Title": "Titolo", "TitlePlaceholder": "Sviluppatore di software, CTO ecc.", - "Characters": "caratteri" + "Characters": "caratteri", + "ABPTestimonialDescription": "ABP Framework: Ascoltiamo la tua testimonianza", } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/nl.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/nl.json index 4d50f0b0e2..5e923a140e 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/nl.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/nl.json @@ -466,6 +466,7 @@ "TestimonialSend": "Bedankt! ", "Title": "Titel", "TitlePlaceholder": "Softwareontwikkelaar, CTO enz.", - "Characters": "tekens" + "Characters": "tekens", + "ABPTestimonialDescription": "ABP Framework: Laten we uw getuigenis horen" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/pl-PL.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/pl-PL.json index ae3280373c..f276cf6082 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/pl-PL.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/pl-PL.json @@ -466,6 +466,7 @@ "TestimonialSend": "Dziękuję! ", "Title": "Tytuł", "TitlePlaceholder": "Programista, CTO itp.", - "Characters": "znaki" + "Characters": "znaki", + "ABPTestimonialDescription": "Struktura ABP: Posłuchajmy Twojej opinii" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/pt-BR.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/pt-BR.json index 7cc714dd75..ee7ca3161b 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/pt-BR.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/pt-BR.json @@ -466,6 +466,7 @@ "TestimonialSend": "Obrigado! ", "Title": "Título", "TitlePlaceholder": "Desenvolvedor de software, CTO etc.", - "Characters": "caracteres" + "Characters": "caracteres", + "ABPTestimonialDescription": "Estrutura ABP: vamos ouvir seu depoimento" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/ro-RO.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/ro-RO.json index c0dd55092f..9822f54642 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/ro-RO.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/ro-RO.json @@ -466,6 +466,7 @@ "TestimonialSend": "Mulțumesc! ", "Title": "Titlu", "TitlePlaceholder": "Dezvoltator software, CTO etc.", - "Characters": "caractere" + "Characters": "caractere", + "ABPTestimonialDescription": "Cadrul ABP: Haideți să vă ascultăm mărturia" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/ru.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/ru.json index c2c325a12a..2b94373e76 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/ru.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/ru.json @@ -464,6 +464,7 @@ "TestimonialSend": "Спасибо! ", "Title": "Заголовок", "TitlePlaceholder": "Разработчик программного обеспечения, технический директор и т. д.", - "Characters": "Символы" + "Characters": "Символы", + "ABPTestimonialDescription": "ABP Framework: давайте послушаем ваш отзыв" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/sk.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/sk.json index 2a94075c32..ca72d2cf19 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/sk.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/sk.json @@ -466,6 +466,7 @@ "TestimonialSend": "Ďakujem! ", "Title": "Názov", "TitlePlaceholder": "Vývojár softvéru, CTO atď.", - "Characters": "znaky" + "Characters": "znaky", + "ABPTestimonialDescription": "Rámec ABP: Vypočujme si vaše svedectvo" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/sl.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/sl.json index 180099a255..e9366a8db9 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/sl.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/sl.json @@ -466,6 +466,7 @@ "TestimonialSend": "Hvala vam! ", "Title": "Naslov", "TitlePlaceholder": "Razvijalec programske opreme, tehnični direktor itd.", - "Characters": "znaki" + "Characters": "znaki", + "ABPTestimonialDescription": "Ogrodje ABP: poslušajmo vaše pričevanje" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/tr.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/tr.json index dda86e5931..779e7b6338 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/tr.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/tr.json @@ -412,6 +412,7 @@ "TestimonialSend": "Referansınızı paylaştığınız için teşekkürler!", "Title": "Başlık", "TitlePlaceholder": "Yazılım Geliştiricisi, CTO vb.", - "Characters": "karakter" + "Characters": "karakter", + "ABPTestimonialDescription": "ABP Framework: Referanslarınızı dinleyelim" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/vi.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/vi.json index f997c4d0c9..b8ad4565be 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/vi.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/vi.json @@ -466,6 +466,7 @@ "TestimonialSend": "Cảm ơn! ", "Title": "Tiêu đề", "TitlePlaceholder": "Nhà phát triển phần mềm, CTO, v.v.", - "Characters": "Ký tự" + "Characters": "Ký tự", + "ABPTestimonialDescription": "Khung ABP: Hãy nghe lời chứng thực của bạn" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/zh-Hans.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/zh-Hans.json index 163308d9cb..9fb6a08b32 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/zh-Hans.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/zh-Hans.json @@ -466,6 +466,7 @@ "TestimonialSend": "谢谢你!", "Title": "标题", "TitlePlaceholder": "软件开发人员、CTO等", - "Characters": "字符" + "Characters": "字符", + "ABPTestimonialDescription": "ABP 框架:让我们听听您的感言" } } \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/zh-Hant.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/zh-Hant.json index 80a7132080..81664b5f8d 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/zh-Hant.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/zh-Hant.json @@ -466,6 +466,7 @@ "TestimonialSend": "谢谢你!", "Title": "标题", "TitlePlaceholder": "软件开发人员、CTO等", - "Characters": "字符" + "Characters": "字符", + "ABPTestimonialDescription": "ABP 框架:让我们听听您的感言" } } \ No newline at end of file diff --git a/docs/en/Blog-Posts/2024-01-14-china-shenzhen-dotnet-conf/1.jpg b/docs/en/Blog-Posts/2024-01-14-china-shenzhen-dotnet-conf/1.jpg new file mode 100644 index 0000000000..dbdf7c96b0 Binary files /dev/null and b/docs/en/Blog-Posts/2024-01-14-china-shenzhen-dotnet-conf/1.jpg differ diff --git a/docs/en/Blog-Posts/2024-01-14-china-shenzhen-dotnet-conf/2.jpg b/docs/en/Blog-Posts/2024-01-14-china-shenzhen-dotnet-conf/2.jpg new file mode 100644 index 0000000000..4af735c16f Binary files /dev/null and b/docs/en/Blog-Posts/2024-01-14-china-shenzhen-dotnet-conf/2.jpg differ diff --git a/docs/en/Blog-Posts/2024-01-14-china-shenzhen-dotnet-conf/3.jpg b/docs/en/Blog-Posts/2024-01-14-china-shenzhen-dotnet-conf/3.jpg new file mode 100644 index 0000000000..660d086cbc Binary files /dev/null and b/docs/en/Blog-Posts/2024-01-14-china-shenzhen-dotnet-conf/3.jpg differ diff --git a/docs/en/Blog-Posts/2024-01-14-china-shenzhen-dotnet-conf/4.jpg b/docs/en/Blog-Posts/2024-01-14-china-shenzhen-dotnet-conf/4.jpg new file mode 100644 index 0000000000..7babc9ab5b Binary files /dev/null and b/docs/en/Blog-Posts/2024-01-14-china-shenzhen-dotnet-conf/4.jpg differ diff --git a/docs/en/Blog-Posts/2024-01-14-china-shenzhen-dotnet-conf/5.jpg b/docs/en/Blog-Posts/2024-01-14-china-shenzhen-dotnet-conf/5.jpg new file mode 100644 index 0000000000..b1ab47d590 Binary files /dev/null and b/docs/en/Blog-Posts/2024-01-14-china-shenzhen-dotnet-conf/5.jpg differ diff --git a/docs/en/Blog-Posts/2024-01-14-china-shenzhen-dotnet-conf/51.jpg b/docs/en/Blog-Posts/2024-01-14-china-shenzhen-dotnet-conf/51.jpg new file mode 100644 index 0000000000..27aad1a716 Binary files /dev/null and b/docs/en/Blog-Posts/2024-01-14-china-shenzhen-dotnet-conf/51.jpg differ diff --git a/docs/en/Blog-Posts/2024-01-14-china-shenzhen-dotnet-conf/6.jpg b/docs/en/Blog-Posts/2024-01-14-china-shenzhen-dotnet-conf/6.jpg new file mode 100644 index 0000000000..d7f0de7fe6 Binary files /dev/null and b/docs/en/Blog-Posts/2024-01-14-china-shenzhen-dotnet-conf/6.jpg differ diff --git a/docs/en/Blog-Posts/2024-01-14-china-shenzhen-dotnet-conf/8.jpg b/docs/en/Blog-Posts/2024-01-14-china-shenzhen-dotnet-conf/8.jpg new file mode 100644 index 0000000000..c271071526 Binary files /dev/null and b/docs/en/Blog-Posts/2024-01-14-china-shenzhen-dotnet-conf/8.jpg differ diff --git a/docs/en/Blog-Posts/2024-01-14-china-shenzhen-dotnet-conf/9.jpg b/docs/en/Blog-Posts/2024-01-14-china-shenzhen-dotnet-conf/9.jpg new file mode 100644 index 0000000000..96e99f2565 Binary files /dev/null and b/docs/en/Blog-Posts/2024-01-14-china-shenzhen-dotnet-conf/9.jpg differ diff --git a/docs/en/Blog-Posts/2024-01-14-china-shenzhen-dotnet-conf/POST.md b/docs/en/Blog-Posts/2024-01-14-china-shenzhen-dotnet-conf/POST.md new file mode 100644 index 0000000000..3fcd4d0da2 --- /dev/null +++ b/docs/en/Blog-Posts/2024-01-14-china-shenzhen-dotnet-conf/POST.md @@ -0,0 +1,20 @@ +# 2024 First Community Event. + +The first .NET community event in 2024 was successfully held in Shenzhen on January 14, 2024. + +This event is co-organized by **Microsoft MVP China Team**, **Microsoft Reactor**, **China .NET Community** and **Shenzhen .NET Club**. + +**ABP.IO** continues to strongly support the community, and we have prepared exquisite gifts for participants. + +The event includes four wonderful technical lectures to reveal big data and AI's potential opportunities and innovations. It is a transfer of knowledge and a platform for communication and cooperation among technology enthusiasts. +![](1.jpg) +![](2.jpg) +![](3.jpg) +![](4.jpg) +![](51.jpg) +![](5.jpg) +![](6.jpg) +![](8.jpg) +![](9.jpg) + +**See you at the next community event!** diff --git a/docs/en/Community-Articles/2024-01-15-Abp-Supports-NET8/Post.md b/docs/en/Community-Articles/2024-01-15-Abp-Supports-NET8/Post.md new file mode 100644 index 0000000000..a45f893b16 --- /dev/null +++ b/docs/en/Community-Articles/2024-01-15-Abp-Supports-NET8/Post.md @@ -0,0 +1,173 @@ +![cover](cover.png) + +# ABP Now Supports .NET 8 + +Recently we have published ABP v8.0. With this version [the ABP Framework](https://github.com/abpframework/abp/blob/dev/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyCompanyName.MyProjectName.Web.csproj#L6) and ABP Commercial both supports for .NET 8, aligning itself with the latest enhancements and new features of the ASP.NET's new version 8. + +Here's the [related PR](https://commercial.abp.io/releases/pr/15676) for this upgrade. This update ensures that developers using ABP can leverage the new features and improvements of .NET 8, enhancing the capability and performance of their applications. With .NET 8 support, ABP 8.0 likely offers improved performance, enhanced security, and greater efficiency, making it a robust choice for building modern web applications. +Here's the summary of .NET 8 features and enhancements: + +## What's new in .NET 8 + +### .NET Aspire + +[.NET Aspire](https://learn.microsoft.com/en-us/dotnet/aspire/) is a tool to observe and manage distributed web applications. It's still preview version. You can manage your containers, executables, logs, traces and metrics of your running web application. For more information see this article https://devblogs.microsoft.com/dotnet/introducing-dotnet-aspire-simplifying-cloud-native-development-with-dotnet-8/ + +### Serialization + +12 features have been implemented for [System.Text.Json](https://learn.microsoft.com/en-us/dotnet/api/system.text.json) serialization and deserialization library with .NET 8. See these enhancements +* [Built-in support for additional types](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8#built-in-support-for-additional-types) +* [Source generator](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8#source-generator) +* [Interface hierarchies](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8#interface-hierarchies) +* [Naming policies](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8#naming-policies) +* [Read-only properties](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8#read-only-properties) +* [Disable reflection-based default](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8#disable-reflection-based-default) +* [New JsonNode API methods](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8#new-jsonnode-api-methods) +* [Non-public members](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8#non-public-members) +* [Streaming deserialization APIs](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8#streaming-deserialization-apis) +* [WithAddedModifier extension method](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8#withaddedmodifier-extension-method) +* [New JsonContent.Create overloads](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8#new-jsoncontentcreate-overloads) +* [Freeze a JsonSerializerOptions instance](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8#freeze-a-jsonserializeroptions-instance) + +### Time abstraction + +You may mock time in test scenarios with the new [TimeProvider class](https://learn.microsoft.com/en-us/dotnet/api/system.timeprovider?view=net-8.0) and [ITimer interface](https://learn.microsoft.com/en-us/dotnet/api/system.threading.itimer?view=net-8.0), which add time abstraction functionality. + +### UTF8 improvements + +The new [IUtf8SpanFormattable](https://learn.microsoft.com/en-us/dotnet/api/system.iutf8spanformattable) interface targets `UTF8` and `Span` instead of `UTF16` and `Span`. Also the new [Utf8.TryWrite](https://learn.microsoft.com/en-us/dotnet/api/system.text.unicode.utf8.trywrite) methods provide a `UTF8` based counterpart to the existing [MemoryExtensions.TryWrite](https://learn.microsoft.com/en-us/dotnet/api/system.memoryextensions.trywrite) methods, which are UTF16-based. + +### Data validation + +There are new validation attributes for cloud-native services. The new properties were added to the [RangeAttribute](https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations.rangeattribute) and [RequiredAttribute](https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations.requiredattribute) types. See the list below for the new validation attributes: + +| New Data Attribute | Description | +| ------------------------------------------------------------ | ------------------------------------------------------------ | +| [MinimumIsExclusive](https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations.rangeattribute.minimumisexclusive#system-componentmodel-dataannotations-rangeattribute-minimumisexclusive), [MaximumIsExclusive](https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations.rangeattribute.maximumisexclusive#system-componentmodel-dataannotations-rangeattribute-maximumisexclusive) | Checks whether bounds are included in the allowable range | +| [Length](https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations.lengthattribute) | Checks both lower and upper bounds for strings or collections. For example, `[Length(5, 10)]` requires at least 5 elements and at most 10 elements in a collection | +| [Base64String](https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations.base64stringattribute) | Checks that a string is a valid Base64 format | +| [AllowedValues](https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations.allowedvaluesattribute), [DeniedValues](https://learn.microsoft.com/en-us/dotnet/api/system.componentmodel.dataannotations.deniedvaluesattribute) | Checks if values are from the allowed or disallowed list eg: `[AllowedValues("man", "woman", "child")]` | + + + +### Randomness methods + +The [System.Random](https://learn.microsoft.com/en-us/dotnet/api/system.random) and [System.Security.Cryptography.RandomNumberGenerator](https://learn.microsoft.com/en-us/dotnet/api/system.security.cryptography.randomnumbergenerator) types present 2 new approaches to handling randomness. + +### Performance-focused types + +* The new [System.Collections.Frozen](https://learn.microsoft.com/en-us/dotnet/api/system.collections.frozen) namespace includes the collection types [FrozenDictionary](https://learn.microsoft.com/en-us/dotnet/api/system.collections.frozen.frozendictionary-2) and [FrozenSet](https://learn.microsoft.com/en-us/dotnet/api/system.collections.frozen.frozenset-1). +* Methods like [MemoryExtensions.IndexOfAny](https://learn.microsoft.com/en-us/dotnet/api/system.memoryextensions.indexofany) look for the first occurrence of *any value in the passed collection*. +* The new [System.Text.CompositeFormat](https://learn.microsoft.com/en-us/dotnet/api/system.text.compositeformat) type is useful for optimizing format strings that aren't known at compile time (for example, if the format string is loaded from a resource file). +* New [System.IO.Hashing.XxHash3](https://learn.microsoft.com/en-us/dotnet/api/system.io.hashing.xxhash3) and [System.IO.Hashing.XxHash128](https://learn.microsoft.com/en-us/dotnet/api/system.io.hashing.xxhash128) types provide implementations of the fast XXH3 and XXH128 hash algorithms. + +### System.Numerics and System.Runtime.Intrinsics + +The [System.Numerics](https://learn.microsoft.com/en-us/dotnet/api/system.numerics) and [System.Runtime.Intrinsics](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.intrinsics) namespaces introduces some hardware improvements with .NET 8. The following structs reimplemented for running with the best performance on ARM64. + +[Vector256](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.intrinsics.vector256-1), [Matrix3x2](https://learn.microsoft.com/en-us/dotnet/api/system.numerics.matrix3x2), and [Matrix4x4](https://learn.microsoft.com/en-us/dotnet/api/system.numerics.matrix4x4) have improved + +### Metrics + +New APIs let you attach key-value pair tags to [Meter](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.metrics.meter) and [Instrument](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.metrics.instrument) objects when you create them. Aggregators of published metric measurements can use the tags to differentiate the aggregated values. + +### Cryptography + +[SHA-3](https://en.wikipedia.org/wiki/SHA-3) (Secure Hash Algorithm 3) is the latest member of the Secure Hash Algorithm family of standards. With .NET 8 SHA-3 is being supported. + +### HTTPS proxy support + +Before [HttpClient](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclient) allows hackers to make [man-in-the-middle attack](https://en.wikipedia.org/wiki/Man-in-the-middle_attack) with .NET 8, it creates an encrypted channel between the client and the proxy so all requests can be handled with full privacy. + +### Stream-based ZipFile methods + +The new [ZipFile.ExtractToDirectory](https://learn.microsoft.com/en-us/dotnet/api/system.io.compression.zipfile.extracttodirectory) provides a stream containing a zipped file and extract its contents into the filesystem. + +### Hosted lifecycle services + +Hosted services now have more options for execution during the application lifecycle. [IHostedService](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.ihostedservice) provided `StartAsync` and `StopAsync`, and now [IHostedLifecycleService](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.ihostedlifecycleservice) provides these additional methods: + +- [StartingAsync(CancellationToken)](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.ihostedlifecycleservice.startingasync#microsoft-extensions-hosting-ihostedlifecycleservice-startingasync(system-threading-cancellationtoken)) +- [StartedAsync(CancellationToken)](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.ihostedlifecycleservice.startedasync#microsoft-extensions-hosting-ihostedlifecycleservice-startedasync(system-threading-cancellationtoken)) +- [StoppingAsync(CancellationToken)](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.ihostedlifecycleservice.stoppingasync#microsoft-extensions-hosting-ihostedlifecycleservice-stoppingasync(system-threading-cancellationtoken)) +- [StoppedAsync(CancellationToken)](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.ihostedlifecycleservice.stoppedasync#microsoft-extensions-hosting-ihostedlifecycleservice-stoppedasync(system-threading-cancellationtoken)) + +### Native AOT support + +The option to [publish as Native AOT](https://learn.microsoft.com/en-us/dotnet/core/deploying/native-aot/) was first introduced in .NET 7. Publishing an app with Native AOT creates a fully self-contained version of your app that doesn't need a runtime—everything is included in a single file. .NET 8 brings the following improvements to Native AOT publishing: + +| Operating system | .NET 7 | .NET 8 | +| :-------------------------------------- | :------ | :------ | +| Linux x64 (with `-p:StripSymbols=true`) | 3.76 MB | 1.84 MB | +| Windows x64 | 2.85 MB | 1.77 MB | + +### Target iOS-like platforms with Native AOT + +.NET 8 starts the work to enable Native AOT support for iOS-like platforms. You can now build and run .NET iOS and .NET MAUI applications with Native AOT on the following platforms: + +- `ios` +- `iossimulator` +- `maccatalyst` +- `tvos` +- `tvossimulator` + +### Performance improvements + +.NET 8 includes improvements to code generation and just-in time (JIT) compilation: + +- Arm64 performance improvements +- SIMD improvements +- Support for AVX-512 ISA extensions (see [Vector512 and AVX-512](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8#vector512-and-avx-512)) +- Cloud-native improvements +- JIT throughput improvements +- Loop and general optimizations +- Optimized access for fields marked with [ThreadStaticAttribute](https://learn.microsoft.com/en-us/dotnet/api/system.threadstaticattribute) +- Consecutive register allocation. Arm64 has two instructions for table vector lookup, which require that all entities in their tuple operands are present in consecutive registers. +- JIT/NativeAOT can now unroll and auto-vectorize some memory operations with SIMD, such as comparison, copying, and zeroing, if it can determine their sizes at compile time. + + + +### .NET SDK + +This section contains the following subtopics: + +- [CLI-based project evaluation](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8#cli-based-project-evaluation) +- [Terminal build output](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8#terminal-build-output) +- [Simplified output paths](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8#simplified-output-paths) +- ['dotnet workload clean' command](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8#dotnet-workload-clean-command) +- ['dotnet publish' and 'dotnet pack' assets](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8#dotnet-publish-and-dotnet-pack-assets) +- [Template engine](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8#template-engine) +- [Source Link](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8#source-link) +- [Source-build SDK](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8#source-build-sdk) + +### ASP.NET composite images + +As part of an effort to improve containerization performance, new ASP.NET Docker images are available that have a composite version of the runtime. This composite is built by compiling multiple MSIL assemblies into a single ready-to-run (R2R) output binary. Because these assemblies are embedded into a single image, jitting takes less time, and the startup performance of apps improves. The other big advantage of the composite over the regular ASP.NET image is that the composite images have a smaller size on disk. + +### Source-generated COM interop + +.NET 8 includes a new source generator that supports interoperating with COM interfaces. + +### Minimum support baselines for Linux + +The minimum support baselines for Linux have been updated for .NET 8. .NET is built targeting Ubuntu 16.04, for all architectures. + +### AOT compilation for Android apps + +To decrease app size, .NET and .NET MAUI apps that target Android use *profiled* ahead-of-time (AOT) compilation mode when they're built in Release mode. Profiled AOT compilation affects fewer methods than regular AOT compilation. .NET 8 introduces the `` property that lets you opt-in to further AOT compilation for Android apps to decrease app size even more. + +### Code analysis + +.NET 8 includes several new code analyzers and fixers to help verify that you're using .NET library APIs correctly and efficiently. The following table summarizes the new analyzers. + +### NuGet + +Starting in .NET 8, NuGet verifies signed packages on Linux by default. NuGet continues to verify signed packages on Windows as well. + +### C# Hot Reload supports modifying generics + +Starting in .NET 8, C# Hot Reload [supports modifying generic types and generic methods](https://devblogs.microsoft.com/dotnet/hot-reload-generics/). When you debug console, desktop, mobile, or WebAssembly applications with Visual Studio, you can apply changes to generic classes and generic methods in C# code or Razor pages. For more information, see the [full list of edits supported by Roslyn](https://github.com/dotnet/roslyn/blob/main/docs/wiki/EnC-Supported-Edits.md) + +*References:* + +* https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8 \ No newline at end of file diff --git a/docs/en/Community-Articles/2024-01-15-Abp-Supports-NET8/cover.png b/docs/en/Community-Articles/2024-01-15-Abp-Supports-NET8/cover.png new file mode 100644 index 0000000000..0f719b4b0b Binary files /dev/null and b/docs/en/Community-Articles/2024-01-15-Abp-Supports-NET8/cover.png differ diff --git a/docs/en/Community-Articles/2024-01-18-ABP-Now-Supports-Keyed-Services/POST.md b/docs/en/Community-Articles/2024-01-18-ABP-Now-Supports-Keyed-Services/POST.md new file mode 100644 index 0000000000..a5f2c694ed --- /dev/null +++ b/docs/en/Community-Articles/2024-01-18-ABP-Now-Supports-Keyed-Services/POST.md @@ -0,0 +1,198 @@ +# ABP Now Supports Keyed Services! + +In this post, I describe the new **"keyed service"** support for the dependency injection container, which came with .NET 8.0. Then, I'll show you an example of usage within the ABP Framework. + +## What Are Keyed Services? + +ASP.NET Core ships with a built-in dependency injection container, which is a pretty basic DI container that supports minimal features a dependency injection container is supposed to have. For that reason, most of the .NET users use third-party containers like [Autofac](https://autofac.org/), or Ninject. + +> ABP Framework uses Autofac by default in startup templates, and it supports more features. For example, the built-in DI container does not natively support property injection but Autofac does. + +[**Keyed dependency injection (DI) services**](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection?view=aspnetcore-8.0#keyed-services) were added to the built-in DI container as a new feature with .NET 8.0. This is an important feature, which allows for registering and retrieving DI services using keys/names. + +Prior to this version, you could register multiple services as follows: + +```csharp +var builder = WebApplication.CreateBuilder(args); + +//service registrations +builder.Services.AddTransient(); +builder.Services.AddTransient(); +builder.Services.AddTransient(); +``` + +But without keyed services, you could only retrieve all of the services like this: + +```csharp +public class BookService(IEnumerable services) {} +``` + +Or the last registered one (`MyServiceThree` in our case): + +```csharp +public class BookService(IMyService service) {} +``` + +There was not a simple way to retrieve a specific implementation type directly. With keyed services, now we can register services with a key and then resolve them by the key anywhere we want and retrieve the only one that we want to use. + +## ABP Framework Now Supports Keyed Services! + +Autofac was already supporting the named/keyed DI services for a long time and with v9.0.0, they made it compatible with the `Microsoft.Extensions.DependencyInjection` package (including the keyed service support). + +After the v9.0.0 was released for Autofac, then the ABP Framework Core team immediately updated the `Autofac.Extensions.DependencyInjection` package to `v9.0.0`, made the related changes in its own Autofac package and included in the v8.0.2 release. + +You can see the related changes in the following PRs: + +* https://github.com/abpframework/abp/pull/18775 +* https://github.com/abpframework/abp/pull/18777 + +## Using Keyed Services with ABP Framework + +After the quick background of the process and the feature itself, now let's see the keyed services in action. + +First, update the ABP CLI to v8.0.2 with the following command (if your CLI version is newer, no need to apply this): + +```bash +dotnet tool update -g Volo.Abp.Cli --version 8.0.2 +``` + +Then, we can create an application template (single-layer) with the following command (MVC as the UI option and EF Core as the DB provider): + +```bash +abp new KeyedServiceDemo -t app-nolayers -csf --version 8.0.2 +``` + +After the application is created, we can open it in an IDE and start developing... + +**Example**: + +Let's assume that we have an interface called `IMyService`, which has three implementation types `MyServiceOne`, `MyServiceTwo` and `MyServiceThree`: + +```csharp +public interface IMyService +{ + string GetMessage(); +} + +public class MyServiceOne : IMyService +{ + public string GetMessage() => "MyServiceOne"; +} + +public class MyServiceTwo : IMyService +{ + public string GetMessage() => "MyServiceTwo"; +} + +public class MyServiceThree : IMyService +{ + public string GetMessage() => "MyServiceThree"; +} +``` + +After creating our services, now we can register them in our module class. So, open the module class and add the following lines in the `ConfigureServices` method: + +```csharp + public override void ConfigureServices(ServiceConfigurationContext context) + { + //code abbreviation for clarity... + + context.Services.AddKeyedTransient("myserviceone"); + context.Services.AddKeyedTransient("myservicetwo"); + context.Services.AddKeyedTransient("myservicethree"); + } +``` + +* To register a keyed service, you can use one of the `AddKeyedTransient`, `AddKeyedScoped`, or `AddKeyedSingleton` overloads, and provide a key for the registration. +* In the example above, I used `AddKeyedTransient` overload, and registered all of these services as in the `Transient` lifetime. + +Then, when you want to retrieve a keyed service, you can use the `[FromKeyedServices(object key)]` attribute: + +```csharp +public class NotificationService([FromKeyedServices("myserviceone")] IMyService myServiceOne) +{ + public string Notify() => myServiceOne.GetMessage(); +} +``` + +With this kind of use, you can be certain that not the latest registered service is resolved and instead the keyed service will be resolved, and then you can use it. + +Also, you can use the `FromKeyedServicesAttribute` with minimal APIs as follows: + +```csharp +public override void OnApplicationInitialization(ApplicationInitializationContext context) +{ + //code abbreviation for clarity... + + app.UseConfiguredEndpoints(endpoints => + { + endpoints.MapGet("/my-service", ([FromKeyedServices("myserviceone")] IMyService myservice) => myservice.GetMessage()); + }); +} +``` + +That's how it's easy to register multiple services in a keyed fashion and resolve them by keys! + +## Advanced + +In this section, I want to briefly mention some advanced topics (relatively 😀), such as what happens if I register more than one service with the same key, or is there a way to get the keyed services from the `ServiceProvider` etc. + +### Registering Multiple Services with the Same Key + +Let's assume that you mistakenly registered multiple services with the same key: + +```csharp + public override void ConfigureServices(ServiceConfigurationContext context) + { + //code abbreviation for clarity... + + context.Services.AddKeyedTransient("myserviceone"); + context.Services.AddKeyedTransient("myserviceone"); + } +``` + +In that case, when you try to resolve the dependency with the key, then the last registered one will be used without having a problem: + +```csharp +//it will resolve the `MyServiceTwo` service! +endpoints.MapGet("/my-service", ([FromKeyedServices("myserviceone")] IMyService myservice) => myservice.GetMessage()); +``` + +Also, since you used the same key for multiple services, then you can inject the `IEnumerable` to get all services for a certain key and use them: + +```csharp +//it will resolve both `MyServiceOne` and `MyServiceTwo` services! +endpoints.MapGet("/my-service", ([FromKeyedServices("myserviceone")] IEnumerable myservices) => +{ + var sb = new StringBuilder(); + foreach (var myService in myservices) + { + sb.AppendLine(myService.GetMessage()); + } + + return sb.ToString(); +}); +``` + +### Resolving Keyed Services from ServiceProvider & LazyServiceProvider + +You can resolve keyed services by using one of the extension methods (`.GetKeyedServices`, `.GetKeyedService<>`, `.GetRequiredKeyedService<>`, ...): + +```csharp +//uses the `MyServiceTwo` service! +var myService = ServiceProvider.GetRequiredKeyedService("myserviceone"); +``` + +On the other hand, resolving keyed services from `LazyServiceProvider` is not supported in v8.0.2, but there is a PR, which you can find at https://github.com/abpframework/abp/pull/18792, it will fix this problem and it will be included in the next version. + +### Automatically Registering Keyed Services + +Currently, if you want to register a keyed service, you need to do it manually as we see in the previous sections by using one of the overloads (`.AddKeyedTransient`, `.AddKeyedScoped` and `.AddKeyedSingleton`). + +It would be good if we could make this process automatically and not need to manually register services, and for that purpose, I have [created an issue](https://github.com/abpframework/abp/issues/18794) that aims to introduce an attribute, which allows us to automatically register multiple services as keyed services. + +You can [follow the issue](https://github.com/abpframework/abp/issues/18794) if you are considering using keyed services in your application and don't want to register them manually. + +## Summary + +In this post, I described the new **"keyed service"** support added to the built-in dependency injection container that was released in .NET 8, and wanted to announce it's being supported from v8.0.2 for ABP Framework. It's a really good addition to the built-in dependency injection container and can be pretty useful for certain use-cases. diff --git a/docs/en/Community-Articles/2024-01-18-ABP-Now-Supports-Keyed-Services/cover-image.png b/docs/en/Community-Articles/2024-01-18-ABP-Now-Supports-Keyed-Services/cover-image.png new file mode 100644 index 0000000000..56b04d353c Binary files /dev/null and b/docs/en/Community-Articles/2024-01-18-ABP-Now-Supports-Keyed-Services/cover-image.png differ diff --git a/docs/en/Dependency-Injection.md b/docs/en/Dependency-Injection.md index 3e49c0814f..826d360e22 100644 --- a/docs/en/Dependency-Injection.md +++ b/docs/en/Dependency-Injection.md @@ -143,6 +143,50 @@ public class TaxCalculator : ITaxCalculator, ITransientDependency } ```` +### ExposeKeyedService Attribute + +`ExposeKeyedServiceAttribute` is used to control which keyed services are provided by the related class. Example: + +````C# +[ExposeKeyedService("taxCalculator")] +[ExposeKeyedService("calculator")] +public class TaxCalculator: ICalculator, ITaxCalculator, ICanCalculate, ITransientDependency +{ +} +```` + +In the example above, the `TaxCalculator` class exposes the `ITaxCalculator` interface with the key `taxCalculator` and the `ICalculator` interface with the key `calculator`. That means you can get keyed services from the `IServiceProvider` as shown below: + +````C# +var taxCalculator = ServiceProvider.GetRequiredKeyedService("taxCalculator"); +var calculator = ServiceProvider.GetRequiredKeyedService("calculator"); +```` + +Also, you can use the [`FromKeyedServicesAttribute`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.fromkeyedservicesattribute?view=dotnet-plat-ext-8.0) to resolve a certain keyed service in the constructor: + +```csharp +public class MyClass +{ + //... + + public MyClass([FromKeyedServices("taxCalculator")] ITaxCalculator taxCalculator) + { + TaxCalculator = taxCalculator; + } +} +``` + +> Notice that the `ExposeKeyedServiceAttribute` only exposes the keyed services. So, you can not inject the `ITaxCalculator` or `ICalculator` interfaces in your application without using the `FromKeyedServicesAttribute` as shown in the example above. If you want to expose both keyed and non-keyed services, you can use the `ExposeServicesAttribute` and `ExposeKeyedServiceAttribute` attributes together as shown below: + +````C# +[ExposeKeyedService("taxCalculator")] +[ExposeKeyedService("calculator")] +[ExposeServices(typeof(ITaxCalculator), typeof(ICalculator))] +public class TaxCalculator: ICalculator, ITaxCalculator, ICanCalculate, ITransientDependency +{ +} +```` + ### Manually Registering In some cases, you may need to register a service to the `IServiceCollection` manually, especially if you need to use custom factory methods or singleton instances. In that case, you can directly add services just as [Microsoft documentation](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection) describes. Example: diff --git a/docs/en/Getting-Started-Running-Solution.md b/docs/en/Getting-Started-Running-Solution.md index 921fb2b247..d71c0a64da 100644 --- a/docs/en/Getting-Started-Running-Solution.md +++ b/docs/en/Getting-Started-Running-Solution.md @@ -127,6 +127,29 @@ abp bundle > **Note**: Before starting the application, run `abp install-libs` command in your Web directory to restore the client-side libraries. This will populate the `libs` folder. +{{ if UI == "BlazorServer" }} + +> **Important:** The `.AuthServer` application serves as the **Authentication Server** for the `.Blazor` application. It is essential to have the `.AuthServer` application running in the background to ensure the proper functioning of the `.Blazor` application. + +To do this, open terminal in `.AuthServer` project folder and run the following command. + +````bash +dotnet run +```` + +Once the `.AuthServer`application has started, it is time to run `.HttpApi.Host` application. + +> **Important:** Prior to launching the `.Blazor` project, it is essential to execute the `.HttpApi.Host` application as well. + +To do this, open terminal in `.HttpApi.Host` project folder and run the following command. + +````bash +dotnet run +```` +Once the `.AuthServer` and `.HttpApi.Host` applications has started, you can proceed to run the `.Blazor` project. + +{{ end # UI }} + {{ if Tiered == "Yes" }} > Tiered solutions use **Redis** as the distributed cache. Ensure that it is installed and running in your local computer. If you are using a remote Redis Server, set the configuration in the `appsettings.json` files of the projects below. @@ -195,6 +218,16 @@ You can see the application APIs and test them here. Get [more info](https://swa ### Running the Blazor Application (Client Side) +> **Important:** The `.HttpApi.Host` application serves as the **Authentication Server** for the `.Blazor` application. It is essential to have the `.HttpApi.Host` application running in the background to ensure the proper functioning of the `.Blazor` application. + +To do this, you can open terminal in `.HttpApi.Host` project folder and run the following command. + +````bash +dotnet run +```` + +Once the `.HttpApi.Host` application has started, you can proceed to run the `.Blazor` application. + Ensure that the `.Blazor` project is the startup project and run the application. > Use Ctrl+F5 in Visual Studio (instead of F5) to run the application without debugging. If you don't have a debug purpose, this will be faster. diff --git a/docs/en/Migration-Guides/Abp-8_0.md b/docs/en/Migration-Guides/Abp-8_0.md index 6d60c80965..8214dfef1a 100644 --- a/docs/en/Migration-Guides/Abp-8_0.md +++ b/docs/en/Migration-Guides/Abp-8_0.md @@ -163,19 +163,20 @@ You can see the following list of NuGet libraries that have been upgraded with . | ------------------- | ----------- | ----------- | | aliyun-net-sdk-sts | 3.1.0 | 3.1.2 | | AsyncKeyedLock | 6.2.1 | 6.2.2 | -| Autofac | 7.0.0 | 7.1.0 | +| Autofac | 7.0.0 | 8.0.0 | +| Autofac.Extensions.DependencyInjection | 8.0.0 | 9.0.0 | | Autofac.Extras.DynamicProxy | 6.0.1 | 7.1.0 | | AutoMapper | 12.0.0 | 12.0.1 | | AWSSDK.S3 | 3.7.9.2 | 3.7.300.2 | | AWSSDK.SecurityToken | 3.7.1.151 | 3.7.300.2 | | Azure.Messaging.ServiceBus | 7.8.1 | 7.17.0 | | Azure.Storage.Blobs | 12.15.0 | 12.19.1 | -| Blazorise | 1.3.1 | 1.3.2 | -| Blazorise.Bootstrap5 | 1.3.1 | 1.3.2 | -| Blazorise.Icons.FontAwesome | 1.3.1 | 1.3.2 | -| Blazorise.Components | 1.3.1 | 1.3.2 | -| Blazorise.DataGrid | 1.3.1 | 1.3.2 | -| Blazorise.Snackbar | 1.3.1 | 1.3.2 | +| Blazorise | 1.3.1 | 1.4.1 | +| Blazorise.Bootstrap5 | 1.3.1 | 1.4.1 | +| Blazorise.Icons.FontAwesome | 1.3.1 | 1.4.1 | +| Blazorise.Components | 1.3.1 | 1.4.1 | +| Blazorise.DataGrid | 1.3.1 | 1.4.1 | +| Blazorise.Snackbar | 1.3.1 | 1.4.1 | | Confluent.Kafka | 1.8.2 | 2.3.0 | | Dapper | 2.0.123 | 2.1.21 | | Dapr.AspNetCore | 1.9.0 | 1.12.0 | diff --git a/docs/en/UI/Angular/Toaster-Service.md b/docs/en/UI/Angular/Toaster-Service.md index 3152eba1af..1dcb1b0153 100644 --- a/docs/en/UI/Angular/Toaster-Service.md +++ b/docs/en/UI/Angular/Toaster-Service.md @@ -47,7 +47,8 @@ const options: Partial = { closable: true, tapToDismiss: true, messageLocalizationParams: ['Demo', '1'], - titleLocalizationParams: [] + titleLocalizationParams: [], + iconClass: 'custom-icon-name'; }; this.toaster.error('AbpUi::EntityNotFoundErrorMessage', 'AbpUi::Error', options); @@ -60,6 +61,7 @@ const options: Partial = { - `yesText` is the text of the confirmation button. A localization key or localization object can be passed. Default value is `AbpUi::Yes`. - `messageLocalizationParams` is the interpolation parameters for the localization of the message. - `titleLocalizationParams` is the interpolation parameters for the localization of the title. +- `iconClass` is the CSS class assigned to the icon displayed on a toast overlay. With the options above, the toast overlay looks like this: diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index 8e766c7430..86b58938aa 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -1085,7 +1085,7 @@ }, { "text": "Account Module", - "path": "UI/Angular/Account-Module" + "path": "UI/Angular/Account-Module.md" } ] }, diff --git a/framework/src/Volo.Abp.AspNetCore.TestBase/Volo/Abp/AspNetCore/TestBase/AbpAspNetCoreAsyncIntegratedTestBase.cs b/framework/src/Volo.Abp.AspNetCore.TestBase/Volo/Abp/AspNetCore/TestBase/AbpAspNetCoreAsyncIntegratedTestBase.cs index e6c49d475c..d47b439728 100644 --- a/framework/src/Volo.Abp.AspNetCore.TestBase/Volo/Abp/AspNetCore/TestBase/AbpAspNetCoreAsyncIntegratedTestBase.cs +++ b/framework/src/Volo.Abp.AspNetCore.TestBase/Volo/Abp/AspNetCore/TestBase/AbpAspNetCoreAsyncIntegratedTestBase.cs @@ -35,6 +35,16 @@ public class AbpAspNetCoreAsyncIntegratedTestBase return ServiceProvider.GetRequiredService(); } + protected virtual T? GetKeyedServices(object? serviceKey) + { + return ServiceProvider.GetKeyedService(serviceKey); + } + + protected virtual T GetRequiredKeyedService(object? serviceKey) where T : notnull + { + return ServiceProvider.GetRequiredKeyedService(serviceKey); + } + public virtual async Task InitializeAsync() { var builder = WebApplication.CreateBuilder(); diff --git a/framework/src/Volo.Abp.AspNetCore.TestBase/Volo/Abp/AspNetCore/TestBase/AbpWebApplicationFactoryIntegratedTest.cs b/framework/src/Volo.Abp.AspNetCore.TestBase/Volo/Abp/AspNetCore/TestBase/AbpWebApplicationFactoryIntegratedTest.cs index 9a6422ca97..2e16c79809 100644 --- a/framework/src/Volo.Abp.AspNetCore.TestBase/Volo/Abp/AspNetCore/TestBase/AbpWebApplicationFactoryIntegratedTest.cs +++ b/framework/src/Volo.Abp.AspNetCore.TestBase/Volo/Abp/AspNetCore/TestBase/AbpWebApplicationFactoryIntegratedTest.cs @@ -43,6 +43,16 @@ public abstract class AbpWebApplicationFactoryIntegratedTest : WebAppl return Services.GetRequiredService(); } + protected virtual T? GetKeyedServices(object? serviceKey) + { + return ServiceProvider.GetKeyedService(serviceKey); + } + + protected virtual T GetRequiredKeyedService(object? serviceKey) where T : notnull + { + return ServiceProvider.GetRequiredKeyedService(serviceKey); + } + protected virtual void ConfigureServices(IServiceCollection services) { diff --git a/framework/src/Volo.Abp.Autofac/Autofac/Extensions/DependencyInjection/AutofacRegistration.cs b/framework/src/Volo.Abp.Autofac/Autofac/Extensions/DependencyInjection/AutofacRegistration.cs index 9e86a0e3ce..7804cbbd98 100644 --- a/framework/src/Volo.Abp.Autofac/Autofac/Extensions/DependencyInjection/AutofacRegistration.cs +++ b/framework/src/Volo.Abp.Autofac/Autofac/Extensions/DependencyInjection/AutofacRegistration.cs @@ -27,6 +27,7 @@ using System; using System.Diagnostics.CodeAnalysis; using System.Reflection; using Autofac.Builder; +using Autofac.Core.Resolving.Pipeline; using Microsoft.Extensions.DependencyInjection; using Volo.Abp; using Volo.Abp.Modularity; @@ -97,6 +98,8 @@ public static class AutofacRegistration builder.RegisterType() .As() .As() + .As() + .As() .ExternallyOwned(); var autofacServiceScopeFactory = typeof(AutofacServiceProvider).Assembly.GetType("Autofac.Extensions.DependencyInjection.AutofacServiceScopeFactory"); @@ -111,9 +114,43 @@ public static class AutofacRegistration .As() .SingleInstance(); + // Shims for keyed service compatibility. + builder.RegisterServiceMiddlewareSource(new KeyedServiceMiddlewareSource()); + builder.RegisterSource(); + Register(builder, services, lifetimeScopeTagForSingletons); } + /// + /// Configures the exposed service type on a service registration. + /// + /// The activator data type. + /// The object registration style. + /// The registration being built. + /// The service descriptor with service type and key information. + /// + /// The , configured with the proper service type, + /// and available for additional configuration. + /// + private static IRegistrationBuilder ConfigureServiceType( + this IRegistrationBuilder registrationBuilder, + ServiceDescriptor descriptor) + { + if (descriptor.IsKeyedService) + { + var key = descriptor.ServiceKey!; + + // If it's keyed, the service key won't be null. A null key results in it _not_ being a keyed service. + registrationBuilder.Keyed(key, descriptor.ServiceType); + } + else + { + registrationBuilder.As(descriptor.ServiceType); + } + + return registrationBuilder; + } + /// /// Configures the lifecycle on a service registration. /// @@ -187,47 +224,78 @@ public static class AutofacRegistration foreach (var descriptor in services) { - if (descriptor.ImplementationType != null) + var implementationType = descriptor.NormalizedImplementationType(); + if (implementationType != null) { // Test if the an open generic type is being registered var serviceTypeInfo = descriptor.ServiceType.GetTypeInfo(); if (serviceTypeInfo.IsGenericTypeDefinition) { builder - .RegisterGeneric(descriptor.ImplementationType) - .As(descriptor.ServiceType) + .RegisterGeneric(implementationType) + .ConfigureServiceType(descriptor) .ConfigureLifecycle(descriptor.Lifetime, lifetimeScopeTagForSingletons) .ConfigureAbpConventions(descriptor, moduleContainer, registrationActionList, activatedActionList); } else { builder - .RegisterType(descriptor.ImplementationType) - .As(descriptor.ServiceType) + .RegisterType(implementationType) + .ConfigureServiceType(descriptor) .ConfigureLifecycle(descriptor.Lifetime, lifetimeScopeTagForSingletons) .ConfigureAbpConventions(descriptor, moduleContainer, registrationActionList, activatedActionList); } + + continue; } - else if (descriptor.ImplementationFactory != null) + + if (descriptor.IsKeyedService && descriptor.KeyedImplementationFactory != null) + { + var registration = RegistrationBuilder.ForDelegate(descriptor.ServiceType, (context, parameters) => + { + // At this point the context is always a ResolveRequestContext, which will expose the actual service type. + var requestContext = (ResolveRequestContext)context; + + var serviceProvider = context.Resolve(); + + var keyedService = (Autofac.Core.KeyedService)requestContext.Service; + + var key = keyedService.ServiceKey; + + return descriptor.KeyedImplementationFactory(serviceProvider, key); + }) + .ConfigureServiceType(descriptor) + .ConfigureLifecycle(descriptor.Lifetime, lifetimeScopeTagForSingletons) + .CreateRegistration(); + //TODO: ConfigureAbpConventions ? + + builder.RegisterComponent(registration); + + continue; + } + + if (!descriptor.IsKeyedService && descriptor.ImplementationFactory != null) { var registration = RegistrationBuilder.ForDelegate(descriptor.ServiceType, (context, parameters) => { var serviceProvider = context.Resolve(); return descriptor.ImplementationFactory(serviceProvider); }) + .ConfigureServiceType(descriptor) .ConfigureLifecycle(descriptor.Lifetime, lifetimeScopeTagForSingletons) .CreateRegistration(); //TODO: ConfigureAbpConventions ? builder.RegisterComponent(registration); + + continue; } - else - { - builder - .RegisterInstance(descriptor.ImplementationInstance!) - .As(descriptor.ServiceType) - .ConfigureLifecycle(descriptor.Lifetime, null); - } + + // It's not a type or factory, so it must be an instance. + builder + .RegisterInstance(descriptor.NormalizedImplementationInstance()!) + .ConfigureServiceType(descriptor) + .ConfigureLifecycle(descriptor.Lifetime, null); } } } diff --git a/framework/src/Volo.Abp.Core/Microsoft/Extensions/DependencyInjection/ServiceCollectionCommonExtensions.cs b/framework/src/Volo.Abp.Core/Microsoft/Extensions/DependencyInjection/ServiceCollectionCommonExtensions.cs index 7301b0b7a9..16f5ba9a86 100644 --- a/framework/src/Volo.Abp.Core/Microsoft/Extensions/DependencyInjection/ServiceCollectionCommonExtensions.cs +++ b/framework/src/Volo.Abp.Core/Microsoft/Extensions/DependencyInjection/ServiceCollectionCommonExtensions.cs @@ -28,7 +28,7 @@ public static class ServiceCollectionCommonExtensions { return (T?)services .FirstOrDefault(d => d.ServiceType == typeof(T)) - ?.ImplementationInstance; + ?.NormalizedImplementationInstance(); } public static T GetSingletonInstance(this IServiceCollection services) @@ -48,7 +48,7 @@ public static class ServiceCollectionCommonExtensions foreach (var service in services) { - var factoryInterface = service.ImplementationInstance?.GetType() + var factoryInterface = service.NormalizedImplementationInstance()?.GetType() .GetTypeInfo() .GetInterfaces() .FirstOrDefault(i => i.GetTypeInfo().IsGenericType && diff --git a/framework/src/Volo.Abp.Core/Microsoft/Extensions/DependencyInjection/ServiceDescriptorExtensions.cs b/framework/src/Volo.Abp.Core/Microsoft/Extensions/DependencyInjection/ServiceDescriptorExtensions.cs new file mode 100644 index 0000000000..1a160edb54 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Microsoft/Extensions/DependencyInjection/ServiceDescriptorExtensions.cs @@ -0,0 +1,31 @@ +using System; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Extensions for working with . +/// +public static class ServiceDescriptorExtensions +{ + /// + /// Normalizes the implementation instance data between keyed and not keyed services. + /// + /// + /// The to normalize. + /// + /// + /// The appropriate implementation instance from the service descriptor. + /// + public static object? NormalizedImplementationInstance(this ServiceDescriptor descriptor) => descriptor.IsKeyedService ? descriptor.KeyedImplementationInstance : descriptor.ImplementationInstance; + + /// + /// Normalizes the implementation type data between keyed and not keyed services. + /// + /// + /// The to normalize. + /// + /// + /// The appropriate implementation type from the service descriptor. + /// + public static Type? NormalizedImplementationType(this ServiceDescriptor descriptor) => descriptor.IsKeyedService ? descriptor.KeyedImplementationType : descriptor.ImplementationType; +} diff --git a/framework/src/Volo.Abp.Core/Microsoft/Extensions/DependencyInjection/ServiceProviderKeyedServiceExtensions.cs b/framework/src/Volo.Abp.Core/Microsoft/Extensions/DependencyInjection/ServiceProviderKeyedServiceExtensions.cs new file mode 100644 index 0000000000..f31923f68c --- /dev/null +++ b/framework/src/Volo.Abp.Core/Microsoft/Extensions/DependencyInjection/ServiceProviderKeyedServiceExtensions.cs @@ -0,0 +1,19 @@ +using System; +using Volo.Abp; + +namespace Microsoft.Extensions.DependencyInjection; + +public static class ServiceProviderKeyedServiceExtensions +{ + public static object? GetKeyedService(this IServiceProvider provider, Type serviceType, object? serviceKey) + { + Check.NotNull(provider, nameof(provider)); + + if (provider is IKeyedServiceProvider keyedServiceProvider) + { + return keyedServiceProvider.GetKeyedService(serviceType, serviceKey); + } + + throw new InvalidOperationException("This service provider doesn't support keyed services."); + } +} diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/CachedServiceProviderBase.cs b/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/CachedServiceProviderBase.cs index 5e8d9d251a..770355428e 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/CachedServiceProviderBase.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/CachedServiceProviderBase.cs @@ -1,28 +1,29 @@ using System; using System.Collections.Concurrent; +using Microsoft.Extensions.DependencyInjection; namespace Volo.Abp.DependencyInjection; public abstract class CachedServiceProviderBase : ICachedServiceProviderBase { protected IServiceProvider ServiceProvider { get; } - protected ConcurrentDictionary> CachedServices { get; } + protected ConcurrentDictionary> CachedServices { get; } protected CachedServiceProviderBase(IServiceProvider serviceProvider) { ServiceProvider = serviceProvider; - CachedServices = new ConcurrentDictionary>(); - CachedServices.TryAdd(typeof(IServiceProvider), new Lazy(() => ServiceProvider)); + CachedServices = new ConcurrentDictionary>(); + CachedServices.TryAdd(new ServiceIdentifier(typeof(IServiceProvider)), new Lazy(() => ServiceProvider)); } public virtual object? GetService(Type serviceType) { return CachedServices.GetOrAdd( - serviceType, + new ServiceIdentifier(serviceType), _ => new Lazy(() => ServiceProvider.GetService(serviceType)) ).Value; } - + public T GetService(T defaultValue) { return (T)GetService(typeof(T), defaultValue!); @@ -37,12 +38,28 @@ public abstract class CachedServiceProviderBase : ICachedServiceProviderBase { return (T)GetService(typeof(T), factory); } - + public object GetService(Type serviceType, Func factory) { return CachedServices.GetOrAdd( - serviceType, + new ServiceIdentifier(serviceType), _ => new Lazy(() => factory(ServiceProvider)) ).Value!; } + + public object? GetKeyedService(Type serviceType, object? serviceKey) + { + return CachedServices.GetOrAdd( + new ServiceIdentifier(serviceKey, serviceType), + _ => new Lazy(() => ServiceProvider.GetKeyedService(serviceType, serviceKey)) + ).Value; + } + + public object GetRequiredKeyedService(Type serviceType, object? serviceKey) + { + return CachedServices.GetOrAdd( + new ServiceIdentifier(serviceKey, serviceType), + _ => new Lazy(() => ServiceProvider.GetRequiredKeyedService(serviceType, serviceKey)) + ).Value!; + } } diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ConventionalRegistrarBase.cs b/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ConventionalRegistrarBase.cs index c7190e6c4d..6d1c345840 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ConventionalRegistrarBase.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ConventionalRegistrarBase.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; -using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection; using Volo.Abp.Reflection; @@ -40,6 +39,11 @@ public abstract class ConventionalRegistrarBase : IConventionalRegistrar } protected virtual void TriggerServiceExposing(IServiceCollection services, Type implementationType, List serviceTypes) + { + TriggerServiceExposing(services, implementationType, serviceTypes.ConvertAll(t => new ServiceIdentifier(t))); + } + + protected virtual void TriggerServiceExposing(IServiceCollection services, Type implementationType, List serviceTypes) { var exposeActions = services.GetExposingActionList(); if (exposeActions.Any()) @@ -92,10 +96,16 @@ public abstract class ConventionalRegistrarBase : IConventionalRegistrar return ExposedServiceExplorer.GetExposedServices(type); } + protected virtual List GetExposedKeyedServiceTypes(Type type) + { + return ExposedServiceExplorer.GetExposedKeyedServices(type); + } + protected virtual ServiceDescriptor CreateServiceDescriptor( Type implementationType, + object? serviceKey, Type exposingServiceType, - List allExposingServiceTypes, + List allExposingServiceTypes, ServiceLifetime lifeTime) { if (lifeTime.IsIn(ServiceLifetime.Singleton, ServiceLifetime.Scoped)) @@ -108,27 +118,41 @@ public abstract class ConventionalRegistrarBase : IConventionalRegistrar if (redirectedType != null) { - return ServiceDescriptor.Describe( - exposingServiceType, - provider => provider.GetService(redirectedType)!, - lifeTime - ); + return serviceKey == null + ? ServiceDescriptor.Describe( + exposingServiceType, + provider => provider.GetService(redirectedType)!, + lifeTime + ) + : ServiceDescriptor.DescribeKeyed( + exposingServiceType, + serviceKey, + (provider, key) => provider.GetKeyedService(redirectedType, key)!, + lifeTime + ); } } - return ServiceDescriptor.Describe( - exposingServiceType, - implementationType, - lifeTime - ); + return serviceKey == null + ? ServiceDescriptor.Describe( + exposingServiceType, + implementationType, + lifeTime + ) + : ServiceDescriptor.DescribeKeyed( + exposingServiceType, + serviceKey, + implementationType, + lifeTime + ); } protected virtual Type? GetRedirectedTypeOrNull( Type implementationType, Type exposingServiceType, - List allExposingServiceTypes) + List allExposingKeyedServiceTypes) { - if (allExposingServiceTypes.Count < 2) + if (allExposingKeyedServiceTypes.Count < 2) { return null; } @@ -138,14 +162,13 @@ public abstract class ConventionalRegistrarBase : IConventionalRegistrar return null; } - if (allExposingServiceTypes.Contains(implementationType)) + if (allExposingKeyedServiceTypes.Any(t => t.ServiceType == implementationType)) { return implementationType; } - return allExposingServiceTypes.FirstOrDefault( - t => t != exposingServiceType && exposingServiceType.IsAssignableFrom(t) - ); + return allExposingKeyedServiceTypes.FirstOrDefault( + t => t.ServiceType != exposingServiceType && exposingServiceType.IsAssignableFrom(t.ServiceType) + ).ServiceType; } - } diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/DefaultConventionalRegistrar.cs b/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/DefaultConventionalRegistrar.cs index 18f36bfb48..0356ab450b 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/DefaultConventionalRegistrar.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/DefaultConventionalRegistrar.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -22,16 +24,21 @@ public class DefaultConventionalRegistrar : ConventionalRegistrarBase return; } - var exposedServiceTypes = GetExposedServiceTypes(type); + var exposedServiceAndKeyedServiceTypes = GetExposedKeyedServiceTypes(type).Concat(GetExposedServiceTypes(type).Select(t => new ServiceIdentifier(t))).ToList(); - TriggerServiceExposing(services, type, exposedServiceTypes); + TriggerServiceExposing(services, type, exposedServiceAndKeyedServiceTypes); - foreach (var exposedServiceType in exposedServiceTypes) + foreach (var exposedServiceType in exposedServiceAndKeyedServiceTypes) { + var allExposingServiceTypes = exposedServiceType.ServiceKey == null + ? exposedServiceAndKeyedServiceTypes.Where(x => x.ServiceKey == null).ToList() + : exposedServiceAndKeyedServiceTypes.Where(x => x.ServiceKey?.ToString() == exposedServiceType.ServiceKey?.ToString()).ToList(); + var serviceDescriptor = CreateServiceDescriptor( type, - exposedServiceType, - exposedServiceTypes, + exposedServiceType.ServiceKey, + exposedServiceType.ServiceType, + allExposingServiceTypes, lifeTime.Value ); diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ExposeKeyedServiceAttribute.cs b/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ExposeKeyedServiceAttribute.cs new file mode 100644 index 0000000000..a7af4d0a79 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ExposeKeyedServiceAttribute.cs @@ -0,0 +1,25 @@ +using System; + +namespace Volo.Abp.DependencyInjection; + +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] +public class ExposeKeyedServiceAttribute : Attribute, IExposedKeyedServiceTypesProvider + where TServiceType : class +{ + public ServiceIdentifier ServiceIdentifier { get; } + + public ExposeKeyedServiceAttribute(object serviceKey) + { + if (serviceKey == null) + { + throw new AbpException($"{nameof(serviceKey)} can not be null! Use {nameof(ExposeServicesAttribute)} instead."); + } + + ServiceIdentifier = new ServiceIdentifier(serviceKey, typeof(TServiceType)); + } + + public ServiceIdentifier[] GetExposedServiceTypes(Type targetType) + { + return new[] { ServiceIdentifier }; + } +} diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ExposeServicesAttribute.cs b/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ExposeServicesAttribute.cs index 286b9a8c76..f05ce07239 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ExposeServicesAttribute.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ExposeServicesAttribute.cs @@ -5,6 +5,7 @@ using System.Reflection; namespace Volo.Abp.DependencyInjection; +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)] public class ExposeServicesAttribute : Attribute, IExposedServiceTypesProvider { public Type[] ServiceTypes { get; } diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ExposedServiceExplorer.cs b/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ExposedServiceExplorer.cs index 126c56fca2..318af5f96e 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ExposedServiceExplorer.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ExposedServiceExplorer.cs @@ -15,12 +15,31 @@ public static class ExposedServiceExplorer public static List GetExposedServices(Type type) { - return type + var exposedServiceTypesProviders = type .GetCustomAttributes(true) .OfType() + .ToList(); + + if (exposedServiceTypesProviders.IsNullOrEmpty() && type.GetCustomAttributes(true).OfType().Any()) + { + // If there is any IExposedKeyedServiceTypesProvider but no IExposedServiceTypesProvider, we will not expose the default services. + return Array.Empty().ToList(); + } + + return exposedServiceTypesProviders .DefaultIfEmpty(DefaultExposeServicesAttribute) .SelectMany(p => p.GetExposedServiceTypes(type)) .Distinct() .ToList(); } + + public static List GetExposedKeyedServices(Type type) + { + return type + .GetCustomAttributes(true) + .OfType() + .SelectMany(p => p.GetExposedServiceTypes(type)) + .Distinct() + .ToList(); + } } diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ICachedServiceProviderBase.cs b/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ICachedServiceProviderBase.cs index 2e15a45a46..ceec3326cc 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ICachedServiceProviderBase.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ICachedServiceProviderBase.cs @@ -1,14 +1,15 @@ using System; +using Microsoft.Extensions.DependencyInjection; namespace Volo.Abp.DependencyInjection; -public interface ICachedServiceProviderBase : IServiceProvider +public interface ICachedServiceProviderBase : IKeyedServiceProvider { T GetService(T defaultValue); - + object GetService(Type serviceType, object defaultValue); T GetService(Func factory); object GetService(Type serviceType, Func factory); -} \ No newline at end of file +} diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/IExposedKeyedServiceTypesProvider.cs b/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/IExposedKeyedServiceTypesProvider.cs new file mode 100644 index 0000000000..84879904b5 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/IExposedKeyedServiceTypesProvider.cs @@ -0,0 +1,8 @@ +using System; + +namespace Volo.Abp.DependencyInjection; + +public interface IExposedKeyedServiceTypesProvider +{ + ServiceIdentifier[] GetExposedServiceTypes(Type targetType); +} diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/IOnServiceExposingContext.cs b/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/IOnServiceExposingContext.cs index 5a96160be2..0dc1c42fe2 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/IOnServiceExposingContext.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/IOnServiceExposingContext.cs @@ -7,5 +7,5 @@ public interface IOnServiceExposingContext { Type ImplementationType { get; } - List ExposedTypes { get; } + List ExposedTypes { get; } } diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/IRootServiceProviderAccessor.cs b/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/IRootServiceProviderAccessor.cs index 8a3c1d09a5..60f6b6461f 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/IRootServiceProviderAccessor.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/IRootServiceProviderAccessor.cs @@ -1,4 +1,4 @@ -using System; +using Microsoft.Extensions.DependencyInjection; namespace Volo.Abp.DependencyInjection; @@ -8,7 +8,7 @@ namespace Volo.Abp.DependencyInjection; /// to release/dispose objects resolved from the root service provider. /// So, always create a new scope if you need to resolve any service. /// -public interface IRootServiceProvider : IServiceProvider +public interface IRootServiceProvider : IKeyedServiceProvider { - -} \ No newline at end of file + +} diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/OnServiceExposingContext.cs b/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/OnServiceExposingContext.cs index 7ce197aea8..2b09430222 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/OnServiceExposingContext.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/OnServiceExposingContext.cs @@ -8,9 +8,15 @@ public class OnServiceExposingContext : IOnServiceExposingContext { public Type ImplementationType { get; } - public List ExposedTypes { get; } + public List ExposedTypes { get; } public OnServiceExposingContext([NotNull] Type implementationType, List exposedTypes) + { + ImplementationType = Check.NotNull(implementationType, nameof(implementationType)); + ExposedTypes = Check.NotNull(exposedTypes, nameof(exposedTypes)).ConvertAll(t => new ServiceIdentifier(t)); + } + + public OnServiceExposingContext([NotNull] Type implementationType, List exposedTypes) { ImplementationType = Check.NotNull(implementationType, nameof(implementationType)); ExposedTypes = Check.NotNull(exposedTypes, nameof(exposedTypes)); diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/RootServiceProvider.cs b/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/RootServiceProvider.cs index a406bc5597..7ba378d76a 100644 --- a/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/RootServiceProvider.cs +++ b/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/RootServiceProvider.cs @@ -1,4 +1,5 @@ using System; +using Microsoft.Extensions.DependencyInjection; namespace Volo.Abp.DependencyInjection; @@ -6,7 +7,7 @@ namespace Volo.Abp.DependencyInjection; public class RootServiceProvider : IRootServiceProvider, ISingletonDependency { protected IServiceProvider ServiceProvider { get; } - + public RootServiceProvider(IObjectAccessor objectAccessor) { ServiceProvider = objectAccessor.Value!; @@ -16,4 +17,14 @@ public class RootServiceProvider : IRootServiceProvider, ISingletonDependency { return ServiceProvider.GetService(serviceType); } -} \ No newline at end of file + + public object? GetKeyedService(Type serviceType, object? serviceKey) + { + return ServiceProvider.GetKeyedService(serviceType, serviceKey); + } + + public virtual object GetRequiredKeyedService(Type serviceType, object? serviceKey) + { + return ServiceProvider.GetRequiredKeyedService(serviceType, serviceKey); + } +} diff --git a/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ServiceIdentifier.cs b/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ServiceIdentifier.cs new file mode 100644 index 0000000000..b6dbcd8a02 --- /dev/null +++ b/framework/src/Volo.Abp.Core/Volo/Abp/DependencyInjection/ServiceIdentifier.cs @@ -0,0 +1,54 @@ +using System; + +namespace Volo.Abp.DependencyInjection; + +/// +/// https://github.com/dotnet/runtime/blob/release/8.0/src/libraries/Microsoft.Extensions.DependencyInjection/src/ServiceLookup/ServiceIdentifier.cs#L9 +/// +public readonly struct ServiceIdentifier : IEquatable +{ + public object? ServiceKey { get; } + + public Type ServiceType { get; } + + public ServiceIdentifier(Type serviceType) + { + ServiceType = serviceType; + } + + public ServiceIdentifier(object? serviceKey, Type serviceType) + { + ServiceKey = serviceKey; + ServiceType = serviceType; + } + + public bool Equals(ServiceIdentifier other) + { + if (ServiceKey == null && other.ServiceKey == null) + { + return ServiceType == other.ServiceType; + } + else if (ServiceKey != null && other.ServiceKey != null) + { + return ServiceType == other.ServiceType && ServiceKey.Equals(other.ServiceKey); + } + return false; + } + + public override bool Equals(object? obj) + { + return obj is ServiceIdentifier && Equals((ServiceIdentifier)obj); + } + + public override int GetHashCode() + { + if (ServiceKey == null) + { + return ServiceType.GetHashCode(); + } + unchecked + { + return (ServiceType.GetHashCode() * 397) ^ ServiceKey.GetHashCode(); + } + } +} diff --git a/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/AbpObjectMappingModule.cs b/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/AbpObjectMappingModule.cs index cfd82a5547..e572d6e155 100644 --- a/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/AbpObjectMappingModule.cs +++ b/framework/src/Volo.Abp.ObjectMapping/Volo/Abp/ObjectMapping/AbpObjectMappingModule.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.DependencyInjection; using Volo.Abp.Modularity; using Volo.Abp.Reflection; @@ -10,12 +11,12 @@ public class AbpObjectMappingModule : AbpModule { context.Services.OnExposing(onServiceExposingContext => { - //Register types for IObjectMapper if implements - onServiceExposingContext.ExposedTypes.AddRange( + //Register types for IObjectMapper if implements + onServiceExposingContext.ExposedTypes.AddRange( ReflectionHelper.GetImplementedGenericTypes( onServiceExposingContext.ImplementationType, typeof(IObjectMapper<,>) - ) + ).ConvertAll(t => new ServiceIdentifier(t)) ); }); } diff --git a/framework/src/Volo.Abp.Serialization/Volo/Abp/Serialization/AbpSerializationModule.cs b/framework/src/Volo.Abp.Serialization/Volo/Abp/Serialization/AbpSerializationModule.cs index 428f0415c8..5a178ba01b 100644 --- a/framework/src/Volo.Abp.Serialization/Volo/Abp/Serialization/AbpSerializationModule.cs +++ b/framework/src/Volo.Abp.Serialization/Volo/Abp/Serialization/AbpSerializationModule.cs @@ -1,4 +1,5 @@ using Microsoft.Extensions.DependencyInjection; +using Volo.Abp.DependencyInjection; using Volo.Abp.Modularity; using Volo.Abp.Reflection; @@ -10,12 +11,12 @@ public class AbpSerializationModule : AbpModule { context.Services.OnExposing(onServiceExposingContext => { - //Register types for IObjectSerializer if implements - onServiceExposingContext.ExposedTypes.AddRange( + //Register types for IObjectSerializer if implements + onServiceExposingContext.ExposedTypes.AddRange( ReflectionHelper.GetImplementedGenericTypes( onServiceExposingContext.ImplementationType, typeof(IObjectSerializer<>) - ) + ).ConvertAll(t => new ServiceIdentifier(t)) ); }); } diff --git a/framework/src/Volo.Abp.TestBase/Volo/Abp/AbpTestBaseWithServiceProvider.cs b/framework/src/Volo.Abp.TestBase/Volo/Abp/AbpTestBaseWithServiceProvider.cs index 04879b4fce..470abcb9d5 100644 --- a/framework/src/Volo.Abp.TestBase/Volo/Abp/AbpTestBaseWithServiceProvider.cs +++ b/framework/src/Volo.Abp.TestBase/Volo/Abp/AbpTestBaseWithServiceProvider.cs @@ -11,9 +11,19 @@ public abstract class AbpTestBaseWithServiceProvider { return ServiceProvider.GetService(); } - + protected virtual T GetRequiredService() where T : notnull { return ServiceProvider.GetRequiredService(); } + + protected virtual T? GetKeyedServices(object? serviceKey) + { + return ServiceProvider.GetKeyedService(serviceKey); + } + + protected virtual T GetRequiredKeyedService(object? serviceKey) where T : notnull + { + return ServiceProvider.GetRequiredKeyedService(serviceKey); + } } diff --git a/framework/test/Volo.Abp.Core.Tests/Microsoft/Extensions/DependencyInjection/DependencyInjection_Tests.cs b/framework/test/Volo.Abp.Core.Tests/Microsoft/Extensions/DependencyInjection/DependencyInjection_Tests.cs index 52f07620fb..0dfe473ee5 100644 --- a/framework/test/Volo.Abp.Core.Tests/Microsoft/Extensions/DependencyInjection/DependencyInjection_Tests.cs +++ b/framework/test/Volo.Abp.Core.Tests/Microsoft/Extensions/DependencyInjection/DependencyInjection_Tests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using Shouldly; using Volo.Abp.DependencyInjection; using Volo.Abp.Modularity; @@ -79,6 +80,29 @@ public abstract class DependencyInjection_Standard_Tests : AbpIntegratedTest>().DisablePropertyInjectionService.ShouldBeNull(); } + + [Fact] + public void ExposeKeyedServices_Should_Expose_Correct_Services() + { + GetService().ShouldBeNull(); + GetService().ShouldBeNull(); + GetService().ShouldBeNull(); + + GetRequiredKeyedService("k1").ShouldNotBeNull(); + GetRequiredKeyedService("k1").ShouldNotBeNull(); + + GetRequiredKeyedService("k2").ShouldNotBeNull(); + GetRequiredKeyedService("k2").ShouldNotBeNull(); + + GetService().ShouldNotBeNull(); + GetRequiredKeyedService("k3").ShouldNotBeNull(); + GetRequiredKeyedService("k3").ShouldNotBeNull(); + + //resolving multiple keyed services + GetKeyedServices>("k1").ShouldNotBeEmpty(); + GetKeyedServices>("k1").Count().ShouldBeGreaterThanOrEqualTo(2); + } + [Fact] public void Singletons_Exposing_Multiple_Services_Should_Returns_The_Same_Instance() { @@ -86,6 +110,30 @@ public abstract class DependencyInjection_Standard_Tests : AbpIntegratedTest(); ReferenceEquals(objectByInterfaceRef, objectByClassRef).ShouldBeTrue(); + + objectByInterfaceRef = GetRequiredKeyedService("k1"); + objectByClassRef = GetRequiredKeyedService("k1"); + + ReferenceEquals(objectByInterfaceRef, objectByClassRef).ShouldBeTrue(); + } + + [Fact] + public void Should_Get_Keyed_Services() + { + var bigCache = GetRequiredKeyedService("big"); + var bigInstanceCache = GetRequiredKeyedService("bigInstance"); + var smallCache = GetRequiredKeyedService("small"); + var smallFactoryCache = GetRequiredKeyedService("smallFactory"); + + bigCache.GetType().ShouldBe(typeof(BigCache)); + bigInstanceCache.GetType().ShouldBe(typeof(BigCache)); + smallCache.GetType().ShouldBe(typeof(SmallCache)); + smallFactoryCache.GetType().ShouldBe(typeof(SmallCache)); + + bigCache.Get("key").ShouldBe("Resolving key from big cache."); + bigInstanceCache.Get("key").ShouldBe("Resolving key from big cache."); + smallCache.Get("key").ShouldBe("Resolving key from small cache."); + smallFactoryCache.Get("key").ShouldBe("Resolving key from small cache."); } public class MySingletonService : ISingletonDependency @@ -147,11 +195,46 @@ public abstract class DependencyInjection_Standard_Tests : AbpIntegratedTest("k1")] + [ExposeKeyedService("k1")] public class MySingletonExposingMultipleServices : IMySingletonExposingMultipleServices, ISingletonDependency { } + public interface IMyExposingKeyedServices + { + + } + + [ExposeKeyedService("k1")] + [ExposeKeyedService("k1")] + public class MyExposingKeyedService1 : IMyExposingKeyedServices, ITransientDependency + { + + } + + [ExposeKeyedService("k2")] + [ExposeKeyedService("k2")] + public class MyExposingKeyedService2 : IMyExposingKeyedServices, ITransientDependency + { + + } + + [ExposeServices(typeof(MyExposingKeyedService3))] + [ExposeKeyedService("k3")] + [ExposeKeyedService("k3")] + public class MyExposingKeyedService3 : IMyExposingKeyedServices, ITransientDependency + { + + } + + [ExposeKeyedService("k1")] + public class MyExposingKeyedService4 : IMyExposingKeyedServices, ITransientDependency + { + + } + public class TestModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) @@ -164,6 +247,10 @@ public abstract class DependencyInjection_Standard_Tests : AbpIntegratedTest)); context.Services.AddTransient(typeof(GenericServiceWithDisablePropertyInjectionOnClass<>)); context.Services.AddTransient(typeof(GenericServiceWithDisablePropertyInjectionOnProperty<>)); + context.Services.AddKeyedSingleton("big"); + context.Services.AddKeyedSingleton("small"); + context.Services.AddKeyedSingleton("bigInstance", new BigCache()); + context.Services.AddKeyedSingleton("smallFactory", (sp, key) => new SmallCache()); } } @@ -215,4 +302,18 @@ public abstract class DependencyInjection_Standard_Tests : AbpIntegratedTest $"Resolving {key} from big cache."; + } + + public class SmallCache : ICache + { + public object Get(string key) => $"Resolving {key} from small cache."; + } } diff --git a/framework/test/Volo.Abp.Core.Tests/Volo/Abp/DependencyInjection/CachedServiceProvider_Tests.cs b/framework/test/Volo.Abp.Core.Tests/Volo/Abp/DependencyInjection/CachedServiceProvider_Tests.cs index b040bbc71f..bc67386c20 100644 --- a/framework/test/Volo.Abp.Core.Tests/Volo/Abp/DependencyInjection/CachedServiceProvider_Tests.cs +++ b/framework/test/Volo.Abp.Core.Tests/Volo/Abp/DependencyInjection/CachedServiceProvider_Tests.cs @@ -21,7 +21,11 @@ public class CachedServiceProvider_Tests var transientTestService1 = cachedServiceProvider1.GetRequiredService(); var transientTestService2 = cachedServiceProvider2.GetRequiredService(); transientTestService1.ShouldBeSameAs(transientTestService2); - + + var transientKeyedTestService1 = cachedServiceProvider1.GetRequiredKeyedService("key1"); + var transientKeyedTestService2 = cachedServiceProvider2.GetRequiredKeyedService("key1"); + transientKeyedTestService1.ShouldBeSameAs(transientKeyedTestService2); + var cachedServiceProvider3 = cachedServiceProvider1.GetRequiredService().GetRequiredService(); cachedServiceProvider3.ShouldBeSameAs(cachedServiceProvider1); } @@ -36,18 +40,18 @@ public class CachedServiceProvider_Tests var testCounter = scope1.ServiceProvider.GetRequiredService(); testCounter.GetValue(nameof(TransientTestService)).ShouldBe(1); } - + using (var scope2 = application.ServiceProvider.CreateScope()) { TestResolvingServices(scope2); var testCounter = scope2.ServiceProvider.GetRequiredService(); - + //Resolved in a different scope, so should not cache the service! testCounter.GetValue(nameof(TransientTestService)).ShouldBe(2); } } } - + [Fact] public void TransientCachedServiceProvider_Should_Cache_Services() { @@ -58,15 +62,26 @@ public class CachedServiceProvider_Tests var transientTestService1_1 = transientCachedServiceProvider1.GetRequiredService(); var transientTestService1_2 = transientCachedServiceProvider1.GetRequiredService(); transientTestService1_1.ShouldBeSameAs(transientTestService1_2); - + + var transientKeyedTestService1 = transientCachedServiceProvider1.GetRequiredKeyedService("key1"); + var transientKeyedTestService2 = transientCachedServiceProvider1.GetRequiredKeyedService("key1"); + transientKeyedTestService1.ShouldBeSameAs(transientKeyedTestService2); + var transientKeyedTestService3 = transientCachedServiceProvider1.GetRequiredKeyedService("key2"); + transientKeyedTestService2.ShouldNotBeSameAs(transientKeyedTestService3); + var transientCachedServiceProvider2 = scope.ServiceProvider.GetRequiredService(); transientCachedServiceProvider1.ShouldNotBeSameAs(transientCachedServiceProvider2); var transientTestService2_1 = transientCachedServiceProvider2.GetRequiredService(); var transientTestService2_2 = transientCachedServiceProvider2.GetRequiredService(); transientTestService2_1.ShouldBeSameAs(transientTestService2_2); - + + var transientKeyedTestService1_1 = transientCachedServiceProvider2.GetRequiredKeyedService("key1"); + var transientKeyedTestService2_1 = transientCachedServiceProvider2.GetRequiredKeyedService("key1"); + transientKeyedTestService1_1.ShouldBeSameAs(transientKeyedTestService2_1); + transientTestService1_1.ShouldNotBeSameAs(transientTestService2_1); + transientKeyedTestService1.ShouldNotBeSameAs(transientKeyedTestService1_1); var transientCachedServiceProvider1_1 = transientCachedServiceProvider1.GetRequiredService().GetRequiredService(); var transientCachedServiceProvider1_2 = transientCachedServiceProvider1.GetRequiredService().GetRequiredService(); @@ -87,7 +102,7 @@ public class CachedServiceProvider_Tests } } } - + [DependsOn(typeof(AbpTestBaseModule))] private class TestModule : AbpModule { @@ -99,6 +114,8 @@ public class CachedServiceProvider_Tests public override void ConfigureServices(ServiceConfigurationContext context) { context.Services.AddType(); + context.Services.AddKeyedTransient("key1"); + context.Services.AddKeyedTransient("key2"); } } @@ -109,4 +126,9 @@ public class CachedServiceProvider_Tests counter.Increment(nameof(TransientTestService)); } } -} \ No newline at end of file + + private class TransientKeyedTestService + { + + } +} diff --git a/framework/test/Volo.Abp.Security.Tests/Volo/Abp/Security/Claims/AbpDynamicClaimsPrincipalContributorBase_Tests.cs b/framework/test/Volo.Abp.Security.Tests/Volo/Abp/Security/Claims/AbpDynamicClaimsPrincipalContributorBase_Tests.cs index 7054319dfa..4d0fc8ff6d 100644 --- a/framework/test/Volo.Abp.Security.Tests/Volo/Abp/Security/Claims/AbpDynamicClaimsPrincipalContributorBase_Tests.cs +++ b/framework/test/Volo.Abp.Security.Tests/Volo/Abp/Security/Claims/AbpDynamicClaimsPrincipalContributorBase_Tests.cs @@ -3,28 +3,55 @@ using System.Collections.Generic; using System.Linq; using System.Security.Claims; using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; using Shouldly; +using Volo.Abp.DependencyInjection; using Volo.Abp.Testing; using Xunit; namespace Volo.Abp.Security.Claims; +[DisableConventionalRegistration] class TestAbpDynamicClaimsPrincipalContributor : AbpDynamicClaimsPrincipalContributorBase { + private readonly List _claims; + + public TestAbpDynamicClaimsPrincipalContributor(List claims) + { + _claims = claims; + } + public async override Task ContributeAsync(AbpClaimsPrincipalContributorContext context) { var identity = context.ClaimsPrincipal.Identities.FirstOrDefault(); Check.NotNull(identity, nameof(identity)); - await AddDynamicClaimsAsync(context, identity, AbpDynamicClaimsPrincipalContributorBase_Tests.DynamicClaims.Claims); + await AddDynamicClaimsAsync(context, identity, _claims); } } public class AbpDynamicClaimsPrincipalContributorBase_Tests : AbpIntegratedTest { - private readonly TestAbpDynamicClaimsPrincipalContributor _dynamicClaimsPrincipalContributorBase = new TestAbpDynamicClaimsPrincipalContributor(); + private readonly TestAbpDynamicClaimsPrincipalContributor _dynamicClaimsPrincipalContributorBase; - public readonly static AbpDynamicClaimCacheItem DynamicClaims = new AbpDynamicClaimCacheItem(); + private readonly AbpDynamicClaimCacheItem _dynamicClaims; + + public AbpDynamicClaimsPrincipalContributorBase_Tests() + { + _dynamicClaims = new AbpDynamicClaimCacheItem(new List() + { + new AbpDynamicClaim("preferred_username", "test-preferred_username"), + new AbpDynamicClaim(ClaimTypes.GivenName, "test-given_name"), + new AbpDynamicClaim("family_name", "test-family_name"), + new AbpDynamicClaim("role", "test-role1"), + new AbpDynamicClaim("roles", "test-role2"), + new AbpDynamicClaim(ClaimTypes.Role, "test-role3"), + new AbpDynamicClaim("email", "test-email"), + new AbpDynamicClaim(AbpClaimTypes.EmailVerified, "test-email-verified"), + new AbpDynamicClaim(AbpClaimTypes.PhoneNumberVerified, null), + }); + _dynamicClaimsPrincipalContributorBase = new TestAbpDynamicClaimsPrincipalContributor(_dynamicClaims.Claims); + } protected override void SetAbpApplicationCreationOptions(AbpApplicationCreationOptions options) { @@ -46,19 +73,6 @@ public class AbpDynamicClaimsPrincipalContributorBase_Tests : AbpIntegratedTest< claimsPrincipal.Identities.First().AddClaim(new Claim(AbpClaimTypes.PhoneNumberVerified, "test-source-phoneNumberVerified")); claimsPrincipal.Identities.First().AddClaim(new Claim("my-claim", "test-source-my-claim")); - DynamicClaims.Claims.AddRange(new [] - { - new AbpDynamicClaim("preferred_username", "test-preferred_username"), - new AbpDynamicClaim(ClaimTypes.GivenName, "test-given_name"), - new AbpDynamicClaim("family_name", "test-family_name"), - new AbpDynamicClaim("role", "test-role1"), - new AbpDynamicClaim("roles", "test-role2"), - new AbpDynamicClaim(ClaimTypes.Role, "test-role3"), - new AbpDynamicClaim("email", "test-email"), - new AbpDynamicClaim(AbpClaimTypes.EmailVerified, "test-email-verified"), - new AbpDynamicClaim(AbpClaimTypes.PhoneNumberVerified, null), - }); - await _dynamicClaimsPrincipalContributorBase.ContributeAsync(new AbpClaimsPrincipalContributorContext(claimsPrincipal, GetRequiredService())); claimsPrincipal.Identities.First().Claims.ShouldContain(c => c.Type == AbpClaimTypes.UserName && c.Value == "test-preferred_username"); diff --git a/latest-versions.json b/latest-versions.json index 90644f1ac5..e216ddc962 100644 --- a/latest-versions.json +++ b/latest-versions.json @@ -1,6 +1,6 @@ [ { - "version": "8.0.1", + "version": "8.0.2", "releaseDate": "", "type": "stable", "message": "" diff --git a/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/ProfileAppService.cs b/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/ProfileAppService.cs index 88d2ccbf5a..9749a607d5 100644 --- a/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/ProfileAppService.cs +++ b/modules/account/src/Volo.Abp.Account.Application/Volo/Abp/Account/ProfileAppService.cs @@ -58,6 +58,11 @@ public class ProfileAppService : IdentityAppServiceBase, IProfileAppService } } + if (user.PhoneNumber.IsNullOrWhiteSpace() && input.PhoneNumber.IsNullOrWhiteSpace()) + { + input.PhoneNumber = user.PhoneNumber; + } + if (!string.Equals(user.PhoneNumber, input.PhoneNumber, StringComparison.InvariantCultureIgnoreCase)) { (await UserManager.SetPhoneNumberAsync(user, input.PhoneNumber)).CheckErrors(); diff --git a/modules/account/test/Volo.Abp.Account.Application.Tests/Volo/Abp/Account/ProfileAppService_Tests.cs b/modules/account/test/Volo.Abp.Account.Application.Tests/Volo/Abp/Account/ProfileAppService_Tests.cs index d5536c9ad9..62ad4e4bc8 100644 --- a/modules/account/test/Volo.Abp.Account.Application.Tests/Volo/Abp/Account/ProfileAppService_Tests.cs +++ b/modules/account/test/Volo.Abp.Account.Application.Tests/Volo/Abp/Account/ProfileAppService_Tests.cs @@ -73,6 +73,36 @@ public class ProfileAppService_Tests : AbpAccountApplicationTestBase result.Name.ShouldBe(input.Name); } + [Fact] + public async Task Should_Not_UpdatePhoneNumber_If_Input_PhoneNumber_IsNullOrWhiteSpace_Test() + { + //Arrange + _currentUser.Id.Returns(_testData.UserJohnId); + _currentUser.IsAuthenticated.Returns(true); + + //Act + var result = await _profileAppService.UpdateAsync(new UpdateProfileDto + { + UserName = CreateRandomString(), + Email = CreateRandomEmail(), + PhoneNumber = "" + }); + + //Assert + result.PhoneNumber.ShouldBe(null); + + //Act + result = await _profileAppService.UpdateAsync(new UpdateProfileDto + { + UserName = CreateRandomString(), + Email = CreateRandomEmail(), + PhoneNumber = "123" + }); + + //Assert + result.PhoneNumber.ShouldBe("123"); + } + [Fact] public async Task ChangePasswordAsync_FailsForSamePassword() { diff --git a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.BasicTheme/Themes/Basic/FirstLevelNavMenuItem.razor b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.BasicTheme/Themes/Basic/FirstLevelNavMenuItem.razor index 070d514cb6..0ccd9b93db 100644 --- a/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.BasicTheme/Themes/Basic/FirstLevelNavMenuItem.razor +++ b/modules/basic-theme/src/Volo.Abp.AspNetCore.Components.Web.BasicTheme/Themes/Basic/FirstLevelNavMenuItem.razor @@ -8,7 +8,7 @@ } @if (MenuItem.IsLeaf) { - if (customComponentType != null) + if (customComponentType != null && typeof(ComponentBase).IsAssignableFrom(customComponentType)) { } @@ -32,7 +32,7 @@ else {