diff --git a/Directory.Packages.props b/Directory.Packages.props index 5d9885ddbd..1b5bfaafb6 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -18,10 +18,10 @@ - - - - + + + + @@ -46,7 +46,7 @@ - + @@ -113,10 +113,10 @@ - - - - + + + + @@ -128,15 +128,16 @@ - - - - - + + + + + - + + @@ -144,6 +145,7 @@ + @@ -154,8 +156,8 @@ - - + + @@ -166,7 +168,7 @@ - + @@ -184,5 +186,6 @@ + \ No newline at end of file diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json index 733e370c54..62f8304467 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Admin/Localization/Resources/en.json @@ -723,6 +723,58 @@ "MakeAnnouncement": "Make Announcement", "MakeAnnouncementInfo": "Check it if you want to make an announcement for this post", "Permission:ViewCounts": "View counts", - "ReadCount": "Read Count" + "ReadCount": "Read Count", + "Menu:Solution": "Solution", + "Enum:LicenseType:1": "Personal", + "Enum:LicenseType:2": "Team", + "Enum:LicenseType:3": "Business", + "Enum:LicenseType:4": "Enterprise", + "Enum:Template:0": "Unknown", + "Enum:Template:1": "App No Layers", + "Enum:Template:2": "App Layered", + "Enum:Template:3": "Microservice", + "Enum:UiFramework:0": "Unknown", + "Enum:UiFramework:1": "None", + "Enum:UiFramework:2": "Mvc Razor Pages", + "Enum:UiFramework:3": "Angular", + "Enum:UiFramework:4": "Blazor Wasm", + "Enum:UiFramework:5": "Blazor Server", + "Enum:UiFramework:6": "Blazor Web App", + "Enum:UiFramework:7": "Blazor MaUI", + "Enum:DatabaseProvider:0": "Unknown", + "Enum:DatabaseProvider:1": "None", + "Enum:DatabaseProvider:2": "EfCore", + "Enum:DatabaseProvider:3": "MongoDb", + "Enum:Dbms:0": "Unknown", + "Enum:Dbms:1": "None", + "Enum:Dbms:2": "SqlServer", + "Enum:Dbms:3": "PostgreSql", + "Enum:Dbms:4": "Oracle", + "Enum:Dbms:5": "OracleDevart", + "Enum:Dbms:6": "MySql", + "Enum:Dbms:7": "Sqlite", + "Enum:UiTheme:0": "Unknown", + "Enum:UiTheme:1": "None", + "Enum:UiTheme:2": "Basic", + "Enum:UiTheme:3": "LeptonX", + "Enum:UiTheme:4": "LeptonX Lite", + "Enum:UiThemeStyle:0": "Unknown", + "Enum:UiThemeStyle:1": "System", + "Enum:UiThemeStyle:2": "Dim", + "Enum:UiThemeStyle:3": "Dark", + "Enum:UiThemeStyle:4": "Light", + "Enum:MobileApp:0": "Unknown", + "Enum:MobileApp:1": "None", + "Enum:MobileApp:2": "Maui", + "Enum:MobileApp:3": "ReactNative", + "Enum:CreationTool:0": "Unknown", + "Enum:CreationTool:1": "StudioUI", + "Enum:CreationTool:2": "StudioCli", + "Enum:CreationTool:3": "OldCli", + "Menu:TelemetryMenu": "Telemetry Reports", + "Menu:Studio": "Studio", + "Menu:Solutions": "Solutions", + "Menu:Users": "Users", + "Menu:UserReports": "Users" } } diff --git a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json index 40a2e878d6..7ef0ca75e9 100644 --- a/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json +++ b/abp_io/AbpIoLocalization/AbpIoLocalization/Www/Localization/Resources/en.json @@ -431,6 +431,9 @@ "WhoWeAre_Expert": "About Me", "CreateSolutionFolder": "Create Solution Folder", "CreateSolutionFolderOption": "Specifies if the project will be in a new folder in the output folder or directly the output folder.", + "CreateCrudPage": "Create CRUD Page", + "CreateCrudPageOption": "Generates a sample CRUD page with a Book entity to demonstrate basic operations (Create, Read, Update, Delete).", + "ConnectionString": "Connection string", "BooksPageTitle": "ABP Books", "BooksPageDescription": "Explore ABP books to deepen your understanding and mastery of ABP.", "PackageDetailPage_NuGetPackageInstallationOptions": "There are three ways to install {0} NuGet package to your project", @@ -907,6 +910,7 @@ "ProudToWorkWith": "Proud to Work With", "JoinOurConsumers": "Join them and build amazing products fast.", "AdditionalServicesExplanation": "Do you need additional or custom services? We and our partners can provide;", + "CustomLicense": "Custom License", "CustomProjectDevelopment": "Custom Project Development", "CustomProjectDevelopmentExplanation": "Dedicated developers for your custom projects.", "PortingExistingProjects": "Porting Existing Projects", @@ -1059,7 +1063,7 @@ "BuyNow": "Buy Now", "PayViaAmexCard": "How can I pay via my AMEX card?", "PayViaAmexCardDescription": "The default payment gateway 'Iyzico' may decline some AMEX credit cards due to security measures. In this case, you can pay through the alternative payment gateway '2Checkout'.", - "InvalidReCaptchaErrorMessage": "There was an error verifying reCAPTCHA. Please try again.", + "InvalidReCaptchaErrorMessage": "There was an error verifying reCAPTCHA.", "YourCompanyName": "Your company name", "FirstName": "First name", "LastName": "Last name", @@ -1795,7 +1799,7 @@ "SpecialDiscount": "Special Discount", "YourOrganizationOverview": "Your Organization Overview", "TrainingDetailsHeaderInfo_TrainingHourSingular": "{0} hour", - "ContactPageError": "Please send your message via email to info@abp.io
Here's what you wrote :", + "ContactPageError": "You can also send your message via email. Copy your message below and send to info@abp.io ", "GoBack": "Go back", "HereWhatYouWrote": "Here's what you wrote :", "Sales": "Sales", @@ -1892,6 +1896,21 @@ "SEOTitle": "SEO URL", "InvalidYouTubeUrl": "The URL you entered is not a valid YouTube video link. Please make sure it points to a specific video and try again.", "SelectAnOption": "Select an option", - "MostPopular": "Most Popular" + "MostPopular": "Most Popular", + "AnnouncmentsPageTitle": "ABP Community Announcements | Stay Updated with the Latest News", + "AnnouncmentsPageDescription": "Get the latest news, feature updates, release notes, and important announcements about the ABP framework and .NET ecosystem. Stay ahead with timely information directly from the ABP team.", + "CanIUseABPProductsOnMoreThanOneComputer": "Can I use ABP products on more than one computer?", + "ABPProductsOnMoreThanOneComputerExplanation": "Yes. Each developer can install the software on up to two machines. A third machine requires approval via email. When you stop using one of your computers, the system understands and automatically invalidates that computer from your paired computer list.", + "CanIShareTheLicensedABPCommercialProducts": "Can I share the licensed ABP commercial products publicly or make them open source?", + "ShareTheLicensedABPCommercialProductsExplanation": "No! Sharing or sublicensing a Commercial (PRO) ABP package is strictly prohibited.", + "AreSubscriptionRenewalsAutomatic": "Are subscription renewals automatic?", + "SubscriptionRenewalsAutomaticExplanation": "By default, no, the renewals are manual. On the other hand, when you purchase a new license and use the payment gateway 'Iyzico' with your credit card, you will see a checkbox called 'Automatic Renewal' in the purchase steps which allows ABP system automatically renew your license. When you check that checkbox, the auto-renewal process lets you renew your license without losing this discount, and your development will never be interrupted. ABP does not save your credit card information, but our payment gateway does secure savings. You can disable auto-renewal at any time by accessing your Organization Management page.", + "DoYouProvideSupportForThird-partyLibraries": "Do you provide support for third-party libraries?", + "ProvideSupportForThird-partyExplanation": "No. Support only covers ABP Framework, ABP commercial packages and products which have been created by Volosoft.", + "DoYouSupportCustomABPArchitectures": "Do you support custom ABP architectures?", + "SupportCustomABPArchitecturesExplanation": "No. Support is only provided for standard ABP solution structures. On the other hand, you can always get support for your custom needs with a paid consultancy from the ABP Team.", + "DoesABPCollectAnyPersonalOrTechnicalData": "Does ABP collect any personal or technical data?", + "ABPCollectAnyDataExplanation": "The software may collect information about you and your use of the software, and send that to Volosoft. Volosoft as the software and service provider may use this information to provide services and improve its products & services. You may opt-out of these scenarios, as described in the EULA under PRIVACY AND COLLECTION OF PERSONAL DATA topic .", + "InThisDocument": "In this document" } } diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/1752664190317-min.jpeg b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/1752664190317-min.jpeg new file mode 100644 index 0000000000..7cd0f2aa47 Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/1752664190317-min.jpeg differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15924-min.jpg b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15924-min.jpg new file mode 100644 index 0000000000..3730feed30 Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15924-min.jpg differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15933-min.jpg b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15933-min.jpg new file mode 100644 index 0000000000..5c4348c2a6 Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15933-min.jpg differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15934-min.jpg b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15934-min.jpg new file mode 100644 index 0000000000..bfaec7bc9d Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15934-min.jpg differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15941-min.jpg b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15941-min.jpg new file mode 100644 index 0000000000..8539d5be5c Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15941-min.jpg differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15944-min.jpg b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15944-min.jpg new file mode 100644 index 0000000000..2502d8904c Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15944-min.jpg differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15946-min.jpg b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15946-min.jpg new file mode 100644 index 0000000000..da423629c7 Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15946-min.jpg differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15947-min.jpg b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15947-min.jpg new file mode 100644 index 0000000000..372f41e3a5 Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15947-min.jpg differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15948-min.jpg b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15948-min.jpg new file mode 100644 index 0000000000..9d98fe4585 Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15948-min.jpg differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15949-min.jpg b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15949-min.jpg new file mode 100644 index 0000000000..dac9184cf3 Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15949-min.jpg differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15956-min.jpg b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15956-min.jpg new file mode 100644 index 0000000000..73ef867711 Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15956-min.jpg differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15959-min.jpg b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15959-min.jpg new file mode 100644 index 0000000000..af4c2b300a Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15959-min.jpg differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15963-min.jpg b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15963-min.jpg new file mode 100644 index 0000000000..e2c3a297f7 Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15963-min.jpg differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15964-min.jpg b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15964-min.jpg new file mode 100644 index 0000000000..4ed6c26b33 Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15964-min.jpg differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15966-min.jpg b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15966-min.jpg new file mode 100644 index 0000000000..80296b7ead Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15966-min.jpg differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15968-min.jpg b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15968-min.jpg new file mode 100644 index 0000000000..6f915ee94c Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15968-min.jpg differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15969-min.jpg b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15969-min.jpg new file mode 100644 index 0000000000..de112a11ea Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15969-min.jpg differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15970-min.jpg b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15970-min.jpg new file mode 100644 index 0000000000..acdd4bb0b0 Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15970-min.jpg differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15971-min.JPG b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15971-min.JPG new file mode 100644 index 0000000000..5fd55d58e1 Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15971-min.JPG differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15972-min.JPG b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15972-min.JPG new file mode 100644 index 0000000000..c1956daebd Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15972-min.JPG differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15973-min.JPG b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15973-min.JPG new file mode 100644 index 0000000000..3d1a5b0789 Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15973-min.JPG differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15974-min.JPG b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15974-min.JPG new file mode 100644 index 0000000000..147f7ef967 Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15974-min.JPG differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15975-min.JPG b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15975-min.JPG new file mode 100644 index 0000000000..173610a7ec Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15975-min.JPG differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15976-min.JPG b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15976-min.JPG new file mode 100644 index 0000000000..c48a7944d6 Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15976-min.JPG differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15977-min.JPG b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15977-min.JPG new file mode 100644 index 0000000000..6f5cb19831 Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15977-min.JPG differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15979-min.JPG b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15979-min.JPG new file mode 100644 index 0000000000..9ca4140717 Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15979-min.JPG differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15980-min.JPG b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15980-min.JPG new file mode 100644 index 0000000000..0ebf59d8b8 Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15980-min.JPG differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15981-min.JPG b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15981-min.JPG new file mode 100644 index 0000000000..10e02621cf Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15981-min.JPG differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15982-min.JPG b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15982-min.JPG new file mode 100644 index 0000000000..63bc90cc76 Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15982-min.JPG differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15983-min.JPG b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15983-min.JPG new file mode 100644 index 0000000000..14e1f16c9a Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15983-min.JPG differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15984-min.jpg b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15984-min.jpg new file mode 100644 index 0000000000..0984be8e2e Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15984-min.jpg differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15985-min.JPG b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15985-min.JPG new file mode 100644 index 0000000000..8d30f62cd3 Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15985-min.JPG differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15986-min.JPG b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15986-min.JPG new file mode 100644 index 0000000000..55f48e13a9 Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15986-min.JPG differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15987-min.JPG b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15987-min.JPG new file mode 100644 index 0000000000..9cbd57efaa Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15987-min.JPG differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15989-min.jpg b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15989-min.jpg new file mode 100644 index 0000000000..f4c0bb2cfc Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15989-min.jpg differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15994-min.jpg b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15994-min.jpg new file mode 100644 index 0000000000..35201fee15 Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15994-min.jpg differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15995-min.jpg b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15995-min.jpg new file mode 100644 index 0000000000..bc4220f83a Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15995-min.jpg differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15996-min.jpg b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15996-min.jpg new file mode 100644 index 0000000000..e5dd4c0c9d Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15996-min.jpg differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15998-min.jpg b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15998-min.jpg new file mode 100644 index 0000000000..d2ae92ff0e Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15998-min.jpg differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15999-min.jpg b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15999-min.jpg new file mode 100644 index 0000000000..22c32abd22 Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_15999-min.jpg differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16001-min.jpg b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16001-min.jpg new file mode 100644 index 0000000000..1f8a500948 Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16001-min.jpg differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16002-min.jpg b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16002-min.jpg new file mode 100644 index 0000000000..8753a09cfe Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16002-min.jpg differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16003-min.jpg b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16003-min.jpg new file mode 100644 index 0000000000..4f4eec7b54 Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16003-min.jpg differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16006-min.JPG b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16006-min.JPG new file mode 100644 index 0000000000..eef991dd38 Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16006-min.JPG differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16007-min.jpg b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16007-min.jpg new file mode 100644 index 0000000000..48cb26cd40 Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16007-min.jpg differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16008-min.jpg b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16008-min.jpg new file mode 100644 index 0000000000..3eadec4490 Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16008-min.jpg differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16009-min.jpg b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16009-min.jpg new file mode 100644 index 0000000000..0e8387065d Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16009-min.jpg differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16011-min.jpg b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16011-min.jpg new file mode 100644 index 0000000000..392a680bdf Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16011-min.jpg differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16012-min.jpg b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16012-min.jpg new file mode 100644 index 0000000000..15a3fbf143 Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16012-min.jpg differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16013-min.jpg b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16013-min.jpg new file mode 100644 index 0000000000..256c2bc22c Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16013-min.jpg differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16019-min.JPG b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16019-min.JPG new file mode 100644 index 0000000000..e657a682ad Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16019-min.JPG differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16021-min.JPG b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16021-min.JPG new file mode 100644 index 0000000000..5a81fddee1 Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16021-min.JPG differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16022-min.JPG b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16022-min.JPG new file mode 100644 index 0000000000..ed8660fdbd Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16022-min.JPG differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16023-min.JPG b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16023-min.JPG new file mode 100644 index 0000000000..05b2040d18 Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16023-min.JPG differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16024-min.JPG b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16024-min.JPG new file mode 100644 index 0000000000..2cc6f9fbee Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16024-min.JPG differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16025-min.JPG b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16025-min.JPG new file mode 100644 index 0000000000..0ede12db76 Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16025-min.JPG differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16026-min.JPG b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16026-min.JPG new file mode 100644 index 0000000000..268e4a4454 Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16026-min.JPG differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16027-min.JPG b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16027-min.JPG new file mode 100644 index 0000000000..68d288fc19 Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16027-min.JPG differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16028-min.JPG b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16028-min.JPG new file mode 100644 index 0000000000..72d060a6e1 Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16028-min.JPG differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16029-min.JPG b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16029-min.JPG new file mode 100644 index 0000000000..47bc5a910c Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16029-min.JPG differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16030-min.JPG b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16030-min.JPG new file mode 100644 index 0000000000..5180e62195 Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16030-min.JPG differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16031-min.JPG b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16031-min.JPG new file mode 100644 index 0000000000..3085d54f3d Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16031-min.JPG differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16032-min.jpg b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16032-min.jpg new file mode 100644 index 0000000000..26ddb2dc75 Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16032-min.jpg differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16040-min.JPG b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16040-min.JPG new file mode 100644 index 0000000000..4bf5c04712 Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16040-min.JPG differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16041-min.JPG b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16041-min.JPG new file mode 100644 index 0000000000..3b38f0c487 Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/IMG_16041-min.JPG differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/cover.png b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/cover.png new file mode 100644 index 0000000000..595446cefa Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/cover.png differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/image-20250722203102576.png b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/image-20250722203102576.png new file mode 100644 index 0000000000..8b8f3a978b Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/image-20250722203102576.png differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/my-talk-1.JPG b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/my-talk-1.JPG new file mode 100644 index 0000000000..6be6b041d3 Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/my-talk-1.JPG differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/my-talk-2.JPG b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/my-talk-2.JPG new file mode 100644 index 0000000000..b05c488c4d Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/my-talk-2.JPG differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/my-talk-3.JPG b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/my-talk-3.JPG new file mode 100644 index 0000000000..51dd5e7e57 Binary files /dev/null and b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/my-talk-3.JPG differ diff --git a/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/post.md b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/post.md new file mode 100644 index 0000000000..1b996e4867 --- /dev/null +++ b/docs/en/Blog-Posts/2025-07-22-My-Impressionf-at-WeAreDevelopers/post.md @@ -0,0 +1,101 @@ +# WeAreDevelopers 2025: A Speaker’s Impressions + +![Conference Opening](IMG_15924-min.jpg) + +After speaking at DotNext Moscow, I had high expectations for WeAreDevelopers 2025—and the event delivered on all fronts. Held in Berlin / Germany, it brought together a truly global crowd of developers, tech leaders, and innovators. As a speaker and software architect , I’m sharing my first-hand highlights, favorite moments, and candid scenes from this 2025’s conference. + +## 🗣 My Talk + +We have a good experience on multi-tenancy topic in SaaS development. My talk's topic was "Building Multi-Tenant ASP.NET Core Applications: Best Practices and Real-World Solutions". It was on the stage 4, 11 July Friday 10:20 am - 10:50 am and [this my presentation file](https://github.com/ebicoglu/presentations/blob/main/multi-tenancy-wearedevelopers-2025_30mins.pptx). + +![My Talk Info](image-20250722203102576.png) + +![Pictures from my talk](my-talk-1.JPG) +![Pictures from my talk](my-talk-2.JPG) +![Pictures from my talk](my-talk-3.JPG) + + +## 🏛 Huge Venue + +![Main Stage](1752664190317-min.jpeg) +*The image is credited to WeAreDevelopers organization* + +First of all, I had been in numerous software conferences, I must say that I've never seen such a big software event. The event spanned **500+ sessions across 20+ stages**, including the HR Leaders Summit for **2 full days**. + +![Main Stage2](IMG_15933-min.jpg) +![Crowd Energy](IMG_15944-min.jpg) + +------ + +## 🎤 Opening Keynote from GitHub + +GitHub CEO Thomas Dohmke initiated the conference on the main stage with a talk on *“Agents for the Sake of Happiness”*. Having introduced Copilot three years ago here, he now launched bold predictions about autonomous AI‍—a fascinating evolution... He demonstrated GitHub Co-Pilot's AI and created a snake game. Altough it didn't work as he planned, we're developers we know live coding is hard. Actually that's because we shouldn't rely on AI. AI is not deterministic even though we set all those temperature, TopP, TopK parameters to minimum. + +> AI is a good but not trustable friend! + +![Thomas Dohmke on Stage](IMG_15941-min.jpg) + +------ + +## 🧭 11 Parallel Stages: Rush + +There were 11 stages where 11 different topics were being explained. And the sessions were 30 minutes. Actually that's the downside of this event. Because there were so nice talks that needs to be minimum 40 minutes. But anyway I understand the organization team because there are many smart speakers whose needs to be included in this event. So as a attendee I was on a hurry to pick the next talk even when I was listening to a talk :) + +The venue consists of 3 buildings. So if you pick a talk on another building, you have 10 mins to go to toilet or drink something and catch the next session on that far building... + +There was HR track with **3 stages and 2 full days** of HR/Talent Acquisition programming, it attracted a notable overlap of developers and HR pros. Themes included AI‑powered recruiting, remote work culture, mental health, diversity & inclusion, and building AI agents + +![Fireside Chat](IMG_15949-min.jpg) +![Panel Discussion](IMG_15948-min.jpg) + + +------ + +## 🤖 AI & AI & AI & Others... + +I'm one of those AI lovers. I love learning cutting-edge information. And as I see AI is being more trendy everyday. That's why most of the talks were about AI. Everything related to AI. I generaly attended AI related talks because I'm also working on some AI topics in Volosoft at the moment. + + +------ + +## 🤝 Expo Floor & Networking + +The expo was a developer’s playground—cloud services, open‑source tools, startups, and enterprise platforms. I found new partners and reconnected with peers in a buzzing atmosphere. Everywhere was full of talking's even outside. If you want to get fresh air and drink coffee, you can go out and listen to the outside talks. + +![Expo Hall](IMG_15956-min.jpg) +![Booth Visit](IMG_15959-min.jpg) + + + +Networking wasn't just daytime chatter—hallway meetups and evening socials were unforgettable. + +![Networking Moments](IMG_15964-min.jpg) +![After Hours](IMG_15972-min.JPG) + +------ + +## 😂 Candid & Fun Moments + +Swag stations, sponsor games, “developer selfies”—these lighter moments kept the vibe upbeat and human. + +![Fun Moment](IMG_15971-min.JPG) +![Developer Selfie](IMG_15980-min.JPG) + +------ + +## ✅ Final Thoughts & Looking Ahead + +WeAreDevelopers 2025 was an unforgettable three-day ride: **15,000 tech minds**, **500+ sessions**, and a true **bridge between developers and HR** +I’m leaving with: + +- Fresh strategies in GenAI and SaaS growth +- Stronger HR-tech understanding and crossover potential +- New professional connections—and fun memories + + +------ + +![Conference Wrap-Up](IMG_15999-min.jpg) + + + diff --git a/docs/en/Blog-Posts/2025-08-08 v9_3_Release_Stable/POST.md b/docs/en/Blog-Posts/2025-08-08 v9_3_Release_Stable/POST.md new file mode 100644 index 0000000000..3acf9a7dd0 --- /dev/null +++ b/docs/en/Blog-Posts/2025-08-08 v9_3_Release_Stable/POST.md @@ -0,0 +1,79 @@ +# ABP.IO Platform 9.3 Final Has Been Released! + +We are glad to announce that [ABP](https://abp.io/) 9.3 stable version has been released today. + +## What's New With Version 9.3? + +All the new features were explained in detail in the [9.3 RC Announcement Post](https://abp.io/community/announcements/announcing-abp-9-3-release-candidate-4dqgiryf), so there is no need to review them again. You can check it out for more details. + +## Getting Started with 9.3 + +### Creating New Solutions + +You can check the [Get Started page](https://abp.io/get-started) to see how to get started with ABP. You can either download [ABP Studio](https://abp.io/get-started#abp-studio-tab) (**recommended**, if you prefer a user-friendly GUI application - desktop application) or use the [ABP CLI](https://abp.io/docs/latest/cli) to create new solutions. + +> **Note**: ABP Studio **v1.2.1** has been released with support for **ABP 9.3**. If you already have ABP Studio installed, update it to v1.2.1 (or later, if available) to create new applications targeting 9.3. ABP Studio checks for updates automatically and will prompt you in-app modal to update to the latest version, or you can download the latest installer from the [Studio](https://abp.io/studio) page. See the [upgrading guide](https://abp.io/docs/latest/studio/installation#upgrading) for details. After updating, the New Solution wizard will create applications with ABP 9.3 by default. You can check the [ABP Studio and ABP Startup Template Version Mappings](https://abp.io/docs/latest/studio/version-mapping) documentation to see the corresponding ABP versions for other versions of Studio. + +### How to Upgrade an Existing Solution + +You can upgrade your existing solutions with either ABP Studio or ABP CLI. In the following sections, both approaches are explained: + +### Upgrading via ABP Studio + +If you are already using the ABP Studio, you can upgrade it to the latest version. ABP Studio periodically checks for updates in the background, and when a new version of ABP Studio is available, you will be notified through a modal. Then, you can update it by confirming the opened modal. See [the documentation](https://abp.io/docs/latest/studio/installation#upgrading) for more info. + +After upgrading the ABP Studio, then you can open your solution in the application, and simply click the **Upgrade ABP Packages** action button to instantly upgrade your solution: + +![](upgrade-abp-packages.png) + +### Upgrading via ABP CLI + +Alternatively, you can upgrade your existing solution via ABP CLI. First, you need to install the ABP CLI or upgrade it to the latest version. + +If you haven't installed it yet, you can run the following command: + +```bash +dotnet tool install -g Volo.Abp.Studio.Cli +``` + +Or to update the existing CLI, you can run the following command: + +```bash +dotnet tool update -g Volo.Abp.Studio.Cli +``` + +After installing/updating the ABP CLI, you can use the [`update` command](https://abp.io/docs/latest/CLI#update) to update all the ABP related NuGet and NPM packages in your solution as follows: + +```bash +abp update +``` + +You can run this command in the root folder of your solution to update all ABP related packages. + +## Migration Guides + +There are a few breaking changes in this version that may affect your application. Please read the migration guide carefully, if you are upgrading from v9.2: [ABP Version 9.3 Migration Guide](https://abp.io/docs/9.3/release-info/migration-guides/abp-9-3) + +## Community News + +### New ABP Community Articles + +As always, exciting articles have been contributed by the ABP community. I will highlight some of them here: + +* [Fahri Gedik](https://abp.io/community/members/fahrigedik) has published 2 new articles: + * [A Modern Approach to Angular Dependency Injection using inject function](https://abp.io/community/articles/a-modern-approach-to-angular-dependency-injection-using-8np4o1ap) + * [Angular Application Builder: Transitioning from Webpack to Esbuild](https://abp.io/community/articles/angular-application-builder-transitioning-from-webpack-to-3yzhzfl0) +* [Benjamin Fadina](https://abp.io/community/members/benjaminsqlserver@gmail.com) has published several videos on various topics such as **Blazor Web Assembly Using ABP.IO**, **CQRS Implementation with MediatR in ABP** and more. You can see all his videos [here](https://abp.io/community/members/benjaminsqlserver@gmail.com). +* [Mansur Besleney](https://abp.io/community/members/mansur.besleney) has published [How to Build Persistent Background Jobs with ABP Framework and Quartz](https://abp.io/community/articles/how-to-build-persistent-background-jobs-with-abp-framework-n9aloh93) +* [Halil Ibrahim Kalkan](https://x.com/hibrahimkalkan) has published [Multitenancy with Separate Databases in .NET and ABP](https://abp.io/community/articles/multitenancy-with-separate-databases-in-dotnet-and-abp-51nvl4u9) +* [Alex Maiereanu](https://abp.io/community/members/alex.maiereanu@3sstudio.com) has published [ABP-Hangfire-AzurePostgreSQL](https://abp.io/community/articles/abphangfireazurepostgresql-s1jnf3yg) +* [Jack Fistelmann](https://abp.io/community/members/jfistelmann) has published [ABP and maildev](https://abp.io/community/articles/abp-and-maildev-gy13cr1p) +* [Harsh Gupta](https://abp.io/community/members/harshgupta) has published [How to Add a Module in the ABP.io Application?](https://abp.io/community/articles/how-to-add-a-module-in-the-abp.io-application-sdeajkn6) +* [Tarık Özdemir](https://abp.io/community/members/mtozdemir) has published [AI-First Architecture for .NET Projects: A Modern Blueprint Inspired by McKinsey](https://abp.io/community/articles/AI-First%20Architecture%20for%20.NET%20Projects%3A%20A%20Modern%20Blueprint-h2wgcoq3) +* [Liming Ma](https://github.com/maliming) has published [Using Hangfire Dashboard in ABP API Website](https://abp.io/community/articles/using-hangfire-dashboard-in-abp-api-website--r32ox497) + +Thanks to the ABP Community for all the content they have published. You can also [post your ABP related (text or video) content](https://abp.io/community/posts/create) to the ABP Community. + +## About the Next Version + +The next feature version will be 10.0. You can follow the [release planning here](https://github.com/abpframework/abp/milestones). Please [submit an issue](https://github.com/abpframework/abp/issues/new) if you have any problems with this version. diff --git a/docs/en/Blog-Posts/2025-08-08 v9_3_Release_Stable/cover-image.png b/docs/en/Blog-Posts/2025-08-08 v9_3_Release_Stable/cover-image.png new file mode 100644 index 0000000000..291da0d04f Binary files /dev/null and b/docs/en/Blog-Posts/2025-08-08 v9_3_Release_Stable/cover-image.png differ diff --git a/docs/en/Blog-Posts/2025-08-08 v9_3_Release_Stable/upgrade-abp-packages.png b/docs/en/Blog-Posts/2025-08-08 v9_3_Release_Stable/upgrade-abp-packages.png new file mode 100644 index 0000000000..ad5e9bd462 Binary files /dev/null and b/docs/en/Blog-Posts/2025-08-08 v9_3_Release_Stable/upgrade-abp-packages.png differ diff --git a/docs/en/Community-Articles/2022-04-18-abp-community-talks-20223/post.md b/docs/en/Community-Articles/2022-04-18-abp-community-talks-20223/post.md index fcd7043594..5d9c9e0d3e 100644 --- a/docs/en/Community-Articles/2022-04-18-abp-community-talks-20223/post.md +++ b/docs/en/Community-Articles/2022-04-18-abp-community-talks-20223/post.md @@ -6,7 +6,7 @@ * ABP Community Talks are scheduled to be held on a monthly basis. * ABP Community Talks are and always will be completely free to attend. Everyone is welcome to join, ask questions and make suggestions before, during and after the event. * ABP Community Talks are created and announced on [Kommunity](https://kommunity.com/volosoft/events). - * ABP Community Talks are announced regularly on [ABP Framework Twitter Account](https://twitter.com/abpframework), [Volosoft LinkedIn account](https://www.linkedin.com/company/volosoft), [Volosoft Facebook Account](https://www.facebook.com/volosoftcompany), [ABP Community Discord Server](https://discord.gg/CrYrd5vcGh). We highly encourage everyone to follow us and make suggestions. + * ABP Community Talks are announced regularly on [ABP Framework Twitter Account](https://twitter.com/abpframework), [Volosoft LinkedIn account](https://www.linkedin.com/company/volosoft), [Volosoft Facebook Account](https://www.facebook.com/volosoftcompany), [ABP Community Discord Server](https://abp.io/join-discord). We highly encourage everyone to follow us and make suggestions. * ABP Community Talks are available to watch after the event on YouTube. See [ABP Community Talks YouTube Playlist](https://www.youtube.com/playlist?list=PLsNclT2aHJcOsPustEkzG6DywiO8eh0lB). # ABP Community Talks 2022.3 diff --git a/docs/en/Community-Articles/2022-04-19-official-abp-discord-server-is-here/post.md b/docs/en/Community-Articles/2022-04-19-official-abp-discord-server-is-here/post.md index 5d099cc1c3..ca765a5860 100644 --- a/docs/en/Community-Articles/2022-04-19-official-abp-discord-server-is-here/post.md +++ b/docs/en/Community-Articles/2022-04-19-official-abp-discord-server-is-here/post.md @@ -1,9 +1,9 @@ - We are excited to announce Official ABP Discord Server is created! You can join the ABP Discord Community by clicking [here](https://discord.gg/wbcQAsUrs9). + We are excited to announce Official ABP Discord Server is created! You can join the ABP Discord Community by clicking [here](https://abp.io/join-discord). In the first week of opening ABP Discord Server, member amount reached more than 500. We are grateful to and blessed by your interest. Thanks to all of you! This also made us sure that an ABP Discord Server was actually a need for the community members to interact with each other. ABP Community is growing by the second, and we are grateful for all your contributions towards ABP Framework. We noticed that ABP Community’s communication were significant on ABP Framework’s GitHub, we wanted to take it to the next level and have an area where all of us can easily chat with each other. -> [Join ABP Discord Server Now](https://discord.gg/wbcQAsUrs9) +> [Join ABP Discord Server Now](https://abp.io/join-discord) # What Can You Do on ABP Community Discord Server? @@ -42,11 +42,11 @@ # How Can You Join To ABP Discord Server? - You can join ABP Discord Server by simply clicking to [https://discord.gg/abp](https://discord.gg/wbcQAsUrs9). + You can join ABP Discord Server by simply clicking to [https://abp.io/join-discord](https://abp.io/join-discord). We are excited to welcome you in ABP Discord Server! -> [Click Here to Join ABP Discord Server Now](https://discord.gg/wbcQAsUrs9) +> [Click Here to Join ABP Discord Server Now](https://abp.io/join-discord) ### What is Discord? @@ -62,4 +62,4 @@ In Discord Servers, users communicate with each other in a way that is convenient for them. Discord allows people to make voice calls, video chats, or simply text messages. Communities are created by wether fans of a specific topic(games, open-source frameworks, NFT, etc.) or by the official authorities of that specific topic(game creator, framework core team, creator of a token, etc). - In ABP Community Discord Server’s case, it is a server created by official authorities with core team being present in the server along with the community members. Even though it is created for the framework community members to communicate with each other easily, everyone who is interested in following the latest news about ABP Platform are welcome to [join ABP Discord Server](https://discord.gg/wbcQAsUrs9)! \ No newline at end of file + In ABP Community Discord Server’s case, it is a server created by official authorities with core team being present in the server along with the community members. Even though it is created for the framework community members to communicate with each other easily, everyone who is interested in following the latest news about ABP Platform are welcome to [join ABP Discord Server](https://abp.io/join-discord)! \ No newline at end of file diff --git a/docs/en/Community-Articles/2022-05-10-abpio-platform-53-rc-has-been-published/post.md b/docs/en/Community-Articles/2022-05-10-abpio-platform-53-rc-has-been-published/post.md index eb157010b8..cc0e57db93 100644 --- a/docs/en/Community-Articles/2022-05-10-abpio-platform-53-rc-has-been-published/post.md +++ b/docs/en/Community-Articles/2022-05-10-abpio-platform-53-rc-has-been-published/post.md @@ -254,4 +254,4 @@ We've created an official ABP Discord server so the ABP Community can interact w Thanks to the ABP Community, **700+** people joined our Discord Server so far and it grows every day. -You can join our Discord Server from [here](https://discord.gg/abp), if you haven't yet. \ No newline at end of file +You can join our Discord Server from [here](https://abp.io/join-discord), if you haven't yet. \ No newline at end of file diff --git a/docs/en/Community-Articles/2023-02-21-abp-year-review-2022-wrap-up/post.md b/docs/en/Community-Articles/2023-02-21-abp-year-review-2022-wrap-up/post.md index 6abf2f3d12..a27aeb9f2c 100644 --- a/docs/en/Community-Articles/2023-02-21-abp-year-review-2022-wrap-up/post.md +++ b/docs/en/Community-Articles/2023-02-21-abp-year-review-2022-wrap-up/post.md @@ -1,100 +1,200 @@ -

ABP Framework is an open source infrastructure that enables developers to create modern web applications by following the best practices and conventions of software development. In 2022, ABP Framework continued to thrive, achieving significant milestones and making waves in the software development community. With more than 9K GitHub stars and over 10 millions of downloads on NuGet, ABP Framework has become a go-to framework for developers seeking a reliable and efficient way to build web applications.

- -

As ABP Team, we owe our success to our vibrant community, and we are immensely grateful for the support and contributions of each and every member. With your help, we achieved a lot in 2022. We remained committed to our values of transparency, openness, and collaboration, engaging with our community members as much as possible to ensure that we are creating a framework that meets their needs.

- -

One of the major highlights of 2022 was the release of .NET Core 7, which provided a powerful platform for ABP Framework to build upon. Additionally, ABP Commercial and our training programs continued to help developers and businesses to leverage the power of the ABP Framework, enabling them to build modern web applications more efficiently and effectively than ever before.

- -

In this article, we'll take a closer look at the key highlights of 2022 for ABP Framework, from major updates to achivements and the community insights. We are excited to share our progress with you and provide insights into how ABP Framework is continuing to shape the future of software development. So, let's dive in!

- - - - -

NuGet Downloads

-

NuGet is a package manager designed specifically for the .NET ecosystem. It simplifies the process of creating and consuming packages, thanks to the NuGet client tools. By using these tools, developers can easily manage their project dependencies and improve their workflow.

-

In 2022, ABP Core NuGet package reached more than 10 million of downloads!

-

On the other hand, overall Volosoft NuGet Packages reached more than half a billion downloads!

-

Thank you all for your interest and support towards Volosoft and ABP packages.

- - -

E-Books

-

Our published e-book amount is reached 3! This year, with our founder Halil İbrahim Kalkan's contributions we now have 3 published e-books.

- - - -

Tutorial Videos

-

In 2022, we tried to be as much active as we could. To give you more insight and let you understand ABP Framework with short videos according to your interests, we published 48 tutorial videos. Though the videos were created by overall team members of ABP Framework, someone deserves a special mention here. Shout out to our ABP Core Team member Hamza Albreem for his hard work.

- - -

GitHub Stars

-

ABP Framework GitHub repository reached more than 9K stars. We appreciate your interest and support for ABP Framework GitHub repository. We are working hard to be worthy of your interest and reach out to more people to simplify and streamline their development processes.

- -

Community Talks

-

ABP Community Talks is our monthly event that brings together members of the ABP Framework community to discuss and exchange ideas. Prior to each event, we collect suggestions from our contributors, monitor trending topics in the industry, and review updates and news related to the ABP Platform to curate the topics for discussion. Once the topics are finalized, we announce them through our social media and community channels to ensure everyone is aware and can join in on the conversation.

-

We did 10 ABP Community Talks Episodes of and 1 ABP Suite webinar. You can take a look at them and check out our videos we have on Volosoft YouTube Channel.

- -

ABP Community Contributions

-

The ABP Community is a hub that offers resources such as articles, video tutorials, and updates on ABP's development progress and events for ABP Framework, .NET, and software development. Developers can also connect with others, help each other, and share their expertise in ABP Community.

-
    You can check out each source from the list below. -
  • ABP Community Events: You can reach them from here.
  • -
  • ABP Community Posts: You can reach them from here
  • -
  • ABP Community Videos: You can reach them from here.
  • -
  • ABP Community Stackoverflow: You can reach them from here.
  • -
-

In 2022, the community's contribution reached a point where more than 100 resources. Thank you for all your effort! Please keep it going! It is becoming a more and more rich resource thanks to your variety of contributions and help.

- - -

ABP Community Discord Server

-

To take community interaction to the next level, we created the official ABP Discord server, providing a platform for the ABP Community to connect and communicate instantly through chatting.

-

We were so excited announcing the official ABP Discord Server. In the first week of announcing it, the server quickly attracted over 500 members. We're grateful for your interest and support, which confirms the need for a dedicated platform for community interaction.

-> Join ABP Discord Server Now - - -

ABP Framework GitHub Contributions

-

In 2022, ABP Core Team worked hard to achieve milestones and give the best value with ABP Framework so users can benefit from its features. Additional to our team's work, ABP Framework is perfected in 2022 with ABP Community members' contributions, 3157 commits pushed from 48 different contributors.

-

We appreciate your hard work and effort you put into making ABP Framework better and improved.

- - -

Events/Summits

-

We try to contribute to the developers community as much as we can since day 1. This year was no different. We tried to give value through sponsorships for developer communities. Especially with us leaving the pandemic behind every day, we try to keep up with the in-person events as well as online events. We plan to do more in next year. So, stay tuned!

-

This year, we sponsored to 4 events. They were, DevNot -Designing Monolith First for Microservice Architecture event, DNF Summit 2022, Developer Summit 2022, and .NET Conference 2022. - - -

ABP Releases

-

ABP Framework released 4 versions from 5.1 to 7.1 in 2022. You can check the release logs from ABP Framework Release Logs.

-

The most important milestone in these releases is that we upgraded ABP Framework to .NET 7.0 in ABP v7.0.

-

Additionally, we switched to OpenIddict for the startup templates in ABP v6.0.

- - -

ABP Commercial

-

It has been a successful year for ABP Commercial as well as ABP Framework. We have already reached to more than 100 countries over the years of ABP Commercial's release. This year, we continued to be streamline businesses' development processes with ABP Commercial.

-
    -
  • We have served to different sizes of businesses from more than 50 countries and more than 40 industries .
  • -
  • We performed 286 hours of training to simplify users' learning curve of ABP Framework.
  • -
  • 1771 support tickets resolved in the premium support forum in which ABP Commercial users can ask their questions directly to ABP Core Team members via ABP Commercial Support Center in addition to community support we provide for ABP Framework users/developers.
  • -
  • We received 39 new testimonials, all from satisfied customers which led us to the other headline, Gartner Badges.
  • -
- - -

LeptonX Theme

-

The Lepton Theme is a module that offers a theme for abp.io-based applications, featuring an Admin Dashboard designed by the ABP Platform. We released a version we called LeptonX Theme which is an upgraded version of Lepton Theme. You can view a live preview of the LeptonX Theme. While the LeptonX theme is currently exclusive to ABP Commercial users, ABP Framework users can still access the Lite version. You can see the documentation for ABP LeptonX Theme light from here.

- - -

Gartner Badges

-

Gartner badges are given as an award to the listed softwares within their software review/suggestion platforms. To be able to get these awards, certain criterias have to be met such as ease of use, likelihood of recommend, functionality, etc. and they are calculated completely according to the users' real reviews.

-

In 2022, ABP Commercial reached to such success thanks to its users' support on Gartner, it has been recognized with 2 badges in Application Development category.

- -

Thank you all for all these recognition you deemed us worthy of.

+

ABP Framework is an open source infrastructure that enables developers to create modern web applications by following the best practices and conventions of software development. In 2022, ABP Framework continued to thrive, achieving significant milestones and making waves in the software development community. With more than 9K GitHub stars and over 10 millions of downloads on NuGet, ABP Framework has become a go-to framework for developers seeking a reliable and efficient way to build web applications.

+ + + +

As ABP Team, we owe our success to our vibrant community, and we are immensely grateful for the support and contributions of each and every member. With your help, we achieved a lot in 2022. We remained committed to our values of transparency, openness, and collaboration, engaging with our community members as much as possible to ensure that we are creating a framework that meets their needs.

+ + + +

One of the major highlights of 2022 was the release of .NET Core 7, which provided a powerful platform for ABP Framework to build upon. Additionally, ABP Commercial and our training programs continued to help developers and businesses to leverage the power of the ABP Framework, enabling them to build modern web applications more efficiently and effectively than ever before.

+ + + +

In this article, we'll take a closer look at the key highlights of 2022 for ABP Framework, from major updates to achivements and the community insights. We are excited to share our progress with you and provide insights into how ABP Framework is continuing to shape the future of software development. So, let's dive in!

+ + + + + + + + + +

NuGet Downloads

+ +

NuGet is a package manager designed specifically for the .NET ecosystem. It simplifies the process of creating and consuming packages, thanks to the NuGet client tools. By using these tools, developers can easily manage their project dependencies and improve their workflow.

+ +

In 2022, ABP Core NuGet package reached more than 10 million of downloads!

+ +

On the other hand, overall Volosoft NuGet Packages reached more than half a billion downloads!

+ +

Thank you all for your interest and support towards Volosoft and ABP packages.

+ + + + + +

E-Books

+ +

Our published e-book amount is reached 3! This year, with our founder Halil İbrahim Kalkan's contributions we now have 3 published e-books.

+ + + + + + + +

Tutorial Videos

+ +

In 2022, we tried to be as much active as we could. To give you more insight and let you understand ABP Framework with short videos according to your interests, we published 48 tutorial videos. Though the videos were created by overall team members of ABP Framework, someone deserves a special mention here. Shout out to our ABP Core Team member Hamza Albreem for his hard work.

+ + + + + +

GitHub Stars

+ +

ABP Framework GitHub repository reached more than 9K stars. We appreciate your interest and support for ABP Framework GitHub repository. We are working hard to be worthy of your interest and reach out to more people to simplify and streamline their development processes.

+ + + +

Community Talks

+ +

ABP Community Talks is our monthly event that brings together members of the ABP Framework community to discuss and exchange ideas. Prior to each event, we collect suggestions from our contributors, monitor trending topics in the industry, and review updates and news related to the ABP Platform to curate the topics for discussion. Once the topics are finalized, we announce them through our social media and community channels to ensure everyone is aware and can join in on the conversation.

+ +

We did 10 ABP Community Talks Episodes of and 1 ABP Suite webinar. You can take a look at them and check out our videos we have on Volosoft YouTube Channel.

+ + + +

ABP Community Contributions

+ +

The ABP Community is a hub that offers resources such as articles, video tutorials, and updates on ABP's development progress and events for ABP Framework, .NET, and software development. Developers can also connect with others, help each other, and share their expertise in ABP Community.

+ +
    You can check out each source from the list below. + +
  • ABP Community Events: You can reach them from here.
  • + +
  • ABP Community Posts: You can reach them from here
  • + +
  • ABP Community Videos: You can reach them from here.
  • + +
  • ABP Community Stackoverflow: You can reach them from here.
  • + +
+ +

In 2022, the community's contribution reached a point where more than 100 resources. Thank you for all your effort! Please keep it going! It is becoming a more and more rich resource thanks to your variety of contributions and help.

+ + + + + +

ABP Community Discord Server

+ +

To take community interaction to the next level, we created the official ABP Discord server, providing a platform for the ABP Community to connect and communicate instantly through chatting.

+ +

We were so excited announcing the official ABP Discord Server. In the first week of announcing it, the server quickly attracted over 500 members. We're grateful for your interest and support, which confirms the need for a dedicated platform for community interaction.

+ +> Join ABP Discord Server Now + + + + + +

ABP Framework GitHub Contributions

+ +

In 2022, ABP Core Team worked hard to achieve milestones and give the best value with ABP Framework so users can benefit from its features. Additional to our team's work, ABP Framework is perfected in 2022 with ABP Community members' contributions, 3157 commits pushed from 48 different contributors.

+ +

We appreciate your hard work and effort you put into making ABP Framework better and improved.

+ + + + + +

Events/Summits

+ +

We try to contribute to the developers community as much as we can since day 1. This year was no different. We tried to give value through sponsorships for developer communities. Especially with us leaving the pandemic behind every day, we try to keep up with the in-person events as well as online events. We plan to do more in next year. So, stay tuned!

+ +

This year, we sponsored to 4 events. They were, DevNot + +Designing Monolith First for Microservice Architecture event, DNF Summit 2022, Developer Summit 2022, and .NET Conference 2022. + + + + + +

ABP Releases

+ +

ABP Framework released 4 versions from 5.1 to 7.1 in 2022. You can check the release logs from ABP Framework Release Logs.

+ +

The most important milestone in these releases is that we upgraded ABP Framework to .NET 7.0 in ABP v7.0.

+ +

Additionally, we switched to OpenIddict for the startup templates in ABP v6.0.

+ + + + + +

ABP Commercial

+ +

It has been a successful year for ABP Commercial as well as ABP Framework. We have already reached to more than 100 countries over the years of ABP Commercial's release. This year, we continued to be streamline businesses' development processes with ABP Commercial.

+ +
    + +
  • We have served to different sizes of businesses from more than 50 countries and more than 40 industries .
  • + +
  • We performed 286 hours of training to simplify users' learning curve of ABP Framework.
  • + +
  • 1771 support tickets resolved in the premium support forum in which ABP Commercial users can ask their questions directly to ABP Core Team members via ABP Commercial Support Center in addition to community support we provide for ABP Framework users/developers.
  • + +
  • We received 39 new testimonials, all from satisfied customers which led us to the other headline, Gartner Badges.
  • + +
+ + + + + +

LeptonX Theme

+ +

The Lepton Theme is a module that offers a theme for abp.io-based applications, featuring an Admin Dashboard designed by the ABP Platform. We released a version we called LeptonX Theme which is an upgraded version of Lepton Theme. You can view a live preview of the LeptonX Theme. While the LeptonX theme is currently exclusive to ABP Commercial users, ABP Framework users can still access the Lite version. You can see the documentation for ABP LeptonX Theme light from here.

+ + + + + +

Gartner Badges

+ +

Gartner badges are given as an award to the listed softwares within their software review/suggestion platforms. To be able to get these awards, certain criterias have to be met such as ease of use, likelihood of recommend, functionality, etc. and they are calculated completely according to the users' real reviews.

+ +

In 2022, ABP Commercial reached to such success thanks to its users' support on Gartner, it has been recognized with 2 badges in Application Development category.

+ + + +

Thank you all for all these recognition you deemed us worthy of.

+ diff --git a/docs/en/Community-Articles/2024-11-25-Global-Assets/POST.md b/docs/en/Community-Articles/2024-11-25-Global-Assets/POST.md index 33dd9eb292..e5196dde34 100644 --- a/docs/en/Community-Articles/2024-11-25-Global-Assets/POST.md +++ b/docs/en/Community-Articles/2024-11-25-Global-Assets/POST.md @@ -51,7 +51,7 @@ public class MyBlazorWebAssemblyBundlingModule : AbpModule options.ScriptBundles.Get(BlazorWebAssemblyStandardBundles.Scripts.Global).AddContributors(typeof(MyModuleBundleScriptContributor)); // Style Bundles - options.ScriptBundles.Get(BlazorWebAssemblyStandardBundles.Scripts.Global).AddContributors(typeof(MyModuleBundleStyleBundleContributor)); + options.StyleBundles.Get(BlazorWebAssemblyStandardBundles.Styles.Global).AddContributors(typeof(MyModuleBundleStyleBundleContributor)); }); } } diff --git a/docs/en/Community-Articles/2025-07-17-summar-campaign/post.md b/docs/en/Community-Articles/2025-07-17-summar-campaign/post.md new file mode 100644 index 0000000000..d1ac596c29 --- /dev/null +++ b/docs/en/Community-Articles/2025-07-17-summar-campaign/post.md @@ -0,0 +1,28 @@ +**It is going to get hotter with ABP’s Summer Campaign!** + +Since it’s summer time, we wanted to make it even hotter by announcing a summer campaign! From July 21 to 31 we are offering a 20% discount on all ABP licenses. Now is the best time to invest in ABP and start developing asp net applications faster without wasting your time with repetitive tasks. + +## Summer Campaign Terms + +Please review the following terms and conditions carefully. + +* This offer is available for extensions and new purchases. +* Developer seat purchases are also included to the campaign. +* Campaign is available from July 21st to July 31st. +* Discounts are valid on selected licenses only. +* This offer cannot be combined with other promotions or discounts. + +**Why Choose ABP?** + +ABP offers a powerful infrastructure, simplifying modern ASP.NET core development. It helps develop modern ASP.NET applications, including ASP.NET core MVC web applications, blazor front-end projects, and angular .NET Core solutions. + +-The core framework and pre-built modules are designed with microservice architecture in mind. +-ABP provides a module system that allows you to develop reusable application modules. +-Helps implement a DDD based layered architecture and build a maintainable code base. +-Easily manage SaaS applications with integrated multi-tenancy, from database to UI. + +**This Offer Ends July 31, So Hurry Up!** + +This summer campaign is running from July 21 to July 31, so don’t miss your chance. Now is the perfect opportunity to enhance your asp net web development with ABP and benefit from our exclusive features. + +Get Your Discount Now: [https://abp.io/pricing?utm_source=abpwebsite&utm_medium=referral&utm_campaign=summer25_blog](https://abp.io/pricing?utm_source=abpwebsite&utm_medium=referral&utm_campaign=summer25_blog) diff --git a/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/Drawings.pptx b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/Drawings.pptx new file mode 100644 index 0000000000..7a59c08c25 Binary files /dev/null and b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/Drawings.pptx differ diff --git a/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/POST.md b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/POST.md new file mode 100644 index 0000000000..e63db935a5 --- /dev/null +++ b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/POST.md @@ -0,0 +1,480 @@ +# Multi-Tenancy with Separate Databases in .NET and ABP Framework + +[Multi-tenancy](https://abp.io/architecture/multi-tenancy) is a common architectural concept for modern SaaS applications, enabling a single application to serve multiple customers (each known as a tenant) while maintaining data isolation, scalability, and operational efficiency. The "Separate database per tenant" approach offers the highest level of data isolation, making it ideal for scenarios with strict data privacy, security, and performance requirements. + +In this article, we’ll explore how to use this advanced multi-tenancy model using the powerful capabilities of the ABP Framework and the .NET platform. + +> In this article, I will use [ABP Studio](https://abp.io/studio) for creating the application. ABP Studio allows to select "separate database per tenant" option only for [commercial licenses](https://abp.io/pricing). + +## Understanding Database Models for a Multi-Tenant Application + +In the next sections, I will explain various models for database models of a multi-tenant solution: + +* Single (shared) Database Model +* Separate Tenant Databases Model +* Hybrid Multi-Tenant Database Model + +Let's start with the first one... + +### Single (shared) Database Model + +In the shared database model, all the application data stored in a single physical database. In the following diagram, you see different kind of users use the application, and the application stored their data in a main database: + +![single-shared-database](single-shared-database.png) + +This is the default behavior when you [create a new ABP application](https://abp.io/docs/latest/get-started), because it is simple to begin with and proper for most applications. + +In this model, a single database table may contain data of multiple tenants. Each row in these tables have a `TenantId` field which is used to distinguish the tenant data and isolate a tenant's data from other tenant users. To make your entities multi-tenant aware, all you have to do is to implement the `IMultiTenant` interface provided by the ABP Framework. + +Here, is an example `Product` entity that should support multi-tenancy: + +````csharp +using System; +using Volo.Abp.Domain.Entities; +using Volo.Abp.MultiTenancy; + +namespace MtDemoApp +{ + public class Product : AggregateRoot, IMultiTenant //Implementing the interface + { + public Guid? TenantId { get; set; } //Defined by the IMultiTenant interface + public string Name { get; set; } + public float Price { get; set; } + } +} +```` + +In this way, ABP Framework automatically isolates data using the `TenantId` property. You don't need to care about how to set `TenantId` or filter data when you need to query from database - all automated. + +### Separate Tenant Databases Model + +In the separate tenant database model, each tenant has a dedicated physical database (with a separate connection string), as shown below: + +![separate-tenant-database-multi-tenancy](separate-tenant-database-multi-tenancy.png) + +ABP Framework can automatically select the right database from the current user's tenant context. Again, it is completely automated. You just need to set a connection string for a tenant, as we will do later in this article. + +Even each tenant has a separate database, we still need to a main database to store host-side data, like a table of tenants, their connection strings and some other management data for tenants. Also, tenant-independent (or tenant-shared) application data is stored in the main database. + +### Hybrid Multi-Tenant Database Model + +Lastly, you may want to have a hybrid model, where some tenants shares a single database (they don't have separate databases) but some of them have dedicated databases. In the following figure, Tenant C has its own physical database, but all other tenants data stored in the main database of the application. + +![hybrid-database-multi-tenancy](hybrid-database-multi-tenancy.png) + +ABP Framework handles the complexity: If a tenant has a separate database it uses that tenant's database, otherwise it filters the tenant data by the `TenantId` field in shared tables. + +## Understanding the Separate Tenant Schema Approach + +When you create a new ABP solution, it has a single `DbContext` class (for Entity Framework Core) by default. It also includes the necessary EF Core code-first database migrations to create and update the database. As a result of this approach, the main database schema (tables and their fields) will be identical with a tenant database schema. As a drawback of that, tenant databases have some tables that are not meaningful and not used. For example, Tenants table (a list of tenants) will be created in the tenant database, but will never be used (because tenant list is stored in the main database). + +As a solution to that problem, ABP Studio provides a "Use separate tenant schema" option on the Multi-Tenancy step of the solution creation wizard: + +![separate-tenant-schema-option](separate-tenant-schema-option.png) + +This option is only available for the [Layered Monolith (optionally Modular) Solution Template](https://abp.io/docs/latest/get-started/layered-web-application). We don't provide that option in other templates, because: + +* [Single-Layer](https://abp.io/docs/latest/get-started/single-layer-web-application) template is recommended for more simpler applications with an easy-to-understand architecture. We don't want to add these kind of complications in that template. +* [Microservice](https://abp.io/docs/latest/get-started/microservice) template already has a separate database for each service. Having multiple database schema (and multiple `DbContext` classes) for each service makes it over complicated without bringing much value. + +While you can manually convert your applications so they support separate database schema approach (ABP is flexible), it is not recommended to do it for these solution types. + +> Note that "Separate database per tenant" approach is already supported by default for the Single-Layer template too. "Separate tenant schema" is something different as I explained in this section. + +## Creating a new Application + +Follow the *[Get Started tutorial](https://abp.io/docs/latest/get-started/layered-web-application)* to create a new ABP application. Remember to select the "*Use separate tenant schema*" option since I want to demonstrate it in this article. + +## Understanding the DbContext Structure + +When you open the solution in your IDE, you will see the following structure under the `.EntityFrameworkCore` project: + +![multi-tenancy-dbcontext-structure](multi-tenancy-dbcontext-structure.png) + +There are 3 DbContext-related classes here (MtDemoApp is your application name): + +* `MtDemoAppDbContext` class is used to map entities for the main (host + shared) database. +* `MtDemoAppTenantDbContext` class is used to map entities for tenant that have separate physical databases. +* `MtDemoAppDbContextBase` is an abstract base class for the classes explained above. In this way, you can configure common mapping logic here. + +Let's see these classes a bit closer... + +### The Main `DbContext` Class + +Here the main `DbContext` class: + +````csharp +public class MtDemoAppDbContext : MtDemoAppDbContextBase +{ + public MtDemoAppDbContext(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder builder) + { + builder.SetMultiTenancySide(MultiTenancySides.Both); + + base.OnModelCreating(builder); + } +} +```` + +* It inherits from the `MtDemoAppDbContextBase` as I mentioned before. So, any configuration made in the base class is also valid here. +* `OnModelCreating` overrides the base method and sets the multi-tenancy side as `MultiTenancySides.Both`. `Both` means this database can store host data as well as tenant data. This is needed because we store data in this database for the tenants who don't have a separate database. + +### The Tenant `DbContext` class + +Here is the tenant-specific `DbContext` class: + +````csharp +public class MtDemoAppTenantDbContext : MtDemoAppDbContextBase +{ + public MtDemoAppTenantDbContext(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder builder) + { + builder.SetMultiTenancySide(MultiTenancySides.Tenant); + + base.OnModelCreating(builder); + } +} +```` + +The only difference is that we used `MultiTenancySides.Tenant` as the multi-tenancy side here, since this `DbContext` will only have entities/tables for tenants that have separate databases. + +### The Base `DbContext` Class + +Here is the base `DbContext` class: + +````csharp +public abstract class MtDemoAppDbContextBase : AbpDbContext + where TDbContext : DbContext +{ + + public MtDemoAppDbContextBase(DbContextOptions options) + : base(options) + { + + } + + protected override void OnModelCreating(ModelBuilder builder) + { + base.OnModelCreating(builder); + + /* Include modules to your migration db context */ + + builder.ConfigurePermissionManagement(); + builder.ConfigureSettingManagement(); + builder.ConfigureBackgroundJobs(); + builder.ConfigureAuditLogging(); + builder.ConfigureIdentityPro(); + builder.ConfigureOpenIddictPro(); + builder.ConfigureFeatureManagement(); + builder.ConfigureLanguageManagement(); + builder.ConfigureSaas(); + builder.ConfigureTextTemplateManagement(); + builder.ConfigureBlobStoring(); + builder.ConfigureGdpr(); + + /* Configure your own tables/entities inside here */ + + //builder.Entity(b => + //{ + // b.ToTable(MtDemoAppConsts.DbTablePrefix + "YourEntities", MtDemoAppConsts.DbSchema); + // b.ConfigureByConvention(); //auto configure for the base class props + // //... + //}); + + //if (builder.IsHostDatabase()) + //{ + // /* Tip: Configure mappings like that for the entities only + * available in the host side, + // * but should not be in the tenant databases. */ + //} + } +} +```` + +This `DbContext` class configures database mappings for all the [application modules](https://abp.io/docs/latest/modules) used by this application by calling their extension methods, like `builder.ConfigureBackgroundJobs()`. Each of these extension methods are defined as multi-tenancy aware and care about what you've set for the multi-tenancy side. + +### Where to Configure Your Entities? + +You can configure your entity mappings in the `OnModelCreating` method in any of the `DbContext` classes that was explained: + +* If you configure in the main `DbContext` class, these configuration will be valid only for the main database. So, don't configure tenant-related configuration here, otherwise, it won't be applied for the tenants who have separate databases. +* If you configure in the tenant `DbContext` class, it will be valid only for the tenants with separate databases. You rarely need to do that. You typically want to make same configuration in the base `DbContext` to support hybrid scenarios (some tenants use the main (shared) database and some tenants have separate databases). +* If you configure in the base `DbContext` class, it will be valid for the main database and tenant databases. You typically define tenant-related configuration here. That means, if you have a multi-tenant `Product` entity, then you should define its EF Core database mapping configuration here, so the Products table is created in the main database as well as in the tenant databases. + +The recommended approach is to configure all the mapping in the base class, but add controls like `builder.IsHostDatabase()` and `builder.IsTenantDatabase()` to conditionally configure the mappings: + +![builder-check-tenant-side](builder-check-tenant-side.png) + +## Adding Database Migrations + +In this section, I will show how to configure your entity mappings, generate database migrations and apply to the database. + +### Defining an Entity + +Let's define a `Product` entity in the `.Domain` layer of your application: + +````csharp +using System; +using Volo.Abp.Domain.Entities; +using Volo.Abp.MultiTenancy; + +namespace MtDemoApp +{ + public class Product : AggregateRoot, IMultiTenant + { + public Guid? TenantId { get; set; } + public string Name { get; set; } + public float Price { get; set; } + } +} +```` + +### Configuring the Database Mapping + +Open the `MtDemoAppDbContextBase` class and add the following property to the class: + +````csharp +public DbSet Products { get; set; } +```` + +Then add the following mapping code inside the `OnModelCreating` method (after all other existing code): + +````csharp +builder.Entity(b => +{ + b.ToTable(MtDemoAppConsts.DbTablePrefix + "Products", MtDemoAppConsts.DbSchema); + b.ConfigureByConvention(); //auto-configure for the base class props + b.Property(x => x.Name).IsRequired().HasMaxLength(100); +}); +```` + +We made the configuration in the base class since the `Products` table should be created in all databases, not only in the main database. + +>`DbTablePrefix` and `DbSchema` are optional and configurable in your application. You can change or remove them. + +### Add a New Database Migration for the Main Database + +To add a new EF Core database migration, we can use ABP Studio UI or EF Core command-line commands. I will show both of these approaches here. + +#### Using the ABP Studio "Add Migrations" UI + +You can right-click the `.EntityFrameworkCore` package in the ABP Studio's *Solution Explorer* panel and select *EF Core CLI* -> *Add Migration* command as shown below: + +![abp-studio-add-migration](abp-studio-add-migration.png) + +You set a migration name on the opened dialog: + +![abp-studio-add-migration-set-name](abp-studio-add-migration-set-name.png) + +If you select the *Update Database* checkbox it will apply changes to the database after generating the migration code. + +Lastly, select the main DbContext class for this migration: + +![abp-studio-add-migration-select-dbcontext](abp-studio-add-migration-select-dbcontext.png) + +This dialog is shown when your application has multiple `DbContext` classes. Once you click the *OK* button, a new migration class is added under the `Migrations` folder of the `.EntityFrameworkCore` project (you can see in your coding editor): + +![added-product-entity-migration-main-context](added-product-entity-migration-main-context.png) + +Since we selected the *Update Database* option, the database table is also created. The following screenshot shows the `AppProducts` table (`App` is the default prefix for your tables, but you can change or remove it) in Microsoft SQL Server Management Studio: + +![product-database-table](product-database-table.png) + +#### Using a Command-Line Terminal + +If you prefer to use the command-line terminal (instead of ABP Studio UI), open a command-line terminal in the directory of the `.EntityFrameworkCore` project. As a shortcut, you can right-click the `.EntityFrameworkCore` project in ABP Studio, then select *Open with* -> *Terminal* command as shown in the following figure: + +![abp-studio-open-with-terminal](abp-studio-open-with-terminal.png) + +Then you can use the [EF Core command-line tool](https://learn.microsoft.com/en-us/ef/core/cli/dotnet) to add a new database migration: + +````bash +dotnet ef migrations add "Added_Product_Entity" --context MtDemoAppDbContext +```` + +It is important to set the `--context` parameter since we have two DbContext classes in the same project. + +After adding the migration, you can update the database: + +````bash +dotnet ef database update "Added_Product_Entity" --context MtDemoAppDbContext +```` + +> If you are using Visual Studio, you can also use the [Package Manager Console](https://learn.microsoft.com/en-us/ef/core/cli/powershell) inside your IDE to add migrations and update the database. + +### Add a New Database Migration for the Tenant Database + +We added a database migration for the main (shared) database. We also need to add a new database migration for tenants who have separate databases. + +This time, no need to configure the DbContext since we did it in the base DbContext class, so it is valid for both of the DbContext classes. Just right-click the `.EntityFrameworkCore` package in the ABP Studio's *Solution Explorer* panel and select *EF Core CLI* -> *Add Migration* command as shown below: + +![abp-studio-add-migration](abp-studio-add-migration.png) + +You can set the same or a different migration name here: + +![abp-studio-add-migration-set-name](abp-studio-add-migration-set-name.png) + +The important part is to select the Tenant DbContext in the next dialog, because we want to change the tenant database this time: + +![abp-studio-context-selection](abp-studio-context-selection.png) + +After clicking the *OK* button, it will add a new database migration class, but this time to the `TenantMigrations` folder: + +![added-product-entity-migration-tenant-context](added-product-entity-migration-tenant-context.png) + +ABP Studio is smart enough to select the right folder name for the new migration by mapping with the DbContext name. However, you could manually type `TenantMigrations` in the *Output directory* textbox. + +Since we selected the *Update Database* option, it also applied changes to the database. But, which database? Interestingly, it automatically creates a second database for tenants with the project name + `_Tenant` suffix: + +![tenant-database](tenant-database.png) + +> This new database is never used on runtime or production. It is only created to allow you to see the schema (tables and their fields) on development time to be sure that everything is as expected. As you see, some tables (like `Saas*` and `OpenIddict*`) are not available in that database, since they are used on the host side and only necessary to be in the main database. +> +> So, where is the real tenant database? If a tenant's database is dedicated (separate), it is created on runtime as I will explain in the *Managing Tenant Databases and Connection Strings* section later. + +You can see that database's connection string in the `appsettings.development.json` file of the `.DbMigrator` project in the solution. If you want to understand how it works, you can check source code of the `DbContextFactory` classes in the `.EntityFrameworkCore` project: + +![dbcontext-factories](dbcontext-factories.png) + +These factory classes are used to create `DbContext` instances when you execute *Add Migration* and *Update Database* commands. + +## Managing Tenant Databases and Connection Strings + +Until now, we even didn't run the application. It is the time to do it. + +### Running the Application with ABP Studio + +You can run the `.Web` project in your IDE. But I prefer to use ABP Studio's *[Solution Runner](https://abp.io/docs/latest/studio/running-applications)* feature here. You can open the *Solution Runner* panel in *ABP Studio* and click the play icon near to the solution root (`MyDemoApp`): + +![abp-studio-solution-runner](abp-studio-solution-runner.png) + +Once the application runs (and you see the blue link icon near to it), right click and select the *Browse* command: + +![abp-studio-browse](abp-studio-browse.png) + +It will open the application's UI in the built-in browser of ABP Studio. You can Login the application (with `admin` as user name and `1q2w3E*` as the default password) and navigate to the *Saas* -> *Tenants* page. + +### Creating a New Tenant with the Shared Database + +The *Tenants* page of the [SaaS module](https://abp.io/modules/Volo.SaaS) is shown below: + +![abp-saas-tenants-page](abp-saas-tenants-page.png) + +As you see, there is no tenant at the beginning. I can click the *+ New tenant* button to create the first tenant: + +![new-tenant-dialog-1](new-tenant-dialog-1.png) + +On this screen, we can set the base tenant information. If you click the *Database connection strings* tab, you can see the following UI: + +![new-tenant-dialog-conn-string-1](new-tenant-dialog-conn-string-1.png) + +For this first tenant, I will keep it as default and use the shared (main) database for this tenant's data. After clicking the *Save* button, the tenant is created and an initial [data seed](https://abp.io/docs/latest/framework/infrastructure/data-seeding) operation is automatically performed for us. To see an example, you can open the database, show rows of the `AbpUsers` table: + +![users-table-new-tenant](users-table-new-tenant.png) + +As you see, a new `admin` user has been created with a `TenantId`. The first row is the `admin` user of the host side. So, ABP allows to define same user name in different tenants, because their data (users in this example) are completely isolated from each other. + +### Sign in with the new Tenant + +We created a new tenant. In this step, we will sign in with the new tenant's `admin` user to see the application UI by that new tenant. To do that, we should logout from the host admin user first. Click the user name (`admin`) on the top right area of the application and select the *Log out* command: + +![user-logout](user-logout.png) + +Click the *Login* button again, which redirects you to the *Login* page: + +![user-login](user-login.png) + +In this page, click the *switch* button near to the *TENANT* selection area and type `acme` as *Name*: + +![switch-tenant-dialog](switch-tenant-dialog.png) + +Once you click the *Save* button, you are now in the acme tenant's context. You can see it on the *TENANT* selection area: + +![tenant-acme-name](tenant-acme-name.png) + +> This kind of tenant switch feature is very useful in development to quickly change tenants to test your application. However, in production, you typically want to use subdomain/domain names or another mechanism to determine tenants automatically. When you configure domain based resolution, the tenant selection area is automatically disappears from the login page. You can check the [multi-tenant document](https://abp.io/docs/latest/framework/architecture/multi-tenancy) to learn how to configure it. + +After switching to the `acme` tenant, we can use `admin` as user name and the password you set during the tenant creation (I had set it as `1q2w3E*`) to login to the application. + +Here a screenshot from the *Roles* page after signing in as the `acme` tenant's `admin` user: + +![acme-tenant-screen](acme-tenant-screen.png) + +> Notice that each tenant has its own roles, users, permissions, and other data. If you change roles here, it doesn't affect other tenants or the host side. +> +> Also, you can see that there are less menu items compared to host side. For example, tenant management page is not available for tenants as you can expect. + +### Switch Back to the Host Side + +To switch back to the host side to add a new tenant, logout from the application, click the *Login* button again to open the login page and then again click the *switch* button to change the current tenant context: + +![switch-host-side](switch-host-side.png) + +In this dialog, clear the *Name* field and then *Save* the dialog to switch back to the host side. Then you can use the standard `admin` user name with `1q2w3E*` password to login to the application as the host administrator. + +### Creating a New Tenant with a Separate Database + +Finally, we came to the point that we will create a new tenant with a separate, dedicated database. Open the *Tenants* page of the SaaS module and click the *+ New tenant* button: + +![new-tenant-dialog-2](new-tenant-dialog-2.png) + +Just fill these information as you wish, then open the *Database connection strings* tab: + +![new-tenant-dialog-conn-string-2](new-tenant-dialog-conn-string-2.png) + +Uncheck the *Use the shared database* option and set a connection string to the *Default connection string* for this tenant. I used `Server=(LocalDb)\MSSQLLocalDB;Database=MtDemoApp_Volosoft;Trusted_Connection=True;TrustServerCertificate=true` as the connection string value. The database name is `MtDemoApp_Volosoft`. You can Test the connection string to be sure that it is a valid connection string. + +Once you click the *Save* button, the new tenant is created, a new database is created on the fly, all the database migrations are applied and the initial data seed is performed. You can open the SQL Server Management Studio to see the new database: + +![separate-database](separate-database.png) + +You can check the tables (e.g. `AbpUsers`) to see that only this new tenant's data is stored in this database. To test the application, switch to the Volosoft tenant (as like explained in the *Sign in with the new Tenant* section before), create a new role or user and check the database. + +## Migrating Existing Tenant Databases + +In the previous section, we've seen that a tenant database is automatically created on runtime if you set a connection string for that tenant. Also, all the current migrations are automatically applied to the database, so it becomes up to date. + +But what about existing tenant databases when a new migration is added to the application? Maybe you have a few tenants with their separate databases, or you may have thousands of tenants with separate databases. How will you apply database schema changes to all of these databases? + +The startup template comes with a solution to this problem. There is a `.DbMigrator` console application in the solution that is responsible to apply schema (table and their fields) changes to all of the databases in the system (the main database and all the separate tenant databases). It also executes the data seeding if seed data is available. All you need to do is to execute this application on your production environment while deploying a new version of your application (of course, it is also very useful in the development environment). It checks and upgrades all the databases before the new version of your application is deployed. + +Here is the console log screen when I run the `.DbMigrator` application on my development environment: + +![dbmigrator-logs](dbmigrator-logs.png) + +As you can see in the logs, it first migrates for the main (host) database, then migrates the tenant databases one by one. It doesn't make schema migration for the `acme` tenant since it has not a separate database, but uses the main database. + +In brief, when you make changes on your entity classes; + +1) Add a new migration for the main DbContext class as I explained in this article. +2) Add a new migration for the tenant DbContext class as I explained in this article. +3) Run the `.DbMigrator` application in your development environment to ensure all the databases are up to date. +4) When you deploy your application to production or test environments, remember to run the `.DbMigrator` application first, then update your application. Or better, setup a CI/CD pipeline that automates this process. You can run the `.DbMigrator` every time while deploying the application, regardless of whether there is a schema change or not. + +> If you have too many tenants with separate database, then the migration process may take too much time. `.DbMigrator` provides the fundamental solution. But for more advanced scenarios or bigger systems, you can always develop your own solution. Just check the `.DbMigrator` application to understand how it was implemented. All the necessary code located in your solution, so you can easily understand and freely customize. + +## Conclusion + +In this article, I covered two important aspects of multi-tenant application development: + +* How ABP startup templates provide a multi-tenant application setup, so some tenants may store their data in a single (main, shared) database while some others may have their own dedicated database. +* Demonstrate how it can manage database migration process on the fly for multiple databases. + +I started by defining different database models for multi-tenant applications (Single database, separate databases, and hybrid), showed how to create an ABP application that supports hybrid model, explained the DbContext structure that is coming with the solution template, demonstrated how to define entities, create and apply database migrations in such an application. + +I hope this article gives you a good understanding the problem and the solution provided by the ABP Framework. Please write your questions or comments under this article. + +Enjoy coding! :) + +## Further Reading + +* [ABP Multi-Tenancy document](https://abp.io/docs/latest/framework/architecture/multi-tenancy) +* [Multi-Tenancy Architecture with .NET](https://abp.io/architecture/multi-tenancy) \ No newline at end of file diff --git a/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/abp-saas-tenants-page.png b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/abp-saas-tenants-page.png new file mode 100644 index 0000000000..9f3a0ce5ca Binary files /dev/null and b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/abp-saas-tenants-page.png differ diff --git a/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/abp-studio-add-migration-select-dbcontext.png b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/abp-studio-add-migration-select-dbcontext.png new file mode 100644 index 0000000000..17b89bee80 Binary files /dev/null and b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/abp-studio-add-migration-select-dbcontext.png differ diff --git a/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/abp-studio-add-migration-set-name.png b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/abp-studio-add-migration-set-name.png new file mode 100644 index 0000000000..9ff5ea75f4 Binary files /dev/null and b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/abp-studio-add-migration-set-name.png differ diff --git a/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/abp-studio-add-migration.png b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/abp-studio-add-migration.png new file mode 100644 index 0000000000..fe59e93230 Binary files /dev/null and b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/abp-studio-add-migration.png differ diff --git a/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/abp-studio-browse.png b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/abp-studio-browse.png new file mode 100644 index 0000000000..3134203343 Binary files /dev/null and b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/abp-studio-browse.png differ diff --git a/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/abp-studio-context-selection.png b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/abp-studio-context-selection.png new file mode 100644 index 0000000000..dc1a993088 Binary files /dev/null and b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/abp-studio-context-selection.png differ diff --git a/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/abp-studio-open-with-terminal.png b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/abp-studio-open-with-terminal.png new file mode 100644 index 0000000000..17dd1fa4dd Binary files /dev/null and b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/abp-studio-open-with-terminal.png differ diff --git a/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/abp-studio-solution-runner.png b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/abp-studio-solution-runner.png new file mode 100644 index 0000000000..a246d01c81 Binary files /dev/null and b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/abp-studio-solution-runner.png differ diff --git a/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/acme-tenant-screen.png b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/acme-tenant-screen.png new file mode 100644 index 0000000000..65f7aa5b48 Binary files /dev/null and b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/acme-tenant-screen.png differ diff --git a/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/added-product-entity-migration-main-context.png b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/added-product-entity-migration-main-context.png new file mode 100644 index 0000000000..a727bcea86 Binary files /dev/null and b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/added-product-entity-migration-main-context.png differ diff --git a/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/added-product-entity-migration-tenant-context.png b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/added-product-entity-migration-tenant-context.png new file mode 100644 index 0000000000..f77c5e1e75 Binary files /dev/null and b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/added-product-entity-migration-tenant-context.png differ diff --git a/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/builder-check-tenant-side.png b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/builder-check-tenant-side.png new file mode 100644 index 0000000000..8b913d4292 Binary files /dev/null and b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/builder-check-tenant-side.png differ diff --git a/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/dbcontext-factories.png b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/dbcontext-factories.png new file mode 100644 index 0000000000..b7f72d9a32 Binary files /dev/null and b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/dbcontext-factories.png differ diff --git a/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/dbmigrator-logs.png b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/dbmigrator-logs.png new file mode 100644 index 0000000000..648d9ccaa8 Binary files /dev/null and b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/dbmigrator-logs.png differ diff --git a/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/hybrid-database-multi-tenancy.png b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/hybrid-database-multi-tenancy.png new file mode 100644 index 0000000000..45930e7fb9 Binary files /dev/null and b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/hybrid-database-multi-tenancy.png differ diff --git a/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/multi-tenancy-dbcontext-structure.png b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/multi-tenancy-dbcontext-structure.png new file mode 100644 index 0000000000..5bb7ad8410 Binary files /dev/null and b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/multi-tenancy-dbcontext-structure.png differ diff --git a/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/new-tenant-dialog-1.png b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/new-tenant-dialog-1.png new file mode 100644 index 0000000000..76a2a33087 Binary files /dev/null and b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/new-tenant-dialog-1.png differ diff --git a/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/new-tenant-dialog-2.png b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/new-tenant-dialog-2.png new file mode 100644 index 0000000000..614689552e Binary files /dev/null and b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/new-tenant-dialog-2.png differ diff --git a/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/new-tenant-dialog-conn-string-1.png b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/new-tenant-dialog-conn-string-1.png new file mode 100644 index 0000000000..5c6aee61d9 Binary files /dev/null and b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/new-tenant-dialog-conn-string-1.png differ diff --git a/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/new-tenant-dialog-conn-string-2.png b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/new-tenant-dialog-conn-string-2.png new file mode 100644 index 0000000000..9404e38a26 Binary files /dev/null and b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/new-tenant-dialog-conn-string-2.png differ diff --git a/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/product-database-table.png b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/product-database-table.png new file mode 100644 index 0000000000..58ef8463f2 Binary files /dev/null and b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/product-database-table.png differ diff --git a/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/separate-database.png b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/separate-database.png new file mode 100644 index 0000000000..1ec6c36ad7 Binary files /dev/null and b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/separate-database.png differ diff --git a/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/separate-tenant-database-multi-tenancy.png b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/separate-tenant-database-multi-tenancy.png new file mode 100644 index 0000000000..69c2bb4940 Binary files /dev/null and b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/separate-tenant-database-multi-tenancy.png differ diff --git a/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/separate-tenant-schema-option.png b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/separate-tenant-schema-option.png new file mode 100644 index 0000000000..40c9c42770 Binary files /dev/null and b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/separate-tenant-schema-option.png differ diff --git a/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/single-shared-database.png b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/single-shared-database.png new file mode 100644 index 0000000000..4363dc792e Binary files /dev/null and b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/single-shared-database.png differ diff --git a/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/switch-host-side.png b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/switch-host-side.png new file mode 100644 index 0000000000..e4e5f8943d Binary files /dev/null and b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/switch-host-side.png differ diff --git a/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/switch-tenant-dialog.png b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/switch-tenant-dialog.png new file mode 100644 index 0000000000..2828906d1e Binary files /dev/null and b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/switch-tenant-dialog.png differ diff --git a/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/tenant-acme-name.png b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/tenant-acme-name.png new file mode 100644 index 0000000000..a08537ee4e Binary files /dev/null and b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/tenant-acme-name.png differ diff --git a/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/tenant-database.png b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/tenant-database.png new file mode 100644 index 0000000000..32f7292e3c Binary files /dev/null and b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/tenant-database.png differ diff --git a/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/user-login.png b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/user-login.png new file mode 100644 index 0000000000..39cdac5aaf Binary files /dev/null and b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/user-login.png differ diff --git a/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/user-logout.png b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/user-logout.png new file mode 100644 index 0000000000..ae7885fa0e Binary files /dev/null and b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/user-logout.png differ diff --git a/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/users-table-new-tenant.png b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/users-table-new-tenant.png new file mode 100644 index 0000000000..0433da73b5 Binary files /dev/null and b/docs/en/Community-Articles/2025-07-26-Separate-Tenant-Schema/users-table-new-tenant.png differ diff --git a/docs/en/Community-Articles/2025-07-31-How-to-build-persistent-background-jobs-with-abp-framework-and-quartz/Post.md b/docs/en/Community-Articles/2025-07-31-How-to-build-persistent-background-jobs-with-abp-framework-and-quartz/Post.md new file mode 100644 index 0000000000..eb69c93908 --- /dev/null +++ b/docs/en/Community-Articles/2025-07-31-How-to-build-persistent-background-jobs-with-abp-framework-and-quartz/Post.md @@ -0,0 +1,548 @@ + +# How to Build Persistent Background Jobs with ABP Framework and Quartz + +## Introduction + +In modern SaaS applications, automated background processing is essential for delivering reliable user experiences. Whether you're sending subscription reminders, processing payments, or generating reports, background jobs ensure critical tasks happen on schedule without blocking your main application flow. + +### What is `Quartz.NET`? + +`Quartz.NET` is a powerful, open-source job scheduling library for .NET applications that provides cron-based scheduling for complex time patterns, job persistence across application restarts, clustering support for high-availability scenarios, flexible trigger types, and the ability to pass parameters to jobs through job data maps. It's the de facto standard for enterprise-grade job scheduling in the .NET ecosystem. + +### Quartz Storage Options: In-Memory vs Persistent + +When configuring **Quartz**, you have two primary storage options, each with significant implications for how your application behaves: + +### 🧠 In-Memory Storage (`RAMJobStore`) +- Keeps all job information in application memory. +- **Very fast** – no database overhead. +- **Volatile** – all jobs, triggers, and schedules are lost when the application stops or restarts. +- Best suited for: + - Development environments. + - Scenarios where job loss is acceptable. + +### 🗃️ Persistent Storage (`JobStoreTX` or similar) +- Stores all job information in a database. +- **Reliable** – schedules persist across: + - Application restarts + - Server crashes + - Deployments +- **Supports horizontal scaling** – multiple application instances can share the same job queue. +- **Slight performance overhead** due to database I/O. +- Best choice for: + - Production systems. + - Any scenario where **business continuity and reliability** are critical. + +### How ABP Simplifies Quartz Integration + +ABP handles Quartz configuration, dependency injection, and lifecycle management automatically. Developers define jobs using `QuartzBackgroundWorkerBase` and access services via `ICachedServiceProvider`, following ABP's standard conventions and leveraging optimal service caching for background job scenarios. + +### Benefits of the Integration + +- Full support for ABP’s cross-cutting concerns (e.g., multi-tenancy, localization) +- Robust scheduling powered by Quartz +- Built-in logging, error handling, and performance monitoring +- Scales easily without modifying business logic + +### Real-World Use Case: Subscription Reminders + +In this tutorial, we'll build a subscription reminder system that monitors client subscriptions, identifies those nearing expiration, sends professional email reminders seven days before expiration, tracks reminder history to prevent duplicates, and runs automatically every day at 9:00 AM using Quartz scheduling with PostgreSQL persistence. This system demonstrates how ABP and Quartz work together to solve real business problems with clean, maintainable code that follows enterprise-grade patterns. + +## Installing and Configuring Quartz + +Getting Quartz up and running in an ABP application is straightforward thanks to ABP's dedicated integration package. We'll replace the default background job system with Quartz for persistent job storage and robust scheduling capabilities. + +### Adding the Quartz Package + +The easiest way to add Quartz support to your ABP application is using the ABP CLI. Open a terminal in your project directory and run: + +```bash +abp add-package Volo.Abp.BackgroundWorkers.Quartz +``` + +This command automatically adds the necessary NuGet package reference and updates your module dependencies. The ABP CLI handles all the heavy lifting, ensuring you get the correct version that matches your ABP Framework version. + +### Configuring Quartz for Persistent Storage + +Once the package is installed, you need to configure Quartz to use your database (in my case it is PostgreSQL) for job persistence. This configuration goes in your main module's `PreConfigureServices` method: + +```csharp +[DependsOn( + // ... other dependencies + typeof(AbpBackgroundJobsQuartzModule), + typeof(AbpBackgroundWorkersQuartzModule) +)] +public class MySaaSApplicationModule : AbpModule +{ + public override void PreConfigureServices(ServiceConfigurationContext context) + { + var hostingEnvironment = context.Services.GetHostingEnvironment(); + var configuration = context.Services.GetConfiguration(); + + ConfigureAuthentication(context, configuration); + ConfigureUrls(configuration); + ConfigureImpersonation(context, configuration); + ConfigureQuartz(); // Add this line + } + + private void ConfigureQuartz() + { + PreConfigure(options => + { + options.Properties = new NameValueCollection + { + ["quartz.scheduler.instanceName"] = "QuartzScheduler", + ["quartz.jobStore.type"] = "Quartz.Impl.AdoJobStore.JobStoreTX, Quartz", + ["quartz.jobStore.tablePrefix"] = "qrtz_", + ["quartz.jobStore.dataSource"] = "myDS", + ["quartz.dataSource.myDS.connectionString"] = _configuration.GetConnectionString("Default"), + ["quartz.dataSource.myDS.provider"] = "Npgsql", + ["quartz.serializer.type"] = "json" + }; + }); + } +} +``` + +This configuration tells Quartz to store all job information in your PostgreSQL database using tables prefixed with "qrtz_". The key points are: + +- **Job Store Type**: Uses ADO.NET with transaction support for reliable job persistence +- **Connection String**: Shares your application's existing database connection +- **Table Prefix**: Keeps Quartz tables separate with the "qrtz_" prefix +- **JSON Serialization**: Makes job data readable and debuggable +- **PostgreSQL Provider**: Uses Npgsql for optimal PostgreSQL integration + +When your application starts, ABP automatically initializes the Quartz scheduler with these settings. Any background workers you create will be registered and scheduled automatically, with their state persisted to the database for reliability across application restarts. + +For detailed installation options and advanced configuration scenarios, check the official [ABP documentation.](https://abp.io/docs/latest/framework/infrastructure/background-workers/quartz) + + +## Database Setup for Quartz + +With Quartz configured for persistent storage, we need to create the necessary database tables where Quartz will store job definitions, triggers, and execution history. Rather than running SQL scripts directly against the database, we'll use Entity Framework migrations to maintain consistency with ABP's database management approach. + +### Creating an Empty Migration for Quartz Tables + +Instead of executing raw SQL scripts against the database, we created an empty Entity Framework migration and populated it with the required Quartz table definitions. This approach keeps all database changes within the migration system, ensuring they're version-controlled, repeatable, and consistent across different environments. + +To create the empty migration, we used the standard Entity Framework CLI command: + +```bash +dotnet ef migrations add AddQuartzTables +``` + +This generates a new migration file with empty `Up` and `Down` methods that we can populate with the Quartz table creation scripts. + +### Adding Quartz SQL Schema to Migration + +Once the empty migration was created, we populated it with the PostgreSQL-specific SQL needed to create all Quartz tables. The SQL scripts were obtained from the official Quartz repository, which provides database schema scripts for various database providers: + +```csharp +public partial class AddQuartzTables : Migration +{ + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@" + CREATE TABLE qrtz_job_details ( + sched_name VARCHAR(120) NOT NULL, + job_name VARCHAR(200) NOT NULL, + job_group VARCHAR(200) NOT NULL, + description VARCHAR(250) NULL, + job_class_name VARCHAR(250) NOT NULL, + is_durable BOOLEAN NOT NULL, + is_nonconcurrent BOOLEAN NOT NULL, + is_update_data BOOLEAN NOT NULL, + requests_recovery BOOLEAN NOT NULL, + job_data BYTEA NULL, + PRIMARY KEY (sched_name, job_name, job_group) + ); + + CREATE TABLE qrtz_triggers ( + sched_name VARCHAR(120) NOT NULL, + trigger_name VARCHAR(200) NOT NULL, + trigger_group VARCHAR(200) NOT NULL, + job_name VARCHAR(200) NOT NULL, + job_group VARCHAR(200) NOT NULL, + -- ... additional columns and constraints + PRIMARY KEY (sched_name, trigger_name, trigger_group), + FOREIGN KEY (sched_name, job_name, job_group) REFERENCES qrtz_job_details(sched_name, job_name, job_group) + ); + + -- Additional tables: qrtz_simple_triggers, qrtz_cron_triggers, + -- qrtz_simprop_triggers, qrtz_blob_triggers, qrtz_calendars, + -- qrtz_paused_trigger_grps, qrtz_fired_triggers, qrtz_scheduler_state, qrtz_locks + "); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql(@" + DROP TABLE IF EXISTS qrtz_locks; + DROP TABLE IF EXISTS qrtz_scheduler_state; + -- ... drop all other Quartz tables in reverse order + DROP TABLE IF EXISTS qrtz_triggers; + DROP TABLE IF EXISTS qrtz_job_details; + "); + } +} +``` + +The complete SQL scripts for all supported database providers, including PostgreSQL, MySQL, SQL Server, and others, can be found in the official `Quartz.NET` repository. You should use the script that matches your specific database provider and version requirements. + +### Why Use Migrations Instead of Direct SQL Scripts? + +This migration-based approach offers several important advantages over running SQL scripts directly: + +**Version Control Integration**: The migration becomes part of your codebase, tracked in source control alongside your application changes. This means every developer and deployment environment gets the exact same database schema. + +**Rollback Capability**: The `Down` method provides a clean way to remove Quartz tables if needed, something that's much harder to manage with standalone SQL scripts. + +**Environment Consistency**: Whether you're setting up a development machine, staging server, or production deployment, running DBMigrator or `dotnet ef database update` command ensures the same schema is created everywhere. + +**Integration with ABP's Database Management**: This approach aligns perfectly with how ABP manages all other database changes, keeping your database evolution strategy consistent. + +The Quartz tables created by this migration handle all aspects of job persistence - from storing job definitions and triggers to tracking execution history and managing scheduler state. With these tables in place, your Quartz scheduler can reliably persist jobs across application restarts and coordinate work across multiple application instances if needed. + +After creating this migration, running DBMigrator `dotnet ef database update` will create all the necessary Quartz infrastructure in your PostgreSQL database, ready to store and manage your background jobs. + +For complete SQL scripts for your specific database provider, visit the official [Quartz documentation.](https://www.quartz-scheduler.net/documentation/quartz-3.x/quick-start.html#creating-and-initializing-database) +## Building the Business Logic + +Before implementing our Quartz background job, we needed to create the essential business entities and services that our subscription reminder system would work with. Since this article focuses on Quartz integration rather than general ABP development patterns, we'll keep this section brief and move quickly to the background job implementation. + +### Core Entities and Services + +For our subscription reminder system, we created the following core components: + +**Entities:** +- **`Client`**: Represents customers with subscription information (Name, Email, SubscriptionEnd, IsActive) +- **`ReminderLog`**: Tracks when reminder emails have been sent to prevent duplicates + +**Application Services:** +- **`ClientAppService`**: Handles CRUD operations and provides methods to find clients with expiring subscriptions +- **`ReminderLogAppService`**: Manages reminder history and prevents duplicate notifications +- **`EmailService`**: Sends professional HTML reminder emails via SMTP + +**Data Transfer Objects (DTOs):** +- Complete set of DTOs for both entities following ABP conventions +- Input/output DTOs for all service operations + +### Business Logic Overview + +The system follows standard ABP patterns with entities inheriting from `FullAuditedAggregateRoot`, services implementing `ICrudAppService` interfaces, and proper AutoMapper configurations for entity-DTO mapping. We also included a data seeder to create sample clients for testing purposes. + +The key business methods our background job will use are: +- `GetExpiringClientsAsync()` - Finds clients whose subscriptions expire in the next 7 days +- `CreateAsync()` - Logs when a reminder has been sent +- `SendSubscriptionExpiryReminderAsync()` - Sends professional email reminders + +### Focus on Background Operations + +Rather than diving deep into ABP entity creation, repository patterns, or service layer implementation details, we'll move directly to the heart of this article: implementing robust background jobs with Quartz. The entities and services we created simply provide the business context for our background job to operate within. + +The real value of this tutorial lies in showing how ABP's `QuartzBackgroundWorkerBase` integrates seamlessly with your business logic to create reliable, persistent background operations that survive application restarts and scale across multiple instances. + +Let's now implement the background job that ties everything together and demonstrates the power of ABP + Quartz integration. + + +## Implementing the Background Job (The ABP Way) + +This is where the magic happens. ABP's integration with Quartz provides a clean, powerful way to create background jobs that follow framework conventions while leveraging Quartz's robust scheduling capabilities. Let's dive into how we implemented our subscription reminder job and explore the advanced features ABP provides. + +### Creating a QuartzBackgroundWorkerBase Job + +Instead of implementing Quartz's raw `IJob` interface, ABP provides `QuartzBackgroundWorkerBase`, which integrates seamlessly with ABP's dependency injection, logging, and lifecycle management systems: + +```csharp +public class SubscriptionExpiryNotifierJob : QuartzBackgroundWorkerBase +{ + public SubscriptionExpiryNotifierJob() + { + // Configure the job to run daily at 9:00 AM + JobDetail = JobBuilder.Create() + .WithIdentity(nameof(SubscriptionExpiryNotifierJob)) + .Build(); + + Trigger = TriggerBuilder.Create() + .WithIdentity(nameof(SubscriptionExpiryNotifierJob)) + .WithCronSchedule("0 0 9 * * ?") // Every day at 9:00 AM + .Build(); + + ScheduleJob = async scheduler => + { + if (!await scheduler.CheckExists(JobDetail.Key)) + { + await scheduler.ScheduleJob(JobDetail, Trigger); + } + }; + } + + public override async Task Execute(IJobExecutionContext context) + { + // Use ICachedServiceProvider for better performance and proper scoping + var serviceProvider = ServiceProvider.GetRequiredService(); + + // These services will be cached and reused throughout the job execution + var clientAppService = serviceProvider.GetRequiredService(); + var reminderLogAppService = serviceProvider.GetRequiredService(); + var emailService = serviceProvider.GetRequiredService(); + + Logger.LogInformation("🔄 Starting subscription expiry notification job..."); + + // 1. Get clients expiring in 7 days + var expiringClients = await clientAppService.GetExpiringClientsAsync(7); + + Logger.LogInformation("📋 Found {Count} clients with expiring subscriptions", expiringClients.Count); + + // 2. Process each client + foreach (var client in expiringClients) + { + await ProcessClientAsync(client, emailService, reminderLogAppService); + } + + Logger.LogInformation("✅ Job completed successfully"); + } +} +``` + +### Key Implementation Features + +**Constructor-Based Configuration**: Unlike traditional Quartz jobs that require external scheduling code, ABP's approach lets you define both the job and its schedule directly in the constructor. This keeps related configuration together and makes the job self-contained. + +**ABP Service Integration**: The `ICachedServiceProvider` gives you access to any service in ABP's dependency injection container, enabling you to use application services, repositories, domain services, or any other ABP component with optimized caching and proper scoping. + +**Built-in Logging**: The `Logger` property provides access to ABP's logging infrastructure, automatically including context like correlation IDs and tenant information in multi-tenant applications. + +**Custom Scheduling Logic**: The `ScheduleJob` property allows you to customize how the job gets registered with Quartz. In our example, we check if the job already exists before scheduling it, preventing duplicate registrations during application restarts. + +### Understanding Quartz Trigger Types + +Quartz provides several trigger types to handle different scheduling requirements. Choosing the right trigger type is crucial for your job's behavior and performance. + +#### CronTrigger - Complex Time-Based Scheduling + +CronTrigger uses cron expressions for sophisticated scheduling patterns. This is what we used for our daily subscription reminders: + +```csharp +// Daily at 9:00 AM +Trigger = TriggerBuilder.Create() + .WithIdentity("DailyReminder") + .WithCronSchedule("0 0 9 * * ?") + .Build(); + +// Every weekday at 2:30 PM +Trigger = TriggerBuilder.Create() + .WithIdentity("WeekdayReport") + .WithCronSchedule("0 30 14 ? * MON-FRI") + .Build(); + +// First day of every month at midnight +Trigger = TriggerBuilder.Create() + .WithIdentity("MonthlyCleanup") + .WithCronSchedule("0 0 0 1 * ?") + .Build(); +``` + +**Cron Expression Format**: `Seconds Minutes Hours Day-of-Month Month Day-of-Week Year(optional)` +- `0 0 9 * * ?` = 9:00 AM every day +- `0 */15 * * * ?` = Every 15 minutes +- `0 0 12 ? * SUN` = Every Sunday at noon + +#### SimpleTrigger - Interval-Based Scheduling + +SimpleTrigger is perfect for jobs that need to run at regular intervals or a specific number of times: + +```csharp +// Run every 30 seconds indefinitely +Trigger = TriggerBuilder.Create() + .WithIdentity("HealthCheck") + .StartNow() + .WithSimpleSchedule(x => x + .WithIntervalInSeconds(30) + .RepeatForever()) + .Build(); + +// Run every 5 minutes, but only 10 times +Trigger = TriggerBuilder.Create() + .WithIdentity("LimitedRetry") + .StartNow() + .WithSimpleSchedule(x => x + .WithIntervalInMinutes(5) + .WithRepeatCount(9)) // 0-based, so 9 = 10 executions + .Build(); + +// One-time execution after 1 hour delay +Trigger = TriggerBuilder.Create() + .WithIdentity("DelayedCleanup") + .StartAt(DateTimeOffset.UtcNow.AddHours(1)) + .Build(); +``` + +#### CalendarIntervalTrigger - Calendar-Aware Intervals + +CalendarIntervalTrigger handles intervals that need to respect calendar boundaries: + +```csharp +// Every month on the same day (handles varying month lengths) +Trigger = TriggerBuilder.Create() + .WithIdentity("MonthlyBilling") + .WithCalendarIntervalSchedule(x => x + .WithIntervalInMonths(1)) + .Build(); + +// Every week, starting Monday +Trigger = TriggerBuilder.Create() + .WithIdentity("WeeklyReport") + .WithCalendarIntervalSchedule(x => x + .WithIntervalInWeeks(1)) + .Build(); +``` + +#### DailyTimeIntervalTrigger - Time Windows + +DailyTimeIntervalTrigger runs jobs within specific time windows on certain days: + +```csharp +// Every 2 hours between 8 AM and 6 PM, Monday through Friday +Trigger = TriggerBuilder.Create() + .WithIdentity("BusinessHoursSync") + .WithDailyTimeIntervalSchedule(x => x + .OnMondayThroughFriday() + .StartingDailyAt(TimeOfDay.HourAndMinuteOfDay(8, 0)) + .EndingDailyAt(TimeOfDay.HourAndMinuteOfDay(18, 0)) + .WithIntervalInHours(2)) + .Build(); +``` + +### Choosing the Right Trigger Type + +For different scenarios, you'd choose different trigger types: + +- **Daily/Weekly/Monthly Operations**: Use **CronTrigger** for maximum flexibility +- **High-Frequency Tasks**: Use **SimpleTrigger** for performance (every few seconds/minutes) +- **Business Calendar Operations**: Use **CalendarIntervalTrigger** for month-end reports, quarterly tasks +- **Business Hours Operations**: Use **DailyTimeIntervalTrigger** for operations that should only run during specific hours + +### Automatic Job Registration + +One of ABP's most powerful features is automatic job discovery and registration. When your application starts, ABP automatically: + +1. **Scans for Background Workers**: ABP discovers all classes inheriting from `QuartzBackgroundWorkerBase` +2. **Registers with DI Container**: Each job is registered as a service in the dependency injection container +3. **Schedules with Quartz**: ABP calls the `ScheduleJob` delegate to register the job with the Quartz scheduler +4. **Handles Lifecycle**: ABP manages starting and stopping jobs with the application lifecycle + +This means you simply create your job class, and ABP handles everything else. No manual registration, no startup code, no configuration files - it just works. + +### Understanding Misfire Handling + +Misfires occur when a scheduled job cannot execute at its intended time, typically due to system downtime, resource constraints, or the scheduler being paused. Quartz provides several misfire instructions to handle these scenarios: + +#### CronTrigger Misfire Instructions + +For cron-based schedules like our daily reminder job, Quartz offers these misfire behaviors: + +**`MisfireInstruction.DoNothing`** (Default): +```csharp +Trigger = TriggerBuilder.Create() + .WithIdentity(nameof(SubscriptionExpiryNotifierJob)) + .WithCronSchedule("0 0 9 * * ?", x => x.WithMisfireHandlingInstructionDoNothing()) + .Build(); +``` +- Skips all missed executions +- Waits for the next naturally scheduled time +- Best for jobs where missing executions is acceptable + +**`MisfireInstruction.FireOnceNow`**: +```csharp +.WithCronSchedule("0 0 9 * * ?", x => x.WithMisfireHandlingInstructionFireAndProceed()) +``` +- Immediately executes one missed job upon recovery +- Then continues with the normal schedule +- Useful when you need to catch up on missed work + +**`MisfireInstruction.IgnoreMisfires`**: +```csharp +.WithCronSchedule("0 0 9 * * ?", x => x.WithMisfireHandlingInstructionIgnoreMisfires()) +``` +- Executes all missed jobs immediately upon recovery +- Can cause a burst of executions after extended downtime +- Use carefully to avoid overwhelming the system + +#### SimpleTrigger Misfire Instructions + +Simple triggers have their own set of misfire behaviors: + +**`MisfireInstruction.FireNow`**: Execute immediately when recovered +**`MisfireInstruction.RescheduleNowWithExistingRepeatCount`**: Start over with remaining repeat count +**`MisfireInstruction.RescheduleNowWithRemainingRepeatCount`**: Continue as if no misfire occurred +**`MisfireInstruction.RescheduleNextWithExistingCount`**: Wait for next interval, keep original repeat count + +### Real-World Misfire Considerations + +For our subscription reminder system, we chose the default `DoNothing` behavior because: + +- **Business Logic**: Sending yesterday's reminder today might confuse customers +- **Duplicate Prevention**: Our job checks for existing reminders, so running late won't cause duplicate emails +- **Resource Management**: We avoid overwhelming the email system after extended downtime + +However, for other scenarios you might choose differently: +- **Financial reporting**: Use `FireOnceNow` to ensure reports are always generated +- **Data synchronization**: Use `IgnoreMisfires` to process all missed sync operations +- **Cache warming**: Use `DoNothing` since stale cache warming provides no value + +### Advanced Job Features + +**Error Handling and Resilience**: Our job implementation includes comprehensive error handling for individual client processing, ensuring one failed email doesn't stop the entire batch: + +```csharp +try +{ + await emailService.SendSubscriptionExpiryReminderAsync(/*...*/); + await LogReminderAsync(client.Id, client.SubscriptionEnd, "Email sent successfully", reminderLogAppService); +} +catch (Exception ex) +{ + Logger.LogError(ex, "❌ Failed to send reminder to {ClientName}", client.Name); + await LogReminderAsync(client.Id, client.SubscriptionEnd, $"Failed: {ex.Message}", reminderLogAppService); +} +``` + +**Duplicate Prevention**: The job checks for existing reminders to prevent sending multiple emails on the same day, even if the job runs multiple times: + +```csharp +private async Task AlreadySentTodayAsync(Guid clientId, IReminderLogAppService reminderLogAppService) +{ + var todayReminders = await reminderLogAppService.GetByClientIdAsync(clientId); + var today = DateTime.UtcNow.Date; + + return todayReminders.Any(r => r.ReminderDate.Date == today); +} +``` + +This implementation demonstrates how ABP's `QuartzBackgroundWorkerBase` provides a clean, powerful foundation for building robust background jobs that integrate seamlessly with your business logic while leveraging Quartz's enterprise-grade scheduling capabilities. + +## Conclusion + +You've successfully built a production-ready subscription reminder system that demonstrates the powerful synergy between ABP Framework and `Quartz.NET`. This isn't just a tutorial example - it's a robust, enterprise-grade solution that handles real business requirements. + +### What We Accomplished + +**✅ Enterprise-Grade Reliability**: PostgreSQL persistence ensures jobs survive restarts and deployments +**✅ ABP Best Practices**: Used `QuartzBackgroundWorkerBase`, `ICachedServiceProvider`, and ABP's logging infrastructure +**✅ Real Business Value**: Automated subscription reminders with duplicate prevention and audit logging +**✅ Flexible Scheduling**: Explored cron expressions, trigger types, and misfire handling strategies + +### The Power of ABP + Quartz Integration + +The combination delivers exceptional value through automatic job discovery, persistent scheduling, built-in dependency injection, and seamless framework integration. You get enterprise reliability with developer-friendly simplicity. + +### Final Thoughts + +Complex background processing doesn't have to be complicated to implement. ABP's thoughtful abstractions combined with Quartz's proven engine create a development experience that's both powerful and enjoyable. + +Whether you're building subscription management, financial reporting, or data synchronization, these patterns provide a solid foundation for reliable, maintainable solutions. + +You can reach sample project's source code from [here](https://github.com/MansurBesleney/MySaaSApplication) + +**Happy coding, and may your background jobs never miss a beat!** 🚀 diff --git a/docs/en/Community-Articles/2025-08-12-Integration-Services-Explained/integration-services.jpeg b/docs/en/Community-Articles/2025-08-12-Integration-Services-Explained/integration-services.jpeg new file mode 100644 index 0000000000..19b1392843 Binary files /dev/null and b/docs/en/Community-Articles/2025-08-12-Integration-Services-Explained/integration-services.jpeg differ diff --git a/docs/en/Community-Articles/2025-08-12-Integration-Services-Explained/post.md b/docs/en/Community-Articles/2025-08-12-Integration-Services-Explained/post.md new file mode 100644 index 0000000000..0e11c505da --- /dev/null +++ b/docs/en/Community-Articles/2025-08-12-Integration-Services-Explained/post.md @@ -0,0 +1,138 @@ +# Integration Services in ABP — What they are, when to use them, and how they behave 🚦 + +If you’ve been building with ABP for a while, you’ve probably used Application Services for your UI and APIs in your .NET and ASP.NET Core apps. Integration Services are similar—but with a different mission: they exist for service-to-service or module-to-module communication, not for end users. + +If you want the formal spec, see the official doc: [Integration Services](../../framework/api-development/integration-services.md). This post is the practical, no-fluff guide. + +## What is an Integration Service? + +An Integration Service is an application service or ASP.NET Core MVC controller marked with the `[IntegrationService]` attribute. That marker tells ABP “this endpoint is for internal communication.” + +- They are not exposed by default (safer for reusable modules and monoliths). +- When exposed, their route prefix is `/integration-api` (so you can easily protect them at your gateway or firewall). +- Auditing is disabled by default for them (less noise for machine-to-machine calls). + +Quick look: + +```csharp +[IntegrationService] +public interface IProductIntegrationService : IApplicationService +{ + Task> GetProductsByIdsAsync(List ids); +} + +public class ProductIntegrationService : ApplicationService, IProductIntegrationService +{ + public Task> GetProductsByIdsAsync(List ids) + { + // fetch and return minimal product info for other services/modules + } +} +``` + +## Are they HTTP endpoints? + +- By default: no (they won’t be reachable over HTTP in the ASP.NET Core routing pipeline). +- If you need them over HTTP (typically for microservices), explicitly enable: + +```csharp +Configure(options => +{ + options.ExposeIntegrationServices = true; +}); +``` + +Once exposed, ABP puts them under `/integration-api/...` instead of `/api/...` in the ASP.NET Core routing pipeline. That’s your hint to restrict them from public internet access. + +## Enable auditing (optional) + +If you want audit logs for integration calls, enable it explicitly: + +```csharp +Configure(options => +{ + options.IsEnabledForIntegrationServices = true; +}); +``` + +## When should you use Integration Services? + +- Internal, synchronous operations between services or modules. +- You need a “thin” API designed for other services (not for UI): minimal DTOs, no view concerns, predictable contracts. +- You want to hide these endpoints from public clients, or only allow them inside your private network or k8s cluster. +- You’re packaging a reusable module that might be used in both monolith and microservice deployments. + +## When NOT to use them + +- Public APIs or anything intended for browsers/mobile apps → use regular application services/controllers. +- Asynchronous cross-service workflows → consider domain events + outbox/inbox; use Integration Services for sync calls. +- Complex, chatty UI endpoints → those belong to your external API surface, not internal integration. + +## Common use-cases and examples + +- Identity lookups across services: an Ordering service needs basic user info from the Identity service. +- Permission checks from another module: a CMS module asks a Permission service for access decisions. +- Product data hydrations: a Cart service needs minimal product details (price, name) from Catalog. +- Internal admin/maintenance operations that aren’t meant for end users but are needed by other services. + +## Example: microservice-to-microservice call + +1) Mark and expose the integration service in the target service: + +```csharp +[IntegrationService] +public interface IUserIntegrationService : IApplicationService +{ + Task FindByIdAsync(Guid id); +} + +Configure(o => o.ExposeIntegrationServices = true); +``` + +2) In the caller service, add an HTTP client proxy only for Integration Services if you like to keep things clean: + +```csharp +services.AddHttpClientProxies( + typeof(TargetServiceApplicationModule).Assembly, + remoteServiceConfigurationName: "TargetService", + asDefaultServices: true, + applicationServiceTypes: ApplicationServiceTypes.IntegrationServices); +``` + +3) Call it just like a local service (ABP’s HTTP proxy handles the wire): + +```csharp +public class OrderAppService : ApplicationService +{ + private readonly IUserIntegrationService _userIntegrationService; + + public OrderAppService(IUserIntegrationService userIntegrationService) + { + _userIntegrationService = userIntegrationService; + } + + public async Task PlaceOrderAsync(CreateOrderDto input) + { + var user = await _userIntegrationService.FindByIdAsync(CurrentUser.GetId()); + // validate user status, continue placing order... + } +} +``` + +## Monolith vs. Microservices + +- Monolith: keep them unexposed and call via DI in-process. You get the same clear contract with zero network overhead. +- Microservices: expose them and route behind your gateway. The `/integration-api` prefix makes it easy to firewall/gateway-restrict. + +## Practical tips + +- Keep integration DTOs lean and stable. These are machine contracts—don’t mix UI concerns. +- Name them clearly (e.g., `UserIntegrationService`) so intent is obvious. +- Guard your ASP.NET Core gateway application: block `/integration-api/*` from public traffic. +- Enable auditing only if you truly need the logs for these calls. + +## Further reading + +- Official docs: [Integration Services](../../framework/api-development/integration-services.md) + +That’s it! Integration Services give you a clean, intentional way to design internal APIs—great in monoliths, essential in microservices. diff --git a/docs/en/Community-Articles/2025-08-19-Best-Practices-Azure-Devops/0-cover.png b/docs/en/Community-Articles/2025-08-19-Best-Practices-Azure-Devops/0-cover.png new file mode 100644 index 0000000000..4864281a32 Binary files /dev/null and b/docs/en/Community-Articles/2025-08-19-Best-Practices-Azure-Devops/0-cover.png differ diff --git a/docs/en/Community-Articles/2025-08-19-Best-Practices-Azure-Devops/1-pipeline-yaml.png b/docs/en/Community-Articles/2025-08-19-Best-Practices-Azure-Devops/1-pipeline-yaml.png new file mode 100644 index 0000000000..63cebfca62 Binary files /dev/null and b/docs/en/Community-Articles/2025-08-19-Best-Practices-Azure-Devops/1-pipeline-yaml.png differ diff --git a/docs/en/Community-Articles/2025-08-19-Best-Practices-Azure-Devops/3-release.png b/docs/en/Community-Articles/2025-08-19-Best-Practices-Azure-Devops/3-release.png new file mode 100644 index 0000000000..a974610907 Binary files /dev/null and b/docs/en/Community-Articles/2025-08-19-Best-Practices-Azure-Devops/3-release.png differ diff --git a/docs/en/Community-Articles/2025-08-19-Best-Practices-Azure-Devops/4-safe-deploy.png b/docs/en/Community-Articles/2025-08-19-Best-Practices-Azure-Devops/4-safe-deploy.png new file mode 100644 index 0000000000..c5771a8447 Binary files /dev/null and b/docs/en/Community-Articles/2025-08-19-Best-Practices-Azure-Devops/4-safe-deploy.png differ diff --git a/docs/en/Community-Articles/2025-08-19-Best-Practices-Azure-Devops/5-summarizing.png b/docs/en/Community-Articles/2025-08-19-Best-Practices-Azure-Devops/5-summarizing.png new file mode 100644 index 0000000000..5afe59b991 Binary files /dev/null and b/docs/en/Community-Articles/2025-08-19-Best-Practices-Azure-Devops/5-summarizing.png differ diff --git a/docs/en/Community-Articles/2025-08-19-Best-Practices-Azure-Devops/POST.md b/docs/en/Community-Articles/2025-08-19-Best-Practices-Azure-Devops/POST.md new file mode 100644 index 0000000000..e297aedb13 --- /dev/null +++ b/docs/en/Community-Articles/2025-08-19-Best-Practices-Azure-Devops/POST.md @@ -0,0 +1,83 @@ +# 🚀 Best Practices for Azure DevOps CI/CD Pipelines + +**CI/CD (Continuous Integration / Continuous Delivery)** is not just fancy tech talk - it's now a must-have for modern software teams. +Microsoft's **Azure DevOps** helps make these processes easier to manage. +But how do you create pipelines that work well for your team? Let's look at some practical tips that will make your life easier. + +--- + +## 1. 📜 Define Your Pipeline as Code + +Don't use the manual setup method that's hard to track. Azure DevOps lets you use **YAML files** for your pipelines, which gives you: + +- A record of all changes - who made them and when +- The same setup across all environments +- The ability to undo changes when something goes wrong + +This stops the common problem where something works on one computer but not another. + +![1-pipeline-yaml](1-pipeline-yaml.png) + +--- + +## 2. 🔑 Store Sensitive Information Safely + +Never put passwords directly in your code, even temporarily. +Each environment should have its own settings, and keep sensitive information in **Azure Key Vault** or **Library Variable Groups**. + +You'll avoid security problems later. + + +--- + +## 3. 🏗️ Keep Building and Releasing Separate + +Think of **Building** like cooking a meal - you prepare everything and package it up. +**Releasing** is like delivering that meal to different people. + +Keeping these as separate steps means: + +- You create your package once, then send it to multiple places +- You save time and resources by not rebuilding the same thing over and over + +![3-release](3-release.png) + +--- + +## 4. 🧪 Add Automatic Testing + +Don't waste time testing the same things manually over and over. +Set up **different types of tests** to run automatically. When tests run every time you make changes: + +- You catch problems before your customers do +- Your software quality stays high without extra manual work + +Azure DevOps has tools to help you see test results easily without searching through technical logs. + +--- + +## 5. 🛡️ Add Safety Checks + +Automatic doesn't mean pushing everything to your live system right away. +For important environments, add **human approval steps** or **automatic checks** like security scans. + +This helps you avoid emergency problems in the middle of the night. + +![4-safe-deploy](4-safe-deploy.png) + + +--- + +## ✅ Conclusion + +Good Azure DevOps pipelines aren't just about automation - they help you feel confident in your process. +Remember these main points: + +✔ Use YAML files to keep everything visible and trackable +✔ Keep passwords and sensitive data in secure storage (not in your code) +✔ Build once, deploy to many places +✔ Let automatic tests find problems before users do +✔ Add safety checks for important systems + +![5-summarizing](5-summarizing.png) +--- diff --git a/docs/en/Community-Articles/2025-08-19-abp-now-supports-angular-standalone-applications/POST.md b/docs/en/Community-Articles/2025-08-19-abp-now-supports-angular-standalone-applications/POST.md new file mode 100644 index 0000000000..1f5c284cb1 --- /dev/null +++ b/docs/en/Community-Articles/2025-08-19-abp-now-supports-angular-standalone-applications/POST.md @@ -0,0 +1,398 @@ +# ABP Now Supports Angular Standalone Applications + +We are excited to announce that **ABP now supports Angular’s standalone component structure** in the latest Studio update. This article walks you through how to generate a standalone application, outlines the migration steps, and highlights the benefits of this shift over traditional module-based architecture. + +--- + +## Why Standalone? + +Angular's standalone component architecture, which is introduced in version 14 and made default in version 19, is a major leap forward for Angular development. Here is why it matters: + +### 🔧 Simplified Project Structure + +Standalone components eliminate the need for `NgModule` wrappers. This leads to: + +- Fewer files to manage +- Cleaner folder organization +- Reduced boilerplate + +Navigating and understanding your codebase becomes easier for everyone on your team. + +### 🚀 Faster Bootstrapping + +Standalone apps simplify app initialization: + +```ts +bootstrapApplication(AppComponent, appConfig); +``` + +This avoids the need for `AppModule` and speeds up startup times. + +### 📦 Smaller Bundle Sizes + +Since components declare their own dependencies, Angular can more effectively tree-shake unused code. Result? Smaller bundle sizes and faster load times. + +### 🧪 Easier Testing & Reusability + +Standalone components are self-contained. They declare their dependencies within the `imports` array, making them: + +- Easier to test in isolation +- Easier to reuse in different contexts + +### 🧠 Clearer Dependency Management + +Standalone components explicitly define what they need. No more hidden dependencies buried in shared modules. + +### 🔄 Gradual Adoption + +You can mix and match standalone and module-based components. This allows for **incremental migration**, reducing risk in larger codebases. Here is the related document for the [standalone migration](https://angular.dev/reference/migrations/standalone). + +--- + +## Getting Started: Creating a Standalone Angular App + +Angular CLI makes it easy to start: + +```bash +ng new my-app +``` + +With Angular 19, new apps follow this bootstrapping model: + +```ts +// main.ts +import { bootstrapApplication } from "@angular/platform-browser"; +import { appConfig } from "./app/app.config"; +import { AppComponent } from "./app/app.component"; + +bootstrapApplication(AppComponent, appConfig).catch((err) => + console.error(err) +); +``` + +The `app.config.ts` file replaces `AppModule`: + +```ts +// app.config.ts +import { ApplicationConfig, provideZoneChangeDetection } from "@angular/core"; +import { provideRouter } from "@angular/router"; +import { routes } from "./app.routes"; + +export const appConfig: ApplicationConfig = { + providers: [ + provideZoneChangeDetection({ eventCoalescing: true }), + provideRouter(routes), + ], +}; +``` + +Routing is defined in a simple `Routes` array: + +```ts +// app.routes.ts +import { Routes } from "@angular/router"; + +export const routes: Routes = []; +``` + +--- + +## ABP Studio Support for Standalone Structure + +Starting with the latest release (insert version number here), ABP Studio fully supports Angular's standalone structure. While the new format is encouraged, module-based structure will continue to be supported for backwards compatibility. + +To try it out, simply update your ABP Studio to create apps with the latest version. + +--- + +## What’s New in ABP Studio Templates? + +When you generate an app using the latest ABP Studio, the project structure aligns with Angular's standalone architecture. + +This migration is split into four parts: + +1. **Package updates** +2. **Schematics updates** +3. **Suite code generation updates** +4. **Template refactors** + +--- + +## Package Migration Details + +Migration has been applied to packages in the [ABP GitHub repository](https://github.com/abpframework/abp/tree/dev/npm/ng-packs/packages). Here is an example from the Identity package. + +### 🧩 Migrating Components + +Components are made standalone, using: + +```bash +ng g @angular/core:standalone +``` + +Example: + +```ts +@Component({ + selector: 'abp-roles', + templateUrl: './roles.component.html', + providers: [...], + imports: [ + ReactiveFormsModule, + LocalizationPipe, + ... + ], +}) +export class RolesComponent implements OnInit { ... } +``` + +### 🛣 Updating Routing + +Old lazy-loaded routes using `forLazy()`: + +```ts +{ + path: 'identity', + loadChildren: () => import('@abp/ng.identity').then(m => m.IdentityModule.forLazy({...})) +} +``` + +Now replaced with: + +```ts +{ + path: 'identity', + loadChildren: () => import('@abp/ng.identity').then(c => c.createRoutes({...})) +} +``` + +### 🧱 Replacing Module Declarations + +The old setup: + +```ts +// identity.module.ts +@NgModule({ + imports: [IdentityRoutingModule, RolesComponent, UsersComponent], +}) +export class IdentityModule {...} +``` + +```ts +//identity-routing.module +const routes: Routes = [...]; +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class IdentityRoutingModule {} +``` + +New setup: + +```ts +// identity-routes.ts +export function provideIdentity(options: IdentityConfigOptions = {}): Provider[] { + return [...]; +} +export const createRoutes = (options: IdentityConfigOptions = {}): Routes => [ + { + path: '', + component: RouterOutletComponent, + providers: provideIdentity(options), + children: [ + { + path: 'roles', + component: ReplaceableRouteContainerComponent, + data: { + requiredPolicy: 'AbpIdentity.Roles', + replaceableComponent: { + key: eIdentityComponents.Roles, + defaultComponent: RolesComponent, + }, + }, + title: 'AbpIdentity::Roles', + }, + ... + ], + }, +]; +``` + +--- + +## ABP Schematics Migration Details + +You can reach details by checking [ABP Schematics codebase](https://github.com/abpframework/abp/tree/dev/npm/ng-packs/packages/schematics). + +### 📚 Library creation + +When you run the `abp create-lib` command, the prompter will ask you the `templateType`. It supports both module and standalone templates. + +```ts +"templateType": { + "type": "string", + "description": "Type of the template", + "enum": ["module", "standalone"], + "x-prompt": { + "message": "Select the type of template to generate:", + "type": "list", + "items": [ + { "value": "module", "label": "Module Template" }, + { "value": "standalone", "label": "Standalone Template" } + ] + } +}, +``` + +--- + +## ABP Suite Code Generation Migration Details + +ABP Suite will also be supporting both structures. If you have a project that is generated with the previous versions, the Suite will detect the structure in that way and generate the related code accordingly. Conversely, here is what is changed for the standalone migration: + +**❌ Discarded module files** + +```ts +// entity-one.module.ts +@NgModule({ + declarations: [], + imports: [EntityOneComponent, EntityOneRoutingModule], +}) +export class EntityOneModule {} +``` + +```ts +// entity-one-routing.module.ts +export const routes: Routes = [ + { + path: "", + component: EntityOneComponent, + canActivate: [authGuard, permissionGuard], + }, +]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule], +}) +export class EntityOneRoutingModule {} +``` + +```ts +// app-routing.module.ts +{ + path: 'entity-ones', + loadChildren: () => + import('./entity-ones/entity-one/entity-one.module').then(m => m.EntityOneModule), +}, +``` + +**✅ Added routes configuration** + +```ts +// entity-one.routes.ts +export const ENTITY_ONE_ROUTES: Routes = [ + { + path: "", + loadComponent: () => { + return import("./components/entity-one.component").then( + (c) => c.EntityOneComponent + ); + }, + canActivate: [authGuard, permissionGuard], + }, +]; +``` + +```ts +// app.routes.ts +{ path: 'entity-ones', children: ENTITY_ONE_ROUTES }, +``` + +--- + +## Template Migration Details + +### 🧭 Routing: `app.routes.ts` + +```ts +// app.routes.ts +import { Routes } from '@angular/router'; + +export const APP_ROUTES: Routes = [ + { + path: '', + pathMatch: 'full', + loadComponent: () => import('./home/home.component').then(m => m.HomeComponent), + }, + { + path: 'account', + loadChildren: () => import('@abp/ng.account').then(m => m.createRoutes()), + }, + ... +]; +``` + +### ⚙ Configuration: `app.config.ts` + +```ts +// app.config.ts +export const appConfig: ApplicationConfig = { + providers: [ + provideRouter(APP_ROUTES), + APP_ROUTE_PROVIDER, + provideAbpCore( + withOptions({ + environment, + registerLocaleFn: registerLocale(), + ... + }) + ), + provideAbpOAuth(), + provideAbpThemeShared(), + ... + ], +}; + +``` + +### 🧼 Removed: `shared.module.ts` + +This file has been removed to reduce unnecessary shared imports. Components now explicitly import what they need—leading to better encapsulation and less coupling. + +--- + +## Common Problems + +You may encounter these common problems that you would need to manage. + +### 1. Missing Imports + +In standalone structure, components must declare all their dependencies in `imports`. Forgetting this often causes template errors. + +### 2. Mixed Structures + +Combining modules and standalone in the same feature leads to confusion. Migrate features fully or keep them module-based. + +### 3. Routing Errors + +Incorrect migration from `forLazy()` to `createRoutes()` or `loadComponent` can break navigation. Double-check route configs. + +### 4. Service Injection + +Services provided in old modules may be missing. Add them in the component’s `providers` or `app.config.ts`. + +### 5. Shared Module Habit + +Reintroducing a shared module reduces the benefits of standalone. Import dependencies directly where needed. + +--- + +## Conclusion + +Angular’s standalone component architecture is a significant improvement for scalability, simplicity, and performance. With latest version of ABP Studio, you can adopt this modern approach with ease—without losing support for existing module-based projects. + +**Ready to modernize your Angular development?** + +Update your ABP Studio today and start building with standalone power! diff --git a/docs/en/Community-Articles/2025-08-25-App-Services-vs-Domain-Services/POST.md b/docs/en/Community-Articles/2025-08-25-App-Services-vs-Domain-Services/POST.md new file mode 100644 index 0000000000..534dc1abe1 --- /dev/null +++ b/docs/en/Community-Articles/2025-08-25-App-Services-vs-Domain-Services/POST.md @@ -0,0 +1,213 @@ +# App Services vs Domain Services: Deep Dive into Two Core Service Types in ABP Framework + +In ABP's layered architecture, we frequently encounter two types of services that appear similar but serve distinctly different purposes: Application Services and Domain Services. Understanding the differences between them is crucial for building clear and maintainable enterprise applications. + +## Architectural Positioning + +In ABP's layered architecture: + +- **Application Services** reside in the application layer and are responsible for coordinating use case execution +- **Domain Services** reside in the domain layer and are responsible for implementing core business logic + +This layered design follows Domain-Driven Design (DDD) principles, ensuring clear separation of business logic and system maintainability. + +## Application Services: Use Case Orchestrators + +### Core Responsibilities + +Application Services are stateless services primarily used to implement application use cases. They act as a bridge between the presentation layer and domain layer, responsible for: + +- **Parameter Validation**: Input validation is automatically handled by ABP using data annotations +- **Authorization**: Checking user permissions and access control using `[Authorize]` attribute or manual authorization checks via `IAuthorizationService` +- **Transaction Management**: Methods automatically run as Unit of Work (transactional by default) +- **Use Case Orchestration**: Organizing and coordinating multiple domain objects to complete specific business use cases +- **Data Transformation**: Handling conversion between DTOs and domain objects using ObjectMapper + +### Design Principles + +1. **DTO Boundaries**: Application service methods should only accept and return DTOs, never directly expose domain entities +2. **Use Case Oriented**: Each method should correspond to a clear user use case +3. **Thin Layer Design**: Avoid implementing complex business logic in application services + +### Typical Execution Flow + +A standard application service method typically follows this pattern: + +```csharp +[Authorize(BookPermissions.Create)] // Declarative authorization +public virtual async Task CreateBookAsync(CreateBookDto input) // input is automatically validated +{ + // Get related data + var author = await _authorRepository.GetAsync(input.AuthorId); + + // Call domain service to execute business logic (if needed) + // You can also use the entity constructor directly if no complex business logic is required + var book = await _bookManager.CreateAsync(input.Title, author, input.Price); + + // Persist changes + await _bookRepository.InsertAsync(book); + + // Return DTO + return ObjectMapper.Map(book); +} +``` + +### Integration Services: Special kind of Application Service + +It's worth mentioning that ABP also provides a special type of application service—Integration Services. They are application services marked with the `[IntegrationService]` attribute, designed for inter-module or inter-microservice communication. + +We have a community article dedicated to integration services: [Integration Services Explained — What they are, when to use them, and how they behave](https://abp.io/community/articles/integration-services-explained-what-they-are-when-to-use-lienmsy8) + +## Domain Services: Guardians of Business Logic + +### Core Responsibilities + +Domain Services implement core business logic and are particularly needed when: + +- **Core domain logic depends on services**: You need to implement logic that requires repositories or other external services +- **Logic spans multiple aggregates**: The business logic is related to more than one aggregate/entity and doesn't properly fit in any single aggregate +- **Complex business rules**: Complex domain rules that don't naturally belong in a single entity + +### Design Principles + +1. **Domain Object Interaction**: Method parameters and return values should be domain objects (entities, value objects), never DTOs +2. **Business Logic Focus**: Focus on implementing pure business rules +3. **Stateless Design**: Maintain the stateless nature of services +4. **State-Changing Operations Only**: Domain services should only define methods that mutate data, not query methods +5. **No Authorization Logic**: Domain services should not perform authorization checks or depend on current user context +6. **Specific Method Names**: Use descriptive, business-meaningful method names (e.g., `AssignToAsync`) instead of generic names (e.g., `UpdateAsync`) + +### Implementation Example + +```csharp +public class IssueManager : DomainService +{ + private readonly IRepository _issueRepository; + + public virtual async Task AssignToAsync(Issue issue, Guid userId) + { + // Business rule: Check user's unfinished task count + var openIssueCount = await _issueRepository.GetCountAsync(i => i.AssignedUserId == userId && !i.IsClosed); + + if (openIssueCount >= 3) + { + throw new BusinessException("IssueTracking:ConcurrentOpenIssueLimit"); + } + + // Execute assignment logic + issue.AssignedUserId = userId; + issue.AssignedDate = Clock.Now; + } +} +``` + +## Key Differences Comparison + +| Dimension | Application Services | Domain Services | +|-----------|---------------------|-----------------| +| **Layer Position** | Application Layer | Domain Layer | +| **Primary Responsibility** | Use Case Orchestration | Business Logic Implementation | +| **Data Interaction** | DTOs | Domain Objects | +| **Callers** | Presentation Layer/Client Applications | Application Services/Other Domain Services | +| **Authorization** | Responsible for permission checks | No authorization logic | +| **Transaction Management** | Manages transaction boundaries (Unit of Work) | Participates in transactions but doesn't manage | +| **Current User Context** | Can access current user information | Should not depend on current user context | +| **Return Types** | Returns DTOs | Returns domain objects only | +| **Query Operations** | Can perform query operations | Should not define GET/query methods | +| **Naming Convention** | `*AppService` | `*Manager` or `*Service` | + +## Collaboration Patterns in Practice + +In real-world development, these two types of services typically work together: + +```csharp +// Application Service +public class BookAppService : ApplicationService +{ + private readonly BookManager _bookManager; + private readonly IRepository _bookRepository; + + [Authorize(BookPermissions.Update)] + public virtual async Task UpdatePriceAsync(Guid id, decimal newPrice) + { + var book = await _bookRepository.GetAsync(id); + + await _bookManager.ChangePriceAsync(book, newPrice); + + await _bookRepository.UpdateAsync(book); + + return ObjectMapper.Map(book); + } +} + +// Domain Service +public class BookManager : DomainService +{ + public virtual async Task ChangePriceAsync(Book book, decimal newPrice) + { + // Domain service focuses on business rules + if (newPrice <= 0) + { + throw new BusinessException("Book:InvalidPrice"); + } + + if (book.IsDiscounted && newPrice > book.OriginalPrice) + { + throw new BusinessException("Book:DiscountedPriceCannotExceedOriginal"); + } + + if (book.Price == newPrice) + { + return; + } + + // Additional business logic: Check if price change requires approval + if (await RequiresApprovalAsync(book, newPrice)) + { + throw new BusinessException("Book:PriceChangeRequiresApproval"); + } + + book.ChangePrice(newPrice); + } + + private Task RequiresApprovalAsync(Book book, decimal newPrice) + { + // Example business rule: Large price increases require approval + var increasePercentage = ((newPrice - book.Price) / book.Price) * 100; + return Task.FromResult(increasePercentage > 50); // 50% increase threshold + } +} +``` + +## Best Practice Recommendations + +### Application Services +- Create a corresponding application service for each aggregate root +- Use clear naming conventions (e.g., `IBookAppService`) +- Implement standard CRUD operation methods (`GetAsync`, `CreateAsync`, `UpdateAsync`, `DeleteAsync`) +- Avoid inter-application service calls within the same module/application +- Always return DTOs, never expose domain entities directly +- Use the `[Authorize]` attribute for declarative authorization or manual checks via `IAuthorizationService` +- Methods automatically run as Unit of Work (transactional) +- Input validation is handled automatically by ABP + +### Domain Services +- Use the `Manager` suffix for naming (e.g., `BookManager`) +- Only define state-changing methods, avoid query methods (use repositories directly in Application Services for queries) +- Throw `BusinessException` with clear, unique error codes for domain validation failures +- Keep methods pure, avoid involving user context or authorization logic +- Accept and return domain objects only, never DTOs +- Use descriptive, business-meaningful method names (e.g., `AssignToAsync`, `ChangePriceAsync`) +- Do not implement interfaces unless there's a specific need for multiple implementations + +## Summary + +Application Services and Domain Services each have their distinct roles in the ABP framework: Application Services serve as use case orchestrators, handling authorization, validation, transaction management, and DTO transformations; Domain Services focus purely on business logic implementation without any infrastructure concerns. Integration Services are a special type of Application Service designed for inter-service communication. + +Correctly understanding and applying these service patterns is key to building high-quality ABP applications. Through clear separation of responsibilities, we can not only build more maintainable code but also flexibly switch between monolithic and microservice architectures—this is precisely the elegance of ABP framework design. + +## References + +- [Application Services](https://abp.io/docs/latest/framework/architecture/domain-driven-design/application-services) +- [Integration Services](https://abp.io/docs/latest/framework/api-development/integration-services) +- [Domain Services](https://abp.io/docs/latest/framework/architecture/domain-driven-design/domain-services) diff --git a/docs/en/Community-Articles/2025-08-25-App-Services-vs-Domain-Services/cover.png b/docs/en/Community-Articles/2025-08-25-App-Services-vs-Domain-Services/cover.png new file mode 100644 index 0000000000..a59643d12a Binary files /dev/null and b/docs/en/Community-Articles/2025-08-25-App-Services-vs-Domain-Services/cover.png differ diff --git a/docs/en/Community-Articles/2025-08-25-AutoMapper-Alternatives/AutoMapper-Alternatives.md b/docs/en/Community-Articles/2025-08-25-AutoMapper-Alternatives/AutoMapper-Alternatives.md new file mode 100644 index 0000000000..e9c4dec4ee --- /dev/null +++ b/docs/en/Community-Articles/2025-08-25-AutoMapper-Alternatives/AutoMapper-Alternatives.md @@ -0,0 +1,338 @@ +# Best Free Alternatives to AutoMapper in .NET — Why We Moved to Mapperly + +--- + +## Introduction + +[AutoMapper](https://automapper.io/) has been one of the most popular mapping library for .NET apps. It has been free and [open-source](https://github.com/LuckyPennySoftware/AutoMapper) since 2009. On 16 April 2025, Jimmy Bogard (the owner of the project) decided to make it commercial for his own reasons. You can read [this announcement](https://www.jimmybogard.com/automapper-and-mediatr-licensing-update/) about what happened to AutoMapper. + + + +### Why AutoMapper’s licensing change matters + +In ABP Framework we have been also using AutoMapper for object mappings. After its commercial transition, we also needed to replace it. Because ABP Framework is open-source and under [LGPL-3.0 license](https://github.com/abpframework/abp#LGPL-3.0-1-ov-file). + +**TL;DR** + +> That's why, **we decided to replace AutoMapper with Mapperly**. + +In this article, we'll discuss the alternatives of AutoMapper so that you can cut down on costs and maximize performance while retaining control over your codebase. Also I'll explain why we chose Mapperly. + +Also AutoMapper uses heavily reflection. And reflection comes with a performance cost if used indiscriminately, and compile-time safety is limited. Let's see how we can overcome these... + + + +## Cost-Free Alternatives to AutoMapper + +Check out the comparison table for key features vs. AutoMapper. + +| | **AutoMapper (Paid)** | **Mapster (Free)** | **Mapperly (Free)** | **AgileMapper (Free)** | **Manual Mapping** | +| ------------------- | ----------------------------------------------- | ----------------------------------------- | -------------------------------------------- | ------------------------------------------- | ------------------------------------------------ | +| **License & Cost** | Paid/commercial | Free, MIT License | Free, MIT License | Free, Apache 2.0 | Free (no library) | +| **Performance** | Slower due to reflection & conventions | Very fast (runtime & compile-time modes) | Very fast (compile-time code generation) | Good, faster than AutoMapper | Fastest (direct assignment) | +| **Ease of Setup** | Easy, but configuration-heavy | Easy, minimal config | Easy, but different approach from AutoMapper | Simple, flexible configuration | Manual coding required | +| **Features** | Rich features, conventions, nested mappings | Strong typed mappings, projection support | Strong typed, compile-time safe mappings | Dynamic & conditional mapping | Whatever you code | +| **Maintainability** | Hidden mappings can be hard to debug | Explicit & predictable | Very explicit, compiler-verified mappings | Readable, good balance | Very explicit, most maintainable | +| **Best For** | Large teams used to AutoMapper & willing to pay | Teams wanting performance + free tool | Teams prioritizing type-safety & performance | Developers needing flexibility & simplicity | Small/medium projects, performance-critical apps | + +There are other libraries such as [**ExpressMapper**](https://github.com/fluentsprings/ExpressMapper) **(308 GitHub stars)**, [**ValueInjecter**](https://github.com/omuleanu/ValueInjecter) **(258 GitHub stars)**, [**AgileMapper**](https://github.com/agileobjects/AgileMapper) **(463 GitHub stars)**. These are not very popular but also free and offer a different balance of simplicity and features. + + + +## Why We Chose Mapperly + +We filtered down all the alternatives into 2: **Mapster** and **Mapperly**. + +The crucial factor was maintainability! As you see from the screenshots below, Mapster is already stopped development. Mapster’s development appears stalled, and its future maintenance is uncertain. On the other hand, Mapperly regularly gets commits. The community support is valuable. + +We looked up different alternatives of AutoMapper also, here's the initial issue of AutoMapper replacement [github.com/abpframework/abp/issues/23243](https://github.com/abpframework/abp/issues/23243). + +The ABP team started Mapperly integration with this initial commit [github.com/abpframework/abp/commit/178d3f56d42b4e5acb7e349470f4a644d4c5214e](https://github.com/abpframework/abp/commit/178d3f56d42b4e5acb7e349470f4a644d4c5214e). And this is our Mapperly integration package : [github.com/abpframework/abp/tree/dev/framework/src/Volo.Abp.Mapperly.](https://github.com/abpframework/abp/tree/dev/framework/src/Volo.Abp.Mapperly.) + +![Community Powers](mapster-mapperly-community-powers.png) + +Here are some considerations for developers who are used to ABP and AutoMapper. + +### [Mapster](https://github.com/MapsterMapper/Mapster): + +* ✔ It is similar to AutoMapper, configuring mappings through code. +* ✔ Support for dependency injection and complex runtime configuration. +* ❌ It is looking additional Mapster maintainers ([Call for additional Mapster maintainers MapsterMapper/Mapster#752](https://github.com/MapsterMapper/Mapster/discussions/752)) + +### [Mapperly](https://github.com/riok/Mapperly): + +- ✔ It generates mapping code(` source generator`) during the build process. +- ✔ It is actively being developed and maintained. +- ❌ It is a static `map` method, which is not friendly to dependency injection. +- ❌ The configuration method is completely different from AutoMapper, and there is a learning curve. + + + +**Mapperly** → generates mapping code at **compile time** using source generators. + +**Mapster** → has two modes: + +- By default, it uses **runtime code generation** (via expression trees and compilation). + +- But with **Mapster.Tool** (source generator), it can also generate mappings at **compile time**. + + + +This is important because it guarantees the mappings are working well. Also they provide type safety and improved performance. Another advantages of these libraries, they eliminate runtime surprises and offer better IDE support. + +--- + +## When Mapperly Will Come To ABP + +Mapperly integration will be delivered with ABP v10. If you have already defined AutoMapper configurations, you can still keep and use them. But the framework will use Mapperly. So there'll be 2 mapping integrations in your app. You can also remove AutoMapper from your final application and use one mapping library: Mapperly. It's up to you! Check [AutoMapper pricing table](https://automapper.io/#pricing). + + + +## Migrating from AutoMapper to Mapperly + +In ABP v10, we will be migrating from AutoMapper to Mapperly. The document about the migration is not delivered by the time I wrote this article, but you can reach the document in our dev docs branch + +* [github.com/abpframework/abp/blob/dev/docs/en/release-info/migration-guides/AutoMapper-To-Mapperly.md](https://github.com/abpframework/abp/blob/dev/docs/en/release-info/migration-guides/AutoMapper-To-Mapperly.md). + +Also for ABP, you can check out how you will define DTO mappings based on Mapperly at this document + +* [github.com/abpframework/abp/blob/dev/docs/en/framework/infrastructure/object-to-object-mapping.md](https://github.com/abpframework/abp/blob/dev/docs/en/framework/infrastructure/object-to-object-mapping.md) + + + +## Mapping Code Examples for AutoMapper, Mapster, AgileMapper + +### AutoMapper vs Mapster vs Mapperly Performance + +Here are concise, drop-in **side-by-side C# snippets** that map the same model with AutoMapper, Mapster, AgileMapper, and manual mapping. + + Models used in all examples + +We'll use these models to show the mapping examples for AutoMapper, Mapster, AgileMapper. + +```csharp +public class Order +{ + public int Id { get; set; } + public Customer Customer { get; set; } = default!; + public List Lines { get; set; } = new(); + public DateTime CreatedAt { get; set; } +} + +public class Customer +{ + public int Id { get; set; } + public string Name { get; set; } = ""; + public string? Email { get; set; } +} + +public class OrderLine +{ + public int ProductId { get; set; } + public int Quantity { get; set; } + public decimal UnitPrice { get; set; } +} + +public class OrderDto +{ + public int Id { get; set; } + public string CustomerName { get; set; } = ""; + public int ItemCount { get; set; } + public decimal Total { get; set; } + public string CreatedAtIso { get; set; } = ""; +} +``` + + + +#### AutoMapper Example (Paid) + +```csharp +public sealed class OrderProfile : Profile +{ + public OrderProfile() + { + CreateMap() + .ForMember(d => d.CustomerName, m => m.MapFrom(s => s.Customer.Name)) + .ForMember(d => d.ItemCount, m => m.MapFrom(s => s.Lines.Sum(l => l.Quantity))) + .ForMember(d => d.Total, m => m.MapFrom(s => s.Lines.Sum(l => l.Quantity * l.UnitPrice))) + .ForMember(d => d.CreatedAtIso,m => m.MapFrom(s => s.CreatedAt.ToString("O"))); + } +} + +// registration +services.AddAutoMapper(typeof(OrderProfile)); + +// mapping +var dto = mapper.Map(order); + +// EF Core projection (common pattern) +var list = dbContext.Orders + .ProjectTo(mapper.ConfigurationProvider) + .ToList(); +``` + +**NuGet Packages:** + +- https://www.nuget.org/packages/AutoMapper +- https://www.nuget.org/packages/AutoMapper.Extensions.Microsoft.DependencyInjection + +--- + +#### Mapperly (Free, Apache-2.0) + +This is compile-time generated mapping. + +```csharp +[Mapper] // generates the implementation at build time +public partial class OrderMapper +{ + // Simple property mapping: Customer.Name -> CustomerName + [MapProperty(nameof(Order.Customer) + "." + nameof(Customer.Name), nameof(OrderDto.CustomerName))] + public partial OrderDto ToDto(Order source); + + // Update an existing target (like MapToExisting) + [MapProperty(nameof(Order.Customer) + "." + nameof(Customer.Name), nameof(OrderDto.CustomerName))] + public partial void UpdateDto(Order source, OrderDto target); + + public OrderDto Map(Order s) + { + var d = ToDto(s); + AfterMap(s, d); + return d; + } + + public void Map(Order source, OrderDto d) + { + UpdateDto(source, d); + AfterMap(source, d); + } + + private void AfterMap(Order source, OrderDto d) + { + d.ItemCount = source.Lines.Sum(l => l.Quantity); + d.Total = source.Lines.Sum(l => l.Quantity * l.UnitPrice); + d.CreatedAtIso = source.CreatedAt.ToString("O"); + } +} + + +//USAGE +var mapper = new OrderMapper(); +var order = new Order +{ + Id = 1, + Customer = new Customer { Id = 1, Name = "John Doe", Email = "johndoe@abp.io" }, + Lines = + [ + new OrderLine {ProductId = 1, Quantity = 2, UnitPrice = 10.0m}, + new OrderLine {ProductId = 2, Quantity = 1, UnitPrice = 20.0m} + ] +}; + +// Map to a new object +var dto = mapper.Map(order); + +// Map to an existing object +var target = new OrderDto(); +mapper.Map(order, target); +``` + +**NuGet Packages:** + +* https://www.nuget.org/packages/Riok.Mapperly/ + +--- + +#### Mapster Example (Free, MIT) + +```csharp +TypeAdapterConfig.NewConfig() + .Map(d => d.CustomerName, s => s.Customer.Name) + .Map(d => d.ItemCount, s => s.Lines.Sum(l => l.Quantity)) + .Map(d => d.Total, s => s.Lines.Sum(l => l.Quantity * l.UnitPrice)) + .Map(d => d.CreatedAtIso, s => s.CreatedAt.ToString("O")); + +// one-off +var dto = order.Adapt(); + +// DI-friendly registration +services.AddSingleton(TypeAdapterConfig.GlobalSettings); +services.AddScoped(); + +// EF Core projection (strong suit) +var mappedList = dbContext.Orders + .ProjectToType() // Mapster projection + .ToList(); +``` + +**NuGet Packages:** + +- https://www.nuget.org/packages/Mapster +- https://www.nuget.org/packages/Mapster.DependencyInjection +- https://www.nuget.org/packages/Mapster.SourceGenerator (for performance improvement) + +--- + +#### AgileMapper Example (Free, Apache-2.0) + +```csharp +var mapper = Mapper.CreateNew(cfg => +{ + cfg.WhenMapping + .From() + .To() + .Map(ctx => ctx.Source.Customer.Name).To(dto => dto.CustomerName) + .Map(ctx => ctx.Source.Lines.Sum(l => l.Quantity)).To(dto => dto.ItemCount) + .Map(ctx => ctx.Source.Lines.Sum(l => l.Quantity * l.UnitPrice)).To(dto => dto.Total) + .Map(ctx => ctx.Source.CreatedAt.ToString("O")).To(dto => dto.CreatedAtIso); +}); + +var mappedDto = mapper.Map(order).ToANew(); +``` + +**NuGet Packages:** + +* https://www.nuget.org/packages/AgileObjects.AgileMapper + + +--- + +#### Manual (Pure) Mapping (no library) + +Straightforward, fastest, and most explicit. Good for simple applications which doesn't need long term maintenance. Hand-written mapping is faster, safer, and more maintainable. And for tiny mappings, you can still use manual mapping. + +* Examples of when manual mapping is better than libraries. + +```csharp +public static class OrderMapping +{ + public static OrderDto ToDto(this Order s) => new() + { + Id = s.Id, + CustomerName = s.Customer.Name, + ItemCount = s.Lines.Sum(l => l.Quantity), + Total = s.Lines.Sum(l => l.Quantity * l.UnitPrice), + CreatedAtIso = s.CreatedAt.ToString("O") + }; +} + +// usage +var dto = order.ToDto(); + +// EF Core projection (best for perf + SQL translation) +var mappedList = dbContext.Orders.Select(s => new OrderDto + { + Id = s.Id, + CustomerName = s.Customer.Name, + ItemCount = s.Lines.Sum(l => l.Quantity), + Total = s.Lines.Sum(l => l.Quantity * l.UnitPrice), + CreatedAtIso = s.CreatedAt.ToString("O") + }).ToList(); +``` + + + +### Conclusion + +If you rely on AutoMapper today, it’s time to evaluate alternatives. For ABP Framework, we chose **Mapperly** due to active development, strong community, and compile-time performance. But your team may prefer **Mapster** for flexibility or even manual mapping for small apps. Your requirements might be different, your project is not a framework so you decide the best one for you. diff --git a/docs/en/Community-Articles/2025-08-25-AutoMapper-Alternatives/cover.png b/docs/en/Community-Articles/2025-08-25-AutoMapper-Alternatives/cover.png new file mode 100644 index 0000000000..d1c7e26166 Binary files /dev/null and b/docs/en/Community-Articles/2025-08-25-AutoMapper-Alternatives/cover.png differ diff --git a/docs/en/Community-Articles/2025-08-25-AutoMapper-Alternatives/mapster-mapperly-community-powers.png b/docs/en/Community-Articles/2025-08-25-AutoMapper-Alternatives/mapster-mapperly-community-powers.png new file mode 100644 index 0000000000..c2e3adbbfc Binary files /dev/null and b/docs/en/Community-Articles/2025-08-25-AutoMapper-Alternatives/mapster-mapperly-community-powers.png differ diff --git a/docs/en/Community-Articles/2025-08-27-Building-a-permission-based-authorization-system-for-net-core/POST.md b/docs/en/Community-Articles/2025-08-27-Building-a-permission-based-authorization-system-for-net-core/POST.md new file mode 100644 index 0000000000..2d6c98102f --- /dev/null +++ b/docs/en/Community-Articles/2025-08-27-Building-a-permission-based-authorization-system-for-net-core/POST.md @@ -0,0 +1,174 @@ +# Building a Permission-Based Authorization System for ASP.NET Core + +In this article, we'll explore different authorization approaches in ASP.NET Core and examine how ABP's permission-based authorization system works. + +First, we'll look at some of the core authorization types that come with ASP.NET Core, such as role-based, claims-based, policy-based, and resource-based authorization. We'll briefly review the pros and cons of each approach. + +Then, we'll dive into [ABP's Permission-Based Authorization System](https://abp.io/docs/latest/framework/fundamentals/authorization#permission-system). This is a more advanced approach that gives you fine-grained control over what users can do in your application. We'll also explore ABP's Permission Management Module, which makes managing permissions through the UI easily. + +## Understanding ASP.NET Core Authorization Types + +Before diving into permission-based authorization, let's examine some of the core authorization types available in ASP.NET Core: + +- **[Role-Based Authorization](https://learn.microsoft.com/en-us/aspnet/core/security/authorization/roles?view=aspnetcore-9.0)** checks if the current user belongs to specific roles (like **"Admin"** or **"User"**) and grants access based on these roles. (For example, only users in the **"Manager"** role can access the employee salary management page.) + +- **[Claims-Based Authorization](https://learn.microsoft.com/en-us/aspnet/core/security/authorization/claims?view=aspnetcore-9.0)** uses key-value pairs (claims) that describe user attributes, such as age, department, or security clearance. (For example, only users with a **"Department=Finance"** claim can view financial reports.) This provides more granular control but requires careful claim management (such as grouping claims under policies). + +- **[Policy-Based Authorization](https://learn.microsoft.com/en-us/aspnet/core/security/authorization/policies?view=aspnetcore-9.0)** combines multiple requirements (roles, claims, custom logic) into reusable policies. It offers flexibility and centralized management, and **this is exactly why ABP's permission system is built on top of it!** (We'll discuss this in more detail later.) + +- **[Resource-Based Authorization](https://learn.microsoft.com/en-us/aspnet/core/security/authorization/resourcebased?view=aspnetcore-9.0)** determines access by examining both the user and the specific item they want to access. (For example, a user can edit only their own blog posts, not others' posts.) Unlike policy-based authorization which applies the same rules everywhere, resource-based authorization makes decisions based on the actual data being accessed, requiring more complex implementation. + +Here's a quick comparison of these approaches: + +| Authorization Type | Pros | Cons | +|-------------------|------|------| +| **Role-Based** | Simple implementation, easy to understand | Becomes inflexible with complex role hierarchies | +| **Claims-Based** | Granular control, flexible user attributes | Complex claim management, potential for claim explosion | +| **Policy-Based** | Centralized logic, combines multiple requirements | Can become complex with numerous policies | +| **Resource-Based** | Fine-grained per-resource control | Implementation complexity, resource-specific code | + +## What is Permission-Based Authorization? + +Permission-based authorization takes a different approach from other authorization types by defining specific permissions (like **"CreateUser"**, **"DeleteOrder"**, **"ViewReports"**) that represent granular actions within your application. These permissions can be assigned to users directly or through roles, providing both flexibility and clear action-based access control. + +ABP Framework's permission system is built on top of this approach and extends ASP.NET Core's policy-based authorization system, working seamlessly with it. + +## ABP Framework's Permission System + +ABP extends [ASP.NET Core Authorization](https://learn.microsoft.com/en-us/aspnet/core/security/authorization/introduction?view=aspnetcore-9.0) by adding **permissions** as automatic [policies](https://learn.microsoft.com/en-us/aspnet/core/security/authorization/policies?view=aspnetcore-9.0) and allows the authorization system to be used in application services as well. + +This system provides a clean abstraction while maintaining full compatibility with ASP.NET Core's authorization infrastructure. + +ABP also provides a [Permission Management Module](https://abp.io/docs/latest/modules/permission-management) that offers a complete UI and API for managing permissions. This allows you to easily manage permissions in the UI, assign permissions to roles or users, and much more. (We'll see how to use it in the following sections.) + +### Defining Permissions in ABP + +In ABP, permissions are defined in classes (typically under the `*.Application.Contracts` project) that inherit from the `PermissionDefinitionProvider` class. Here's how you can define permissions for a book management system: + +```csharp +public class BookStorePermissionDefinitionProvider : PermissionDefinitionProvider +{ + public override void Define(IPermissionDefinitionContext context) + { + var bookStoreGroup = context.AddGroup("BookStore"); + + var booksPermission = bookStoreGroup.AddPermission("BookStore.Books", L("Permission:Books")); + booksPermission.AddChild("BookStore.Books.Create", L("Permission:Books.Create")); + booksPermission.AddChild("BookStore.Books.Edit", L("Permission:Books.Edit")); + booksPermission.AddChild("BookStore.Books.Delete", L("Permission:Books.Delete")); + } + + private static LocalizableString L(string name) + { + return LocalizableString.Create(name); + } +} +``` + +ABP automatically discovers this class and registers the permissions/policies in the system. You can then assign these permissions/policies to users/roles. There are two ways to do this: + +* Using the [Permission Management Module](https://abp.io/docs/latest/modules/permission-management) +* Using the `IPermissionManager` service (via code) + +#### Setting Permissions to Roles and Users via Permission Management Module + +When you define a permission, it also becomes usable in the ASP.NET Core authorization system as a **policy name**. If you are using the [Permission Management Module](https://abp.io/docs/latest/modules/permission-management), you can manage the permissions through the UI: + +![](permission-management-module.png) + +In the permission management UI, you can grant permissions to roles and users through the **Role Management** and **User Management** pages within the "permissions" modals. You can then easily check these permissions in your code. In the screenshot above, you can see the permission modal for the user's page, clearly showing the permissions granted to the user by their role. (**(R)** in the UI indicates that the permission is granted by one of the current user's roles.) + +#### Setting Permissions to Roles and Users via Code + +You can also set permissions for roles and users programmatically. You just need to inject the `IPermissionManager` service and use its `SetForRoleAsync` and `SetForUserAsync` methods (or similar methods): + +```csharp +public class MyService : ITransientDependency +{ + private readonly IPermissionManager _permissionManager; + + public MyService(IPermissionManager permissionManager) + { + _permissionManager = permissionManager; + } + + public async Task GrantPermissionForUserAsync(Guid userId, string permissionName) + { + await _permissionManager.SetForUserAsync(userId, permissionName, true); + } + + public async Task ProhibitPermissionForUserAsync(Guid userId, string permissionName) + { + await _permissionManager.SetForUserAsync(userId, permissionName, false); + } +} +``` + +### Checking Permissions in AppServices and Controllers + +ABP provides multiple ways to check permissions. The most common approach is using the `[Authorize]` attribute and passing the permission/policy name. + +Here is an example of how to check permissions in an application service: + +```csharp +[Authorize("BookStore.Books")] +public class BookAppService : ApplicationService, IBookAppService +{ + [Authorize("BookStore.Books.Create")] + public async Task CreateAsync(CreateBookDto input) + { + //logic here + } +} +``` + +> Notice that you can use the `[Authorize]` attribute at both class and method levels. In the example above, the `CreateAsync` method is marked with the `[Authorize]` attribute, so it will check the user's permission before executing the method. Since the application service class also has a permission requirement, both permissions must be granted to the user to execute the method! + +And here is an example of how to check permissions in a controller: + +```csharp +[Authorize("BookStore.Books")] +public class CreateBookController : AbpController +{ + //omitted for brevity... +} +``` + +### Programmatic Permission Checking + +To conditionally control authorization in your code, you can use the `IAuthorizationService` service: + +```csharp +public class BookAppService : ApplicationService, IBookAppService +{ + public async Task CreateAsync(CreateBookDto input) + { + // Checks the permission and throws an exception if the user does not have the permission + await AuthorizationService.CheckAsync(BookStorePermissions.Books.Create); + + // Your logic here + } + + public async Task CanUserCreateBooksAsync() + { + // Checks if the permission is granted for the current user + return await AuthorizationService.IsGrantedAsync(BookStorePermissions.Books.Create); + } +} +``` + +You can use the `IAuthorizationService`'s helpful methods for authorization checking, as shown in the example above: + +- `IsGrantedAsync` checks if the current user has the given permission. +- `CheckAsync` throws an exception if the current user does not have the given permission. +- `AuthorizeAsync` checks if the current user has the given permission and returns an `AuthorizationResult`, which has a `Succeeded` property that you can use to verify if the user has the permission. + +Also notice that we did not inject the `IAuthorizationService` in the constructor, because we are using the `ApplicationService` base class, which already provides property injection for it. This means we can directly use it in our application services, just like other helpful base services (such as `ICurrentUser` and `ICurrentTenant`). + +## Conclusion + +Permission-based authorization in ABP Framework provides a powerful and flexible approach to securing your applications. By building on ASP.NET Core's policy-based authorization, ABP offers a clean abstraction that simplifies permission management while maintaining the full power of the underlying system. + +The ability to check permissions in both application services and controllers makes ABP Framework's authorization system very flexible and powerful, yet easy to use. + +Additionally, the Permission Management Module makes it very easy to manage permissions and roles through the UI. You can learn more about how it works in the [documentation](https://abp.io/docs/latest/modules/permission-management). diff --git a/docs/en/Community-Articles/2025-08-27-Building-a-permission-based-authorization-system-for-net-core/cover-image.png b/docs/en/Community-Articles/2025-08-27-Building-a-permission-based-authorization-system-for-net-core/cover-image.png new file mode 100644 index 0000000000..f0a0796034 Binary files /dev/null and b/docs/en/Community-Articles/2025-08-27-Building-a-permission-based-authorization-system-for-net-core/cover-image.png differ diff --git a/docs/en/Community-Articles/2025-08-27-Building-a-permission-based-authorization-system-for-net-core/permission-management-module.png b/docs/en/Community-Articles/2025-08-27-Building-a-permission-based-authorization-system-for-net-core/permission-management-module.png new file mode 100644 index 0000000000..e22de787af Binary files /dev/null and b/docs/en/Community-Articles/2025-08-27-Building-a-permission-based-authorization-system-for-net-core/permission-management-module.png differ diff --git a/docs/en/Community-Articles/2025-08-27-backcompat-rest-apis-ms-dotnet/article.md b/docs/en/Community-Articles/2025-08-27-backcompat-rest-apis-ms-dotnet/article.md new file mode 100644 index 0000000000..f88bb0e41c --- /dev/null +++ b/docs/en/Community-Articles/2025-08-27-backcompat-rest-apis-ms-dotnet/article.md @@ -0,0 +1,169 @@ +# Best Practices for Designing Backward‑Compatible REST APIs in a Microservice Solution for .NET Developers + +## Introduction +With microservice architecture, each service develops and ships independently at its own pace, and clients infrequently update in lockstep. **Backward compatibility** means that when you release new versions, current consumers continue to function without changing code. This article provides a practical, 6–7 minute tutorial specific to **.NET developers**. + +--- +## What Counts as “Breaking”? (and what doesn’t) +A change is **breaking** if a client that previously conformed can **fail at compile time or runtime**, or exhibit **different business‑critical behavior**, **without** changing that client in any way. In other words: if an old client needs to be altered in order to continue functioning as it did, your change is breaking. + +### Examples of breaking changes +- **Deleting or renaming an endpoint** or modifying its URL/route. +- **Making an existing field required** (e.g., requiring `address`). +- **Data type or format changes** (e.g., `price: string` → `price: number`, or date format changes). +- **Altering default behavior or ordering** that clients implicitly depend on (hidden contracts). +- **Changing the error model** or HTTP status codes in a manner that breaks pre-existing error handling. +- **Renaming fields** or **making optional fields required** in requests or responses. +- **Reinterpreting semantics** (e.g., `status="closed"` formerly included archived items, but no longer does). + +### Examples of non‑breaking changes +- **Optional fields or query parameters can be added** (clients may disregard them). +- **Adding new enum values** (if the clients default to a safe behavior for unrecognized values). +- **Adding a new endpoint** while leaving the previous one unchanged. +- **Performance enhancements** that leave input/output unchanged. +- **Including metadata** (e.g., pagination links) without changing the current payload shape. + +> Golden rule: **Old clients should continue to work exactly as they did before—without any changes.** + +--- +## Versioning Strategy +Versioning is your master control lever for managing change. Typical methods: + +1) **URI Segment** (simplest) +``` +GET /api/v1/orders +GET /api/v2/orders +``` +Pros: Cache/gateway‑friendly; explicit in docs. Cons: URL noise. + +2) **Header‑Based** +``` +GET /api/orders +x-api-version: 2.0 +``` +Pros: Clean URLs; multiple reader support. Cons: Needs proxy/CDN rules. + +3) **Media Type** + Accept: application/json;v=2 + + Pros: Semantically accurate.
Cons: More complicated to test and implement.
**Recommendation:** For the majority of teams, favor **URI segments**, with an optional **`x-api-version`** header for flexibility. + +### Quick Setup in ASP.NET Core (Asp.Versioning) +```csharp +// Program.cs +using Asp.Versioning; + +builder.Services.AddControllers(); +builder.Services.AddApiVersioning(o => +{ + o.DefaultApiVersion = new ApiVersion(1, 0); + o.AssumeDefaultVersionWhenUnspecified = true; + o.ReportApiVersions = true; // response header: api-supported-versions + o.ApiVersionReader = ApiVersionReader.Combine( + new UrlSegmentApiVersionReader(), + new HeaderApiVersionReader("x-api-version") + ); +}); + +builder.Services.AddVersionedApiExplorer(o => +{ + o.GroupNameFormat = "'v'VVV"; // v1, v2 + o.SubstituteApiVersionInUrl = true; +}); +``` +```csharp +// Controller +using Asp.Versioning; + +[ApiController] +[Route("api/v{version:apiVersion}/orders")] +public class OrdersController : ControllerBase +{ + [HttpGet] + [ApiVersion("1.0", Deprecated = true)] + public IActionResult GetV1() => Ok(new { message = "v1" }); + + [HttpGet] + [MapToApiVersion("2.0")] + public IActionResult GetV2() => Ok(new { message = "v2", includes = new []{"items"} }); +} +``` + +--- +## Schema Evolution Playbook (JSON & DTO) +Obey the following rules for compatibility‑safe evolution: + +- **Add‑only changes**: Favor adding **optional** fields; do not remove/rename fields. +- **Maintain defaults**: When the new field is disregarded, the old functionality must not change. +- **Enum extension**: Clients should handle unknown enum values gracefully (default behavior). +- **Deprecation pipeline**: Mark fields/endpoints as deprecated **at least one version** prior to removal and publicize extensively. - **Stability by contract**: Record any unspoken contracts (ordering, casing, formats) that clients depend on. + +### Example: adding a non‑breaking field +```csharp +public record OrderDto( + Guid Id, + decimal Total, + string Currency, + string? SalesChannel // new, optional +); +``` + +--- +## Compatibility‑Safe API Behaviors +- **Error model**: Use a standard structure (e.g., RFC 7807 `ProblemDetails`). Avoid ad‑hoc error shapes on a per-endpoint basis. +- **Versioning/Deprecation communication** through headers: +- `api-supported-versions: 1.0, 2.0` +- `Deprecation: true` (in deprecated endpoints) +- `Sunset: Wed, 01 Oct 2025 00:00:00 GMT` (planned deprecation date) +- **Idempotency**: Use an `Idempotency-Key` header for retry-safe POSTs. +- **Optimistic concurrency**: Utilize `ETag`/`If-Match` to prevent lost updates. +- **Pagination**: Prefer cursor tokens (`nextPageToken`) to protect clients from sorting/index changes. +- **Time**: Employ ISO‑8601 in UTC; record time‑zone semantics and rounding conventions. + +--- +## Rollout & Deprecation Policy +A good deprecation policy is **announce → coexist → remove**: + +1) **Announce**: Release changelog, docs, and comms (mail/Slack) with v2 information and the sunset date. +2) **Coexist**: Operate v1 and v2 side by side. Employ gateway percentage routing for progressive cutover. +3) **Observability**: Monitor errors/latency/usage **by version**. When v1 traffic falls below ~5%, plan for removal. 4) **Remove**: Post sunset date, return **410 (Gone)** with a link to migration documentation. + +**Canary & Blue‑Green**: Initialize v2 with a small traffic portion and compare error/latency budgets prior to scaling up. + +--- +## Contract & Compatibility Testing +- **Consumer‑Driven Contracts**: Write expectations using Pact.NET; verify at provider CI. +- **Golden files / snapshots**: Freeze representative JSON payloads and automatically detect regressions. +- **Version-specific smoke tests**: Maintain separate, minimal test suites for v1 and v2. +- **SemVer discipline**: Minor = backward‑compatible; Major = breaking (avoid when possible). + +Minimal example (xUnit + snapshot style): +```csharp +[Fact] +public async Task Orders_v1_contract_should_match_snapshot() +{ + var resp = await _client.GetStringAsync("/api/v1/orders"); + Approvals.VerifyJson(resp); // snapshot comparison +} +``` + +--- +## Tooling & Docs (for .NET) +- **Asp.Versioning (NuGet)**: API versioning + ApiExplorer integration. +- **Swashbuckle / NSwag**: Generate an OpenAPI definition **for every version** (`/swagger/v1/swagger.json`, `/swagger/v2/swagger.json`). Display both in Swagger UI. +- **Polly**: Client‑side retries/fallbacks to handle transient failures and ensure resilience. +- **Serilog + OpenTelemetry**: Collect metrics/logs/traces by version for observability and SLOs. + +Swagger UI configuration by group name: +```csharp +app.UseSwagger(); +app.UseSwaggerUI(c => +{ +c.SwaggerEndpoint("/swagger/v1/swagger.json", "API v1"); +c.SwaggerEndpoint("/swagger/v2/swagger.json", "API v2"); +}); +``` +--- + +## Conclusion +Backward compatibility is not a version number—it is **disciplined change management**. When you use add‑only schema evolution, a well‑defined versioning strategy, strict contract testing, and rolling rollout, you maintain microservice independence and safeguard consumer experience. diff --git a/docs/en/Community-Articles/2025-08-27-backcompat-rest-apis-ms-dotnet/cover.png b/docs/en/Community-Articles/2025-08-27-backcompat-rest-apis-ms-dotnet/cover.png new file mode 100644 index 0000000000..07f0782775 Binary files /dev/null and b/docs/en/Community-Articles/2025-08-27-backcompat-rest-apis-ms-dotnet/cover.png differ diff --git a/docs/en/Community-Articles/2025-09-02-training-campaign/post.md b/docs/en/Community-Articles/2025-09-02-training-campaign/post.md new file mode 100644 index 0000000000..20f2bcf4bd --- /dev/null +++ b/docs/en/Community-Articles/2025-09-02-training-campaign/post.md @@ -0,0 +1,29 @@ +# IMPROVE YOUR ABP SKILLS WITH 33% OFF LIVE TRAININGS! + +We have exciting news to share\! As you know, we offer live training packages to help you improve your skills and knowledge of ABP. From September 8th to 19th, we are giving you 33% OFF our live trainings, so you can learn more about the product at a discounted price\! + +#### Why Join ABP.IO Training? + +ABP training programs are designed to help developers, architects, and teams master the ABP Framework efficiently. Whether you're new to the framework or looking to deepen your knowledge, our courses cover everything you need to build robust and scalable applications with ABP. + +#### What You’ll Gain: + +✔ Comprehensive live training from ABP Experts +✔ Hands-on learning with real-world applications +✔ Best practices for building modern web applications +✔ Certification to showcase your expertise + +#### [Limited-Time 33% Discount – Don’t Miss Out\!](https://abp.io/trainings?utm_source=referral&utm_medium=website&utm_campaign=training_abpblogpost) + +For a short period, all training packages are available at a 33% discount. This is a great opportunity to upskill yourself or train your team at a significantly reduced cost. + +#### How to Get the Discount? + +Simply visit our training page, select your preferred package, add your note if needed and send your training request, that's all\! ABP Training Team will reply to your request via email soon. + +#### Take Advantage of This Offer Today + +Invest in your skills and advance your career with ABP.IO training. This offer won’t last long, so grab your spot now\! + +### 🔗[Pick your package and send your training request now!](https://abp.io/trainings?utm_source=referral&utm_medium=website&utm_campaign=training_abpblogpost) + diff --git a/docs/en/Community-Articles/2025-09-03-Keep-Track-of-Your-Users-in-an-ASP.NET-Core-Application/1.png b/docs/en/Community-Articles/2025-09-03-Keep-Track-of-Your-Users-in-an-ASP.NET-Core-Application/1.png new file mode 100644 index 0000000000..9168907741 Binary files /dev/null and b/docs/en/Community-Articles/2025-09-03-Keep-Track-of-Your-Users-in-an-ASP.NET-Core-Application/1.png differ diff --git a/docs/en/Community-Articles/2025-09-03-Keep-Track-of-Your-Users-in-an-ASP.NET-Core-Application/2.png b/docs/en/Community-Articles/2025-09-03-Keep-Track-of-Your-Users-in-an-ASP.NET-Core-Application/2.png new file mode 100644 index 0000000000..ff84b51766 Binary files /dev/null and b/docs/en/Community-Articles/2025-09-03-Keep-Track-of-Your-Users-in-an-ASP.NET-Core-Application/2.png differ diff --git a/docs/en/Community-Articles/2025-09-03-Keep-Track-of-Your-Users-in-an-ASP.NET-Core-Application/POST.md b/docs/en/Community-Articles/2025-09-03-Keep-Track-of-Your-Users-in-an-ASP.NET-Core-Application/POST.md new file mode 100644 index 0000000000..076e623ad8 --- /dev/null +++ b/docs/en/Community-Articles/2025-09-03-Keep-Track-of-Your-Users-in-an-ASP.NET-Core-Application/POST.md @@ -0,0 +1,335 @@ +# Keep Track of Your Users in an ASP.NET Core Application + +Tracking what users do in your app matters for security, debugging, and business insights. Doing it by hand usually means lots of boilerplate: managing request context, logging operations, tracking entity changes, and more. It adds complexity and makes mistakes more likely. + +## Why Applications Need Audit Logs + +Audit logs are time-ordered records that show what happened in your app. + +A good audit log should capture details for every web request, including: + +### 1. Request and Response Details +- Basic info like **URL, HTTP method, browser**, and **HTTP status code** +- Network info like **client IP address** and **user agent** +- **Request parameters** and **response content** when needed + +### 2. Operations Performed +- **Controller actions** and **application service method calls** with parameters +- **Execution time** and **duration** for performance tracking +- **Call chains** and **dependencies** where helpful + +### 3. Entity Changes +- **Entity changes** that happen during requests +- **Property-level changes**, with old and new values +- **Change types** (create, update, delete) and timestamps + +### 4. Exception Information +- **Errors and exceptions** during request execution +- **Exception stack traces** and **error context** +- Clear records of failed operations + +### 5. Request Duration +- Key metrics for **measuring performance** +- **Finding bottlenecks** and optimization opportunities +- Useful data for **monitoring system health** + +## The Challenge with Doing It by Hand + +In ASP.NET Core, developers often use middleware or MVC filters for tracking. Here’s what that looks like and the common problems you’ll hit. + +### Using Middleware + +Middleware are components in the ASP.NET Core pipeline that run during request processing. + +Manual tracking typically requires: +- Writing custom middleware to intercept HTTP requests +- Extracting user info (user ID, username, IP address, and so on) +- Recording request start time and execution duration +- Handling both success and failure cases +- Saving audit data to logs or a database + +### Tracking Inside Business Methods + +In your business code, you also need to: +- Log the start and end of important operations +- Capture errors and related context +- Link business operations to the request-level audit data +- Make sure you track all critical actions + +### Problems with Manual Tracking + +Manual tracking has some big downsides: + +**Code duplication and maintenance pain**: Each controller ends up repeating similar tracking logic. Changing the rules means touching many places, and it’s easy to miss some. + +**Consistency and reliability issues**: Different people implement tracking differently. Exception paths are easy to forget. It’s hard to ensure complete coverage. + +**Performance and scalability concerns**: Homegrown tracking can slow the app if not designed well. Tuning and extending it takes effort. + +**Entity change tracking is especially hard**. It often requires: +- Recording original values before updates +- Comparing old and new values for each property +- Handling complex types, collections, and navigation properties +- Designing and saving change records +- Capturing data even when exceptions happen + +This usually leads to: +- **A lot of code** in every update method +- **Easy-to-miss edge cases** and subtle bugs +- **High maintenance** when entity models change +- **Extra queries and comparisons** that can hurt performance +- **Incomplete coverage** for complex scenarios + +## ABP Framework’s Built-in Solution + +ABP Framework includes a built-in audit logging system. It solves the problems above and adds useful features on top. + +### Simple Setup vs. Manual Tracking + +Instead of writing lots of code, you configure it once: + +```csharp +// Configure audit log options in the module's ConfigureServices method +Configure(options => +{ + options.IsEnabled = true; // Enable audit log system (default value) + options.IsEnabledForAnonymousUsers = true; // Track anonymous users (default value) + options.IsEnabledForGetRequests = false; // Skip GET requests (default value) + options.AlwaysLogOnException = true; // Always log on errors (default value) + options.HideErrors = true; // Hide audit log errors (default value) + options.EntityHistorySelectors.AddAllEntities(); // Track all entity changes +}); +``` + +```csharp +// Add middleware in the module's OnApplicationInitialization method +public override void OnApplicationInitialization(ApplicationInitializationContext context) +{ + var app = context.GetApplicationBuilder(); + + // Add audit log middleware - one line of code solves all problems! + app.UseAuditing(); +} +``` + +By contrast, manual tracking needs middleware, controller logic, exception handling, and often hundreds of lines. With ABP, a couple of lines enable it and it just works. + +## What You Get with ABP + +Here’s how ABP removes tracking code from your application and still captures what you need. + +### 1. Application Services: No Tracking Code + +Manual approach: You’d log inside each method and still risk missing cases. + +ABP approach: Tracking is automatic—no tracking code in your methods. + +```csharp +public class BookAppService : ApplicationService +{ + private readonly IRepository _bookRepository; + private readonly IRepository _authorRepository; + + [Authorize(BookPermissions.Create)] + public virtual async Task CreateAsync(CreateBookDto input) + { + // No need to write any tracking code! + // ABP automatically tracks: + // - Method calls and parameters + // - Calling user + // - Execution duration + // - Any exceptions thrown + + var author = await _authorRepository.GetAsync(input.AuthorId); + var book = new Book(input.Title, author, input.Price); + + await _bookRepository.InsertAsync(book); + + return ObjectMapper.Map(book); + } + + [Authorize(BookPermissions.Update)] + public virtual async Task UpdateAsync(Guid id, UpdateBookDto input) + { + var book = await _bookRepository.GetAsync(id); + + // No need to write any entity change tracking code! + // ABP automatically tracks entity changes: + // - Which properties changed + // - Old and new values + // - When the change happened + + book.ChangeTitle(input.Title); + book.ChangePrice(input.Price); + + await _bookRepository.UpdateAsync(book); + + return ObjectMapper.Map(book); + } +} +``` + +With manual code, each method might need 20–30 lines for tracking. With ABP, it’s zero—and you still get richer data. + +For entity changes, ABP also saves you from writing comparison code. It handles: +- Property change detection +- Recording old and new values +- Complex types and collections +- Navigation property changes +- All with no extra code to maintain + +### 2. Entity Change Tracking: One Line to Turn It On + +Manual approach: You’d compare properties, serialize complex types, track collection changes, and write to storage. + +ABP approach: Mark the entity or select entities globally. + +```csharp +// Enable audit log for specific entity - one line of code solves all problems! +[Audited] +public class MyEntity : Entity +{ + public string Name { get; set; } + public string Description { get; set; } + + [DisableAuditing] // Exclude sensitive data - security control + public string InternalNotes { get; set; } +} +``` + +```csharp +// Or global configuration - batch processing +Configure(options => +{ + // Track all entities - one line of code tracks all entity changes + options.EntityHistorySelectors.AddAllEntities(); + + // Or use custom selector - precise control + options.EntityHistorySelectors.Add( + new NamedTypeSelector( + "MySelectorName", + type => typeof(IEntity).IsAssignableFrom(type) + ) + ); +}); +``` + +### 3. Extension Features + +Manual approach: Adding custom tracking usually spreads across many places and is hard to test. + +ABP approach: Use a contributor for clean, centralized extensions. + +```csharp +public class MyAuditLogContributor : AuditLogContributor +{ + public override void PreContribute(AuditLogContributionContext context) + { + var currentUser = context.ServiceProvider.GetRequiredService(); + + // Easily add custom properties - manual implementation needs lots of work + context.AuditInfo.SetProperty( + "MyCustomClaimValue", + currentUser.FindClaimValue("MyCustomClaim") + ); + } + + public override void PostContribute(AuditLogContributionContext context) + { + // Add custom comments - business logic integration + context.AuditInfo.Comments.Add("Some comment..."); + } +} + +// Register contributor - one line of code enables extension features +Configure(options => +{ + options.Contributors.Add(new MyAuditLogContributor()); +}); +``` + +### 4. Precise Control + +Manual approach: You end up with complex conditional logic. + +ABP approach: Use attributes for simple, precise control. + +```csharp +// Disable audit log for specific controller - precise control +[DisableAuditing] +public class HomeController : AbpController +{ + // Health check endpoints won't be audited - avoid meaningless logs +} + +// Disable for specific action - method-level control +public class HomeController : AbpController +{ + [DisableAuditing] + public async Task Home() + { + // This action won't be audited - public data access + } + + public async Task OtherActionLogged() + { + // This action will be audited - important business operation + } +} +``` + +### 5. Visual Management of Audit Logs + +ABP also provides a UI to browse and inspect audit logs: + +![](1.png) + +![](2.png) + +## Manual vs. ABP: A Quick Comparison + +The benefits of ABP’s audit log system compared to doing it by hand: + +| Aspect | Manual Implementation | ABP Audit Logs | +|--------|----------------------|----------------| +| **Setup Complexity** | High — Write middleware, services, repository code | Low — A few lines of config, works out of the box | +| **Code Maintenance** | High — Tracking code spread across the app | Low — Centralized, convention-based | +| **Consistency** | Variable — Depends on discipline | Consistent — Automated and standardized | +| **Performance** | Risky without careful tuning | Built-in optimizations and scope control | +| **Functionality Completeness** | Basic tracking only | Comprehensive by default | +| **Error Handling** | Easy to miss edge cases | Automatic and reliable | +| **Data Integrity** | Manual effort required | Handled by the framework | +| **Extensibility** | Custom work is costly | Rich extension points | +| **Development Efficiency** | Weeks to build | Minutes to enable | +| **Learning Cost** | Understand many details | Convention-based, low effort | + +## Why ABP Audit Logs Matter + +ABP’s audit logging removes the boilerplate from user tracking in ASP.NET Core apps. + +### Core Idea + +Manual tracking is error-prone and hard to maintain. ABP gives you a convention-based, automated system that works with minimal setup. + +### Key Benefits + +ABP runs by convention, so you don’t need repetitive code. You can control behavior at the request, entity, and method levels. It automatically captures request details, operations, entity changes, and exceptions, and you can extend it with contributors when needed. + +### Results in Practice + +| Metric | Manual Implementation | ABP Implementation | Improvement | +|--------|----------------------|-------------------|-------------| +| Development Time | Weeks | Minutes | **99%+** | +| Lines of Code | Hundreds of lines | 2 lines of config | **99%+** | +| Maintenance Cost | High | Low | **Significant** | +| Functionality Completeness | Basic | Comprehensive | **Significant** | +| Error Rate | Higher risk | Lower risk | **Improved** | + +### Recommendation + +If you need audit logs, start with ABP’s built-in system. It reduces effort, improves consistency, and stays flexible as your app grows. You can focus on your business logic and let the framework handle the infrastructure. + +## References + +- [ABP Audit Logging](https://abp.io/docs/latest/framework/infrastructure/audit-logging) +- [ABP Audit Logging UI](https://abp.io/modules/Volo.AuditLogging.Ui) diff --git a/docs/en/Community-Articles/2025-09-03-Keep-Track-of-Your-Users-in-an-ASP.NET-Core-Application/cover.png b/docs/en/Community-Articles/2025-09-03-Keep-Track-of-Your-Users-in-an-ASP.NET-Core-Application/cover.png new file mode 100644 index 0000000000..7030e285b2 Binary files /dev/null and b/docs/en/Community-Articles/2025-09-03-Keep-Track-of-Your-Users-in-an-ASP.NET-Core-Application/cover.png differ diff --git a/docs/en/Community-Articles/2025-09-10-NET10-What-You-Need-To-Know/Post.md b/docs/en/Community-Articles/2025-09-10-NET10-What-You-Need-To-Know/Post.md new file mode 100644 index 0000000000..3de235e86d --- /dev/null +++ b/docs/en/Community-Articles/2025-09-10-NET10-What-You-Need-To-Know/Post.md @@ -0,0 +1,173 @@ +# .NET 10: What You Need to Know (LTS Release, Coming November 2025) + +The next version of .NET is .NET 10 and it is coming with **Long-Term Support (LTS)**, scheduled for **November 2025**. + +On **September 9, 2025**, Microsoft released **.NET 10 Release Candidate 1 (RC1)**, which supports go-live usage and is compatible with [Visual Studio 2026 Insider](https://visualstudio.microsoft.com/insiders/) and [Visual Studio Code Insider](https://code.visualstudio.com/insiders/) via the [C# Dev Kit](https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csdevkit) extension. + +------ + +## .NET 10 Runtime Enhancements + +- **JIT Speed-ups**: Enhanced struct argument handling—members now go directly into registers, reducing memory load/store operations. +- **Advanced Loop Optimization**: New graph-based loop inversion improves precision and boosts further optimizations. +- **Array Interface De-virtualization**: Critical for performance, now array-based enumerations inline and skip virtual calls including de-abstraction of array enumeration and small-array stack allocation. +- **General JIT Improvements**: Better code layout and branch reduction support overall efficiency. + +------ + +## Language & Library Upgrades + +### C# 14 Enhancements + +- Field-backed properties: easier custom getters/setters. +- `nameof` for unbound generics like `List<>`. +- Implicit conversions for `Span` and `ReadOnlySpan`. +- Lambda parameter modifiers (`ref`, `in`, `out`). +- Partial constructors/events. +- `extension` blocks for static extension members. +- Null-conditional assignment (`?.=`) and custom compound/increment operators. + +### F# & Visual Basic Enhancements + +- F# improvements via `preview`, updated `FSharp.Core`, and compiler fixes. +- VB compiler supports `unmanaged` generics and respects `OverloadResolutionPriorityAttribute` for performance and overload clarity. + +## .NET Libraries & SDK + +### Libraries: + +- Better ZipArchive performance (lazy entry loading). +- JSON improvements, including `JsonSourceGenerationOptions` and reference-handling tweaks. +- Enhanced `OrderedDictionary`, ISOWeek date APIs, PEM data and certificate handling, `CompareOptions.NumericOrdering` + +### SDK & CLI: + +- No major new SDK features in RC1—you should expect stability fixes rather than additions. +- Earlier previews brought JSON support improvements (e.g., `PipeReader` for JSON, WebSocketStream, ML-DSA crypto, AES KeyWrap), TLS 1.3 for macOS + +------ + +## ASP.NET Core & Blazor + +### Blazor & Web App Security: + +Enhanced OIDC and Microsoft Entra ID integration, including encrypted token caching and Key Vault use. + +### UI Enhancements: + +- `QuickGrid` gains `RowClass` for conditional styling. +- Scripts now served as static assets with compression and fingerprinting. +- NavigationManager no longer scrolls to top for same-page updates. + +### API Improvements: + +Full support for OpenAPI 3.1 (JSON Schema draft 2020-12), and metrics for authentication/authorization events (e.g., sign-ins, logins) . + +------ + +## .NET MAUI + +Updates include multiple file selection, image compression, WebView request interception, and support for Android API 35/36. + +## EF Core + +LINQ enhancements, performance boosts, better Azure Cosmos DB support, and more flexible named query filters. + + + +## Breaking Changes in .NET 10 + +### ASP.NET Core - Breaking Changes in .NET 10: + +.NET 10 Preview 7 brings **several deprecations + behavior changes**, while **RC1 removes the old WebHost model**. + +- **[Cookie login redirects disabled](https://learn.microsoft.com/en-us/dotnet/core/compatibility/aspnet-core/10/cookie-authentication-api-endpoints)** → Redirects no longer occur for API endpoints; APIs now return `401`/`403`. *(Behavioral change)* +- **[WithOpenApi deprecated](https://learn.microsoft.com/en-us/dotnet/core/compatibility/aspnet-core/10/withopenapi-deprecated)** → Extension method removed; use updated OpenAPI generator features. *(Source incompatible)* +- **[Exception diagnostics suppressed](https://learn.microsoft.com/en-us/dotnet/core/compatibility/aspnet-core/10/exception-handler-diagnostics-suppressed)** → When `TryHandleAsync` returns true, exception details aren’t logged. *(Behavioral change)* +- **[IActionContextAccessor obsolete](https://learn.microsoft.com/en-us/dotnet/core/compatibility/aspnet-core/10/iactioncontextaccessor-obsolete)** → Marked obsolete; may break code depending on it. *(Source/behavioral change)* +- **[IncludeOpenAPIAnalyzers deprecated](https://learn.microsoft.com/en-us/dotnet/core/compatibility/aspnet-core/10/openapi-analyzers-deprecated)** → Property and MVC API analyzers removed. *(Source incompatible)* +- **[IPNetwork & KnownNetworks obsolete](https://learn.microsoft.com/en-us/dotnet/core/compatibility/aspnet-core/10/ipnetwork-knownnetworks-obsolete)** → Old networking APIs removed in favor of new ones. *(Source incompatible)* +- **[ApiDescription.Client package deprecated](https://learn.microsoft.com/en-us/dotnet/core/compatibility/aspnet-core/10/apidescription-client-deprecated)** → No longer maintained; migrate to other tools. *(Source incompatible)* +- **[Razor run-time compilation obsolete](https://learn.microsoft.com/en-us/dotnet/core/compatibility/aspnet-core/10/razor-runtime-compilation-obsolete)** → Disabled at runtime; precompilation required. *(Source incompatible)* +- **[WebHostBuilder, IWebHost, WebHost obsolete](https://learn.microsoft.com/en-us/dotnet/core/compatibility/aspnet-core/10/webhostbuilder-deprecated)** → Legacy hosting model deprecated; use `WebApplicationBuilder`. *(Source incompatible, RC1)* + +### EF Core - Breaking Changes in .NET 10: + +You can find the complete list at [Microsoft EF Core 10 Breaking Changes page](https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-10.0/breaking-changes). Here's the brief summary: + +#### EF Core - SQL Server + +- **JSON column type by default (Azure SQL / compat level ≥170).** Primitive collections and owned types mapped to JSON now use SQL Server’s native `json` type instead of `nvarchar(max)`. A migration may alter existing columns. Mitigate by setting compat level <170 or explicitly forcing `nvarchar(max)`. +- **`ExecuteUpdateAsync` signature change.** Column setters now take a regular `Func<…>` (not an expression). Dynamic expression-tree code won’t compile; replace with imperative setters inside the lambda. + +#### Microsoft.Data.Sqlite + +- **`GetDateTimeOffset` (no offset) assumes UTC.** Previously assumed local time. You can temporarily revert via `AppContext.SetSwitch("Microsoft.Data.Sqlite.Pre10TimeZoneHandling", true)`. +- **Writing `DateTimeOffset` to REAL stores UTC.** Conversion now happens before writing; revertable with the same switch. +- **`GetDateTime` (with offset) returns UTC `DateTime` (`DateTimeKind.Utc`).** Was `Local` before. Same temporary switch if needed. + +#### Who’s most affected + +- Apps on **Azure SQL / SQL Server 2025** using JSON mapping. +- Codebases building **expression trees** for bulk updates. +- Apps using **SQLite** with date/time parsing or REAL timestamp storage. + +#### Quick mitigations + +- Set SQL Server compatibility <170 or force column type. +- Rewrite `ExecuteUpdateAsync` callers to use the new delegate form. +- For SQLite, update handling to UTC or use the temporary AppContext switch while transitioning. + +### Containers - Breaking Changes in .NET 10: + +Default .NET images now use [Ubuntu](https://learn.microsoft.com/en-us/dotnet/core/compatibility/containers/10.0/default-images-use-ubuntu). + +### Core Libraries - Breaking Changes in .NET 10: + +[ActivitySource](https://learn.microsoft.com/en-us/dotnet/core/compatibility/core-libraries/10.0/activity-sampling) behavior tweaks; [generic math](https://learn.microsoft.com/en-us/dotnet/core/compatibility/core-libraries/10.0/generic-math) shift behavior aligned; W3C trace context is [default](https://learn.microsoft.com/en-us/dotnet/core/compatibility/core-libraries/10.0/default-trace-context-propagator); [DriveInfo](https://learn.microsoft.com/en-us/dotnet/core/compatibility/core-libraries/10.0/driveinfo-driveformat-linux) reports Linux FS types; InlineArray size rules tightened; [System.Linq.AsyncEnumerable](https://learn.microsoft.com/en-us/dotnet/core/compatibility/core-libraries/10.0/asyncenumerable) included in core libs... + +### Cryptography - Breaking Changes in .NET 10: + +[Stricter X500](https://learn.microsoft.com/en-us/dotnet/core/compatibility/cryptography/10.0/x500distinguishedname-validation) name validation; [OpenSSL](https://learn.microsoft.com/en-us/dotnet/core/compatibility/cryptography/10.0/openssl-macos-unsupported) primitives unsupported on macOS; [some key members](https://learn.microsoft.com/en-us/dotnet/core/compatibility/cryptography/10.0/mldsa-slhdsa-secretkey-to-privatekey) nullable/renamed; env var [rename to](https://learn.microsoft.com/en-us/dotnet/core/compatibility/cryptography/10.0/version-override) `DOTNET_OPENSSL_VERSION_OVERRIDE`. + +### Extensions - Breaking Changes in .NET 10: + +[Config preserves](https://learn.microsoft.com/en-us/dotnet/core/compatibility/extensions/10.0/configuration-null-values-preserved) nulls; [logging](https://learn.microsoft.com/en-us/dotnet/core/compatibility/extensions/10.0/console-json-logging-duplicate-messages)/[package](https://learn.microsoft.com/en-us/dotnet/core/compatibility/extensions/10.0/provideraliasattribute-moved-assembly)/trim annotations changes; some [trim-unsafe](https://learn.microsoft.com/en-us/dotnet/core/compatibility/extensions/10.0/dynamically-accessed-members-configuration) code annotations removed. + +### Globalization & Interop - Breaking Changes in .NET 10: + +[ICU](https://learn.microsoft.com/en-us/dotnet/core/compatibility/globalization/10.0/version-override) env var renamed; single-file apps stop probing executable dir for native libs; [DllImport](https://learn.microsoft.com/en-us/dotnet/core/compatibility/interop/10.0/search-assembly-directory) search path tightened. + +Networking: + +[HTTP/3 disabled ](https://learn.microsoft.com/en-us/dotnet/core/compatibility/networking/10.0/http3-disabled-with-publishtrimmed) by default when trimming; [default cert revocation](https://learn.microsoft.com/en-us/dotnet/core/compatibility/networking/10.0/ssl-certificate-revocation-check-default) check now Online; [browser clients](https://learn.microsoft.com/en-us/dotnet/core/compatibility/networking/10.0/default-http-streaming) stream responses by default; [URI length limits](https://learn.microsoft.com/en-us/dotnet/core/compatibility/networking/10.0/uri-length-limits-removed) removed. + +### SDK & MSBuild/NuGet - Breaking Changes in .NET 10: + +`dotnet --interactive` [defaults to true](https://learn.microsoft.com/en-us/dotnet/core/compatibility/sdk/10.0/dotnet-cli-interactive); tool packages are [RID-specific](https://learn.microsoft.com/en-us/dotnet/core/compatibility/sdk/10.0/dotnet-tool-pack-publish); [workload](https://learn.microsoft.com/en-us/dotnet/core/compatibility/sdk/10.0/default-workload-config) sets default; `dotnet new sln` uses [SLNX](https://learn.microsoft.com/en-us/dotnet/core/compatibility/sdk/10.0/dotnet-new-sln-slnx-default); restore audits transitives; [local tool](https://learn.microsoft.com/en-us/dotnet/core/compatibility/sdk/10.0/dotnet-tool-install-local-manifest) install creates manifest by default; `project.json` [not supported](https://learn.microsoft.com/en-us/dotnet/core/compatibility/sdk/10.0/dotnet-restore-project-json-unsupported); stricter NuGet [validation](https://learn.microsoft.com/en-us/dotnet/core/compatibility/sdk/10.0/nuget-packageid-validation)/[errors](https://learn.microsoft.com/en-us/dotnet/core/compatibility/sdk/10.0/http-warnings-to-errors). + +WinForms/WPF: + +Multiple [API obsoletions](https://learn.microsoft.com/en-us/dotnet/core/compatibility/windows-forms/10.0/obsolete-apis)/parameter [renames](https://learn.microsoft.com/en-us/dotnet/core/compatibility/windows-forms/10.0/insertadjacentelement-orientation); [rendering](https://learn.microsoft.com/en-us/dotnet/core/compatibility/windows-forms/10.0/statusstrip-renderer)/behavior tweaks; stricter XAML rules (e.g., [disallow empty row](https://learn.microsoft.com/en-us/dotnet/core/compatibility/wpf/10.0/empty-grid-definitions)/column definitions or incorrect usage of [DynamicResource](https://learn.microsoft.com/en-us/dotnet/core/compatibility/wpf/10.0/dynamicresource-crash) will crash). + + + +------ + + + +## Support Policy for .NET 10 +As you can see from the picture below, **.NET 10 has long term support** therefore it will be maintained for 3 years **until November 2028**. + +[![.NET 10 Support Policy](image-2.png)](https://dotnet.microsoft.com/en-us/platform/support/policy/dotnet-core) + + + +## Download .NET10 + +Click 👉 https://dotnet.microsoft.com/en-us/download/dotnet/10.0 to download the latest release candidate (currently RC.1). + +[![Download .NET 10](image-1.png)](https://dotnet.microsoft.com/en-us/download/dotnet/10.0) + +Also to use the latest features, download/update your Visual Studio to the latest 👉 https://visualstudio.microsoft.com/downloads/ + diff --git a/docs/en/Community-Articles/2025-09-10-NET10-What-You-Need-To-Know/cover.png b/docs/en/Community-Articles/2025-09-10-NET10-What-You-Need-To-Know/cover.png new file mode 100644 index 0000000000..e85f53b9ec Binary files /dev/null and b/docs/en/Community-Articles/2025-09-10-NET10-What-You-Need-To-Know/cover.png differ diff --git a/docs/en/Community-Articles/2025-09-10-NET10-What-You-Need-To-Know/image-1.png b/docs/en/Community-Articles/2025-09-10-NET10-What-You-Need-To-Know/image-1.png new file mode 100644 index 0000000000..970f3194bc Binary files /dev/null and b/docs/en/Community-Articles/2025-09-10-NET10-What-You-Need-To-Know/image-1.png differ diff --git a/docs/en/Community-Articles/2025-09-10-NET10-What-You-Need-To-Know/image-2.png b/docs/en/Community-Articles/2025-09-10-NET10-What-You-Need-To-Know/image-2.png new file mode 100644 index 0000000000..6e5b57c588 Binary files /dev/null and b/docs/en/Community-Articles/2025-09-10-NET10-What-You-Need-To-Know/image-2.png differ diff --git a/docs/en/Community-Articles/2025-09-10-Truly-Layering-a-NET-Application-Based-on-DDD-Principles/coverimage.png b/docs/en/Community-Articles/2025-09-10-Truly-Layering-a-NET-Application-Based-on-DDD-Principles/coverimage.png new file mode 100644 index 0000000000..41499bea9a Binary files /dev/null and b/docs/en/Community-Articles/2025-09-10-Truly-Layering-a-NET-Application-Based-on-DDD-Principles/coverimage.png differ diff --git a/docs/en/Community-Articles/2025-09-10-Truly-Layering-a-NET-Application-Based-on-DDD-Principles/post.md b/docs/en/Community-Articles/2025-09-10-Truly-Layering-a-NET-Application-Based-on-DDD-Principles/post.md new file mode 100644 index 0000000000..03981e04c6 --- /dev/null +++ b/docs/en/Community-Articles/2025-09-10-Truly-Layering-a-NET-Application-Based-on-DDD-Principles/post.md @@ -0,0 +1,191 @@ +# **Truly Layering a .NET Application Based on DDD Principles** + +Okay, so we ALL been there, right? You start new project thinking "this time will be different" - clean code, perfect architecture, everything organized. Fast forward 3 months and your codebase look like someone throw grenade into bowl of spaghetti. Business logic everywhere, your controllers doing database work, and every new feature feel like defusing bomb. + +I been there too many times, and honestly, it suck. But here thing - there actually way to build .NET apps that not turn into maintenance nightmare. It called **Layered Architecture** + **Domain-Driven Design (DDD)**, and once you get it, it game changer. + +Let me walk you through this step by step, no fluff, just practical stuff that actually work. + +### **Layered Architecture 101 (The Foundation)** + +So layered architecture basically about keeping your code organized. Instead of having everything mixed together like bad smoothie, you separate concerns into different layers. Think like organizing your room - clothes go in closet, books on shelf, etc. + +Here how it typically break down: + + * **Presentation Layer (UI):** This what users actually see and click on - your ASP.NET Core MVC stuff, Razor Pages, Blazor, whatever float your boat. + * **Application Layer:** The conductor of orchestra. It not do heavy lifting itself, but tell everyone else what to do. It like middle manager of your code. + * **Domain Layer:** The VIP section. This where all your business rules live - entities, value objects, whole nine yards. This layer pure and not give damn about databases or UI. + * **Infrastructure Layer:** The "how-to" guy. Database stuff, email sending, API calls - basically all technical plumbing that make everything work. + +The golden rule? **Dependency Rule**: Layers can only talk to layers below them (or more central). UI talk to Application, Application talk to Domain, but Domain? Domain not talk to anyone. It the cool kid that everyone want to hang out with. + +### **DDD: Where Magic Happen** + +Alright, so DDD not some fancy framework you install from NuGet. It more like mindset - basically saying "hey, let make our code actually reflect business we building for." Instead of having bunch of random classes, we organize everything around actual business domain. + +Think like this: if you building e-commerce app, your code should scream "I'M E-COMMERCE APP" not "I'M BUNCH OF RANDOM CLASSES." + +Here toolkit DDD give you (all living in your Domain Layer): + + * **Entity:** This something that have identity. Like `Customer` - two customers with same name still different people because they have different IDs. It like having two friends named John - they not same person. + * **Value Object:** Opposite of entity. It defined by what it contain, not who it is. `Address` perfect for this - if two addresses have same street, city, and zip code, they same address. Usually immutable too. + * **Aggregate & Aggregate Root:** This where it get interesting. Aggregate like family of related objects that stick together. **Aggregate Root** head of family - only one you talk to when you want change something. Like `Order` that contain `OrderItem`s. You not mess with `OrderItem` directly, you tell `Order` to handle it. + * **Repository (Interface):** Think like your data access contract. It say "here how you can get and save stuff" without caring about whether it SQL Server, MongoDB, or file on your desktop. Interface live in Domain, implementation go in Infrastructure. + * **Domain Service:** When business logic too complex for single entity or value object, this your go-to. It like utility class but for business rules. + +### **Putting It All Together: Real C# Code** + +Alright, enough theory. Let see what this actually look like in real .NET solution. You typically have projects like: + + * `MyProject.Domain` (or `.Core`) - The VIP section + * `MyProject.Application` - The middle manager + * `MyProject.Infrastructure` - The technical guy + * `MyProject.Web` (or whatever UI you using) - The pretty face + +**1. The Domain Layer (`MyProject.Domain`) - The Heart** + +This where magic happen. Zero dependencies on other projects (maybe some basic utility libraries, but that it). Pure business logic, no database nonsense, no UI concerns. + +```csharp +// In MyProject.Domain/Orders/Order.cs +public class Order : AggregateRoot +{ + public Address ShippingAddress { get; private set; } + private readonly List _orderItems = new(); + public IReadOnlyCollection OrderItems => _orderItems.AsReadOnly(); + + // Private constructor for ORM + private Order() { } + + public Order(Guid id, Address shippingAddress) : base(id) + { + ShippingAddress = shippingAddress; + } + + public void AddOrderItem(Guid productId, int quantity, decimal price) + { + if (quantity <= 0) + { + throw new BusinessException("Quantity must be greater than zero."); + } + // More business rules... + _orderItems.Add(new OrderItem(productId, quantity, price)); + } +} + +// In MyProject.Domain/Orders/IOrderRepository.cs +public interface IOrderRepository +{ + Task GetAsync(Guid id); + Task AddAsync(Order order); + Task UpdateAsync(Order order); +} +``` + +See what I mean? The `Order` class all about business rules (`AddOrderItem` with validation and all that jazz). It not give damn about databases or how it get saved. That someone else problem. + +**2. The Application Layer (`MyProject.Application`) - The Conductor** + +This where we orchestrate everything. It talk to domain objects and use repositories to get/save data. Think like middle manager that coordinate work but not do heavy lifting. + +```csharp +// In MyProject.Application/Orders/OrderAppService.cs +public class OrderAppService +{ + private readonly IOrderRepository _orderRepository; + + public OrderAppService(IOrderRepository orderRepository) + { + _orderRepository = orderRepository; + } + + public async Task CreateOrderAsync(CreateOrderDto input) + { + var shippingAddress = new Address(input.Street, input.City, input.ZipCode); + var order = new Order(Guid.NewGuid(), shippingAddress); + + foreach (var item in input.Items) + { + order.AddOrderItem(item.ProductId, item.Quantity, item.Price); + } + + await _orderRepository.AddAsync(order); + } +} +``` + +The application service coordinate everything but let domain objects handle actual business rules. Clean separation! + +**3. The Infrastructure Layer (`MyProject.Infrastructure`) - The Technical Guy** + +This where we implement all interfaces we defined in domain. Entity Framework Core, email services, API clients - all technical plumbing live here. + +```csharp +// In MyProject.Infrastructure/Orders/EfCoreOrderRepository.cs +public class EfCoreOrderRepository : IOrderRepository +{ + private readonly MyDbContext _dbContext; + + public EfCoreOrderRepository(MyDbContext dbContext) + { + _dbContext = dbContext; + } + + public async Task GetAsync(Guid id) + { + // EF Core logic to get the order + return await _dbContext.Orders.FindAsync(id); + } + + public async Task AddAsync(Order order) + { + await _dbContext.Orders.AddAsync(order); + } + + // ... other implementations +} +``` + +### **ABP Framework: The Shortcut (Because We Lazy)** + +Look, setting all this up from scratch pain. That where **ABP Framework** come in clutch. It basically DDD and layered architecture on steroids, and it do all boring setup work for you. + +ABP not just talk talk - it walk walk. When you create new ABP solution, boom! Perfect project structure, all layered and DDD-compliant, ready to go. + +Here what you get out of box: + + * **Base Classes:** `AggregateRoot`, `Entity`, `ValueObject` - all with good stuff like optimistic concurrency and domain events. No more writing boilerplate. + * **Generic Repositories:** No more writing `IRepository` interfaces for every single entity. ABP give you `IRepository` with all standard CRUD methods. Just inject it and go. + * **Application Services:** Inherit from `ApplicationService` and boom - you done. It handle validation, authorization, exception handling, all that cross-cutting concern stuff without cluttering your actual business logic. + +With ABP, our `OrderAppService` become way cleaner: + +```csharp +// In ABP project, this much cleaner +public class OrderAppService : ApplicationService, IOrderAppService +{ + private readonly IRepository _orderRepository; + + public OrderAppService(IRepository orderRepository) + { + _orderRepository = orderRepository; + } + + public async Task CreateAsync(CreateOrderDto input) + { + // ... same logic as before, but using ABP generic repository + var order = new Order(...); + await _orderRepository.InsertAsync(order); + } +} +``` + +### **Wrapping Up** + +Look, I get it - this stuff take discipline and it not always fastest way to get features out door. But here thing: when you actually layer your app properly and put solid Domain Model at center, you end up with software that not suck to maintain. + +Your code start speaking language of business instead of some random technical jargon. That whole point of DDD - make your code reflect what you actually building for. + +Yeah, it take work upfront, but payoff huge. And frameworks like ABP make journey way less painful. Trust me, your future self will thank you when you not debugging spaghetti code at 2 AM. + +What you think? You try this approach before, or you still stuck in spaghetti code phase? Let me know in comments! \ No newline at end of file diff --git a/docs/en/Community-Articles/2025-09-11-Best-Practices-Guide-for-REST-API-Design/post.md b/docs/en/Community-Articles/2025-09-11-Best-Practices-Guide-for-REST-API-Design/post.md new file mode 100644 index 0000000000..6a20336307 --- /dev/null +++ b/docs/en/Community-Articles/2025-09-11-Best-Practices-Guide-for-REST-API-Design/post.md @@ -0,0 +1,333 @@ +# Best Practices Guide for REST API Design + +This guide compiles best practices for building robust, scalable, and sustainable RESTful APIs, based on information gathered from various sources. + +## 1. Fundamentals of REST Architecture + +REST is based on specific constraints and principles that support features like simplicity, scalability, and statelessness. The six core principles of RESTful architecture are: + +- **Uniform Interface**: This is about consistency. You use standard HTTP methods (GET, POST, PUT, DELETE) and URIs to interact with resources. The client knows how to talk to the server without needing some custom instruction manual. + +- **Client-Server**: The client (e.g., a frontend app) and the server are separate. The server handles data and logic, the client handles the user interface. They can evolve independently as long as the API contract doesn't change. + +- **Stateless**: This is a big one. The server doesn't remember anything about the client between requests. Every single request must contain all the info needed to process it (like an auth token). This is key for scalability. + +- **Cacheable**: Responses should declare whether they can be cached or not. Good caching can massively improve performance and reduce server load. + +- **Layered System**: You can have things like proxies or load balancers between the client and the server without the client knowing. It just talks to one endpoint, and the layers in between handle the rest. + +- **Code on Demand (Optional)**: This is the only optional one. It means the server can send back executable code (like JavaScript) to the client. Less common in the world of modern SPAs, but it's part of the spec. + +## 2. URI Design and Naming Conventions + +The URI structure is critical for making your API understandable and intuitive. + +### Use Nouns Instead of Verbs + +Your URIs should represent things (resources), not actions. The HTTP method already tells you what the action is. + +- **Good:** `/api/users` + +- **Bad:** `/api/getUsers` + +### Use Plural Nouns for Resource Names + +Stick with plural nouns for collections. It keeps things consistent, even when you're accessing a single item from that collection. + +- **Get all users:** `GET /api/users` + +- **Get a single user:** `GET /api/users/{id}` + +### Use Nested Routes to Show Relationships + +If a resource only exists in the context of another (like a user's orders), reflect that in the URL. + +- **Good:** `/api/users/{userId}/orders` (All orders for a user) + +- **Bad:** `/api/orders?userId={userId}` + +- **Good:** `/api/users/{userId}/orders/{orderId}` (A specific order for a user) + +**Note:** Use this structure only if the child resource is tightly coupled to the parent. Avoid nesting deeper than two or three levels, as this can complicate the URIs. + +### Path Parameters vs. Query Parameters + +Use the correct parameter type based on its function. + +- **Path Parameters (`/users/{id}`):** Use these to identify a specific resource or a collection. They are mandatory for the endpoint to resolve. + + - *Example:* `GET /api/users/123` uniquely identifies user 123. + +- **Query Parameters (`?key=value`):** Use these for optional actions like filtering, sorting, or pagination on a collection. + + - *Example:* `GET /api/users?role=admin&sort=lastName` filters the user collection. + +### Keep the URL Structure Consistent + +- **Use lowercase letters:** Since some systems are case-sensitive, always use lowercase in URIs for consistency. + + - *Example:* Use `/api/product-offers` instead of `/api/Product-Offers`. + +- **Use special characters correctly:** Use characters like `/`, `?`, and `#` only for their defined purposes. + + - *Example:* To get comments for a specific post, use the path `/posts/123/comments`. To filter those comments, use a query parameter: `/posts/123/comments?authorId=45`. + +## 3. Correct Usage of HTTP Methods + +Each HTTP method has a specific purpose. Sticking to these standards makes your API predictable. + +| **HTTP Method** | **Description** | **Idempotent*** | **Safe**** | +| --------------- | --------------------------------------------------------------------------- | --------------- | ---------- | +| **GET** | Retrieves a resource or a collection of resources. | Yes | Yes | +| **POST** | Creates a new resource. | No | No | +| **PUT** | Updates an existing resource completely or creates it if it does not exist. | Yes | No | +| **PATCH** | Partially updates an existing resource. | No | No | +| **DELETE** | Deletes a resource. | Yes | No | + +- **Idempotent:** Doing it once has the same effect as doing it 100 times. Deleting a user is idempotent; once it's gone, it's gone. + +- **Safe:** The request doesn't change anything on the server. GET is safe. + +**Example in practice:** + +Let's consider a resource endpoint for a collection of articles: `/api/articles`. + +- **`GET /api/articles`**: Retrieves a list of all articles. + +- **`GET /api/articles/123`**: Retrieves the specific article with ID 123. + +- **`POST /api/articles`**: Creates a new article. The data for the new article is sent in the request body. + +- **`PUT /api/articles/123`**: Replaces the entire article with ID 123 using the new data sent in the request body. + +- **`PATCH /api/articles/123`**: Partially updates the article with ID 123. For example, you could send only the `{"title": "New Title"}` in the request body to update just the title. + +- **`DELETE /api/articles/123`**: Deletes the article with ID 123. + +## 4. Data Exchange and Responses + +### Prefer the JSON Format + +It's the standard. It's lightweight, human-readable, and every language can parse it easily. Send and receive your data as JSON. + +- *Example Request Body:* + + ``` + { + "title": "Best Practices for APIs", + "authorId": 5, + "content": "An article about designing great APIs..." + } + ``` + +### Use Appropriate HTTP Status Codes + +Use standard HTTP status codes to provide clear information to the client about the outcome of their request. + +- **2xx (Success):** + + - `200 OK`: The request was successful. (For GET, PUT, PATCH) + + - `201 Created`: The resource was successfully created. (For POST) The response should include a `Location` header with the URI of the new resource. + + - *Example:* `POST /api/articles` responds with `201 Created` and the header `Location: /api/articles/124`. + + - `204 No Content`: The request was successful, but there is no response body. (For DELETE) + +- **4xx (Client Error):** + + - `400 Bad Request`: Invalid request (e.g., missing or incorrect data). + + - `401 Unauthorized`: Authentication is required. + + - `403 Forbidden`: No permission. + + - `404 Not Found`: The requested resource could not be found. + +- **5xx (Server Error):** + + - `500 Internal Server Error`: An unexpected error occurred on the server. + +### Provide Clear and Consistent Error Responses + +When something goes wrong, give back a useful JSON error message. Your future self and any developer using your API will thank you. + +- *Example of a detailed error response:* + +``` +{ + "type": "[https://---.com/probs/validation-error](https://example.com/probs/validation-error)", + "title": "Your request parameters didn't validate.", + "status": 400, + "detail": "The 'email' field must be a valid email address.", + "instance": "/api/users" +} +``` + +## 5. Performance Optimization + +Optimizing API performance is crucial for providing a good user experience and ensuring the scalability of your service. Key strategies include caching, efficient data retrieval, and controlling traffic. + +### Caching + +Caching is one of the most effective ways to improve performance. By storing and reusing frequently accessed data, you can significantly reduce latency and server load. + +- **How it works:** Caching can be implemented at various levels (client-side, CDN, server-side). REST APIs can facilitate this by using standard HTTP caching headers. + +- **Key Headers:** + + - `Cache-Control`: Tells the client how long to cache something (e.g., `public, max-age=600`). + + - `ETag`: A unique version identifier for a resource. The client can send this back in an `If-None-Match` header. If the data hasn't changed, you can just return `304 Not Modified` with an empty body, saving bandwidth. + + - `Last-Modified`: Indicates when the resource was last changed. Similar to `ETag`, it can be used for conditional requests with the `If-Modified-Since` header. + +- *Example Response Header for Caching:* + + ``` + Cache-Control: public, max-age=600 + ETag: "x234dff" + ``` + +### Filtering, Sorting, and Pagination + +For endpoints that return lists of resources, it's inefficient to return the entire dataset at once, especially if it's large. Implementing these features gives clients more control over the data they receive. + +- **Filtering:** Allows clients to narrow down the result set based on specific criteria. This reduces the amount of data transferred and makes it easier for the client to find what it needs. + + - *Example:* `GET /api/orders?status=shipped&customer_id=123` + +- **Sorting:** Enables clients to request the data in a specific order. A common convention is to specify the field to sort by and the direction (ascending or descending). + + - *Example:* `GET /api/users?sort=lastName_asc` or `GET /api/products?sort=-price` (the `-` indicates descending order). + +- **Pagination:** Breaks down a large result set into smaller, manageable chunks called "pages". This prevents overloading the server and client with massive amounts of data in a single response. + + - *Example:* `GET /api/articles?page=2&pageSize=20` (retrieves the second page, with 20 articles per page). + +### Rate Limiting + +Protect your API from abuse by limiting how many requests a client can make in a given time. If they exceed the limit, return a `429 Too Many Requests`. +It's also super helpful to return these headers so the client knows what's going on: + +- `X-RateLimit-Limit`: Total requests allowed. + +- `X-RateLimit-Remaining`: How many requests they have left. + +- `Retry-After`: How many seconds they should wait before trying again. + +## 6. Security + +Security is not an optional feature; it must be a core part of your API design. + +- **Always Use HTTPS (TLS):** Encrypt all traffic to prevent man-in-the-middle attacks. There are no exceptions to this rule for production APIs. + +- **Authentication & Authorization:** + + - **Authentication** (Who are you?): Use a standard like OAuth 2.0 or JWT Bearer Tokens. + + - **Authorization** (What are you allowed to do?): Check permissions for every request. Just because a user is logged in doesn't mean they can delete another user's data. + +- **Input Validation**: Always validate and sanitize data coming from the client to prevent injection attacks. If the data is bad, reject it with a `400 Bad Request`. + +- **Use Security Headers**: Add headers like `Strict-Transport-Security` and `Content-Security-Policy` to add extra layers of browser-level protection. + +## 7. API Lifecycle Management + +### Versioning + +Your API will change. Versioning lets you make breaking changes without messing up existing clients. The most common way is in the URI. + +- **URI Versioning (Most Common):** `https://api.example.com/v1/users` + + - **Pros:** Simple, explicit, and easy to explore in a browser. + +- **Header Versioning:** The client requests a version via a custom HTTP header. + + - *Example:* `Accept-Version: v1` + + - **Pros:** Keeps the URI clean. + +- **Media Type Versioning (Content Negotiation):** The version is included in the `Accept` header. + + - *Example:* `Accept: application/vnd.example.v1+json` + + - **Pros:** Technically the "purest" REST approach. + +### Backward Compatibility & Deprecation + +When you release v2, don't just kill v1. Keep it running for a while and communicate a clear shutdown schedule to your users. + +### Documentation + +An API is only as good as its documentation. Use tools like the **OpenAPI Specification (formerly Swagger)** to generate interactive, machine-readable documentation. Good docs should include: + +- Authentication instructions. + +- Clear explanations of each endpoint. + +- Request/response examples. + +- Error code definitions. + +## 8. Monitoring and Testing + +### Monitoring and Logging + +To ensure your API is reliable, you must monitor its health and log important events. + +- **Structured Logging:** Log in a machine-readable format like JSON. Include a `correlationId` to track a single request across multiple services. + +- **Monitoring:** Track key metrics like latency (response time), error rate, and requests per second. Use tools like Prometheus, Grafana, or Datadog to visualize these metrics and set up alerts. + +### API Testing + +Thorough testing is essential to prevent bugs and regressions. + +- **Unit Tests:** Test individual components and business logic in isolation. + +- **Integration Tests:** Test the interaction between different parts of your API, including the database. + +- **Contract Tests:** Verify that your API adheres to its documented contract (e.g., the OpenAPI spec). + +## 9. Advanced Level: HATEOAS + +**HATEOAS (Hypermedia as the Engine of Application State)** is a REST principle that allows your API to be self-documenting and more discoverable. It involves including hyperlinks in responses for actions that can be performed on the relevant resource. + +For example, a response for a user resource might look like this: + +``` +{ + "id": 1, + "name": "Deo Steel", + "links": [ + { "rel": "self", "href": "/users/1", "method": "GET" }, + { "rel": "update", "href": "/users/1", "method": "PUT" }, + { "rel": "delete", "href": "/users/1", "method": "DELETE" } + ] +} +``` + +This way, the client can follow the links in the response to take the next step, rather than manually constructing the URIs. + +## 10. A Practical Shortcut: Leveraging Frameworks like ABP.IO + +Okay, that was a lot. While it's crucial to understand all these principles, you don't have to build everything from scratch. Modern frameworks can handle a ton of this for you. I work a lot in the .NET space, and **ABP Framework** is a great example of this. + +Here’s how it automates many of the things we just talked about: + +- **Automatic API Controllers**: You write your business logic in an "Application Service," and ABP automatically creates the REST API endpoints for you, following all the correct naming and HTTP method conventions. (Covers sections 2 & 3). + +- **Built-in Best Practices**: + + - **Standardized Error Responses**: It has a built-in exception handling system that automatically generates clean, consistent JSON error responses. (Covers section 4). + + - **Input Validation**: It has automatic validation for your DTOs. If a request is invalid, it returns a detailed `400 Bad Request` without you writing a single line of code for it. (Covers section 6). + + - **Paging, Sorting, Filtering**: You get these out of the box by just using their predefined interfaces. (Covers section 5). + +- **Integrated Security**: It comes with a full auth system. You just add an `[Authorize]` attribute to a method, and it handles the rest. It also automatically manages database transactions per API request (Unit of Work) to ensure data consistency. (Covers section 6). + +- **Automatic Documentation**: It automatically generates an OpenAPI/Swagger UI for your API, which is a massive help for anyone who needs to use it. (Covers section 7). + +Using a framework like this lets you focus on your core business logic, confident that the foundation is built on solid, established best practices. diff --git a/docs/en/Community-Articles/2025-09-11-High-Perf-DotNet-Libs/BenchmarkDotnet.png b/docs/en/Community-Articles/2025-09-11-High-Perf-DotNet-Libs/BenchmarkDotnet.png new file mode 100644 index 0000000000..ed81071118 Binary files /dev/null and b/docs/en/Community-Articles/2025-09-11-High-Perf-DotNet-Libs/BenchmarkDotnet.png differ diff --git a/docs/en/Community-Articles/2025-09-11-High-Perf-DotNet-Libs/Disruptor.png b/docs/en/Community-Articles/2025-09-11-High-Perf-DotNet-Libs/Disruptor.png new file mode 100644 index 0000000000..d66333865c Binary files /dev/null and b/docs/en/Community-Articles/2025-09-11-High-Perf-DotNet-Libs/Disruptor.png differ diff --git a/docs/en/Community-Articles/2025-09-11-High-Perf-DotNet-Libs/MemoryPack.png b/docs/en/Community-Articles/2025-09-11-High-Perf-DotNet-Libs/MemoryPack.png new file mode 100644 index 0000000000..cdb4f81e38 Binary files /dev/null and b/docs/en/Community-Articles/2025-09-11-High-Perf-DotNet-Libs/MemoryPack.png differ diff --git a/docs/en/Community-Articles/2025-09-11-High-Perf-DotNet-Libs/MessagePack.png b/docs/en/Community-Articles/2025-09-11-High-Perf-DotNet-Libs/MessagePack.png new file mode 100644 index 0000000000..dc7d76dd2f Binary files /dev/null and b/docs/en/Community-Articles/2025-09-11-High-Perf-DotNet-Libs/MessagePack.png differ diff --git a/docs/en/Community-Articles/2025-09-11-High-Perf-DotNet-Libs/Polly.png b/docs/en/Community-Articles/2025-09-11-High-Perf-DotNet-Libs/Polly.png new file mode 100644 index 0000000000..18a930df0e Binary files /dev/null and b/docs/en/Community-Articles/2025-09-11-High-Perf-DotNet-Libs/Polly.png differ diff --git a/docs/en/Community-Articles/2025-09-11-High-Perf-DotNet-Libs/Post.md b/docs/en/Community-Articles/2025-09-11-High-Perf-DotNet-Libs/Post.md new file mode 100644 index 0000000000..fbe358c8cb --- /dev/null +++ b/docs/en/Community-Articles/2025-09-11-High-Perf-DotNet-Libs/Post.md @@ -0,0 +1,157 @@ +# High-Performance .NET Libraries You Didn’t Know You Needed + +Whether you’re building enterprise apps, microservices, or SaaS platforms, using the right libraries can help you ship faster and scale effortlessly. +Here are some **high-performance .NET libraries** you might not know but definitely should. + + + +## 1. BenchmarkDotNet – Measure before you optimize + +![BenchmarkDotnet](BenchmarkDotnet.png) + +BenchmarkDotNet makes it simple to **benchmark .NET code with precision**. + +- Easy setup with `[Benchmark]` attributes +- Generates detailed performance reports +- Works with .NET Core, .NET Framework, and Mono + +Perfect for spotting bottlenecks in APIs, background services, or CPU-bound operations. + +- **NuGet** (40M downloads) 🔗 https://www.nuget.org/packages/BenchmarkDotNet +- **GitHub** (11k stars) 🔗 https://github.com/dotnet/BenchmarkDotNet + + + +## 2. MessagePack – Fastest JSON serializer + +Need speed beyond System.Text.Json or Newtonsoft.Json? MessagePack is the fastest serializer for C# (.NET, .NET Core, Unity, Xamarin). MessagePack has a compact binary size and a full set of general-purpose expressive data types. Ideal for high-traffic APIs, IoT data processing, and microservices. + +![MessagePack Benchmark](MessagePack.png) + +- **NuGet** (204M downloads) 🔗 https://www.nuget.org/packages/messagepack +- **GitHub** (6.4K stars) 🔗 https://github.com/MessagePack-CSharp/MessagePack-CSharp + + + +## 3. Polly – Resilience at scale + +![Polly](Polly.png) + +In distributed systems, failures are inevitable. **Polly** provides a fluent way to add **retry, circuit-breaker, and fallback** strategies. + +- Handle transient faults gracefully +- Improve uptime and user experience +- Works seamlessly with HttpClient and gRPC + +A must-have for cloud-native .NET applications. + +- **NuGet** (1B downloads) 🔗 https://www.nuget.org/packages/polly/ +- **GitHub** (14K stars) 🔗 https://github.com/App-vNext/Polly + + + +## 4. MemoryPack – Zero-cost binary serialization + +If you need **blazing-fast serialization** for in-memory caching or network transport, **MemoryPack** is a game-changer. + +- Zero-copy, zero-alloc serialization +- Perfect for high-performance caching or game servers +- Strongly typed and version-tolerant + +![MemoryPack](MemoryPack.png) + +Great for real-time multiplayer games, chat apps, or financial systems. + +- **NuGet** (5.3M downloads) 🔗 https://www.nuget.org/packages/MemoryPack +- **GitHub** (4K stars) 🔗 https://github.com/Cysharp/MemoryPack + + + +## 5. WolverineFx – Ultra-low latency messaging + +![wolverine](wolverine-logo.png) + +MediatR was one of the best mediator libraries, but now it's a paid library. Wolverine is a toolset for command execution and message handling within .NET applications. The killer feature of Wolverine is its very efficient command execution pipeline that can be used as: + +- An [inline "mediator" pipeline](https://wolverinefx.net/tutorials/mediator.html) for executing commands +- A [local message bus](https://wolverinefx.net/guide/messaging/transports/local.html) for in-application communication +- A full-fledged [asynchronous messaging framework](https://wolverinefx.net/guide/messaging/introduction.html) for robust communication and interaction between services when used in conjunction with low-level messaging infrastructure tools like RabbitMQ +- With the [WolverineFx.Http](https://wolverinefx.net/guide/http/) library, Wolverine's execution pipeline can be used directly as an alternative ASP.NET Core Endpoint provider + +*image below is from [codecrash.net](https://www.codecrash.net/2024/02/06/Mediatr-versus-Wolverine-performance.html)* +![WolverineFx](wolverine.png) + +WolverineFx is great for cleanly separating business logic from controllers while unifying in-process mediator patterns with powerful distributed messaging in a single, high-performance .NET library. + +- **NuGet** (1.5M downloads) 🔗 https://www.nuget.org/packages/WolverineFx +- **GitHub** (1.7K stars) 🔗 https://github.com/JasperFx/wolverine + + +## 6. Disruptor-net – Next generation free .NET mediator + +The Disruptor is a high-performance inter-thread message passing framework. A lock-free ring buffer for ultra-low latency messaging. +Features are: + +- Zero memory allocation after initial setup (the events are pre-allocated). + +- Push-based consumers. + +- Optionally lock-free. + +- Configurable wait strategies. + +![Disruptor](Disruptor.png) + +- **NuGet** (1.2M downloads) 🔗 https://www.nuget.org/packages/Disruptor/ +- **GitHub** (1.3K stars) 🔗 https://github.com/disruptor-net/Disruptor-net + + +## 7. CliWrap - Running command-line processes + +![CLIWrap](cliwrap.png) + +CliWrap makes it easy to **run and manage external CLI processes in .NET**. + +- Fluent, task-based API for starting commands +- Streams standard input/output and error in real time +- Supports cancellation, timeouts, and piping between processes + +Ideal for automation, build tools, and integrating external executables. + +- **NuGet** (14.1M downloads) 🔗 https://www.nuget.org/packages/CliWrap +- **GitHub** (4.7K stars) 🔗 https://github.com/Tyrrrz/CliWrap + + +--- + + +## Hidden Libs from the Community + +### Sylvan.Csv & **Sep** + +- **Sylvan.Csv**: Up to *10× faster* and *100× less memory allocations* than `CsvHelper`, making CSV processing lightning-fast. ([Reddit](https://www.reddit.com/r/csharp/comments/191rwgt/extremely_highperformance_libraries_for_common/?utm_source=chatgpt.com)) +- **Sep**: Even faster than Sylvan, but trades off some flexibility. Great when performance matters more than API richness. ([Reddit](https://www.reddit.com/r/csharp/comments/191rwgt/extremely_highperformance_libraries_for_common/?utm_source=chatgpt.com)) + +### String Parsing: **csFastFloat** + +- Parses `float` and `double` around *8–9× faster* than `.Parse` methods—perfect for high-volume parsing tasks. ([Reddit](https://www.reddit.com/r/csharp/comments/191rwgt/extremely_highperformance_libraries_for_common/?utm_source=chatgpt.com)) + +### CySharp’s Suite: MemoryPack, MasterMemory, SimdLinq + +- **MemoryPack**: One of the fastest serializers available, with low allocations and high throughput. Ideal for Web APIs or microservices. ([Reddit](https://www.reddit.com/r/csharp/comments/191rwgt/extremely_highperformance_libraries_for_common/?utm_source=chatgpt.com)) +- **MasterMemory**: Designed for databases or config storage. Claims *4,700× faster than SQLite* with zero-allocations per query. ([Reddit](https://www.reddit.com/r/csharp/comments/191rwgt/extremely_highperformance_libraries_for_common/?utm_source=chatgpt.com)) +- **SimdLinq**: SIMD-accelerated LINQ operations supporting a broader set of methods than .NET's built-in SIMD. Works when slight floating-point differences are acceptable. ([Reddit](https://www.reddit.com/r/csharp/comments/191rwgt/extremely_highperformance_libraries_for_common/?utm_source=chatgpt.com)) + +### Jil – JSON Deserializer + +- Ultra-fast JSON (de)serializer with low memory overhead, used in high-scale systems. ([Performance is a Feature!](https://www.mattwarren.org/2014/09/05/stack-overflow-performance-lessons-part-2/?utm_source=chatgpt.com)) + +### StackExchange.NetGain – WebSocket Efficiency + +- High-performance WebSocket server library designed for low-latency IO scenarios. (Now mostly replaced by Kestrel's built-in support, but worth knowing.) ([GitHub](https://github.com/StackExchange/NetGain?utm_source=chatgpt.com)) + +### Math Libraries: Math.NET Numerics & ILNumerics + +- **Math.NET Numerics**: Core numerical methods and matrix math, similar to BLAS/LAPACK. ([Wikipedia](https://en.wikipedia.org/wiki/Math.NET_Numerics?utm_source=chatgpt.com)) +- **ILNumerics**: Efficient numerical arrays with parallelized processing, loop unrolling and cache optimizations. Great for scientific computing. ([Wikipedia](https://en.wikipedia.org/wiki/ILNumerics?utm_source=chatgpt.com)) + diff --git a/docs/en/Community-Articles/2025-09-11-High-Perf-DotNet-Libs/cliwrap.png b/docs/en/Community-Articles/2025-09-11-High-Perf-DotNet-Libs/cliwrap.png new file mode 100644 index 0000000000..718d1b558b Binary files /dev/null and b/docs/en/Community-Articles/2025-09-11-High-Perf-DotNet-Libs/cliwrap.png differ diff --git a/docs/en/Community-Articles/2025-09-11-High-Perf-DotNet-Libs/cover.png b/docs/en/Community-Articles/2025-09-11-High-Perf-DotNet-Libs/cover.png new file mode 100644 index 0000000000..a1b4c825e2 Binary files /dev/null and b/docs/en/Community-Articles/2025-09-11-High-Perf-DotNet-Libs/cover.png differ diff --git a/docs/en/Community-Articles/2025-09-11-High-Perf-DotNet-Libs/wolverine-logo.png b/docs/en/Community-Articles/2025-09-11-High-Perf-DotNet-Libs/wolverine-logo.png new file mode 100644 index 0000000000..b14dc2b7ed Binary files /dev/null and b/docs/en/Community-Articles/2025-09-11-High-Perf-DotNet-Libs/wolverine-logo.png differ diff --git a/docs/en/Community-Articles/2025-09-11-High-Perf-DotNet-Libs/wolverine.png b/docs/en/Community-Articles/2025-09-11-High-Perf-DotNet-Libs/wolverine.png new file mode 100644 index 0000000000..5ef73f0149 Binary files /dev/null and b/docs/en/Community-Articles/2025-09-11-High-Perf-DotNet-Libs/wolverine.png differ diff --git a/docs/en/Community-Articles/2025-09-11-Web-Design-Basics-for-Graphic-Designers-Who-Dont-Code/images/img_1.png b/docs/en/Community-Articles/2025-09-11-Web-Design-Basics-for-Graphic-Designers-Who-Dont-Code/images/img_1.png new file mode 100644 index 0000000000..9889c8d813 Binary files /dev/null and b/docs/en/Community-Articles/2025-09-11-Web-Design-Basics-for-Graphic-Designers-Who-Dont-Code/images/img_1.png differ diff --git a/docs/en/Community-Articles/2025-09-11-Web-Design-Basics-for-Graphic-Designers-Who-Dont-Code/images/img_2.png b/docs/en/Community-Articles/2025-09-11-Web-Design-Basics-for-Graphic-Designers-Who-Dont-Code/images/img_2.png new file mode 100644 index 0000000000..5e00300c81 Binary files /dev/null and b/docs/en/Community-Articles/2025-09-11-Web-Design-Basics-for-Graphic-Designers-Who-Dont-Code/images/img_2.png differ diff --git a/docs/en/Community-Articles/2025-09-11-Web-Design-Basics-for-Graphic-Designers-Who-Dont-Code/images/img_3.png b/docs/en/Community-Articles/2025-09-11-Web-Design-Basics-for-Graphic-Designers-Who-Dont-Code/images/img_3.png new file mode 100644 index 0000000000..3675376cb3 Binary files /dev/null and b/docs/en/Community-Articles/2025-09-11-Web-Design-Basics-for-Graphic-Designers-Who-Dont-Code/images/img_3.png differ diff --git a/docs/en/Community-Articles/2025-09-11-Web-Design-Basics-for-Graphic-Designers-Who-Dont-Code/images/img_4.png b/docs/en/Community-Articles/2025-09-11-Web-Design-Basics-for-Graphic-Designers-Who-Dont-Code/images/img_4.png new file mode 100644 index 0000000000..e0b94bb47c Binary files /dev/null and b/docs/en/Community-Articles/2025-09-11-Web-Design-Basics-for-Graphic-Designers-Who-Dont-Code/images/img_4.png differ diff --git a/docs/en/Community-Articles/2025-09-11-Web-Design-Basics-for-Graphic-Designers-Who-Dont-Code/images/img_5.png b/docs/en/Community-Articles/2025-09-11-Web-Design-Basics-for-Graphic-Designers-Who-Dont-Code/images/img_5.png new file mode 100644 index 0000000000..4ed8c4eabe Binary files /dev/null and b/docs/en/Community-Articles/2025-09-11-Web-Design-Basics-for-Graphic-Designers-Who-Dont-Code/images/img_5.png differ diff --git a/docs/en/Community-Articles/2025-09-11-Web-Design-Basics-for-Graphic-Designers-Who-Dont-Code/images/img_6.png b/docs/en/Community-Articles/2025-09-11-Web-Design-Basics-for-Graphic-Designers-Who-Dont-Code/images/img_6.png new file mode 100644 index 0000000000..ad2640da1f Binary files /dev/null and b/docs/en/Community-Articles/2025-09-11-Web-Design-Basics-for-Graphic-Designers-Who-Dont-Code/images/img_6.png differ diff --git a/docs/en/Community-Articles/2025-09-11-Web-Design-Basics-for-Graphic-Designers-Who-Dont-Code/images/img_7.png b/docs/en/Community-Articles/2025-09-11-Web-Design-Basics-for-Graphic-Designers-Who-Dont-Code/images/img_7.png new file mode 100644 index 0000000000..cfdf76ea7e Binary files /dev/null and b/docs/en/Community-Articles/2025-09-11-Web-Design-Basics-for-Graphic-Designers-Who-Dont-Code/images/img_7.png differ diff --git a/docs/en/Community-Articles/2025-09-11-Web-Design-Basics-for-Graphic-Designers-Who-Dont-Code/images/img_8.png b/docs/en/Community-Articles/2025-09-11-Web-Design-Basics-for-Graphic-Designers-Who-Dont-Code/images/img_8.png new file mode 100644 index 0000000000..f399892ee8 Binary files /dev/null and b/docs/en/Community-Articles/2025-09-11-Web-Design-Basics-for-Graphic-Designers-Who-Dont-Code/images/img_8.png differ diff --git a/docs/en/Community-Articles/2025-09-11-Web-Design-Basics-for-Graphic-Designers-Who-Dont-Code/images/img_9.png b/docs/en/Community-Articles/2025-09-11-Web-Design-Basics-for-Graphic-Designers-Who-Dont-Code/images/img_9.png new file mode 100644 index 0000000000..171ff61851 Binary files /dev/null and b/docs/en/Community-Articles/2025-09-11-Web-Design-Basics-for-Graphic-Designers-Who-Dont-Code/images/img_9.png differ diff --git a/docs/en/Community-Articles/2025-09-11-Web-Design-Basics-for-Graphic-Designers-Who-Dont-Code/post.md b/docs/en/Community-Articles/2025-09-11-Web-Design-Basics-for-Graphic-Designers-Who-Dont-Code/post.md new file mode 100644 index 0000000000..cfd4db30bf --- /dev/null +++ b/docs/en/Community-Articles/2025-09-11-Web-Design-Basics-for-Graphic-Designers-Who-Dont-Code/post.md @@ -0,0 +1,282 @@ +# Web Design Basics for Graphic Designers Who Don't Code + +## Introduction + +As a **designer**, I have been working on **logos**, **posters**, and **social media announcement** **visuals** for years. However, when it comes to the web, I used to hold back saying “I **don’t know how to code**.” We have all thought about this at some point and unfortunately, we still think about it from time to time. + +🚀 **Good news**: We can learn **web design** without writing code and design **user-friendly**, **aesthetic, and functional web interfaces** using basic knowledge. + +In this article, we will talk about the **basics of web design**, its **differences from graphic design**, and whether it is possible to do **web design without knowing how to code**. + +## Differences Between Graphic Design & Web Design + +![](images/img_2.png) + +### What is Graphic Design? + +Graphic design is creating **visual content** that conveys a message to a **specific audience**. Graphic designers use various **visual elements** such as **color**, **typography**, **imagery**, and **layout** to communicate a message effectively. They work on a wide range of projects, including **logos**, **websites**, **packaging**, **advertisements, and branding**. + +### What is Web Design? + +Web design is the process of creating a website that can be viewed on computers or mobile devices. Like graphic design, web design also involves creating **graphics**, **typography**, **and visuals**, but they use the **internet** as the communication channel. + +### Graphic Design + +* Graphic Design is concerned with **visuals** and **appearance**. +* Graphic design focuses on visually conveying specific messages or ideas through **typography**, **visuals**, **colors**, and i**llustrations**. +* Graphic design focuses on how objects **look**. +* Graphic designers **do not need coding knowledge**. +* Graphic design is **static**. + +### Web Design + +* Web design is user experience–focused. +* Web design aims to create **functional** and **user-friendly** **websites** that provide the **best experience** for users. +* Web design considers **search engine optimization** when creating websites. +* Web designers need to have **knowledge of HTML**, **CSS**, and other web development languages to create **functional and responsive designs**. +* Web design is **dynamic**. + +## Fundamental Principles of Web Design (Applicable Without Coding) + +Companies and **brands** from almost every sector request the **creation of their own websites**. This way, they gain the opportunity to introduce their **services**, **prices**, and themselves to their **target audiences**. However, for this to have the desired effect, the website must be **designed properly**. What are the **fundamental principles** to pay attention to when designing a website? Now, it’s time to answer this question by introducing the basics. Here are the **indispensable principles in web design**. + + ### 1\) User-Centered Designs: + +Users always value **ease and practicality** when receiving a service. For this reason, it is important for websites to be designed in a user-centered way. **Easy to find menus**, **fast usage**, and the **easy to locate any information** are very important. With **user centered design**, it is possible to create websites that are **easy to use** and also **satisfy users**. + + + ### 2\) Responsive Designs: + +It is very important for the website and its design to be **usable on every digital platform**. Therefore, the designed sites must have a **responsive design**. This means easy access to the site on a **phone**, **tablet**, or **computer**. This also ensures that users continue to prefer the site. + + ### 3\) Visual Hierarchy: + +The page must have **visuals related to itself**, and **product content** should be matched with the **correct visuals**. It is also very important for visual elements to be placed according to their order of importance. On web pages, content compatible with visuals must be provided with **sufficient and accurate information**. + + ### 4\) Color and Typography Selection: + +Color and typography selection is very important for handling visuals, colors, and text in a certain **harmony**. For the site to **attract attention** or for the relevant pages to achieve the expected interaction, the use of color and the chosen font style and font color must be harmonious. A design that is both **easy to read** and **eye catching** without causing any disturbance should be preferred. + + ### 5\) Content and Layout Structure: + +One of the most desired features on web pages is **content organization**. Content that is **unrelated to the pages, creates confusion while reading**, or **lacks simplicity**, such as overly frequent paragraphs, incorrect fonts, and similar factors, causes web pages to have less impact. **Content structure** also includes **placing related topics sequentially** and **adding them to the menu**. For example, on a website created for shoes, if shoe types are grouped separately, users find it easier. Options like high heels, sandals, and sneakers help users find what they are looking for more easily, which in turn ensures positive site feedback. + + ### 6\) Speed Optimization: + +As with every type of service, speed is very important for services provided through web pages. Easy navigation between pages, error-free performance, and ease of use are very important for users. No one wants to shop or use a service on a website that takes a long time to load, because everyone prefers websites to save time. + + ### 7\) Consistency: + +For a service to be preferred, it must first be reliable. This is directly related to the information, visuals, and everything on the website. The information in the content, visuals, and content details must be consistent and should not raise any questions in visitors’ minds. Otherwise, negative feedback can later affect customer preferences and damage the brand image. + +## Is It Possible to Do Web Design Without Coding? + +In the past, it was not possible to create a website without at least some basic coding knowledge. However, today, almost anyone can build a website. Even if you have not written a **single line of code**. + +The biggest helpers for those who want to do **web development without coding** are **No-Code** and **Low-Code** platforms. These tools help users **design websites** without dealing with **technical details**. + +Systems like **Wix**, **Webflow**, **Shopify**, and **WordPress** are very common in this area. + +![](images/img_3.png) + +The web development process on these platforms is carried out through practical methods such as **drag-and-drop**, selecting **ready-made templates**, and filling out forms. + +### **No Code** + +As the name suggests, it allows you to create websites, mobile applications, automations, and workflows **without writing a single line of code**. + +* **How Does It Work**? They usually have a visual editor. You create the skeleton of your application by dragging and dropping ready “building blocks” such as buttons, forms, and visuals onto your canvas. Then, you determine what these elements will do (for example, “go to this page when this button is clicked”) by selecting options from the menus. +* **Who Uses It**? It is perfect for entrepreneurs, marketers, product managers, designers, and anyone who wants to quickly test an idea. +* **Examples**: Platforms like Webflow, Bubble, Adalo, and Glide allow you to create a wide range of products, from complex web applications to mobile apps. + +### **Low Code** + +Low-Code systems require a bit more **technical knowledge** but still **do not require learning full-scale programming**. + +* **How Does It Work**? You handle 80% of the work with drag-and-drop, and for the remaining 20% that requires customization, you add small code snippets. +* **Who Uses It**? It is generally preferred by IT departments of corporate companies and technical teams that want to develop more complex, scalable applications. +* **Examples**: Platforms like OutSystems and Mendix are used to build large, integrated systems that manage a company’s internal processes. + +## What Should the Web Design Process be Like? + +![](images/img_4.png) + +**Web design** is a passionate field but can be **overwhelming** at times. When starting out, coming up with a plan on how to tackle your website or a web app idea often feels daunting: Where should you begin?Web designers often think about the **web design process** with a focus on **technical matters** such as wireframes, code, and content management. But great + +design isn’t about how you integrate the social media buttons or even slick visuals. **Great design** is actually about having **a website creation process** that aligns with an **overarching strategy.** + +Doing all the thinking beforehand ensures that you don’t forget anything crucial. It also frees up headspace for doing the actual work, avoids overwhelm, improves efficiency, and allows you to build better websites on repeat. + +But how do you achieve that harmonious synthesis of elements? Through a **holistic web design** process that takes both **form and function** into account. + +We have already covered the fundamentals, now, I'll share the steps to an **effective web design process.** + +Let's get started. + +### 1\) Goal Identification + +In this **initial stage**, the designer needs to identify the end goal of the website design, usually in close collaboration with the client or other stakeholders. Questions to explore and answer in this stage of the design and website development process include: + +* Who is the site for? +* What do they hope to find or do there? +* Is the main purpose of this website to inform, to sell (e-commerce, for everyone?), or to entertain? +* Does the website need to clearly convey the **brand's core message**, or is it part of a broader **brand strategy** with its own unique focus? +* If there are any, which **competitor sites** exist, and how should this site be **inspired by them** / how should it differ from them? + +To have clear answers to above questions will lead to the **successful execution** of the project. + +### What Purpose Will the Website Serve? + +Whatever the project you’re taking on, you always want each and every initiative you take to achieve the goals you’ve set for it. **Goal setting is critical** because it will be key in making decisions throughout the project by asking yourself the right questions and **prioritizing tasks and efforts**. + +As basic as it may seem, following the **SMART framework** is always a great idea when setting your goals, to **ensure effectiveness:** + +**S \- SPECIFIC** +Your goal is direct, detailed, and meaningful. + +**M \- MEASURABLE** +Your goal is quantifiable to track progress or success. + +**A \- ATTAINABLE** +Your goal is realistic and you have the tools and/or resources to attain it. + +**R \- RELEVANT** +Your goal aligns with your company mission. + +**T \- TIME-BASED** +Your goal has a deadline. + +### 2\) Scope Definition + +This is easier said than done when starting out, so it is best to approach it with caution : Everyone has once been guilty of saying a project “will be done by next week” before realizing they dramatically **underestimated** how hard it would be. + +Nevertheless, **setting** a timeline will help a lot with **accountability**, both internal and external, and will help **break down the project in distinct stages**. + +You don’t have to reinvent anything from scratch, as a lot of tools such as Airtable’s timeline view will help you put the timeline together. + +![](images/img_5.png) + +Source: [Airtable](https://blog.airtable.com/introducing-airtables-new-timeline-view/) + +### 3\) Sitemap and Wireframe Creation + +The site map forms the foundation of a well-designed website. It gives web designers a clear idea of the **information architecture** of the website and explains the **relationships** between various **pages and content elements**. + +![](images/img_6.png) + +Building a web site without a site map is like building a house without a plan. And it rarely ends well. + +Time to start building the first iteration of your project\! To put it shortly, **wireframes** serve as a blueprint, a visual guide representing the skeletal framework of a website or application. It will be a raw version of your project, a great way to get your **initial idea down** in its first “physical” form. + +![](images/img_7.png) + +Source: [Afolayan Daniel](https://medium.com/fbdevclagos/4-reasons-why-wire-frame-is-important-during-website-or-mobile-app-development-46fabdf47190) + +While it won’t be functional yet, it’ll be a major web design step to share with your team, potential leads or even investors, and will highlight issues that you might not have thought about previously. Wireframes are a great opportunity to move fast, once they’re ready, you’ll be able to: + +* Gather early feedback; +* Run UX testing groups; +* Iterate on your timeline if necessary; +* Get concept validation. + +There are different ways to create wireframes. You can of course sketch them out on paper to start with, but creating a digital version will eventually be much more practical to share them. + +#### Tools for sitemapping and wireframing; + +* Pen/pencil and paper. +* Balsamiq. +* Moqups. +* Sketch. +* Axure. +* Webflow. +* Slickplan. +* Writemaps. +* Mindnode. +* Figma. +* Sketch. + +### 4\) Content Creation +A website should offer more than just a simple design and attractive graphics. An effective content strategy is essential to capture users’ interest and to make the site stand out in search engines. + +![](images/img_8.png) +When it comes to content, search engine optimization is only +half of the battle. + +There are two main goals that you need to focus on while creating content. + +#### **Goal 1 Content encourages engagement and action:** + +First of all, content drives readers to take action and encourages them to perform the actions necessary to achieve a site's goals. This is influenced both by the content itself (writing) and by the way it is presented (typography and structural elements). + +Boring, lifeless, and lengthy writing rarely holds visitors' attention for long enough. Short, fluent, and engaging content captures them and makes them click through to other pages. Even if your pages need a lot of content (which they often do), properly "breaking it up" into short paragraphs supported by visuals can help create a light and engaging feel. + +#### **Goal 2 Search Engine Optimization**: + +Content also increases a site's visibility in the eyes of search engines. The practice of creating and developing content to achieve a good ranking in search results is called search engine optimization or SEO. + +Identifying your keywords and key phrases correctly is very important for the success of any website. + +### 5\) Visual Elements + +![](images/img_9.png) + +Style Tile: a free style tile / moodboard template built by Mat Vogels. + +It is time to create the **visual style** of the site. This part of the design process is usually shaped by **existing brand elements, color choices**, and **logos** specified by the client. However, it is also the stage of the web design process where a **good web designer can truly shine**. + +**Visuals play a more important** **role** in web design than ever before. High quality visuals not only give a website a professional look and feel, but also convey a message, are mobile friendly, and help build trust. + +**Visual design is a way of communicating** with the web site users to make the site as **appealing to them** as possible. When done right, it can determine the site’s being one of the major successes amongst competitors. On the other hand, any mistake might put it in risk of becoming just another ordinary web site. + +**Tools for visual elements**: + +* (Sketch, Illustrator, Photoshop, Figma, vb.) +* Visual Style Guides. + + + +### 6\) Development & Platforms + +**Front-End Development**: The parts that users interact with (HTML, CSS, JavaScript). + +**Back-End Developmen**t: Database and server-side processes (PHP, Python, Node.js). + +**No-Code Platforms**: Publishing on platforms like Webflow, Bubble, Adalo, Glide. + +### 7\) Testing + +When your site has all the visuals and content, you are ready to test. +Once the **first iteration** of your website/web app is ready, it’s time for some **testing** to make sure it **runs smoothly**. + +A website should undergo a detailed testing process before going live. +Items to check during the testing process: + +* Mobile Compatibility. +* Functionality across different browsers. +* Functionality of forms and buttons. + +Alongside these steps, setting up website uptime monitoring is essential to ensure the site remains functional after launch, providing immediate alerts if any downtime occurs. Ultimately, while testing is an important part of the web design process, it’s not worth losing sleep over. **Done is always better than perfect** and when in doubt, keep this quote in mind. + +*“If you are not embarrassed by the first version of your product, you've launched too late.” \- Reid Hoffman, founder of LinkedIn* + +### 8\) Website Launch + +Now it’s time for everyone’s favorite part of the website design process: When everything has been thoroughly tested and you’re happy with the site, you can start. + +Don’t expect this to go perfectly. There may still be some elements that need fixing. Web design is a fluid and ongoing process that requires constant maintenance. + +Web design and design in general is about finding the right balance between form and function. You need to use the right fonts, colors, and design motifs. But the way users navigate and experience your site is just as important. + +## Conclusion + +Previously, when we wanted to turn our designs into reality, the barrier of learning and using a programming language tool stood in our way. This barrier has now been removed thanks to **No-Code tools**. With these tools, even without coding knowledge, there is now a way to bring your designs to life. + +## Resources + +* Bulut, B. (2025, July 20). *Kod yazmayı bilmeden yazılımcı olmak nasıl mümkün oldu?* Webtekno. [https://www.webtekno.com/kod-bilmeden-yazilimci-olmak-nasil-mumkun-oldu-h159799.html](https://www.webtekno.com/kod-bilmeden-yazilimci-olmak-nasil-mumkun-oldu-h159799.html) + +* Ectasarim. (2024, Kasım 10). *Web tasarım ilkeleri nelerdir? Önemli hususlar*. [https://www.ectasarim.com/web-tasarim-ilkeleri/](https://www.ectasarim.com/web-tasarim-ilkeleri/?utm_source=chatgpt.com) + +* Meazey, M. (2020, February 12). *The web design process in 7 simple steps*. *Webflow Blog*. [https://webflow.com/blog/the-web-design-process-in-7-simple-steps](https://webflow.com/blog/the-web-design-process-in-7-simple-steps) + +* University of California Office of the President. (2016). *How to write SMART goals: A how-to guide.* University of California. [https://www.ucop.edu/local-human-resources/\_files/performance-appraisal/How+to+write+SMART+Goals+v2.pdf](https://www.ucop.edu/local-human-resources/_files/performance-appraisal/How+to+write+SMART+Goals+v2.pdf) diff --git a/docs/en/Community-Articles/2025-09-11-aws-secrets-manager-in-abp-framework/article.md b/docs/en/Community-Articles/2025-09-11-aws-secrets-manager-in-abp-framework/article.md new file mode 100644 index 0000000000..bcd96834b8 --- /dev/null +++ b/docs/en/Community-Articles/2025-09-11-aws-secrets-manager-in-abp-framework/article.md @@ -0,0 +1,803 @@ +# Step-by-Step AWS Secrets Manager Integration in ABP Framework Projects + +## Introduction +In this article, we are going to discuss how to secure sensitive data in ABP Framework projects using AWS Secrets Manager and explain various aspects and concepts of _secret_ data management. We will explain step-by-step AWS Secrets Manager integration. + + +## What is the Problem? +Modern applications must store sensitive data such as API keys, database connection strings, OAuth client credentials, and other similar sensitive data. These are at the center of functionality but if stored in the wrong place can be massive security issues. + +At build time, the first place that comes to mind is usually **appsettings.json**. This is a configuration file; it is not a secure place to store secret information, especially in production. + +### Common Security Risks: +- **Plain text storage**: Plain text storage of passwords +- **Exposure to version control**: Secrets are rendered encrypted in Git repositories +- **No access control**: Anyone who has file access can see the secrets +- **No rotation**: We must change them manually +- **No audit trail**: Who accessed which secret when is not known + +## .NET User Secrets Tool vs AWS Secrets Manager + +**User Secrets (.NET Secret Manager Tools)** is a dev environment only, local file-based solution that keeps sensitive information out of the repository. + +**AWS Secrets Manager** is production. It's a centralized, encrypted, and audited secret management service. + +| Feature | User Secrets (Dev) | AWS Secrets Manager (Prod) | +| ---------------------- | ---------------------------- | ------------------------------ | +| Scope | Local developer machine | All environments (dev/stage/prod) | +| Storage | JSON in user profile | Managed service (centralized) | +| Encryption | None (plain text file) | Encrypted with KMS | +| Access Control | OS file permissions | IAM policies | +| Rotation | None | Yes (automatic) | +| Audit / Traceability | None | Yes (CloudTrail) | +| Typical Usage | Quick dev outside repo | Production secret management | + +--- + +## AWS Secrets Manager +Especially designed to securely store and handle sensitive and confidential data for our applications. It even supports features such as secret rotation, replication, and many more. + +AWS Secrets Manager offers a trial of 30 days. After that, there is a $0.40 USD/month charge per stored secret. There is also a $0.05 USD fee per 10,000 API requests. + +### Key Features: +- **Automatic encryption**: KMS automatic encryption +- **Automatic rotation**: Scheduled secret rotation +- **Fine-grained access control**: IAM fine-grained access control +- **Audit logging**: Full audit logging with CloudTrail +- **Cross-region replication**: Cross-region replication +- **API integration**: Programmatic access support + +--- + +## Step 1: AWS Secrets Manager Setup + +### 1.1 Creating a Secret in AWS Console +First, search for the Secrets Manager service in the AWS Management Console. + +1. **AWS Console** → **Secrets Manager** → **Store a new secret** +2. Select **Secret type**: + - **Other type of secret** (For custom key-value pairs) + - **Credentials for RDS database** (For databases) + - **Credentials for DocumentDB database** + - **Credentials for Redshift cluster** + + + +3. Enter **Secret value**: +```json +{ + "ConnectionString": "Server=myserver;Database=mydb;User Id=myuser;Password=mypassword;" +} +``` + +4. Set **Secret name**: `prod/ABPAWSTest/ConnectionString` +5. Add **Description**: "ABP Framework connection string for production" +6. Choose **Encryption key** (default KMS key is sufficient) +7. Configure **Automatic rotation** settings (optional) + +### 1.2 IAM Permissions +Create an IAM policy for secret access: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "secretsmanager:GetSecretValue", + "secretsmanager:DescribeSecret" + ], + "Resource": "arn:aws:secretsmanager:eu-north-1:588118819172:secret:prod/ABPAWSTest/ConnectionString-*" + } + ] +} +``` + +--- + +## Step 2: ABP Framework Project Setup + +### 2.1 NuGet Packages +Add the required AWS packages to your project: + +```bash +dotnet add package AWSSDK.SecretsManager +dotnet add package AWSSDK.Extensions.NETCore.Setup +``` + +### 2.2 Configuration Files + +**appsettings.json** (Development): +```json +{ + "AWS": { + "Profile": "default", + "Region": "eu-north-1", + "AccessKey": "YOUR_ACCESS_KEY", + "SecretKey": "YOUR_SECRET_KEY" + }, + "SecretsManager": { + "SecretName": "prod/ABPAWSTest/ConnectionString", + "SecretArn": "arn:aws:secretsmanager:eu-north-1:588118819172:secret:prod/ABPAWSTest/ConnectionString-xtYQxv" + } +} +``` + +**appsettings.Production.json** (Production): +```json +{ + "AWS": { + "Region": "eu-north-1" + // Use environment variables or IAM roles in production + }, + "SecretsManager": { + "SecretName": "prod/ABPAWSTest/ConnectionString" + } +} +``` + +### 2.3 Environment Variables (Production) +```bash +export AWS_ACCESS_KEY_ID=your_access_key +export AWS_SECRET_ACCESS_KEY=your_secret_key +export AWS_DEFAULT_REGION=eu-north-1 +``` + +--- + +## Step 3: AWS Integration Implementation + +### 3.1 Program.cs Configuration + +```csharp +using Amazon; +using Amazon.SecretsManager; + +public class Program +{ + public async static Task Main(string[] args) + { + var builder = WebApplication.CreateBuilder(args); + + // AWS Secrets Manager configuration + var awsOptions = builder.Configuration.GetAWSOptions(); + + // Read AWS credentials from appsettings + var accessKey = builder.Configuration["AWS:AccessKey"]; + var secretKey = builder.Configuration["AWS:SecretKey"]; + var region = builder.Configuration["AWS:Region"]; + + if (!string.IsNullOrEmpty(accessKey) && !string.IsNullOrEmpty(secretKey)) + { + awsOptions.Credentials = new Amazon.Runtime.BasicAWSCredentials(accessKey, secretKey); + } + + if (!string.IsNullOrEmpty(region)) + { + awsOptions.Region = RegionEndpoint.GetBySystemName(region); + } + + builder.Services.AddDefaultAWSOptions(awsOptions); + builder.Services.AddAWSService(); + + // ... ABP configuration + await builder.AddApplicationAsync(); + var app = builder.Build(); + + await app.InitializeApplicationAsync(); + await app.RunAsync(); + } +} +``` + +### 3.2 Secrets Manager Service + +**Interface:** +```csharp +public interface ISecretsManagerService +{ + Task GetSecretAsync(string secretName); + Task GetSecretAsync(string secretName) where T : class; + Task GetConnectionStringAsync(); +} +``` + +**Implementation:** +```csharp +using Amazon.SecretsManager; +using Amazon.SecretsManager.Model; +using Volo.Abp.DependencyInjection; +using System.Text.Json; + +public class SecretsManagerService : ISecretsManagerService, IScopedDependency +{ + private readonly IAmazonSecretsManager _secretsManager; + private readonly IConfiguration _configuration; + private readonly ILogger _logger; + + public SecretsManagerService( + IAmazonSecretsManager secretsManager, + IConfiguration configuration, + ILogger logger) + { + _secretsManager = secretsManager; + _configuration = configuration; + _logger = logger; + } + + public async Task GetSecretAsync(string secretName) + { + try + { + var request = new GetSecretValueRequest + { + SecretId = secretName + }; + + var response = await _secretsManager.GetSecretValueAsync(request); + + _logger.LogInformation("Successfully retrieved secret: {SecretName}", secretName); + + return response.SecretString; + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to retrieve secret: {SecretName}", secretName); + throw; + } + } + + public async Task GetSecretAsync(string secretName) where T : class + { + var secretValue = await GetSecretAsync(secretName); + + try + { + return JsonSerializer.Deserialize(secretValue) + ?? throw new InvalidOperationException($"Failed to deserialize secret {secretName}"); + } + catch (JsonException ex) + { + _logger.LogError(ex, "Failed to deserialize secret {SecretName}", secretName); + throw; + } + } + + public async Task GetConnectionStringAsync() + { + var secretName = _configuration["SecretsManager:SecretName"] + ?? throw new InvalidOperationException("SecretsManager:SecretName configuration is missing"); + + return await GetSecretAsync(secretName); + } +} +``` + +--- + +## Step 4: Usage Examples + +### 4.1 Using in Application Service + +```csharp +[RemoteService(false)] +public class DatabaseService : ApplicationService +{ + private readonly ISecretsManagerService _secretsManager; + + public DatabaseService(ISecretsManagerService secretsManager) + { + _secretsManager = secretsManager; + } + + public async Task GetDatabaseConnectionAsync() + { + // Get connection string from AWS Secrets Manager + var connectionString = await _secretsManager.GetConnectionStringAsync(); + + // Use the connection string + return connectionString; + } + + public async Task GetApiConfigAsync() + { + // Deserialize JSON secret + var config = await _secretsManager.GetSecretAsync("prod/MyApp/ApiConfig"); + + return config; + } +} +``` + +### 4.2 DbContext Configuration + +```csharp +public class YourDbContextConfigurer +{ + public static void Configure(DbContextOptionsBuilder builder, string connectionString) + { + builder.UseSqlServer(connectionString); + } + + public static void Configure(DbContextOptionsBuilder builder, DbConnection connection) + { + builder.UseSqlServer(connection); + } +} + +// Usage in Module +public override void ConfigureServices(ServiceConfigurationContext context) +{ + var configuration = context.Services.GetConfiguration(); + var secretsManager = context.Services.GetRequiredService(); + + // Get secret at startup and pass to DbContext + var connectionString = await secretsManager.GetConnectionStringAsync(); + + context.Services.AddAbpDbContext(options => + { + options.AddDefaultRepositories(includeAllEntities: true); + options.DbContextOptions.UseSqlServer(connectionString); + }); +} +``` + +--- + +## Step 5: Best Practices & Security + +### 5.1 Security Best Practices + +1. **Environment-based Configuration:** + - Development: appsettings.json + - Production: Environment variables or IAM roles + +2. **Principle of Least Privilege:** + ```json + { + "Effect": "Allow", + "Action": "secretsmanager:GetSecretValue", + "Resource": "arn:aws:secretsmanager:region:account:secret:specific-secret-*" + } + ``` + +3. **Secret Rotation:** + - Set up automatic rotation + - Custom rotation logic with Lambda functions + +4. **Caching Strategy:** + ```csharp + public class CachedSecretsManagerService : ISecretsManagerService + { + private readonly IMemoryCache _cache; + private readonly SecretsManagerService _secretsManager; + + public async Task GetSecretAsync(string secretName) + { + var cacheKey = $"secret:{secretName}"; + + if (_cache.TryGetValue(cacheKey, out string cachedValue)) + { + return cachedValue; + } + + var value = await _secretsManager.GetSecretAsync(secretName); + + _cache.Set(cacheKey, value, TimeSpan.FromMinutes(30)); + + return value; + } + } + ``` + +### 5.2 Error Handling + +```csharp +public async Task GetSecretWithRetryAsync(string secretName) +{ + const int maxRetries = 3; + var delay = TimeSpan.FromSeconds(1); + + for (int i = 0; i < maxRetries; i++) + { + try + { + return await GetSecretAsync(secretName); + } + catch (AmazonSecretsManagerException ex) when (i < maxRetries - 1) + { + _logger.LogWarning(ex, "Retry {Attempt} for secret {SecretName}", i + 1, secretName); + await Task.Delay(delay); + delay = TimeSpan.FromMilliseconds(delay.TotalMilliseconds * 2); // Exponential backoff + } + } + + throw new InvalidOperationException($"Failed to retrieve secret {secretName} after {maxRetries} attempts"); +} +``` + +### 5.3 Performance Optimization + +```csharp +public class PerformantSecretsManagerService : ISecretsManagerService +{ + private readonly IAmazonSecretsManager _secretsManager; + private readonly IMemoryCache _cache; + private readonly ILogger _logger; + private readonly SemaphoreSlim _semaphore = new(1, 1); + + public async Task GetSecretAsync(string secretName) + { + var cacheKey = $"secret:{secretName}"; + + // Try to get from cache first + if (_cache.TryGetValue(cacheKey, out string cachedValue)) + { + return cachedValue; + } + + // Use semaphore to prevent multiple concurrent requests for the same secret + await _semaphore.WaitAsync(); + try + { + // Double-check pattern + if (_cache.TryGetValue(cacheKey, out cachedValue)) + { + return cachedValue; + } + + // Fetch from AWS + var value = await GetSecretFromAwsAsync(secretName); + + // Cache for 30 minutes + _cache.Set(cacheKey, value, TimeSpan.FromMinutes(30)); + + return value; + } + finally + { + _semaphore.Release(); + } + } +} +``` + +--- + +## Step 6: Testing & Debugging + +### 6.1 Unit Testing + +```csharp +public class SecretsManagerServiceTests : AbpIntegratedTest +{ + private readonly ISecretsManagerService _secretsManager; + + public SecretsManagerServiceTests() + { + _secretsManager = GetRequiredService(); + } + + [Fact] + public async Task Should_Get_Connection_String() + { + // Act + var connectionString = await _secretsManager.GetConnectionStringAsync(); + + // Assert + connectionString.ShouldNotBeNullOrEmpty(); + connectionString.ShouldContain("Server="); + } + + [Fact] + public async Task Should_Deserialize_Json_Secret() + { + // Arrange + var secretName = "test/json/config"; + + // Act + var config = await _secretsManager.GetSecretAsync(secretName); + + // Assert + config.ShouldNotBeNull(); + config.ApiKey.ShouldNotBeNullOrEmpty(); + } +} +``` + +### 6.2 Mock Implementation for Testing + +```csharp +public class MockSecretsManagerService : ISecretsManagerService, ISingletonDependency +{ + private readonly Dictionary _secrets = new() + { + ["prod/ABPAWSTest/ConnectionString"] = "Server=localhost;Database=TestDb;Trusted_Connection=true;", + ["prod/MyApp/ApiKey"] = "test-api-key", + ["prod/MyApp/Config"] = """{"ApiUrl": "https://api.test.com", "Timeout": 30}""" + }; + + public Task GetSecretAsync(string secretName) + { + if (_secrets.TryGetValue(secretName, out var secret)) + { + return Task.FromResult(secret); + } + + throw new ArgumentException($"Unknown secret: {secretName}"); + } + + public async Task GetSecretAsync(string secretName) where T : class + { + var json = await GetSecretAsync(secretName); + return JsonSerializer.Deserialize(json) + ?? throw new InvalidOperationException($"Failed to deserialize {secretName}"); + } + + public Task GetConnectionStringAsync() + { + return GetSecretAsync("prod/ABPAWSTest/ConnectionString"); + } +} +``` + +### 6.3 Integration Testing + +```csharp +public class SecretsManagerIntegrationTests : IClassFixture> +{ + private readonly WebApplicationFactory _factory; + private readonly HttpClient _client; + + public SecretsManagerIntegrationTests(WebApplicationFactory factory) + { + _factory = factory; + _client = _factory.CreateClient(); + } + + [Fact] + public async Task Should_Connect_To_Database_With_Secret() + { + // Arrange & Act + var response = await _client.GetAsync("/api/health"); + + // Assert + response.EnsureSuccessStatusCode(); + } +} +``` + +--- + +## Step 7: Monitoring & Observability + +### 7.1 CloudWatch Metrics + +```csharp +public class MonitoredSecretsManagerService : ISecretsManagerService +{ + private readonly ISecretsManagerService _inner; + private readonly IMetrics _metrics; + private readonly ILogger _logger; + + public async Task GetSecretAsync(string secretName) + { + using var activity = Activity.StartActivity("SecretsManager.GetSecret"); + activity?.SetTag("secret.name", secretName); + + var stopwatch = Stopwatch.StartNew(); + + try + { + var result = await _inner.GetSecretAsync(secretName); + + _metrics.Counter("secrets_manager.requests") + .WithTag("secret_name", secretName) + .WithTag("status", "success") + .Increment(); + + _metrics.Timer("secrets_manager.duration") + .WithTag("secret_name", secretName) + .Record(stopwatch.ElapsedMilliseconds); + + return result; + } + catch (Exception ex) + { + _metrics.Counter("secrets_manager.requests") + .WithTag("secret_name", secretName) + .WithTag("status", "error") + .WithTag("error_type", ex.GetType().Name) + .Increment(); + + _logger.LogError(ex, "Failed to retrieve secret {SecretName}", secretName); + throw; + } + } +} +``` + +### 7.2 Health Checks + +```csharp +public class SecretsManagerHealthCheck : IHealthCheck +{ + private readonly IAmazonSecretsManager _secretsManager; + private readonly ILogger _logger; + + public SecretsManagerHealthCheck( + IAmazonSecretsManager secretsManager, + ILogger logger) + { + _secretsManager = secretsManager; + _logger = logger; + } + + public async Task CheckHealthAsync( + HealthCheckContext context, + CancellationToken cancellationToken = default) + { + try + { + // Try to list secrets to verify connection + var request = new ListSecretsRequest { MaxResults = 1 }; + await _secretsManager.ListSecretsAsync(request, cancellationToken); + + return HealthCheckResult.Healthy("AWS Secrets Manager is accessible"); + } + catch (Exception ex) + { + _logger.LogError(ex, "AWS Secrets Manager health check failed"); + return HealthCheckResult.Unhealthy("AWS Secrets Manager is not accessible", ex); + } + } +} + +// Register in Program.cs +builder.Services.AddHealthChecks() + .AddCheck("secrets-manager"); +``` + +--- + +## Step 8: Advanced Scenarios + +### 8.1 Dynamic Configuration Reload + +```csharp +public class DynamicSecretsConfigurationProvider : ConfigurationProvider, IDisposable +{ + private readonly ISecretsManagerService _secretsManager; + private readonly Timer _reloadTimer; + private readonly string _secretName; + + public DynamicSecretsConfigurationProvider( + ISecretsManagerService secretsManager, + string secretName) + { + _secretsManager = secretsManager; + _secretName = secretName; + + // Reload every 5 minutes + _reloadTimer = new Timer(ReloadSecrets, null, TimeSpan.Zero, TimeSpan.FromMinutes(5)); + } + + private async void ReloadSecrets(object state) + { + try + { + var secretValue = await _secretsManager.GetSecretAsync(_secretName); + var config = JsonSerializer.Deserialize>(secretValue); + + Data.Clear(); + foreach (var kvp in config) + { + Data[kvp.Key] = kvp.Value; + } + + OnReload(); + } + catch (Exception ex) + { + // Log error but don't throw to avoid crashing the timer + Console.WriteLine($"Failed to reload secrets: {ex.Message}"); + } + } + + public void Dispose() + { + _reloadTimer?.Dispose(); + } +} +``` + +### 8.2 Multi-Region Failover + +```csharp +public class MultiRegionSecretsManagerService : ISecretsManagerService +{ + private readonly List _clients; + private readonly ILogger _logger; + + public MultiRegionSecretsManagerService( + IConfiguration configuration, + ILogger logger) + { + _logger = logger; + _clients = new List(); + + // Create clients for multiple regions + var regions = new[] { "us-east-1", "us-west-2", "eu-west-1" }; + foreach (var region in regions) + { + var config = new AmazonSecretsManagerConfig + { + RegionEndpoint = RegionEndpoint.GetBySystemName(region) + }; + _clients.Add(new AmazonSecretsManagerClient(config)); + } + } + + public async Task GetSecretAsync(string secretName) + { + Exception lastException = null; + + foreach (var client in _clients) + { + try + { + var request = new GetSecretValueRequest { SecretId = secretName }; + var response = await client.GetSecretValueAsync(request); + + _logger.LogInformation("Retrieved secret from region {Region}", + client.Config.RegionEndpoint.SystemName); + + return response.SecretString; + } + catch (Exception ex) + { + lastException = ex; + _logger.LogWarning(ex, "Failed to retrieve secret from region {Region}", + client.Config.RegionEndpoint.SystemName); + } + } + + throw new InvalidOperationException( + "Failed to retrieve secret from all regions", lastException); + } +} +``` + +--- + +## Conclusion + +AWS Secrets Manager integration with ABP Framework significantly enhances the security of your applications. With this integration: + + **Centralized Secret Management**: All secrets are managed centrally + **Better Security**: Encryption through KMS and access control through IAM + **Audit Trail**: Complete recording of who accessed which secret when + **Automatic Rotation**: Secrets can be rotated automatically + **High Availability**: AWS high availability guarantee +**Easy Integration**: Native integration with ABP Framework +**Cost Effective**: Pay only for what you use +**Scalable**: Scales with your application needs + +With this post, you can securely utilize AWS Secrets Manager in your ABP Framework applications and bid farewell to secret management concerns in production. + +### Key Benefits: +- **Developer Productivity**: No hardcoded secrets in config files +- **Operational Excellence**: Automation of rotation and monitoring +- **Security Compliance**: Meet enterprise security requirements +- **Peace of Mind**: Professional-grade secret management + +--- + +## Additional Resources + +- [AWS Secrets Manager Documentation](https://docs.aws.amazon.com/secretsmanager/) +- [ABP Framework Documentation](https://docs.abp.io/) +- [AWS SDK for .NET](https://docs.aws.amazon.com/sdk-for-net/) +- [AWS Security Best Practices](https://aws.amazon.com/architecture/security-identity-compliance/) +- [Sample Project Repository](https://github.com/fahrigedik/AWSIntegrationABP) \ No newline at end of file diff --git a/docs/en/Community-Articles/2025-09-11-aws-secrets-manager-in-abp-framework/cover.png b/docs/en/Community-Articles/2025-09-11-aws-secrets-manager-in-abp-framework/cover.png new file mode 100644 index 0000000000..ac2b5557a5 Binary files /dev/null and b/docs/en/Community-Articles/2025-09-11-aws-secrets-manager-in-abp-framework/cover.png differ diff --git a/docs/en/Community-Articles/2025-09-12-Demystified-Aggregates-in-DDD-&-.NET/post.md b/docs/en/Community-Articles/2025-09-12-Demystified-Aggregates-in-DDD-&-.NET/post.md new file mode 100644 index 0000000000..a06eecb3d8 --- /dev/null +++ b/docs/en/Community-Articles/2025-09-12-Demystified-Aggregates-in-DDD-&-.NET/post.md @@ -0,0 +1,316 @@ + +# Demystified Aggregates in DDD & .NET: From Theory to Practice + +## Introduction + +Domain-Driven Design (DDD) is one of the key foundations of modern software architecture and has taken a strong place in the .NET world. At the center of DDD are Aggregates, which protect the consistency of business rules. While they are one of DDD’s biggest strengths, they’re also one of the most commonly misunderstood ideas. Trying to follow “pure” DDD rules to the letter often clashes with the complexity and performance needs of real-world projects, leaving developers in tough situations. The goal of this article is to take a fresh, practical look at Aggregates and show how they can be applied in a way that works in real life. + +---------- + +### **Chapter 1: Laying the Groundwork: What Is a Classic Aggregate?** + +Before jumping into pragmatic shortcuts, let’s make sure we’re all on the same page. To do that, we’ll start with the classic “by the book” definition of an Aggregate and the rules that make it tick. + +#### **What Exactly Is an Aggregate?** + +At its simplest, an **Aggregate** is a group of related objects (Entities and Value Objects) that are treated as **one unit of change**. And this group has a leader: the **Aggregate Root**. + +- **Aggregate Root** → Think of it as the gatekeeper. All outside commands (like “add a product to the order”) must go through the root. You can’t just poke around and change stuff inside. + +- **Entity** → Objects within the Aggregate that have their own identity (ID). Example: an `OrderLine` inside an `Order`. + +- **Value Object** → Objects without an identity. They’re defined entirely by their values, like an `Address` or `Money`. + + +The Aggregate’s main purpose isn’t just grouping things together—it’s about **protecting business rules (invariants).** For example: _“an order’s total amount can never be negative.”_ The Aggregate Root makes sure rules like this are never broken. + + +#### **The Role of Aggregates: Transaction Boundaries** + +The most important job of an Aggregate is defining the **transactional consistency boundary**. In other words: + +👉 Any change you make inside an Aggregate either **fully succeeds** or **fully fails**. There’s no half-done state. + +From a database perspective, when you call `SaveChanges()` or `Commit()`, everything within one Aggregate gets saved in a single transaction. If you add a product and update the total price, those two actions are atomic—they succeed together. Thanks to Aggregates, you’ll never end up in weird states like _“product was added but total wasn’t updated.”_ + + +#### **The Golden Rules of Aggregates** + +Classic DDD lays out three golden rules for working with Aggregates: + +1. **Talk Only to the Root** + You can’t directly update something like an `OrderLine`. You must go through the root: `Order.AddOrderLine(...)` or `Order.RemoveOrderLine(...)`. That way, the root always enforces the rules. + +2. **Reference Other Aggregates by ID Only** + An `Order` shouldn’t hold a `Customer` object directly. Instead, it should just store `CustomerId`. This keeps Aggregates independent and avoids loading massive object graphs. + +3. **Change Only One Aggregate per Transaction** + Need to create an order _and_ update loyalty points? Classic DDD says: do it in two steps. First, save the `Order`. Then publish a **domain event** to update the `Customer`. This enables scalability but introduces **eventual consistency**. + + + +#### **A Classic Example: The Order Aggregate in .NET** + +Here’s a simple example showing an `Order` Aggregate that enforces a business rule: + +```csharp +// Aggregate Root: The entry point and rule enforcer +public class Order +{ + public Guid Id { get; private set; } + public Guid CustomerId { get; private set; } + + private readonly List _orderLines = new(); + public IReadOnlyCollection OrderLines => _orderLines.AsReadOnly(); + + public decimal TotalPrice { get; private set; } + + public Order(Guid id, Guid customerId) + { + Id = id; + CustomerId = customerId; + } + + public void AddOrderLine(Guid productId, int quantity, decimal price) + { + // Rule 1: Max 10 order lines + if (_orderLines.Count >= 10) + throw new InvalidOperationException("An order can contain at most 10 products."); + + // Rule 2: No duplicate products + var existingLine = _orderLines.FirstOrDefault(ol => ol.ProductId == productId); + if (existingLine != null) + throw new InvalidOperationException("This product is already in the order."); + + var orderLine = new OrderLine(productId, quantity, price); + _orderLines.Add(orderLine); + + RecalculateTotalPrice(); + } + + private void RecalculateTotalPrice() + { + TotalPrice = _orderLines.Sum(ol => ol.TotalPrice); + } +} + +public class OrderLine +{ + public Guid Id { get; private set; } + public Guid ProductId { get; private set; } + public int Quantity { get; private set; } + public decimal UnitPrice { get; private set; } + public decimal TotalPrice => Quantity * UnitPrice; + + public OrderLine(Guid productId, int quantity, decimal unitPrice) + { + Id = Guid.NewGuid(); + ProductId = productId; + Quantity = quantity; + UnitPrice = unitPrice; + } +} + +``` + +Here, the `Order` enforces the rule _“an order can have at most 10 items”_ inside its `AddOrderLine` method. Nobody outside the class can bypass this, because `_orderLines` is private. + +👉 That’s the real strength of a classic Aggregate: **business rules are always protected at the boundary.** + +---------- + +### **Chapter 2: Theory in Books vs. Reality in Code — Why Classic Aggregates Struggle** + +In Chapter 1, we painted the “ideal” world of DDD. Aggregates were like fortresses guarding our business rules… +But what happens when we try to build that fortress in a real project with tools like Entity Framework Core? That’s when the gap between theory and practice starts to show up. + +#### **1. That `.Include()` Chain — Do We Really Need It? The Performance Trap** + +DDD books tell us: _“To validate a business rule, you must load the entire aggregate into memory.”_ +Sounds reasonable if consistency is the goal. + +But let’s picture a scenario: we have an `Order` aggregate with **500 order lines** inside it. And all we want to do is change its status to `Confirmed`. + +```csharp +// Just to update a single field... +var order = await _context.Orders + .Include(o => o.OrderLines) // <-- 500 rows pulled in! + .SingleOrDefaultAsync(o => o.Id == orderId); + +order.Confirm(); // Just sets order.Status = "Confirmed"; + +await _context.SaveChangesAsync(); + +``` + +This query pulls **all 500 order lines into memory** just so we can flip a single `Status` field. Even in small projects, this is a silent performance killer. As the system grows, it will drag your app down. + + +#### **2. The Abandoned Fortress — Sliding into Anemic Domain Models** + +Now, what’s a developer’s natural reaction to this? Something like: + +_“Pulling this much data is expensive. Maybe I should strip down the aggregate into a plain POCO with properties only, and move the logic into an `OrderService` class.”_ + +This is how we slip straight into the **Anemic Domain Model** trap. Our classes lose their behavior, becoming nothing more than data bags. +The whole DDD principle of _“keep behavior close to data”_ evaporates. Business logic leaks out of the aggregate and spreads across services. We think we’re doing DDD, but in reality, we’ve fallen back into classic transaction-script style coding. + + +#### **3. One Model Doesn’t Fit All — The Clash of Command and Query** + +Aggregates are designed for **commands** — write operations where business rules must be enforced. + +But what about **queries**? Imagine a dashboard where we just want to list the last 10 orders. All we need is `OrderId`, `CustomerName`, and `TotalAmount`. + +Loading 10 fully-hydrated `Order` aggregates (with all their order lines) just for that list? That’s like using a cannon to hunt a sparrow. Wasteful, slow, and clumsy. +Aggregates simply aren’t built for reporting or read-heavy scenarios. + + +And there you have it — the three usual suspects that make developers doubt DDD in real life: + +- Performance headaches + +- The risk of falling into an Anemic Model + +- Aggregates being too heavy for read operations + + +So, should we give up on DDD? Absolutely not! +The key is to stop following the rules blindly and instead focus on their **real intent**. In the next chapter, we’ll explore the pragmatic approach — **Demystified Aggregates** — and how they can actually help us solve these problems. + +---------- + +### **Chapter 3: Enter the Solution — What Exactly Is a "Demystified Aggregate"?** + +The issues we listed in the last chapter don’t mean DDD is bad. They just show that blindly applying textbook rules without considering the realities of your project creates friction. + +A **Demystified Aggregate** isn’t a library or a framework. It’s a **way of thinking**. Its philosophy is simple: focus on the Aggregate’s real job, and make sure it does that job **as efficiently as possible.** + + +#### **1. Philosophy: Focus on Purpose, Not Rules** + +What’s the Aggregate’s most sacred duty? +**To protect business rules (invariants) during a data change (command).** + +Here’s the key: an Aggregate’s job isn’t to always hold all data in memory. Its job is to **ensure consistency while performing an operation**. + +Think of it like a security guard at a bank vault. Their job is to make sure transfers are done correctly. They don’t need to memorize the serial number of every single banknote. They just need the critical info for the current operation: the balance and the transfer amount. + +The Demystified Aggregate says the same thing: when running a method, you **only load the data that method actually needs**, not the entire Aggregate. + + +#### **2. The Core Idea: What “State” Does a Behavior Actually Need?** + +To apply this idea in code, ask yourself: +_“What data does the `Confirm()` method on my `Order` Aggregate actually need?”_ + +- Maybe just the order’s current `Status`. (`"Pending"` can become `"Confirmed"`, `"Cancelled"` throws an error.) + +- What about `AddItem(product, quantity)`? + + - It needs the `Status` (can’t add items to a cancelled order). + + - And maybe the existing `OrderLines` (to increase quantity if the item already exists). + + +See the pattern? Each behavior needs different data. So why load everything every single time? + + +#### **3. How Do We Do This in .NET & EF Core? Practical Solutions** + +Putting this philosophy into code is easier than you might think. + +**The Approach: Purpose-Built Repository Methods** + +Instead of a generic `GetByIdAsync()`, create methods tailored to the operation at hand. Let’s revisit our classic **Order Confirmation** scenario in a “Before & After” style. + +**BEFORE (Classic & Inefficient Approach)** + +```csharp +// Repository Layer +public async Task GetByIdAsync(Guid id) +{ + // LOAD EVERYTHING! + return await _context.Orders + .Include(o => o.OrderLines) + .SingleOrDefaultAsync(o => o.Id == id); +} + +// Application Service Layer +public async Task ConfirmOrderAsync(Guid orderId) +{ + var order = await _orderRepository.GetByIdAsync(orderId); + order.Confirm(); // This method might not even care about OrderLines! + await _unitOfWork.SaveChangesAsync(); +} + +``` + +**AFTER (Demystified & Focused Approach)** + +```csharp +// Repository Layer +public async Task GetForConfirmationAsync(Guid id) +{ + // LOAD ONLY WHAT WE NEED! (No OrderLines needed) + return await _context.Orders + .SingleOrDefaultAsync(o => o.Id == id); +} + +// Application Service Layer +public async Task ConfirmOrderAsync(Guid orderId) +{ + // Intent is crystal clear in the code! + var order = await _orderRepository.GetForConfirmationAsync(orderId); + + // Aggregate still protects the business rule. + // Confirm() checks status, etc. + order.Confirm(); + + await _unitOfWork.SaveChangesAsync(); +} + +``` + +**What Do We Gain?** + +1. **Awesome Performance:** We avoid unnecessary JOINs and data transfer. + +2. **Clear Intent:** Anyone reading `GetForConfirmationAsync` immediately knows this operation only cares about the order itself, not its items. Code documents itself. + +3. **No Compromise:** Our Aggregate still enforces the business rules via `Confirm()`. DDD’s spirit remains intact. + + +For **read/query operations**, the answer is even simpler: skip Aggregates altogether! Use optimized queries that return DTOs via `Select` projections, or even raw SQL with Dapper. + +That’s the essence of a Demystified Aggregate: **using the right tool for the right job.** + +In the next chapter, we’ll wrap everything up and tie all the concepts together. + +---------- + +### **Conclusion: Pragmatism Beats Dogmatism in DDD** + +We’ve reached the finish line. We started with the “pure” textbook definition of Aggregates in the ideal world of Domain-Driven Design. Then we hit the real-world walls of performance and complexity. Finally, we learned how to break through those walls. + +The biggest lesson from the **Demystified Aggregates** approach is simple: + +**DDD isn’t a rigid rulebook — it’s a way of thinking.** + +Our goal isn’t to implement the “most pure DDD ever written in a book.” It’s to make our domain logic clean, solid, understandable, and performant. In this journey, patterns and rules should serve us, not the other way around. + + +### **Key Takeaways** + +1. **Focus on the Core Purpose:** + The primary reason an Aggregate exists is to enforce business rules (invariants) and ensure consistency while handling a command. Every design decision should revolve around this purpose. + +2. **Load Only What You Need:** + You don’t have to load the entire Aggregate to execute a behavior. Use purpose-built repository methods (`GetForX()`) to fetch just the data needed for the operation. This can drastically improve both performance and readability. + +3. **Separate Writing from Reading:** + Use rich, protected Aggregates for commands (write operations). For queries (read operations), don’t burden your Aggregates. Instead, rely on projections, DTOs, or optimized queries. This is one of the simplest, most practical ways to embrace CQRS principles. + +Don’t be afraid to shape your Aggregates based on your project and the realities of your tools (like Entity Framework Core). The power of DDD lies in its **flexibility and pragmatism**. + + diff --git a/docs/en/Community-Articles/2025-09-16-Color-Psychology-in-Web-Design-How-to-Choose-the-Perfect-Palette-in-2025/img-1.png b/docs/en/Community-Articles/2025-09-16-Color-Psychology-in-Web-Design-How-to-Choose-the-Perfect-Palette-in-2025/img-1.png new file mode 100644 index 0000000000..834769912a Binary files /dev/null and b/docs/en/Community-Articles/2025-09-16-Color-Psychology-in-Web-Design-How-to-Choose-the-Perfect-Palette-in-2025/img-1.png differ diff --git a/docs/en/Community-Articles/2025-09-16-Color-Psychology-in-Web-Design-How-to-Choose-the-Perfect-Palette-in-2025/img-2.png b/docs/en/Community-Articles/2025-09-16-Color-Psychology-in-Web-Design-How-to-Choose-the-Perfect-Palette-in-2025/img-2.png new file mode 100644 index 0000000000..03ae54f4a3 Binary files /dev/null and b/docs/en/Community-Articles/2025-09-16-Color-Psychology-in-Web-Design-How-to-Choose-the-Perfect-Palette-in-2025/img-2.png differ diff --git a/docs/en/Community-Articles/2025-09-16-Color-Psychology-in-Web-Design-How-to-Choose-the-Perfect-Palette-in-2025/img-3.png b/docs/en/Community-Articles/2025-09-16-Color-Psychology-in-Web-Design-How-to-Choose-the-Perfect-Palette-in-2025/img-3.png new file mode 100644 index 0000000000..8694547875 Binary files /dev/null and b/docs/en/Community-Articles/2025-09-16-Color-Psychology-in-Web-Design-How-to-Choose-the-Perfect-Palette-in-2025/img-3.png differ diff --git a/docs/en/Community-Articles/2025-09-16-Color-Psychology-in-Web-Design-How-to-Choose-the-Perfect-Palette-in-2025/post.md b/docs/en/Community-Articles/2025-09-16-Color-Psychology-in-Web-Design-How-to-Choose-the-Perfect-Palette-in-2025/post.md new file mode 100644 index 0000000000..c9722795da --- /dev/null +++ b/docs/en/Community-Articles/2025-09-16-Color-Psychology-in-Web-Design-How-to-Choose-the-Perfect-Palette-in-2025/post.md @@ -0,0 +1,138 @@ +# Color Psychology in Web Design: How to Choose the Perfect Palette in 2025 + +## The Importance of Color Psychology in Web Design + +First impressions online happen in under 50 milliseconds, and studies show that up to 90% of a user’s initial perception is based on color. In 2025, as digital competition intensifies, brands cannot rely on functionality alone—users expect emotionally engaging and visually strategic designs. + +Whether you’re designing for e-commerce, SaaS, lifestyle blogs, or creative portfolios, the right color palette is more than decoration. It directly influences: + +- Trust and credibility (blue = reliability, green = balance) +- Conversions and urgency (red for CTAs, orange for enthusiasm) +- Brand recall (consistent use of signature colors builds recognition) +- User comfort and inclusivity (high contrast, accessibility standards) + +![image](./img-1.png) + +👉 In short, color psychology is not a design afterthought—it’s a core driver of user engagement, retention, and revenue growth. + +## The Psychology of Color: How Hues Shape Human Behavior + +Color psychology explains how different hues influence emotion, perception, and decision-making. Let’s explore the most common colors in web design: + +- 🔴 **Red** – Sparks urgency and excitement. Common in sales promotions, e-commerce banners, and notifications. + Example: YouTube’s red play button signals attention and action. + +- 🟠 **Orange** – Represents energy, warmth, and enthusiasm. + Example: Amazon’s orange “Add to Cart” button guides users toward purchases. + +- 🟡 **Yellow** – Radiates optimism, creativity, and energy. + Example: Snapchat’s bright yellow interface reflects playfulness and innovation. + +- 🟢 **Green** – Suggests growth, sustainability, and wellness. + Example: Whole Foods and health apps use green to symbolize balance and eco-conscious values. + +- 🔵 **Blue** – Signals trust, calm, and stability. + Example: PayPal and LinkedIn use blue to reinforce security and professionalism. + +- 🟣 **Purple** – Symbolizes luxury, creativity, and imagination. + Example: Beauty brands like Urban Decay use purple to highlight creativity and exclusivity. + +- ⚫ **Black** – Conveys sophistication and exclusivity. + Example: Apple and Chanel use black to emphasize premium value. + +- ⚪ **White** – Symbolizes purity, simplicity, and clarity. + Example: Apple’s predominantly white interfaces highlight elegance and minimalism. + +👉 Pro Insight: Combine psychology with analytics. Tools like Hotjar and Google Optimize reveal how color placement (e.g., green vs. red CTA buttons) impacts conversion rates. + +## Beyond Psychology: Cultural Differences in Color Meaning + +In global web design, one palette does not fit all. Colors have different cultural associations that can influence user perception: + +- **White** – Purity and minimalism in Western cultures, but associated with mourning in parts of East Asia. +- **Red** – Danger in the West, but luck and prosperity in China. +- **Green** – Islamically significant in the Middle East, while in the West it often symbolizes money. +- **Black** – Luxury in the West, but mourning in many cultures. + +![image](./img-2.png) + +👉 For international brands, it’s crucial to localize website color palettes to avoid misinterpretations. + +## Web Design Color Trends for 2025 + +The digital landscape evolves rapidly, and so do color preferences. The biggest color palette trends in 2025 include: + +**Neo-Minimalist Neutrals** ✨ +Soft beiges, warm grays, and creamy whites offer calm, distraction-free interfaces—ideal for B2B websites and productivity apps. + +**Vibrant Gradients & Neon Accents** 🌈 +Tech and e-commerce brands embrace multi-tone gradients and neon highlights to reflect energy and innovation. + +**Dark Mode with High-Contrast Highlights** 🌑 +Now a default in many apps, dark mode improves eye comfort while allowing accent colors to stand out for easier navigation. + +**Nature-Inspired Palettes** 🍃 +Eco-conscious businesses are adopting earthy greens, ocean blues, and muted browns to connect with sustainability-minded users. + +**Soft Pastels for Inclusivity** 💕 +Gentle tones—lavenders, light pinks, and mints—are trending in lifestyle, wellness, and community-driven platforms thanks to their approachable feel. + +**Dynamic AI-Driven Palettes** 🤖 +AI enables adaptive color systems that change in real-time based on time of day, user mood, or demographics. + +👉 Case Study: Spotify uses dark backgrounds but personalizes accents with vibrant colors that align with playlists and campaigns. + +## Best Practices for Choosing the Perfect Website Color Palette + +A successful palette balances brand identity, psychology, accessibility, and testing. Follow these steps: + +**Anchor in Brand Identity** +Select a primary color that embodies your company’s values. +Example: Tiffany & Co.’s teal blue has become globally iconic. +**Apply the 60-30-10 Rule** + +- 60% background +- 30% secondary tone +- 10% accent (CTAs, highlights) + This ratio maintains visual balance. + + **Leverage Color Theory** + Use complementary or triadic harmonies for better contrast and consistency. + + **Prioritize Accessibility (WCAG Standards)** + Ensure a minimum 4.5:1 contrast ratio for text and use color-blind–friendly palettes. + + **Account for Cultural Meanings** + Research local associations before targeting global markets. + + **Test Across Devices & Modes** + Preview palettes in both light and dark modes, and across desktop, mobile, and tablet. + + **A/B Test for Conversions** + Experiment with button colors, background shades, and highlights to find what performs best. + +## The Role of Color in Branding and SEO + +Color impacts brand authority and indirectly influences SEO by shaping user behavior: + +- **Bounce Rate** – Engaging palettes keep users onsite longer. +- **Dwell Time** – Comfortable color schemes encourage scrolling. +- **Conversion Rates** – Optimized CTAs improve click-throughs. +- **Social Sharing** – Bold, appealing colors increase shareability + +![image](./img-3.png) + +## Future Outlook: Where Color Psychology Is Headed + +- **Mood-Adaptive Websites** + Platforms will adjust palettes based on detected user mood or behavior. +- **AR & VR Color Experiences** + Immersive 3D design will require palettes that adapt across realities. +- **Inclusive Color Systems** + Accessibility-first palettes will become the standard, ensuring usability for all. + +## Final Thoughts + +In 2025, the perfect color palette blends psychological insights, cultural awareness, and data-driven testing. Colors are no longer just visual accents—they are storytelling tools that build trust, guide behavior, and boost conversions. + +👉 Brands that embrace color psychology in web design today will not only stand out visually but also gain lasting competitive and business advantages. diff --git a/docs/en/Community-Articles/2025-09-17-Integration-Testing-Best-Practices-for-Building-a-Robust-Application-Layer/article.md b/docs/en/Community-Articles/2025-09-17-Integration-Testing-Best-Practices-for-Building-a-Robust-Application-Layer/article.md new file mode 100644 index 0000000000..f5e02c6ff3 --- /dev/null +++ b/docs/en/Community-Articles/2025-09-17-Integration-Testing-Best-Practices-for-Building-a-Robust-Application-Layer/article.md @@ -0,0 +1,257 @@ +# Integration Testing Best Practices for Building a Robust Application Layer + +## 1. Introduction to Integration Testing + +For software development purposes, it is not sufficient to validate individual components individually in a vacuum. In practical usage, programs consist of holistic systems comprising a myriad of components such as **databases, services, APIs, and existing tools**, which **must work together**. An application would fail testing parameters for an individual component sufficiently but fail as a whole if such components fail to interact appropriately. + +Whereas unit tests verify individual parts one by one, **integration tests confirm software reliability with integrated parts**. Integration testing guarantees that the overall system performs as per design parameters whenever parts of the system are interfederated. + +**Why Integration Testing Is Important:** + +* **Reliability:** Checks the system works as it should, even with network problems, service stops, or incorrect data. +* **Easy to Update:** Makes sure adding or changing parts doesn't break what already works. +* **Good Quality:** Finds problems before users do. +* **Strong Application Layer:** Checks that database actions, service handling, and API communications all work together correctly. + +**Example: Detailed Integration Test with Dependency Injection** + +```csharp +[Fact] +public async Task OrderCreation_WithValidInput_ShouldPersistAndReturnSuccess() +{ + // Set up: Use a complete Test Server + var factory = new WebApplicationFactory(); + var client = factory.CreateClient(); + + // Do: + var request = new CreateOrderRequest { ProductId = 1, Quantity = 2 }; + var response = await client.PostAsJsonAsync("/api/orders", request); + + // Check + response.EnsureSuccessStatusCode(); // Stops if the code isn't successful + var order = await response.Content.ReadFromJsonAsync(); + Assert.NotNull(order); + Assert.Equal(2, order.Quantity); +} +``` + +This example uses a memory `TestServer` to act like the full HTTP process, from the controller to the database, for a more real test. + +## 2. Setting Up an Isolated Integration Test Environment + +Integration tests should happen in their own area to prevent: + +* **Slow tests** because of network issues or big data amounts +* **Unreliable results** from live data changes +* **Tests messing with each other** + +**Tips for Keeping Tests Separate:** + +* **Memory databases:** Fast and easy to reset for simple data tasks. +* **Container-based areas (Docker/TestContainers):** Copy real areas safely for complex setups (like PostgreSQL, Redis). +* **Database Actions:** Undo changes after each test to keep things separate and fast. + +**Example: Using Actions to Keep Things Separate** + +```csharp +[Fact] +public async Task OrderCreation_RollsBack_AfterTest() +{ + // Set up: Start an action that will be undone later + await using var transaction = await _dbContext.Database.BeginTransactionAsync(); + + var service = new OrderService(_dbContext); + var result = service.CreateOrder(1, 2); + Assert.True(result.IsSuccess); + + // Do & Check + var orderCount = await _dbContext.Orders.CountAsync(); + Assert.Equal(1, orderCount); + + // Clean up: Undo the action to reset all changes + await transaction.RollbackAsync(); +} +``` + +Using actions makes sure tests stay separate and can run at the same time safely. + +## 3. Seeding Test Data for Consistent Integration Results + +Start data gives a steady base for tests. Include complex links for real situations. + +```csharp +// Use a special way to get the data started +private static async Task SeedData(AppDbContext dbContext) +{ + await dbContext.Products.AddRangeAsync( + new Product { Id = 1, Name = "Coffee", Price = 5, Stock = 50 }, + new Product { Id = 2, Name = "Tea", Price = 3, Stock = 20 } + ); + await dbContext.Users.AddAsync(new User { Id = 1, Name = "Alice", IsActive = true }); + await dbContext.Orders.AddAsync(new Order { Id = 1, UserId = 1, ProductId = 1, Quantity = 2 }); + + await dbContext.SaveChangesAsync(); +} +``` + +**Tip:** Use the Builder style or a special `TestSeeder` class to make detailed, reusable data setups. + +## 4. Validating API and Service Layer Interactions + +Integration tests should copy what real users do to check the whole request-response process. + +```csharp +// Detailed API Test with CancellationToken and Custom Headers +var request = new CreateOrderRequest { ProductId = 1, Quantity = 2 }; +var requestMessage = new HttpRequestMessage(HttpMethod.Post, "/api/orders") +{ + Content = JsonContent.Create(request) +}; +requestMessage.Headers.Add("X-Request-Id", Guid.NewGuid().ToString()); + +// Act like a timeout is happening +var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(50)); +await Assert.ThrowsAsync(async () => +{ + await _client.SendAsync(requestMessage, cts.Token); +}); +``` + +Test both normal uses and unusual ones like timeouts, bad requests, and too many users at once for a truly reliable API. + +## 5. Mocking External Dependencies in Integration Tests + +Use copies or fake versions for services like payment systems, emails, or other APIs to keep your application logic separate. + +```csharp +// Set up: Copy an outside payment service +var paymentServiceMock = new Mock(); +paymentServiceMock.Setup(x => x.ProcessPayment(It.IsAny(), It.IsAny())) + .ThrowsAsync(new TimeoutException("Payment gateway timeout")); + +// Do: Put the copy into the test area +services.AddScoped(_ => paymentServiceMock.Object); + +// Check: See how the system handles the timeout +var response = await _client.PostAsJsonAsync("/api/payments", new { Amount = 100 }); +Assert.Equal(HttpStatusCode.InternalServerError, response.StatusCode); +Assert.Contains("Payment service unavailable", await response.Content.ReadAsStringAsync()); +``` + +Mimicking lets you act like things are failing (like APIs timing out or services being down) and check that your application handles these issues well without using real network calls. + +## 6. Covering Success and Failure Scenarios in Tests + +A good test set has full coverage of both successful actions and error handling. + +```csharp +// Normal use: A successful order creation +var validOrder = new CreateOrderRequest { ProductId = 1, Quantity = 1 }; +var successResponse = await _client.PostAsJsonAsync("/api/orders", validOrder); +Assert.Equal(HttpStatusCode.Created, successResponse.StatusCode); + +// Bad request: Wrong product ID +var invalidProductOrder = new CreateOrderRequest { ProductId = 999, Quantity = 1 }; +var badProductResponse = await _client.PostAsJsonAsync("/api/orders", invalidProductOrder); +Assert.Equal(HttpStatusCode.NotFound, badProductResponse.StatusCode); + +// Conflict: Not enough stock +var largeOrder = new CreateOrderRequest { ProductId = 1, Quantity = 1000 }; +var stockResponse = await _client.PostAsJsonAsync("/api/orders", largeOrder); +Assert.Equal(HttpStatusCode.Conflict, stockResponse.StatusCode); +``` + +Testing everything makes sure your application layer gives helpful and correct error messages to users. + +## 7. Ensuring Cleanup and Test Isolation + +Keep test results steady by making sure each test is totally separate from others. + +```csharp +// Use a special cleanup method or a test setup +public class OrderTests : IDisposable +{ + private readonly AppDbContext _dbContext; + + public OrderTests() + { + _dbContext = new AppDbContext(GetDbContextOptions()); + // Setup logic here + } + + // Cleanup logic + public void Dispose() + { + _dbContext.Database.EnsureDeleted(); + _dbContext.Dispose(); + } +} +``` + +Clean up automatically for tests that run at the same time using `IDisposable` or by putting each test in a database action. + +## 8. Automating Integration Tests with CI/CD Pipelines + +Automate tests in CI/CD lines for regular, consistent checking. + +```yaml +jobs: + test-and-build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: 7.0.x + - name: Build + run: dotnet build --configuration Release + - name: Run Integration Tests + run: dotnet test --filter Category=Integration --logger trx;LogFileName=testresults.trx +``` + +Regular testing finds integration problems early, saving effort over time. + +## 9. Complete Integration Test Example with a Basic Comparison + +```csharp +[Fact] +public async Task CompleteOrderWorkflow_ShouldPersistAllSteps() +{ + // Set up: Start complex data for a full action + await SeedData(_dbContext); + + // Do: Act like a real user request + var request = new CreateOrderRequest { ProductId = 1, Quantity = 2, UserId = 1 }; + var response = await _client.PostAsJsonAsync("/api/orders", request); + Assert.Equal(HttpStatusCode.Created, response.StatusCode); + + // Check 1: See the database state + var order = await _dbContext.Orders.FirstOrDefaultAsync(o => o.UserId == 1 && o.ProductId == 1); + Assert.NotNull(order); + Assert.Equal(2, order.Quantity); + + // Check 2: See side effects (like stock going down) + var product = await _dbContext.Products.FirstOrDefaultAsync(p => p.Id == 1); + Assert.Equal(48, product.Stock); +} +``` + +🍪 **Cookie Comparison** + +* **Starting products** are like the dough you have ready. +* **The new order** is like the new stuff you mix in. +* **The oven (application)** mixes both, baking (business logic). +* **The cookie** is the right outcome, using the old dough and new stuff well. + +This shows how a good application layer uses new and old data well together. + +--- + +### 10. Summary: + +* **Separate Area:** Ensures tests run on their own. +* **Start Data:** Gives results that are consistent. +* **Mimicking Outside Parts:** Keeps tests stable. +* **Testing Both Good and Bad:** Makes the situation solid. +* **CI/CD Automation:** Keeps the system stable. diff --git a/docs/en/Community-Articles/2025-09-17-Integration-Testing-Best-Practices-for-Building-a-Robust-Application-Layer/cover.png b/docs/en/Community-Articles/2025-09-17-Integration-Testing-Best-Practices-for-Building-a-Robust-Application-Layer/cover.png new file mode 100644 index 0000000000..9ff6df4764 Binary files /dev/null and b/docs/en/Community-Articles/2025-09-17-Integration-Testing-Best-Practices-for-Building-a-Robust-Application-Layer/cover.png differ diff --git a/docs/en/Community-Articles/2025-09-17-Unit-of-Work-with-Aspnetcore/img-1.png b/docs/en/Community-Articles/2025-09-17-Unit-of-Work-with-Aspnetcore/img-1.png new file mode 100644 index 0000000000..82609db662 Binary files /dev/null and b/docs/en/Community-Articles/2025-09-17-Unit-of-Work-with-Aspnetcore/img-1.png differ diff --git a/docs/en/Community-Articles/2025-09-17-Unit-of-Work-with-Aspnetcore/img-2.png b/docs/en/Community-Articles/2025-09-17-Unit-of-Work-with-Aspnetcore/img-2.png new file mode 100644 index 0000000000..1c9c0fc091 Binary files /dev/null and b/docs/en/Community-Articles/2025-09-17-Unit-of-Work-with-Aspnetcore/img-2.png differ diff --git a/docs/en/Community-Articles/2025-09-17-Unit-of-Work-with-Aspnetcore/img-3.png b/docs/en/Community-Articles/2025-09-17-Unit-of-Work-with-Aspnetcore/img-3.png new file mode 100644 index 0000000000..5efff2b07f Binary files /dev/null and b/docs/en/Community-Articles/2025-09-17-Unit-of-Work-with-Aspnetcore/img-3.png differ diff --git a/docs/en/Community-Articles/2025-09-17-Unit-of-Work-with-Aspnetcore/post.md b/docs/en/Community-Articles/2025-09-17-Unit-of-Work-with-Aspnetcore/post.md new file mode 100644 index 0000000000..701a9e985b --- /dev/null +++ b/docs/en/Community-Articles/2025-09-17-Unit-of-Work-with-Aspnetcore/post.md @@ -0,0 +1,820 @@ +# Unit of Work Pattern with Generic Repository in ASP.NET Core + +Picture this: You're building an e-commerce system and a customer places an order. You need to create the order record, update inventory, charge payment, and send a confirmation email. What happens if the payment succeeds but the inventory update fails? You're left with inconsistent data and an angry customer. + +This is exactly where the Unit of Work pattern becomes invaluable. Instead of managing transactions manually across multiple repositories, this pattern coordinates all your data changes as a single, atomic operation. + +## Understanding the Unit of Work Pattern + +The Unit of Work pattern maintains a list of objects affected by a business transaction and coordinates writing out changes while resolving concurrency problems. Think of it as your transaction coordinator that ensures all-or-nothing operations. + +### The Problem: Scattered Transaction Management + +Without proper coordination, you often end up with code like this: + +```csharp +// Each repository manages its own context - risky! +await _productRepository.CreateAsync(product); +await _inventoryRepository.UpdateStockAsync(productId, -quantity); +await _orderRepository.CreateAsync(order); +``` + +This approach has several problems: +- Each operation might use a different database context +- No automatic rollback if one operation fails +- Manual transaction management becomes complex +- Data consistency isn't guaranteed + +### The Solution: Coordinated Operations + +With Unit of Work, the same scenario becomes much cleaner: + +```csharp +await _unitOfWork.BeginTransactionAsync(); + +try +{ + var productRepo = _unitOfWork.Repository(); + var inventoryRepo = _unitOfWork.Repository(); + var orderRepo = _unitOfWork.Repository(); + + await productRepo.AddAsync(product); + await inventoryRepo.UpdateStockAsync(productId, -quantity); + await orderRepo.AddAsync(order); + + await _unitOfWork.SaveChangesAsync(); + await _unitOfWork.CommitTransactionAsync(); +} +catch +{ + await _unitOfWork.RollbackTransactionAsync(); + throw; +} +``` + +Now all operations either succeed together or fail together, guaranteeing data consistency. + +## Sample Implementation + +> To keep the example short, I will only show the Product entity along with the implementation of the Generic Repository and Unit of Work. In this example, I will use Blazor and .NET 9. + +You can access the sample project here https://github.com/m-aliozkaya/UnitOfWorkDemo. + +### 1. Generic Repository Implementation + +The repository interface defines the contract for data operations. Here's what we're working with: + +_IRepository.cs_ at `~/Data/Repositories` +```csharp +using System.Linq.Expressions; + +namespace UOWDemo.Repositories; + +public interface IRepository where T : class +{ + Task GetByIdAsync(int id); + Task> GetAllAsync(); + Task> FindAsync(Expression> predicate); + Task SingleOrDefaultAsync(Expression> predicate); + + Task AddAsync(T entity); + void Update(T entity); + void Remove(T entity); + void RemoveRange(IEnumerable entities); + + Task ExistsAsync(int id); + Task CountAsync(); + Task CountAsync(Expression> predicate); +} +``` + +_Repository.cs_ at `~/Data/Repositories` +```csharp +using Microsoft.EntityFrameworkCore; +using System.Linq.Expressions; +using UOWDemo.Data; + +namespace UOWDemo.Repositories; + +public class Repository : IRepository where T : class +{ + protected readonly ApplicationDbContext _context; + protected readonly DbSet _dbSet; + + public Repository(ApplicationDbContext context) + { + _context = context; + _dbSet = context.Set(); + } + + public virtual async Task GetByIdAsync(int id) + { + return await _dbSet.FindAsync(id); + } + + public virtual async Task> GetAllAsync() + { + return await _dbSet.ToListAsync(); + } + + public virtual async Task> FindAsync(Expression> predicate) + { + return await _dbSet.Where(predicate).ToListAsync(); + } + + public virtual async Task SingleOrDefaultAsync(Expression> predicate) + { + return await _dbSet.SingleOrDefaultAsync(predicate); + } + + public virtual async Task AddAsync(T entity) + { + await _dbSet.AddAsync(entity); + } + + public virtual void Update(T entity) + { + _dbSet.Update(entity); + } + + public virtual void Remove(T entity) + { + _dbSet.Remove(entity); + } + + public virtual void RemoveRange(IEnumerable entities) + { + _dbSet.RemoveRange(entities); + } + + public virtual async Task ExistsAsync(int id) + { + var entity = await _dbSet.FindAsync(id); + return entity != null; + } + + public virtual async Task CountAsync() + { + return await _dbSet.CountAsync(); + } + + public virtual async Task CountAsync(Expression> predicate) + { + return await _dbSet.CountAsync(predicate); + } +} +``` + +### 2. Unit of Work Implementation + +_IUnitOfWork.cs_ at `~/Data/UnitOfWork` +```csharp +public interface IUnitOfWork : IDisposable, IAsyncDisposable +{ + IRepository Repository() where T : class; + Task SaveChangesAsync(); + Task BeginTransactionAsync(); + Task CommitTransactionAsync(); + Task RollbackTransactionAsync(); +} +``` + +_UnitOfWork.cs_ at `~/Data/UnitOfWork` +```csharp +public class UnitOfWork : IUnitOfWork +{ + private readonly ApplicationDbContext _context; + private bool _disposed; + private IDbContextTransaction? _currentTransaction; + + // ConcurrentDictionary for thread-safe repository caching + private readonly ConcurrentDictionary _repositories = new(); + + public UnitOfWork(ApplicationDbContext context) + { + _context = context; + } + + public IRepository Repository() where T : class + { + return (IRepository)_repositories.GetOrAdd( + typeof(T), + _ => new Repository(_context) + ); + } + + public int SaveChanges() => context.SaveChanges(); + public Task SaveChangesAsync(CancellationToken cancellationToken = default) => + context.SaveChangesAsync(cancellationToken); + + public async Task BeginTransactionAsync() + { + if (_currentTransaction != null) + return; + + _currentTransaction = await _context.Database.BeginTransactionAsync(); + } + + public async Task CommitTransactionAsync() + { + if (_currentTransaction == null) + return; + + await _currentTransaction.CommitAsync(); + await _currentTransaction.DisposeAsync(); + _currentTransaction = null; + } + + public async Task RollbackTransactionAsync() + { + if (_currentTransaction == null) + return; + + await _currentTransaction.RollbackAsync(); + await _currentTransaction.DisposeAsync(); + _currentTransaction = null; + } + + public void Dispose() + { + if (!_disposed) + { + _context.Dispose(); + _currentTransaction?.Dispose(); + _disposed = true; + } + GC.SuppressFinalize(this); + } + + public async ValueTask DisposeAsync() + { + if (!_disposed) + { + await _context.DisposeAsync(); + if (_currentTransaction != null) + { + await _currentTransaction.DisposeAsync(); + } + _disposed = true; + } + GC.SuppressFinalize(this); + } +} +``` + +### 3. Configure Dependency Injection + +Register the services in your `Program.cs`: + +```csharp +builder.Services.AddScoped(); +``` + + +### 4. Defining and Registering Entity to DbContext + +First, let's define a simple `Product` entity: + +_Product.cs_ at `~/Data/Entities` +```csharp +using System.ComponentModel.DataAnnotations; + +namespace UOWDemo.Models; + +public class Product +{ + public int Id { get; set; } + + [Required] + [MaxLength(100)] + public string Name { get; set; } = string.Empty; + + [MaxLength(500)] + public string? Description { get; set; } + + [Required] + [Range(0.01, double.MaxValue)] + public decimal Price { get; set; } + + [Required] + [Range(0, int.MaxValue)] + public int Stock { get; set; } + + public DateTime CreatedDate { get; set; } = DateTime.UtcNow; +} +``` + +* Go to your `DbContext` and implement following code. + +```csharp + public DbSet Products { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Id); + entity.Property(e => e.Price).HasPrecision(18, 2); + entity.Property(e => e.CreatedDate).HasDefaultValueSql("GETUTCDATE()"); + }); + + modelBuilder.Entity().HasData( + new Product { Id = 1, Name = "Laptop", Description = "High-performance laptop", Price = 1299.99m, Stock = 15, CreatedDate = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc) }, + new Product { Id = 2, Name = "Mouse", Description = "Wireless gaming mouse", Price = 79.99m, Stock = 50, CreatedDate = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc) }, + new Product { Id = 3, Name = "Keyboard", Description = "Mechanical keyboard", Price = 149.99m, Stock = 25, CreatedDate = new DateTime(2024, 1, 1, 0, 0, 0, DateTimeKind.Utc) } + ); + } +``` + +### 5. Implement the UI + +_ProductService.cs_ at `~/Services` +```csharp +using System.Text.Json; +using UOWDemo.Models; + +namespace UOWDemo.Services; + +public class ProductService +{ + private readonly HttpClient _httpClient; + + public ProductService(HttpClient httpClient) + { + _httpClient = httpClient; + } + + public async Task> GetAllProductsAsync() + { + var response = await _httpClient.GetAsync("/api/products"); + response.EnsureSuccessStatusCode(); + var json = await response.Content.ReadAsStringAsync(); + return JsonSerializer.Deserialize>(json, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }) ?? new List(); + } + + public async Task CreateTwoProductsWithUowAsync(Product product1, Product product2) + { + var request = new TwoProductsRequest { Product1 = product1, Product2 = product2 }; + var json = JsonSerializer.Serialize(request); + var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json"); + + var response = await _httpClient.PostAsync("/api/products/two-products-with-uow", content); + response.EnsureSuccessStatusCode(); + } + + public async Task CreateTwoProductsWithoutUowAsync(Product product1, Product product2) + { + var request = new TwoProductsRequest { Product1 = product1, Product2 = product2 }; + var json = JsonSerializer.Serialize(request); + var content = new StringContent(json, System.Text.Encoding.UTF8, "application/json"); + + var response = await _httpClient.PostAsync("/api/products/two-products-without-uow", content); + response.EnsureSuccessStatusCode(); + } + + public async Task DeleteProductAsync(int id) + { + var response = await _httpClient.DeleteAsync($"/api/products/{id}"); + response.EnsureSuccessStatusCode(); + } +} +``` + +_Products.razor_ at `~/Components/Pages` +```csharp +@page "/products" +@using UOWDemo.Models +@using UOWDemo.Services +@inject ProductService ProductService +@inject IJSRuntime JSRuntime +@rendermode InteractiveServer + +Products + +
+
+
+

Product Management

+

This page demonstrates Unit of Work transaction patterns with bulk operations.

+
+
+ +
+
+ +
+
+ + @if (isLoading) + { +
+
+ Loading... +
+
+ } + else + { +
+
+
+ + + + + + + + + + + + + @foreach (var product in products) + { + + + + + + + + + } + +
IDNameDescriptionPriceStockActions
@product.Id@product.Name@product.Description$@product.Price.ToString("F2")@product.Stock + +
+
+
+
+ } +
+ + +@if (showTwoProductsModal) +{ + +} + +@code { + private List products = new(); + private bool isLoading = true; + + private bool showTwoProductsModal = false; + private Product product1 = new(); + private Product product2 = new(); + + protected override async Task OnInitializedAsync() + { + await LoadProducts(); + } + + private async Task LoadProducts() + { + try + { + isLoading = true; + products = await ProductService.GetAllProductsAsync(); + } + catch (Exception ex) + { + await JSRuntime.InvokeVoidAsync("alert", $"Error loading products: {ex.Message}"); + } + finally + { + isLoading = false; + } + } + + + private void ShowTwoProductsForm() + { + product1 = new Product { Name = "Product 1", Description = "First product description", Price = 10.00m, Stock = 100 }; + product2 = new Product { Name = "Product 2", Description = "Second product description", Price = 20.00m, Stock = 50 }; + showTwoProductsModal = true; + } + + private void HideTwoProductsModal() + { + showTwoProductsModal = false; + product1 = new Product(); + product2 = new Product(); + } + + private async Task SaveTwoProductsWithUow() + { + try + { + await ProductService.CreateTwoProductsWithUowAsync(product1, product2); + await JSRuntime.InvokeVoidAsync("alert", "Two products saved with Unit of Work! Check console logs to see single transaction."); + await LoadProducts(); + HideTwoProductsModal(); + } + catch (Exception ex) + { + await JSRuntime.InvokeVoidAsync("alert", $"Error saving products with UoW: {ex.Message}"); + } + } + + private async Task SaveTwoProductsWithoutUow() + { + try + { + await ProductService.CreateTwoProductsWithoutUowAsync(product1, product2); + await JSRuntime.InvokeVoidAsync("alert", "Two products saved without Unit of Work! Check console logs to see separate transactions."); + await LoadProducts(); + HideTwoProductsModal(); + } + catch (Exception ex) + { + await JSRuntime.InvokeVoidAsync("alert", $"Error saving products without UoW: {ex.Message}"); + } + } + + private async Task DeleteProduct(int id) + { + if (await JSRuntime.InvokeAsync("confirm", "Are you sure you want to delete this product?")) + { + try + { + await ProductService.DeleteProductAsync(id); + await LoadProducts(); + } + catch (Exception ex) + { + await JSRuntime.InvokeVoidAsync("alert", $"Error deleting product: {ex.Message}"); + } + } + } +} +``` + +UI Preview + +![UI Preview](img-1.png) + +_ProductsController.cs_ at `~/Controllers` +```csharp +using Microsoft.AspNetCore.Mvc; +using UOWDemo.Data; +using UOWDemo.Models; +using UOWDemo.UnitOfWork; + +namespace UOWDemo.Controllers; + +[ApiController] +[Route("api/[controller]")] +public class ProductsController : ControllerBase +{ + private readonly IUnitOfWork _unitOfWork; + private readonly ApplicationDbContext _context; + + public ProductsController(IUnitOfWork unitOfWork, ApplicationDbContext context) + { + _unitOfWork = unitOfWork; + _context = context; + } + + [HttpGet] + public async Task GetProducts() + { + var repository = _unitOfWork.Repository(); + var products = await repository.GetAllAsync(); + return Ok(products); + } + + + [HttpPost("two-products-with-uow")] + public async Task CreateTwoProductsWithUow(TwoProductsRequest request) + { + if (!ModelState.IsValid) + return BadRequest(ModelState); + + await _unitOfWork.BeginTransactionAsync(); + + try + { + var repository = _unitOfWork.Repository(); + await repository.AddAsync(request.Product1); + await repository.AddAsync(request.Product2); + await _unitOfWork.SaveChangesAsync(); + await _unitOfWork.CommitTransactionAsync(); + + return Ok(new { message = "Two products created with Unit of Work (single transaction)", + products = new[] { request.Product1, request.Product2 } }); + } + catch + { + await _unitOfWork.RollbackTransactionAsync(); + throw; + } + } + + [HttpPost("two-products-without-uow")] + public async Task CreateTwoProductsWithoutUow(TwoProductsRequest request) + { + if (!ModelState.IsValid) + return BadRequest(ModelState); + + // First product - separate transaction + await _context.Products.AddAsync(request.Product1); + await _context.SaveChangesAsync(); + + // Second product - separate transaction + await _context.Products.AddAsync(request.Product2); + await _context.SaveChangesAsync(); + + return Ok(new { message = "Two products created without Unit of Work (separate transactions)", + products = new[] { request.Product1, request.Product2 } }); + } + + [HttpDelete("{id}")] + public async Task DeleteProduct(int id) + { + var repository = _unitOfWork.Repository(); + var product = await repository.GetByIdAsync(id); + + if (product == null) + return NotFound(); + + repository.Remove(product); + await _unitOfWork.SaveChangesAsync(); + + return NoContent(); + } +} +``` + +The code base should be shown like this. + +![Code base](img-2.png) + +### 6. Seeing the Difference: Transaction Logging + +To see the transaction behavior in action, configure Entity Framework logging in `appsettings.Development.json`: + +```json +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning", + "Microsoft.EntityFrameworkCore.Database.Command": "Information", + "Microsoft.EntityFrameworkCore.Database.Transaction": "Information" + } + } +} +``` + +Now when you run the demo, the console will show: + +**With Unit of Work - One Transaction:** +``` +BEGIN TRANSACTION +INSERT INTO [Products] ([Name], [Description], [Price], [Stock]) VALUES ('Product 1', ...) +INSERT INTO [Products] ([Name], [Description], [Price], [Stock]) VALUES ('Product 2', ...) +COMMIT TRANSACTION +``` + +**Without Unit of Work - Two Separate Transactions:** +``` +BEGIN TRANSACTION +INSERT INTO [Products] ([Name], [Description], [Price], [Stock]) VALUES ('Product 1', ...) +COMMIT TRANSACTION + +BEGIN TRANSACTION +INSERT INTO [Products] ([Name], [Description], [Price], [Stock]) VALUES ('Product 2', ...) +COMMIT TRANSACTION +``` + +## What If? + +![What if](img-3.png) + +What if we were using the ABP Framework instead of manually implementing the Unit of Work and Generic Repository? Well, most of the heavy lifting we did in this example would be handled automatically. ABP provides built-in support for Unit of Work, transactions, and repository patterns, allowing you to focus on business logic rather than plumbing code. + +### Key Advantages with ABP + +Automatic Transaction Management: Every application service method runs within a transaction by default. If an exception occurs, changes are automatically rolled back. + +* [UnitOfWork] Attribute: You can simply annotate a method with [UnitOfWork] to ensure all repository operations within it run in a single transaction. + +* Automatic SaveChanges: You don’t need to call SaveChanges() manually; ABP takes care of persisting changes at the end of a Unit of Work. + +* Configurable Transaction Options: Transaction isolation levels and timeouts can be easily configured, helping with performance and data consistency. + +* Event-Based Completion: After a successful transaction, related domain events can be triggered automatically—for example, sending a confirmation email when an entity is created. + +And many of them. If you interested in check this document. https://abp.io/docs/latest/framework/architecture/domain-driven-design/unit-of-work + +📌 Example: + +```csharp +[UnitOfWork] +public void CreatePerson(CreatePersonInput input) +{ + var person = new Person { Name = input.Name, EmailAddress = input.EmailAddress }; + _personRepository.Insert(person); + _statisticsRepository.IncrementPeopleCount(); +} +``` + +In this example, both repository operations execute within the same transaction. ABP handles the commit automatically, so either all changes succeed or none are applied. + +> Takeaway: With ABP, developers don’t need to manually implement Unit of Work or manage transactions. This reduces boilerplate code, ensures consistency, and lets you focus on the domain logic. + +## Conclusion + +The Unit of Work pattern shines in scenarios where multiple operations must succeed or fail together. By centralizing transaction management and repository access, it reduces complexity and ensures consistency. + +This demo kept things simple with a single Product entity, but the same approach scales to more complex domains. Whether you’re building an e-commerce app, a financial system, or any data-heavy application, adopting Unit of Work with a Generic Repository can make your codebase cleaner, safer, and easier to maintain. + +Feel free to clone the sample project, experiment with it, and adapt the pattern to your own needs. 🚀 \ No newline at end of file diff --git a/docs/en/Community-Articles/2025-09-18-10-Modern-HTML-CSS-Techniques-Every-Designer-Should-Know-in-2025/10-Modern-HTML-CSS-Techniques-Every-Designer-Should-Know-in-2025..md b/docs/en/Community-Articles/2025-09-18-10-Modern-HTML-CSS-Techniques-Every-Designer-Should-Know-in-2025/10-Modern-HTML-CSS-Techniques-Every-Designer-Should-Know-in-2025..md new file mode 100644 index 0000000000..c010594d44 --- /dev/null +++ b/docs/en/Community-Articles/2025-09-18-10-Modern-HTML-CSS-Techniques-Every-Designer-Should-Know-in-2025/10-Modern-HTML-CSS-Techniques-Every-Designer-Should-Know-in-2025..md @@ -0,0 +1,608 @@ +# 10 Modern HTML & CSS Techniques Every Designer Should Know in 2025 + +HTML and CSS are developing with new features every year. In 2025, things are much more excited than before; because now only classic labels and simple styles are not enough. There are many modern techniques that strengthen the user experience, make the design more flexible and make our business serious easier. In this article, I have compiled 10 of each designer for you. + +Let's start ... + +## Modern HTML Techniques + + +### 1. `
` and `` tag + +This structure offers a structure where users can open and turn off according to their wishes. +If you need to examine in detail; + +```` +
+

Proin magna felis, vestibulum non felis quis, consequat commodo ligula.

+

Sed at purus magna. Sed auctor nisl velit.

+
+```` +This is the general use. If we do not specify a special title, the **Details** title comes by default. To change this, we should use ``. + +```` +
+ Lorem Ipsum +

Proin magna felis, vestibulum non felis quis, consequat commodo ligula.

+

Sed at purus magna. Sed auctor nisl velit.

+
+```` + +#### Styling + +The `` element can be styled with CSS; its color, font, background, and other properties can be customized. + +Additionally, the default triangle marker of `` can also be styled using `::marker`. +The marker is a small sign indicating that the structure is open or closed. +We can use the `:: marker` pseudo-element to style it. But we should use it as `::marker` which belongs to the ``. + +**👉** *HTML Demo* : https://codepen.io/halimekarayay/pen/OPyKBZM + + +#### Attributes + +##### Open Attribute +By default, the structure is **closed**. We can change the default open/closed state using `open`. + +
+ This is a title +

Lorem Ipsum is simply dummy text of the printing and typesetting industry

+

Lorem Ipsum is simply dummy text of the printing and typesetting industry

+
+ + +##### Name + +This feature allows multiple `
` to move into a group by connecting to each other. Only one of them is open at the same time. This feature allows developers to easily create user interface features such as accordion without writing a script. + + +
+ Graduation Requirements +

+ Sed eu ipsum magna. Ut ultricies arcu nec lectus interdum, sit amet elementum diam elementum. +

+
+
+ System Requirements +

+ Curabitur porta quis mi id gravida. Ut convallis, ligula quis blandit sagittis. +

+
+
+ Job Requirements +

+ Suspendisse malesuada arcu eget condimentum pretium. +

+
+ +**👉** *HTML Demo* : [https://codepen.io/halimekarayay/pen/MYaNzmP](https://codepen.io/halimekarayay/pen/MYaNzmP) + + + +##### *More Information* + - [https://css-tricks.com/using-styling-the-details-element/](https://css-tricks.com/using-styling-the-details-element/) + - [https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/details](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/details) +--- + + + +### 2. `` Tag +`` tag is a modern label used to create native modal and popup in HTML. +As of 2025, it is now supported in many browser and can be easily controlled with Javascript. + + // HTML + +

This is a modal window.

+ +
+ + + + // JS + + + - `showModal()`: It opens the `` modal. + - `show()`: It opens the `` like a normal popup. + - `close():` Closes `` + +**👉** *HTML Demo* :[https://codepen.io/halimekarayay/pen/XJmvojX](https://codepen.io/halimekarayay/pen/XJmvojX) + +#### Use in Form + + + // HTML + + +
+

+ + + + + +
+
+ + + // JS + + + - `method = "dialog"` form is switched off automatically. + - `button value="..."` with the user's selection value can be obtained. + +**👉** *HTML Demo* : [https://codepen.io/halimekarayay/pen/VYvoqPr](https://codepen.io/halimekarayay/pen/VYvoqPr) + + +#### Style and Design + + // CSS + dialog { + border: none; + border-radius: 10px; + padding: 20px; + width: 400px; + box-shadow: 0 5px 15px rgba(0,0,0,0.3); + } + dialog::backdrop { + background: rgba(0, 0, 0, 0.5); + } + + + + - `dialog::backdrop` when the modal is opened, the background darkens. + - Size, color and shade can be fully customized with CSS. + +**👉** *HTML Demo* : [https://codepen.io/halimekarayay/pen/VYvoqPr](https://codepen.io/halimekarayay/pen/VYvoqPr) + + + +##### *More Information* + + - [https://developer.mozilla.org/enUS/docs/Web/HTML/Reference/Elements/dialog](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/dialog) + +--- + +### 3. Inert Attribute + +`inert attr()` temporarily makes an element interactive. +So the user cannot focus on that area with the Tab key, Screen Reader does not see, clickable links do not work. + +
+ +
+ + + +#### Specifically, `inert` does the following: +- Prevents the click event from being fired when the user clicks on the element. +- Prevents the focus event from being raised by preventing the element from gaining focus. +- Prevents any contents of the element from being found/matched during any use of the browser's find-in-page feature. +- Prevents users from selecting text within the element — akin to using the CSS property user-select to disable text selection. +- Prevents users from editing any contents of the element that are otherwise editable. +- Hides the element and its content from assistive technologies by excluding them from the accessibility tree. + +````` +
+ + +
+ +
+ + +
+````` + + + +**👉** *HTML Demo* : [https://codepen.io/halimekarayay/pen/WbQVLBb](https://codepen.io/halimekarayay/pen/WbQVLBb) + + + +##### *More Information*: +- [https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/inert](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/inert) + +--- +### 4. Popover Attribute + +In the past, we had to write javascript to create opened windows such as tooltip, modal or dropdown. No need for this anymore: Thanks to HTML5's new butt attribute, we can manage the pop-up windows completely without writing a single line of Javascript. This feature supports accessibility, provides automatic focus management and eliminates the additional library load. + + +
+

This content opens when clicking.

+
+ +- `popover attribute` makes `
` into a pop-up window +- `popovertarget="info"` button triggers the related popover. + + +#### Styling +Popovers are actually normal DOM elements, only the browser adds the logic of **opening/closing**. So you can style it as you wish: + + [popover] { + padding: 1rem; + border-radius: 8px; + background: white; + box-shadow: 0 4px 16px rgba(0,0,0,.2); + } + +*In summary:* A brand new feature that allows the `popover attr()` HTML to produce opensable windows on its own. +Less JavaScript means more accessibility and easier care. + +**👉** *HTML Demo* : [https://codepen.io/halimekarayay/pen/ByoXMwo](https://codepen.io/halimekarayay/pen/ByoXMwo) + + + +##### *More Information*: + https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/popover + +--- +### 5. Fetchpriority Attribute + +Performance on web pages has always been one of the most critical issues. As of 2025, we can clearly state which resource should be loaded first thanks to the `fetchpriority attribute` supported by browsers. This seriously improves the user experience, especially for visuals and important files. + +Scanners normally load resources according to their algorithms. But for example, if there is a large hero image at the top of the page, it is very important that it comes quickly. That's where `fetchpriority` comes into play. + +`fetchpriority` can take three values: + - `high` → priority. + - `low` → then load it. + - `auto` → default. + + +Improves the LCP (Largest Contentful Paint) metric, the “visible” of the page is accelerated. +It provides a more fluent first experience to the user. +It makes a big difference, especially in multi -ly illustrated pages or heavy -based projects. + + + + +##### *More Information*: +- [https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/fetchPriority](https://developer.mozilla.org/en-US/docs/Web/API/HTMLImageElement/fetchPriority) + +--- + +## Modern CSS Techniques + +### 1. CSS Nesting + +CSS Nesting provides the ability to write a style rule into another style rule. In the past, this feature was possible only in prepromessors (eg Sass, Less), but now most of the browsers began to gain native support. With this feature, you can make style files more read, modular and easier to manage. In addition, the repetitive selector spelling is more comfortable to maintain the code. + +Let's think of a simple **HTML** scenario: + +
+

Lorem Ipsum

+

Morbi maximus elit leo, in molestie mi dapibus vel.

+ Continue +
+ + + +If the common classic css is to be used, it is as follows: + + .card { + padding: 1rem; + border: 1px solid #ccc; + } + .card h2 { + font-size: 1.5rem; + } + .card p { + font-size: 1rem; + } + .card a { + color: blue; + text-decoration: none; + } + .card a:hover { + text-decoration: underline; + } + + + +You can write the same style as CSS nesting as follows: + + .card { + padding: 1rem; + border: 1px solid #ccc; + h2 { + font-size: 1.5rem; + } + p { + font-size: 1rem; + } + a { + color: blue; + text-decoration: none; + &:hover { + text-decoration: underline; + } + } + } +This structure increases readability by collecting style rules in a single block for the items in `.card`. + +#### Recommendation + +- You can use CSS Nesting directly in your project, but I suggest you pay attention to the following points: +- If the users of the target audience use old browsers (eg old Android browser, old iOS safari), think of Fallback Style or Polyfill. +- If the code is compiled (such as PostCSS), use the correct versions of Plugin who control nesting support. +- Deep Nesting can cause style complexity; Limit 2–3 levels for readability. + + +##### *More Information*: +- https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_nesting/Using_CSS_nesting +- https://www.w3.org/TR/css-nesting-1 + +--- + +### 2. @container Queries (Container Query) + + +We have used **media query** for **responsive design** for years: we have written different styles according to the screen width. But sometimes a component (eg a card) should behave differently in a different container. Here `@Container Queries` comes into play. + +**Media query** looks at the width of the entire page. But sometimes it looks small when a card is lined up side by side, it should look big when it is alone. In this case, instead of looking at the page width, it would make more sense to look at the inclusive width of the card. + +- The browser checks the condition in `@container` for each parent (parent) element. +- If the parent element is **marked as a container,** (`container-type` is given), its size is examined. +- So `@container` automatically connects to the nearest **“ container ”** upper element. + + + .card-list { + container-type: inline-size; + } + + @container (min-width: 400px) { + .card { + flex-direction: row; + } + } + +- `.card-list` → container. +- `.card` → child. +- When we say `@container (min-width: 400px),` the browser measures the width of the `.card` `.card-list`. +- If the `.card-list` width is greater than 400px, the style is working. + + +#### If there is more than one container + +If there is more than one container in the same hierarchy, the browser is based on the closest. + + + // HTML +
+
+
...
+
+
+ + // CSS + .wrapper { + container-type: inline-size; + } + + .card-list { + container-type: inline-size; + } + + @container (min-width: 600px) { + .card { + background: lightblue; + } + } + + +#### container-name + + If you want to say, **"which container should be looked at",** you should add the `container-name`: + + .card-list { + container-type: inline-size; + container-name: cards; + } + + @container cards (min-width: 600px) { + .card { + background: lightblue; + } + } + +The browser is directly targeted by `.card-list`, and does not look at another container. + +##### *More Information*: +https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_containment/Container_queries + + +--- +### 3. :has() Selector + +CSS has always been able to give style for years. But it was not possible to “choose the parent according to his child”. Here `:has()` filled this gap and created the **parent selector revolution** in the world of CSS. + +- It is possible to replace the upper element according to user interactions. +- We can only solve many conditions with JavaScript with CSS. +- Form validation, card structures, Dropdown menus are very practical. + +For example, marking the form with incorrect input with red edges: + + form:has(input:invalid) { + border: 2px solid red; + } + +In the dropdown menu, emphasizing the menu, which has an element of hover: + + .menu:has(li:hover) { + background: #f0f0f0; + } + +In the card structure, giving different styles to the cards that contain pictures: + + .card:has(img) { + border: 1px solid #ccc; + padding: 1rem; + } + +- It allows you to write more **readable css**. +- Reduces the need for JavaScript in many places. +- It provides great convenience especially for **forms, navigation menus and card grids**. + +##### *More Information*: +- http://developer.mozilla.org/en-US/docs/Web/CSS/:has +- https://www.w3.org/TR/selectors-4/#has-pseudo + +--- + + ### 4. Subgrid +CSS grid revolutionized the world of layout, but there was a missing: +The child grid elements in a grid could not directly use the line and column alignment of the parent grid. Here `subgrid` solves this problem. + +- Provides consistent alignment. +- Nested saves grid from repetitive definitions in their structures. +- It produces cleaner, flexible and sustainable layouts. + + + +Main grid: + + .grid { + display: grid; + grid-template-columns: 200px 1fr; + grid-template-rows: auto auto; + gap: 1rem; + } + + + +Child grid (subgrid): + + .article { + display: grid; + grid-template-columns: subgrid; + grid-column: 1 / -1; + } + + + +HTML example: + +
+
Title
+
+

Article Title

+

+
+
Footer content
+
+ +In this structure, `.article` forms its own grid, but it takes over its columns ** from parent grid **. +Thus, both the title, the article content and the foother appear to the same. + + +- Layout is more regular with **less CSS code**. +- Especially in **complex page designs** (blog, dashboard, magazine designs) great convenience + +##### *More Information*: +- https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_grid_layout/Subgrid +- https://web.dev/articles/css-subgrid + +--- + +### 5. Scroll-driven Animations (scroll-timeline, view-timeline) + +In the past, we had to use a **javascript** to move a item with scroll or to start animation. +But now thanks to the new feature of CSS, we can do this **directly with CSS**. This provides both performance and less coded solution. + +- `@scroll-Timeline:` Allows us to use a scroll field as “timeline .. So the animation progresses as the page shifts down. +- `animation-Timeline:` It ensures that animation is synchronized with this scroll movement. + +Let's make a box move to the right as a box shifts down: + + // HTML +
+
+
+ + // CSS + .scroller { + height: 200vh; + background: linear-gradient(white, lightblue); + } + + .box { + width: 100px; + height: 100px; + background: tomato; + + animation: moveRight 1s linear; + animation-timeline: scroll(); + } + + @keyframes moveRight { + from { transform: translateX(0); } + to { transform: translateX(300px); } + } + +📌 Here, `animation-timeline: scroll ();` when we say, the box is moving as scroll progresses. In other words, the percentage of progression of scroll is → animation progress. + +#### Usage with view-timeline + +In some cases, we may want the animation to **work while a particular item appears**. +That's where the `view-timeline` comes into play. + + // HTML +
+
I am animated
+
+ + // CSS + .container { + height: 150vh; + background: lightgray; + } + + .card { + margin: 100px auto; + width: 200px; + height: 100px; + background: pink; + + animation: fadeIn 1s linear; + animation-timeline: view(); + } + + @keyframes fadeIn { + from { opacity: 0; transform: translateY(50px); } + to { opacity: 1; transform: translateY(0); } + } + +📌 Here `.card` when it begins to be visible, the animation comes into play. + +**👉** *HTML Demo* : https://codepen.io/halimekarayay/pen/myVbmZy + + +##### *More Information*: +https://developer.chrome.com/docs/css-ui/scroll-driven-animations + +----- + +**In conclusion,** HTML and CSS are evolving every year, giving us the opportunity to do more with less code. The 10 techniques we covered in this article are among the must-know tools for modern web projects in 2025. Of course, technology keeps moving forward; but once you start adding these features to your projects, you’ll not only improve the user experience but also speed up your development process. Don’t be afraid to experiment—because the future of the web is being shaped by these new standards. 🚀 diff --git a/docs/en/Community-Articles/2025-09-18-10-Modern-HTML-CSS-Techniques-Every-Designer-Should-Know-in-2025/browser-support.png b/docs/en/Community-Articles/2025-09-18-10-Modern-HTML-CSS-Techniques-Every-Designer-Should-Know-in-2025/browser-support.png new file mode 100644 index 0000000000..2fddb6a541 Binary files /dev/null and b/docs/en/Community-Articles/2025-09-18-10-Modern-HTML-CSS-Techniques-Every-Designer-Should-Know-in-2025/browser-support.png differ diff --git a/docs/en/Community-Articles/2025-09-18-Building-a-Background-Job-Queue-in-ASP.NET-Core-Application/post.md b/docs/en/Community-Articles/2025-09-18-Building-a-Background-Job-Queue-in-ASP.NET-Core-Application/post.md new file mode 100644 index 0000000000..c44644470a --- /dev/null +++ b/docs/en/Community-Articles/2025-09-18-Building-a-Background-Job-Queue-in-ASP.NET-Core-Application/post.md @@ -0,0 +1,308 @@ +# How to Build an In Memory Background Job Queue in ASP.NET Core from Scratch + +In web applications, providing a fast and responsive API is crucial for the user experience. However, some operations, such as sending emails, generating reports, or adding users in bulk, can take significant time. Performing these tasks during a request can block threads, resulting in slow response times. + +To significantly alleviate this issue, it would be more effective to run long running tasks in a background process. Instead of waiting for the request to complete, we can queue the task, provide an immediate response to the user, and perform the work in a separate background process. + +While libraries like Hangfire or message brokers like RabbitMQ exist for this purpose, you don't need any external dependencies. In this blog post, we'll build an in memory background job queue from scratch in ASP.NET Core using only built in .NET features. + +![Background Job Queue Structure in ASP.NET Core](./structure.png) + +## Core Components of Background Job System + +Before implementing the code, it is important to first understand the basic parts that make up the background job system and how they work to process queued tasks. + +### What are IHostedService and BackgroundService? + +`IHostedService` is an interface used in ASP.NET Core to implement long running background tasks managed by the application's lifecycle. When your application starts, it starts all registered `IHostedService` applications. When it shuts down, it instructs them to stop. + +`BackgroundService` is an abstract base class that implements `IHostedService`. It gives you a more efficient way to create a timed service by giving you a single overridable method `ExecuteAsync(CancellationToken stoppingToken)`. We'll use this as the basis for our job processor. + +### The Producer/Consumer Structure + +* **Producer:** This is the part of your application that creates jobs and adds them to the queue. In our example, the producer will be an API Controller. +* **Consumer:** This is a background service that constantly monitors the queue, pulls jobs from the queue, and executes them. Our `BackgroundService` application will be the consumer. +* **Queue:** This is the data structure between the producer and consumer that holds jobs waiting to be processed. + + +![Producer-Consumer Background Job Queue Structure](./producer-consumer.png) + +### Why System.Threading.Channels? + +Introduced in .NET Core 3.0, `System.Threading.Channels` provides a synchronization data structure designed for asynchronous producer, consumer scenarios. + +* **Thread Safety:** Handles all complex locking and synchronization operations internally, making it usable as a whole from multiple threads (for example, when multiple API requests are adding work simultaneously). +* **Async Native:** Designed for use with `async`/`await`, preventing thread blocking. You can asynchronously wait for an item in the queue to become available. +* **Performance:** Optimized for speed and low memory allocation. Generally superior to legacy constructs like `ConcurrentQueue` for asynchronous workflows because it eliminates the need for polling. + +## Implementing the Background Job Queue + +Now that we understand the architecture, we can implement the background job queue. + +### Create the Project + +First, create a new ASP.NET Core Web API project using the .NET CLI. + +```bash +dotnet new sln -n BackgroundJobQueue + +dotnet new webapi -n BackgroundJobQueue.Api + +dotnet sln BackgroundJobQueue.sln add BackgroundJobQueue.Api/BackgroundJobQueue.Api.csproj +``` + +### Define the Job Queue Interface + +For dependency injection and testability, we'll start by defining an interface for our queue. This interface will define methods for adding a job (`EnqueueAsync`) and removing a job (`DequeueAsync`). + +Create a new file named `IBackgroundTaskQueue.cs`. + +```csharp +namespace BackgroundJobQueue; + +public interface IBackgroundTaskQueue +{ + // Adds a work item to the queue + ValueTask EnqueueAsync(Func workItem); + + // Removes and returns a work item from the queue + ValueTask> DequeueAsync(CancellationToken cancellationToken); +} +``` + +We'll use `Func` to represent a work item. This allows us to queue any asynchronous method that accepts a `CancellationToken`. + +### Implement the Queue Service with Channels + +Now, let's implement the `IBackgroundTaskQueue` interface using the `System.Threading.Channels` class. This class will manage the in memory channel. + +Create a new file named `BackgroundTaskQueue.cs`. + +```csharp +using System.Threading.Channels; + +namespace BackgroundJobQueue; + +public class BackgroundTaskQueue : IBackgroundTaskQueue +{ + private readonly Channel> _queue; + + public BackgroundTaskQueue(int capacity) + { + // BoundedChannelOptions specifies the behavior of the channel. + var options = new BoundedChannelOptions(capacity) + { + // FullMode.Wait tells the writer to wait asynchronously if the queue is full. + FullMode = BoundedChannelFullMode.Wait + }; + _queue = Channel.CreateBounded>(options); + } + + public async ValueTask EnqueueAsync(Func workItem) + { + if (workItem is null) + { + throw new ArgumentNullException(nameof(workItem)); + } + + // Writes an item to the channel. If the channel is full, + await _queue.Writer.WriteAsync(workItem); + } + + public async ValueTask> DequeueAsync(CancellationToken cancellationToken) + { + // Reads an item from the channel. If the channel is empty, + var workItem = await _queue.Reader.ReadAsync(cancellationToken); + + return workItem; + } +} +``` + +### Create the Consumer Service (QueuedHostedService) + +This is our consumer. It's a `BackgroundService` that continuously dequeues and executes jobs. + +Create a new file named `QueuedHostedService.cs`. + +```csharp +namespace BackgroundJobQueue; + +public class QueuedHostedService : BackgroundService +{ + private readonly IBackgroundTaskQueue _taskQueue; + private readonly ILogger _logger; + + public QueuedHostedService(IBackgroundTaskQueue taskQueue, ILogger logger) + { + _taskQueue = taskQueue; + _logger = logger; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + _logger.LogInformation("Queued Hosted Service is running."); + + while (!stoppingToken.IsCancellationRequested) + { + // Dequeue a work item + var workItem = await _taskQueue.DequeueAsync(stoppingToken); + + try + { + // Execute the work item + await workItem(stoppingToken); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error occurred executing {WorkItem}.", nameof(workItem)); + } + } + + _logger.LogInformation("Queued Hosted Service is stopping."); + } +} +``` + +The `try-catch` block here is critical. It ensures that if one job fails with an exception, it won't crash the entire background service. + +### Register the Services in Program.cs + +Next we need to register the queue and hosted service in the dependency injection container inside `Program.cs`. + +```csharp +using BackgroundJobQueue; + +var builder = WebApplication.CreateBuilder(args); + +//... + +// Register the background task queue as a Singleton +builder.Services.AddSingleton(_ => +{ + // You can configure the capacity of the queue here + if (!int.TryParse(builder.Configuration["QueueCapacity"], out var queueCapacity)) + { + queueCapacity = 100; + } + return new BackgroundTaskQueue(queueCapacity); +}); + +// Register the hosted service +builder.Services.AddHostedService(); + + +var app = builder.Build(); + +//... + +app.Run(); +``` + +We register `IBackgroundTaskQueue` as a **Singleton** because we need a single, shared queue instance for the entire application. We register `QueuedHostedService` using `AddHostedService()` to ensure the .NET runtime manages its lifecycle. + +### Create a Producer (API Controller) + +Finally, let's create a producer. This will be a simple API controller with an endpoint that enqueues a new background job. + +Create a new controller named `JobsController.cs`. + +```csharp +using Microsoft.AspNetCore.Mvc; + +namespace BackgroundJobQueue.Controllers; + +[ApiController] +[Route("[controller]")] +public class JobsController : ControllerBase +{ + private readonly IBackgroundTaskQueue _queue; + private readonly ILogger _logger; + + public JobsController(IBackgroundTaskQueue queue, ILogger logger) + { + _queue = queue; + _logger = logger; + } + + [HttpPost] + public async Task EnqueueJob() + { + // Enqueue a job that simulates a long running task + await _queue.EnqueueAsync(async token => + { + // Simulate a 5 second task + var guid = Guid.NewGuid(); + _logger.LogInformation("Job {Guid} started.", guid); + await Task.Delay(TimeSpan.FromSeconds(5), token); + _logger.LogInformation("Job {Guid} finished.", guid); + }); + + return Ok("Job has been enqueued."); + } +} +``` + +Now, when you run your application and send a POST request to `/jobs`, the API will respond instantly with "Job has been enqueued." while the 5 second task runs in the background. You'll see the log messages appear in your console after the delay. + +## Important Considerations and Advanced Topics + +### Shutdown + +The `CancellationToken` passed to ExecuteAsync is important. The host triggers this token when you stop your application. Our while loop condition `(!stoppingToken.IsCancellationRequested)` and passing the token to `DequeueAsync` cause the service to stop listening for new items and exit. + +### Error Handling + +Our `try-catch` block prevents a single failed job from crashing the consumer. For production scenarios, you can consider a more advanced error handling strategy, such as a retry mechanism. Libraries like **Polly** can be integrated here to automatically retry failed jobs with policies like fallback. + +### Limits of the In Memory Queue + +The biggest drawback of this approach is that the queue is **in memory**. If your application process restarts or crashes for any reason, any work waiting in the queue will be lost. This solution is best for idempotent or non critical tasks. + +### When Should You Use External Libraries? + +Built from the ground up, this solution is powerful, but it's important to know when to turn to more advanced tools. + +* **Hangfire:** Use Hangfire when you need job continuity (saves jobs to a database), automatic retries, a management dashboard, scheduled jobs, and delayed execution. Background tasks are an ideal solution while processing. + +* **RabbitMQ** When building a distributed system (such as a microservices architecture), you can use a custom message broker. These brokers provide scalable queues that isolate your services and guarantee message delivery. + +## Frequently Asked Questions + +**Q1: What happens to the jobs in the queue if my application restarts or crashes?** + +**A:** Because this is an **in-memory** queue, any unprocessed work is **lost** when the application closes or crashes. This solution is best suited for non-critical or idempotent tasks (i.e., they can be safely rerun without causing problems). For guaranteed business continuity, you should use a solution that stores jobs in a database or message broker, such as **Hangfire** or **RabbitMQ**. + +**Q2: How can I implement retry logic for jobs that fail due to an exception?** + +**A:** The `try-catch` block in `QueuedHostedService` prevents the entire background service from crashing, but it doesn't automatically retry the failed job. You can integrate a library like **Polly** to add robust retry capabilities. You can wrap the `await workItem(stoppingToken);` call in a Polly retry policy to automatically rerun the job a configured number of times with delays. + +[Polly Official Documentation](https://github.com/App-vNext/Polly) + +**Q3: Will this work in a load balanced environment with multiple server instances?** + +**A:** No, this implementation is instance-specific. The queue exists only in the memory of the server instance that receives the API request. If you deploy your application across multiple servers, a job queued on Server A will be processed only by Server A. For a shared queue that can be processed by any server in a web environment, you should use a centralized, distributed message broker such as **RabbitMQ**, **Azure Service Bus**. + +**Q4: What happens if jobs are added to the queue faster than the background service can process them?** + +**A:** This application uses a BoundedChannel with a fixed capacity (for example, 100 in our example). When the queue is full, setting `FullMode = BoundedChannelFullMode.Wait` causes the producer (API controller) to asynchronously wait until space becomes available in the queue. This creates "backpressure" that prevents your application from running out of memory. However, this also means that if the background service is constantly overloaded, your API endpoint will become slower to respond as it waits to queue new jobs. + +**Q5: How can I monitor the number of jobs currently in the queue?** + +**A:** For basic monitoring, you can inject `IBackgroundTaskQueue` into a service and periodically record the queue count. A more advanced approach is to create a custom metrics endpoint (for example, for Prometheus). You can create a custom controller that accesses the `Count` property of the `ChannelReader` to report the current queue depth and set alerts if the queue grows too long. + + +## Conclusion + +We've built a performant, in memory background job queue in ASP.NET Core using only built in framework features. This is a way to increase your API's responsiveness and improve the user experience by disabling long running tasks. + +Using `IHostedService` and `System.Threading.Channels`, we've created an efficient implementation of the producer, consumer model. While this in memory approach has some limitations, it's a powerful tool for many common scenarios. As your needs grow, you can consider more feature rich tools like Hangfire or RabbitMQ, knowing you've mastered the fundamentals. + +## References + + * [Background tasks with hosted services in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services) + * [An Introduction to System.Threading.Channels](https://devblogs.microsoft.com/dotnet/an-introduction-to-system-threading-channels/) + * [ABP Framework - Background Jobs](https://docs.abp.io/en/abp/latest/Background-Jobs) + * [Creating a queued background task service with IHostedService by Andrew Lock](https://andrewlock.net/controlling-ihostedservice-execution-order-in-aspnetcore-3/) + * [Hangfire Documentation](https://docs.hangfire.io/en/latest/getting-started/aspnet-core-applications.html) + * [RabbitMQ Tutorials](https://www.rabbitmq.com/getstarted.html) \ No newline at end of file diff --git a/docs/en/Community-Articles/2025-09-18-Building-a-Background-Job-Queue-in-ASP.NET-Core-Application/producer-consumer.png b/docs/en/Community-Articles/2025-09-18-Building-a-Background-Job-Queue-in-ASP.NET-Core-Application/producer-consumer.png new file mode 100644 index 0000000000..6ebfb5140b Binary files /dev/null and b/docs/en/Community-Articles/2025-09-18-Building-a-Background-Job-Queue-in-ASP.NET-Core-Application/producer-consumer.png differ diff --git a/docs/en/Community-Articles/2025-09-18-Building-a-Background-Job-Queue-in-ASP.NET-Core-Application/structure.png b/docs/en/Community-Articles/2025-09-18-Building-a-Background-Job-Queue-in-ASP.NET-Core-Application/structure.png new file mode 100644 index 0000000000..ffb4a5a1de Binary files /dev/null and b/docs/en/Community-Articles/2025-09-18-Building-a-Background-Job-Queue-in-ASP.NET-Core-Application/structure.png differ diff --git a/docs/en/Community-Articles/2025-09-18-Distributed-Event-Buses-With-.NET-Clients/post.md b/docs/en/Community-Articles/2025-09-18-Distributed-Event-Buses-With-.NET-Clients/post.md new file mode 100644 index 0000000000..c7c56e9b67 --- /dev/null +++ b/docs/en/Community-Articles/2025-09-18-Distributed-Event-Buses-With-.NET-Clients/post.md @@ -0,0 +1,360 @@ +# The Most Popular & Best Distributed Event Buses with .NET Clients + +## Why Event Buses Matter + +In distributed systems, the messaging layer often determines whether projects succeed or fail. When microservices struggle to communicate effectively, it's typically due to messaging architecture problems. + +Event buses solve this communication challenge. Instead of services calling each other directly (which becomes complex quickly), they publish events when something happens. Other services subscribe to the events they care about. While the concept is simple, implementation details matter significantly - especially in the .NET ecosystem. + +This article examines the major event bus technologies available today, covering their strengths, weaknesses, and practical implementation considerations. + +## The Main Contenders + +The following analysis covers the major event bus technologies, examining their practical strengths and limitations based on real-world usage patterns. + +### RabbitMQ - The Old Reliable + +RabbitMQ is known for its reliability and consistent performance. Built on AMQP (Advanced Message Queuing Protocol), it provides a solid foundation for enterprise messaging scenarios. + +RabbitMQ's key advantage lies in its routing flexibility, allowing sophisticated message flow patterns throughout distributed systems. + +**Key Strengths:** +- Message persistence and guaranteed delivery +- Flexible routing patterns (direct, topic, fanout, headers) +- Management UI and monitoring tools +- Mature .NET client library + +**.NET Integration Example:** +```csharp +using RabbitMQ.Client; +using RabbitMQ.Client.Events; + +// Publisher +var factory = new ConnectionFactory() { HostName = "localhost" }; +using var connection = factory.CreateConnection(); +using var channel = connection.CreateModel(); + +channel.QueueDeclare(queue: "order_events", durable: true, exclusive: false, autoDelete: false); + +var message = JsonSerializer.Serialize(new OrderCreated { OrderId = Guid.NewGuid() }); +var body = Encoding.UTF8.GetBytes(message); + +channel.BasicPublish(exchange: "", routingKey: "order_events", basicProperties: null, body: body); +``` + +**Works best when:** You need complex routing, can't afford to lose messages, or you're dealing with traditional enterprise patterns. + +### Apache Kafka - The Heavy Hitter + +Kafka represents a different approach to messaging. Rather than being a traditional message broker, it functions as a distributed log system that excels at messaging workloads. + +While Kafka's concepts (partitions, offsets, consumer groups) can seem complex initially, understanding them reveals why the platform has gained widespread adoption. The throughput capabilities are exceptional. + +**Key Strengths:** +- Exceptional throughput (millions of messages/second) +- Built-in partitioning and replication +- Message replay capabilities +- Strong ordering guarantees within partitions + +**.NET Integration Example:** +```csharp +using Confluent.Kafka; + +// Producer +var config = new ProducerConfig { BootstrapServers = "localhost:9092" }; +using var producer = new ProducerBuilder(config).Build(); + +var result = await producer.ProduceAsync("order-events", + new Message + { + Key = orderId.ToString(), + Value = JsonSerializer.Serialize(orderEvent) + }); + +// Consumer +var consumerConfig = new ConsumerConfig +{ + GroupId = "order-processor", + BootstrapServers = "localhost:9092", + AutoOffsetReset = AutoOffsetReset.Earliest +}; + +using var consumer = new ConsumerBuilder(consumerConfig).Build(); +consumer.Subscribe("order-events"); + +while (true) +{ + var consumeResult = consumer.Consume(cancellationToken); + // Process message + consumer.Commit(consumeResult); +} +``` + +**Perfect for:** High-volume streaming, event sourcing, or when you need to replay messages later. + +### Azure Service Bus - The Microsoft Way + +For organizations using the Microsoft ecosystem, Azure Service Bus provides a natural fit. It offers enterprise-grade messaging without infrastructure management overhead. + +The integration with other Azure services is seamless, and features like dead letter queues provide robust error handling capabilities. + +**Key Strengths:** +- Dead letter queues and message sessions +- Duplicate detection and scheduled messages +- Integration with Azure ecosystem +- Auto-scaling capabilities + +**.NET Integration Example:** +```csharp +using Azure.Messaging.ServiceBus; + +await using var client = new ServiceBusClient(connectionString); +var sender = client.CreateSender("order-queue"); + +var message = new ServiceBusMessage(JsonSerializer.Serialize(orderEvent)) +{ + MessageId = Guid.NewGuid().ToString(), + ContentType = "application/json" +}; + +await sender.SendMessageAsync(message); + +// Processor +var processor = client.CreateProcessor("order-queue"); +processor.ProcessMessageAsync += async args => +{ + var order = JsonSerializer.Deserialize(args.Message.Body); + // Process order + await args.CompleteMessageAsync(args.Message); +}; +await processor.StartProcessingAsync(); +``` + +**Great choice when:** You're on Azure, need enterprise features, or want someone else to handle the operations. + +### Amazon SQS - Keep It Simple + +Amazon SQS prioritizes simplicity and reliability over extensive features. While not the most feature-rich option, this approach often aligns well with practical requirements. + +SQS works particularly well in serverless architectures where reliable queuing is needed without operational complexity. + +**Key Strengths:** +- Virtually unlimited scalability +- Server-side encryption +- Dead letter queue support +- Pay-per-use pricing model + +**.NET Integration Example:** +```csharp +using Amazon.SQS; +using Amazon.SQS.Model; + +var sqsClient = new AmazonSQSClient(); +var queueUrl = await sqsClient.GetQueueUrlAsync("order-events"); + +// Send message +await sqsClient.SendMessageAsync(new SendMessageRequest +{ + QueueUrl = queueUrl.QueueUrl, + MessageBody = JsonSerializer.Serialize(orderEvent) +}); + +// Receive messages +var response = await sqsClient.ReceiveMessageAsync(new ReceiveMessageRequest +{ + QueueUrl = queueUrl.QueueUrl, + MaxNumberOfMessages = 10, + WaitTimeSeconds = 20 +}); + +foreach (var message in response.Messages) +{ + // Process message + await sqsClient.DeleteMessageAsync(queueUrl.QueueUrl, message.ReceiptHandle); +} +``` + +**Use it when:** You're on AWS, building serverless, or just want something that works without complexity. + +### Apache ActiveMQ - The Veteran + +ActiveMQ has been serving enterprise messaging needs for many years, predating the current microservices trend. + +While not the most modern option, it supports an extensive range of messaging protocols and continues to operate reliably in legacy enterprise environments. + +**Key Strengths:** +- Multiple protocol support (AMQP, STOMP, MQTT) +- Clustering and high availability +- JMS compliance +- Web-based administration + +**.NET Integration Example:** +```csharp +using Apache.NMS; +using Apache.NMS.ActiveMQ; + +var factory = new ConnectionFactory("tcp://localhost:61616"); +using var connection = factory.CreateConnection(); +using var session = connection.CreateSession(); + +var destination = session.GetQueue("order.events"); +var producer = session.CreateProducer(destination); + +var message = session.CreateTextMessage(JsonSerializer.Serialize(orderEvent)); +producer.Send(message); +``` + +**Consider it for:** Legacy environments, multi-protocol needs, or when you're stuck with JMS requirements. + +### Redpanda - Kafka Without the Pain + +Redpanda is a newer entrant that addresses Kafka's operational complexity. The project maintains Kafka API compatibility while eliminating JVM overhead and Zookeeper dependencies. + +This approach significantly reduces operational burden while preserving the familiar Kafka programming model. + +**Key Strengths:** +- Kafka API compatibility +- No dependency on JVM or Zookeeper +- Lower resource consumption +- Built-in schema registry + +**.NET Integration:** +Uses the same Confluent.Kafka client library, making migration seamless: + +```csharp +var config = new ProducerConfig { BootstrapServers = "localhost:9092" }; +// Same code as Kafka - drop-in replacement +``` + +**Try it if:** You want Kafka's power but hate managing Kafka clusters. + +### Amazon Kinesis - The Analytics Focused One + +Amazon Kinesis is AWS's streaming platform, designed primarily for analytics and machine learning workloads rather than general messaging. + +While Kinesis excels in real-time analytics pipelines, other AWS services like SQS may be more suitable for general event-driven architecture patterns. + +**Key Strengths:** +- Real-time data processing +- Integration with AWS analytics services +- Automatic scaling +- Built-in data transformation + +**.NET Integration Example:** +```csharp +using Amazon.Kinesis; +using Amazon.Kinesis.Model; + +var kinesisClient = new AmazonKinesisClient(); + +await kinesisClient.PutRecordAsync(new PutRecordRequest +{ + StreamName = "order-stream", + Data = new MemoryStream(Encoding.UTF8.GetBytes(JsonSerializer.Serialize(orderEvent))), + PartitionKey = orderId.ToString() +}); +``` + +**Good for:** Real-time analytics, ML pipelines, or when you're deep in the AWS ecosystem. + +### Apache Pulsar - The Ambitious One + +Apache Pulsar (originally developed by Yahoo) aims to provide comprehensive messaging capabilities with advanced features like multi-tenancy support. + +While Pulsar offers sophisticated functionality, its complexity may exceed requirements for many use cases. However, organizations needing its specific features may find the additional complexity justified. + +**Key Strengths:** +- Multi-tenancy support +- Geo-replication +- Flexible consumption models +- Built-in schema registry + +**.NET Integration Example:** +```csharp +using DotPulsar; + +await using var client = PulsarClient.Builder() + .ServiceUrl(new Uri("pulsar://localhost:6650")) + .Build(); + +var producer = client.NewProducer(Schema.String) + .Topic("order-events") + .Create(); + +await producer.Send("Hello World"); +``` + +**Consider it for:** Multi-tenant SaaS platforms or when you need geo-replication out of the box. + +## Performance Comparison + +The following comparison presents performance characteristics based on industry benchmarks and real-world implementations. Actual results will vary depending on specific use cases and configurations: + +| Feature | RabbitMQ | Kafka | Azure Service Bus | Amazon SQS | ActiveMQ | Redpanda | Kinesis | Pulsar | +|---------|----------|-------|------------------|------------|----------|----------|---------|---------| +| **Throughput** | 10K-100K msg/sec | 1M+ msg/sec | 100K+ msg/sec | Unlimited | 50K msg/sec | 1M+ msg/sec | 1M+ records/sec | 1M+ msg/sec | +| **Latency** | <10ms | <10ms | <100ms | 200-1000ms | <50ms | <10ms | <100ms | <10ms | +| **Data Retention** | Until consumed | Days to weeks | 14 days max | 14 days max | Until consumed | Days to weeks | 24hrs-365 days | Configurable | +| **Ordering Guarantees** | Queue-level | Partition-level | Session-level | FIFO queues | Queue-level | Partition-level | Shard-level | Partition-level | +| **Operational Complexity** | Medium | High | Low (managed) | Low (managed) | Medium | Low | Low (managed) | Medium | +| **Multi-tenancy** | Basic | Manual setup | Native | IAM-based | Basic | Native | IAM-based | Native | +| **.NET Client Maturity** | Excellent | Excellent | Excellent | Good | Good | Excellent (Kafka-compatible) | Good | Fair | + +## Real-World Use Cases + +The following scenarios demonstrate how different technologies perform in production environments: + +**E-commerce Order Processing** +- RabbitMQ: Effective for complex order workflows until reaching approximately 50K orders/day +- Kafka: Enables analytics teams to replay months of historical order events for analysis +- Azure Service Bus: Provides reliable order processing with minimal operational overhead for .NET-focused organizations + +**Financial Trading** +- Market data processing: Migration from traditional MQ to Kafka reduced latency from 50ms to 5ms +- Multi-tenant platforms: Pulsar's advanced features prove valuable despite requiring significant learning investment + +**IoT Projects** +- Sensor data ingestion: Kafka handles high-volume sensor data effectively but requires substantial computational resources +- Smart city implementations: Kinesis performs well for real-time analytics but creates vendor lock-in considerations + +## Selection Guidelines + +**RabbitMQ is suitable for:** +- Traditional enterprise messaging scenarios +- Teams familiar with established messaging patterns +- Applications requiring guaranteed delivery and message persistence +- Systems not requiring massive scale initially + +**Kafka works best when:** +- Analytics or machine learning workloads are involved +- High throughput is a genuine requirement +- Event replay capabilities are needed +- Teams have Kafka expertise available + +**Azure Service Bus fits well for:** +- Organizations already using Azure infrastructure +- Requirements for enterprise features with minimal operational overhead +- .NET-focused development teams + +**Amazon SQS is appropriate when:** +- AWS ecosystem integration is preferred +- Serverless architectures are being implemented +- Simple, reliable queuing is the primary requirement + +**Alternative considerations:** +- **Redpanda**: Kafka compatibility with reduced operational complexity +- **Pulsar**: Multi-tenancy requirements justify additional complexity +- **Kinesis**: Real-time analytics within the AWS ecosystem +- **ActiveMQ**: Legacy system integration requirements + +## Conclusion + +No event bus technology is universally perfect. Each option involves trade-offs, and the optimal choice varies significantly between organizations and use cases. + +**Starting Simple**: Beginning with straightforward solutions like RabbitMQ or managed services such as Azure Service Bus addresses most initial requirements effectively. Migration to more specialized platforms remains possible as needs evolve. + +**Complex Requirements**: Organizations with immediate high-scale or analytics requirements may justify Kafka's complexity from the start. However, adequate team expertise is essential for successful implementation. + +**Operational Considerations**: Technology selection should align with team capabilities. The most advanced event bus provides no value if the team cannot effectively operate and troubleshoot it during critical situations. + +**Monitoring and Reliability**: Regardless of the chosen platform, understanding failure modes and implementing comprehensive monitoring is crucial. Event buses often serve as system backbones - their failure typically cascades throughout the entire architecture. diff --git a/docs/en/cli/index.md b/docs/en/cli/index.md index 60d4e3ff0b..415cef1878 100644 --- a/docs/en/cli/index.md +++ b/docs/en/cli/index.md @@ -33,44 +33,44 @@ While each command may have a set of options, there are some global options that Here, is the list of all available commands before explaining their details: -* **`help`**: Shows help on the usage of the ABP CLI. -* **`cli`**: Update or remove ABP CLI. -* **`new`**: Generates a new solution based on the ABP [startup templates](../solution-templates/index.md). -* **`new-module`**: Generates a new module based on the given template. -* **`new-package`**: Generates a new package based on the given template. -* **`update`**: Automatically updates all ABP related NuGet and NPM packages in a solution. -* **`clean`**: Deletes all `BIN` and `OBJ` folders in the current folder. -* **`add-package`**: Adds an ABP package to a project. -* **`add-package-ref`**: Adds package to given project. -* **`install-module`**: Adds a [multi-package application module](../modules/index.md) to a given module. -* **`install-local-module`**: Installs a local module to given module. -* **`list-modules`**: Lists names of application modules. -* **`list-templates`**: Lists the names of available templates to create a solution. -* **`get-source`**: Downloads the source code of a module. -* **`add-source-code`**: Downloads the source code and replaces package references with project references. -* **`init-solution`**: Creates ABP Studio configuration files for a given solution. -* **`kube-connect`**: Connects to kubernetes environment. (*Available for* ***Business*** *or higher licenses*) -* **`kube-intercept`**: Intercepts a service running in Kubernetes environment. (*Available for* ***Business*** *or higher licenses*) -* **`list-module-sources`**: Lists the remote module sources. -* **`add-module-source`**: Adds a remote module source. -* **`delete-module-source`**: Deletes a remote module source. -* **`generate-proxy`**: Generates client side proxies to use HTTP API endpoints. -* **`remove-proxy`**: Removes previously generated client side proxies. -* **`switch-to-preview`**: Switches to the latest preview version of the ABP. -* **`switch-to-nightly`**: Switches to the latest [nightly builds](../release-info/nightly-builds.md) of the ABP related packages on a solution. -* **`switch-to-stable`**: Switches to the latest stable versions of the ABP related packages on a solution. -* **`switch-to-local`**: Changes NuGet package references on a solution to local project references. -* **`upgrade`**: It converts the application to use pro modules. -* **`translate`**: Simplifies to translate localization files when you have multiple JSON [localization](../framework/fundamentals/localization.md) files in a source control repository. -* **`login`**: Authenticates on your computer with your [abp.io](https://abp.io/) username and password. -* **`login-info`**: Shows the current user's login information. -* **`logout`**: Logouts from your computer if you've authenticated before. -* **`bundle`**: Generates script and style references for ABP Blazor and MAUI Blazor project. -* **`install-libs`**: Install NPM Packages for MVC / Razor Pages and Blazor Server UI types. -* **`clear-download-cache`**: Clears the templates download cache. -* **`check-extensions`**: Checks the latest version of the ABP CLI extensions. -* **`install-old-cli`**: Installs old ABP CLI. -* **`generate-razor-page`**: Generates a page class that you can use it in the ASP NET Core pipeline to return an HTML page. +* **[`help`](../cli#help)**: Shows help on the usage of the ABP CLI. +* **[`cli`](../cli#cli)**: Update or remove ABP CLI. +* **[`new`](../cli#new)**: Generates a new solution based on the ABP [startup templates](../solution-templates/index.md). +* **[`new-module`](../cli#new-module)**: Generates a new module based on the given template. +* **[`new-package`](../cli#new-package)**: Generates a new package based on the given template. +* **[`update`](../cli#update)**: Automatically updates all ABP related NuGet and NPM packages in a solution. +* **[`clean`](../cli#clean)**: Deletes all `BIN` and `OBJ` folders in the current folder. +* **[`add-package`](../cli#add-package)**: Adds an ABP package to a project. +* **[`add-package-ref`](../cli#add-package-ref)**: Adds package to given project. +* **[`install-module`](../cli#install-module)**: Adds a [multi-package application module](../modules/index.md) to a given module. +* **[`install-local-module`](../cli#install-local-module)**: Installs a local module to given module. +* **[`list-modules`](../cli#list-modules)**: Lists names of application modules. +* **[`list-templates`](../cli#list-templates)**: Lists the names of available templates to create a solution. +* **[`get-source`](../cli#get-source)**: Downloads the source code of a module. +* **[`add-source-code`](../cli#add-source-code)**: Downloads the source code and replaces package references with project references. +* **[`init-solution`](../cli#init-solution)**: Creates ABP Studio configuration files for a given solution. +* **[`kube-connect`](../cli#kube-connect)**: Connects to kubernetes environment. (*Available for* ***Business*** *or higher licenses*) +* **[`kube-intercept`](../cli#kube-intercept)**: Intercepts a service running in Kubernetes environment. (*Available for* ***Business*** *or higher licenses*) +* **[`list-module-sources`](../cli#list-module-sources)**: Lists the remote module sources. +* **[`add-module-source`](../cli#add-module-source)**: Adds a remote module source. +* **[`delete-module-source`](../cli#delete-module-source)**: Deletes a remote module source. +* **[`generate-proxy`](../cli#generate-proxy)**: Generates client side proxies to use HTTP API endpoints. +* **[`remove-proxy`](../cli#remove-proxy)**: Removes previously generated client side proxies. +* **[`switch-to-preview`](../cli#switch-to-preview)**: Switches to the latest preview version of the ABP. +* **[`switch-to-nightly`](../cli#switch-to-nightly)**: Switches to the latest [nightly builds](../release-info/nightly-builds.md) of the ABP related packages on a solution. +* **[`switch-to-stable`](../cli#switch-to-stable)**: Switches to the latest stable versions of the ABP related packages on a solution. +* **[`switch-to-local`](../cli#switch-to-local)**: Changes NuGet package references on a solution to local project references. +* **[`upgrade`](../cli#upgrade)**: It converts the application to use pro modules. +* **[`translate`](../cli#translate)**: Simplifies to translate localization files when you have multiple JSON [localization](../framework/fundamentals/localization.md) files in a source control repository. +* **[`login`](../cli#login)**: Authenticates on your computer with your [abp.io](https://abp.io/) username and password. +* **[`login-info`](../cli#login-info)**: Shows the current user's login information. +* **[`logout`](../cli#logout)**: Logouts from your computer if you've authenticated before. +* **[`bundle`](../cli#bundle)**: Generates script and style references for ABP Blazor and MAUI Blazor project. +* **[`install-libs`](../cli#install-libs)**: Install NPM Packages for MVC / Razor Pages and Blazor Server UI types. +* **[`clear-download-cache`](../cli#clear-download-cache)**: Clears the templates download cache. +* **[`check-extensions`](../cli#check-extensions)**: Checks the latest version of the ABP CLI extensions. +* **[`install-old-cli`](../cli#install-old-cli)**: Installs old ABP CLI. +* **[`generate-razor-page`](../cli#generate-razor-page)**: Generates a page class that you can use it in the ASP NET Core pipeline to return an HTML page. ### help diff --git a/docs/en/docs-nav.json b/docs/en/docs-nav.json index 4b5e2908a6..585309d8eb 100644 --- a/docs/en/docs-nav.json +++ b/docs/en/docs-nav.json @@ -745,6 +745,10 @@ "text": "Image Manipulation", "path": "framework/infrastructure/image-manipulation.md" }, + { + "text": "Interceptors", + "path": "framework/infrastructure/interceptors.md" + }, { "text": "JSON", "path": "framework/infrastructure/json.md" @@ -2127,11 +2131,11 @@ { "text": "Microservice Solution", "isLazyExpandable": true, - "path": "solution-templates/microservice", + "path": "solution-templates/microservice/index.md", "items":[ { "text": "Overview", - "path": "solution-templates/microservice" + "path": "solution-templates/microservice/overview.md" }, { "text": "Solution Structure", @@ -2187,7 +2191,7 @@ }, { "text": "Swagger integration", - "path": "solution-templates/microservice/swagger.md" + "path": "solution-templates/microservice/swagger-integration.md" }, { "text": "Permission management", @@ -2345,11 +2349,91 @@ }, { "text": "CMS Kit", - "path": "modules/cms-kit" + "isLazyExpandable": true, + "path": "modules/cms-kit", + "items": [ + { + "text": "Overview", + "path": "modules/cms-kit.md", + "isIndex": true + }, + { + "text": "Pages", + "path": "modules/cms-kit/pages.md" + }, + { + "text": "Blogging", + "path": "modules/cms-kit/blogging.md" + }, + { + "text": "Tag Management", + "path": "modules/cms-kit/tags.md" + }, + { + "text": "Comments", + "path": "modules/cms-kit/comments.md" + }, + { + "text": "Reaction System", + "path": "modules/cms-kit/reactions.md" + }, + { + "text": "Rating System", + "path": "modules/cms-kit/ratings.md" + }, + { + "text": "Menus", + "path": "modules/cms-kit/menus.md" + }, + { + "text": "Global Resources", + "path": "modules/cms-kit/global-resources.md" + }, + { + "text": "Dynamic Widget", + "path": "modules/cms-kit/dynamic-widget.md" + }, + { + "text": "Marked Item System", + "path": "modules/cms-kit/marked-items.md" + } + ] }, { "text": "CMS Kit (Pro)", - "path": "modules/cms-kit-pro" + "isLazyExpandable": true, + "path": "modules/cms-kit-pro", + "items": [ + { + "text": "Overview", + "path": "modules/cms-kit-pro.md", + "isIndex": true + }, + { + "text": "Newsletter System", + "path": "modules/cms-kit-pro/newsletter.md" + }, + { + "text": "Contact Management", + "path": "modules/cms-kit-pro/contact-form.md" + }, + { + "text": "URL Forwarding System", + "path": "modules/cms-kit-pro/URL-forwarding.md" + }, + { + "text": "Poll System", + "path": "modules/cms-kit-pro/poll.md" + }, + { + "text": "Page Feedback System", + "path": "modules/cms-kit-pro/page-feedback.md" + }, + { + "text": "FAQ System", + "path": "modules/cms-kit-pro/faq.md" + } + ] }, { "text": "Docs", diff --git a/docs/en/dynamic-proxying-interceptors.md b/docs/en/dynamic-proxying-interceptors.md deleted file mode 100644 index 722eadc249..0000000000 --- a/docs/en/dynamic-proxying-interceptors.md +++ /dev/null @@ -1,7 +0,0 @@ -# Dynamic Proxying / Interceptors - -This document is planned to be written later. - -## See Also - -* [Video tutorial](https://abp.io/video-courses/essentials/interception) \ No newline at end of file diff --git a/docs/en/framework/api-development/standard-apis/configuration.md b/docs/en/framework/api-development/standard-apis/configuration.md index c1470fcf71..11e754f214 100644 --- a/docs/en/framework/api-development/standard-apis/configuration.md +++ b/docs/en/framework/api-development/standard-apis/configuration.md @@ -55,7 +55,16 @@ namespace Acme.BookStore.Web } ``` +Add your contributor instance to the `AbpApplicationConfigurationOptions` + +```csharp +Configure(options => +{ + options.Contributors.AddIfNotContains(new MyApplicationConfigurationContributor()); +}); +``` + * `IApplicationConfigurationContributor` defines the `ContributeAsync` method to extend the **application-configuration** endpoint with the specified additional data. -* You can inject services and perform any logic needed to extend the endpoint as you wish. +* You can get services from `context.ServiceProvider` and perform any logic needed to extend the endpoint as you wish. -> Application configuration contributors are automatically discovered by the ABP and executed as a part of the application configuration initialization process. +> Application configuration contributors are executed as a part of the application configuration initialization process. diff --git a/docs/en/framework/architecture/domain-driven-design/application-services.md b/docs/en/framework/architecture/domain-driven-design/application-services.md index 4d32f853dd..968ceb434d 100644 --- a/docs/en/framework/architecture/domain-driven-design/application-services.md +++ b/docs/en/framework/architecture/domain-driven-design/application-services.md @@ -134,7 +134,7 @@ The `CreateAsync` method above manually creates a `Book` entity from given `Crea However, in many cases, it's very practical to use **auto object mapping** to set properties of an object from a similar object. ABP provides an [object to object mapping](../../infrastructure/object-to-object-mapping.md) infrastructure to make this even easier. -Object to object mapping provides abstractions and it is implemented by the [AutoMapper](https://automapper.org/) library by default. +Object to object mapping provides abstractions and it is implemented by the [Mapperly](https://mapperly.riok.app/) library by default. Let's create another method to get a book. First, define the method in the `IBookAppService` interface: @@ -162,36 +162,32 @@ public class BookDto } ```` -AutoMapper requires to create a mapping [profile class](https://docs.automapper.org/en/stable/Configuration.html#profile-instances). Example: +[Mapperly](https://mapperly.riok.app/) requires to create a mapping class that implements the `MapperBase` class with the `[Mapper]` attribute as follows: -````csharp -public class MyProfile : Profile +```csharp +[Mapper] +public partial class BookToBookDtoMapper : MapperBase { - public MyProfile() - { - CreateMap(); - } + public override partial BookDto Map(Book source); + + public override partial void Map(Book source, BookDto destination); } -```` +``` -You should then register profiles using the `AbpAutoMapperOptions`: +Then, if your application uses multiple mapping providers, you should add the following configuration to your module's `ConfigureServices` method to decide which mapping provider to use: ````csharp -[DependsOn(typeof(AbpAutoMapperModule))] +[DependsOn(typeof(AbpMapperlyModule))] public class MyModule : AbpModule { public override void ConfigureServices(ServiceConfigurationContext context) { - Configure(options => - { - //Add all mappings defined in the assembly of the MyModule class - options.AddMaps(); - }); + context.Services.AddMapperlyObjectMapper(); } } ```` -`AddMaps` registers all profile classes defined in the assembly of the given class, typically your module class. It also registers for the [attribute mapping](https://docs.automapper.org/en/stable/Attribute-mapping.html). +With this configuration, your module will use [Mapperly](https://mapperly.riok.app/) as the default mapping provider and you don't need to register mapping classes manually. Then you can implement the `GetAsync` method as shown below: @@ -291,16 +287,21 @@ public class CreateUpdateBookDto } ```` -[Profile](https://docs.automapper.org/en/stable/Configuration.html#profile-instances) class of DTO class. +Define the mapping classes for [Mapperly](https://mapperly.riok.app/) as follows: ```csharp -public class MyProfile : Profile +[Mapper] +public partial class BookToBookDtoMapper : MapperBase { - public MyProfile() - { - CreateMap(); - CreateMap(); - } + public override partial BookDto Map(Book source); + public override partial void Map(Book source, BookDto destination); +} + +[Mapper] +public partial class CreateUpdateBookDtoToBookMapper : MapperBase +{ + public override partial Book Map(CreateUpdateBookDto source); + public override partial void Map(CreateUpdateBookDto source, Book destination); } ``` diff --git a/docs/en/framework/architecture/modularity/installer-projects.md b/docs/en/framework/architecture/modularity/installer-projects.md new file mode 100644 index 0000000000..81c75ab015 --- /dev/null +++ b/docs/en/framework/architecture/modularity/installer-projects.md @@ -0,0 +1,221 @@ +# Module Installer Projects + +Each ABP module includes an `.Installer` project (e.g., `Volo.Abp.Account.Installer`) that serves as a **Virtual File System container** for module installation and resource management. These projects are essential for the ABP CLI to understand and install modules properly. + +## Purpose of Installer Projects + +Installer projects have three main purposes: + +1. **Virtual File System Integration**: Register the module's embedded resources with ABP's Virtual File System +2. **Resource Packaging**: Package module metadata files (`.abpmdl` and `.abppkg`) as embedded resources +3. **CLI Integration**: Enable the ABP CLI to understand module structure and install modules automatically + +## Structure of Installer Projects + +### Project Files + +- **`{ModuleName}.Installer.csproj`**: References `Volo.Abp.VirtualFileSystem` and embeds module metadata files +- **`InstallationNotes.md`**: Documentation for the module +- **`Volo/Abp/{ModuleName}/Abp{ModuleName}InstallerModule.cs`**: The core module class that registers embedded resources + +### Example Installer Module + +```csharp +using Volo.Abp.Modularity; +using Volo.Abp.VirtualFileSystem; + +namespace Volo.Abp.Account; + +[DependsOn(typeof(AbpVirtualFileSystemModule))] +public class AbpAccountInstallerModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + Configure(options => + { + options.FileSets.AddEmbedded(); + }); + } +} +``` + +### Project Configuration + +The `.csproj` file embeds module metadata as content: + +```xml + + + net9.0 + true + + + + + + + + + + + true + content\ + + + + + true + content\ + + + +``` + +## Module Metadata Files + +### `.abpmdl` (Module Definition) + +The module definition file describes the module's structure and packages: + +```json +{ + "folders": { + "items": { + "src": {}, + "test": {} + } + }, + "packages": { + "Volo.Abp.Account.Web": { + "path": "src/Volo.Abp.Account.Web/Volo.Abp.Account.Web.abppkg", + "folder": "src" + }, + "Volo.Abp.Account.Application": { + "path": "src/Volo.Abp.Account.Application/Volo.Abp.Account.Application.abppkg", + "folder": "src" + } + } +} +``` + +### `.abppkg` (Package Definition) + +Each package has a definition file that specifies its role: + +```json +{ + "role": "lib.application" +} +``` + +Common roles: +- `lib.application`: Application layer package +- `lib.mvc`: MVC/Web layer package +- `lib.domain`: Domain layer package +- `lib.domain-shared`: Shared domain layer package +- `lib.efcore`: Entity Framework Core package + +## How Installer Projects Work + +### 1. CLI Installation Process + +When you run `abp add-module Volo.Abp.Account`: + +1. **Download Installer Package**: CLI downloads `Volo.Abp.Account.Installer` from NuGet +2. **Read Module Definition**: CLI reads the embedded `.abpmdl` file to understand module structure +3. **Read Package Definitions**: CLI reads `.abppkg` files to understand package roles +4. **Install Packages**: CLI installs appropriate packages to correct project types based on roles +5. **Add Dependencies**: CLI adds module dependencies to project module classes + +### 2. Virtual File System Integration + +The `InstallerModule` registers itself with the Virtual File System: + +```csharp +options.FileSets.AddEmbedded(); +``` + +This makes embedded resources available at runtime and enables: +- Access to module metadata +- Resource file management +- Module configuration + +## Creating Installer Projects for New Modules + +### Required Files + +1. **Project File**: `{ModuleName}.Installer.csproj` +2. **Module Class**: `Abp{ModuleName}InstallerModule.cs` +3. **Documentation**: `InstallationNotes.md` +4. **Module Definition**: `{ModuleName}.abpmdl` (in module root) +5. **Package Definitions**: `{PackageName}.abppkg` (in each package) + +### Template Structure + +``` +modules/your-module/ +├── src/ +│ ├── Volo.Abp.YourModule.Installer/ +│ │ ├── Volo.Abp.YourModule.Installer.csproj +│ │ ├── InstallationNotes.md +│ │ └── Volo/ +│ │ └── Abp/ +│ │ └── YourModule/ +│ │ └── AbpYourModuleInstallerModule.cs +│ └── [other packages]/ +├── Volo.Abp.YourModule.abpmdl +└── [other module files] +``` + +### Package Definition Examples + +For different package types: + +```json +// Application package +{ "role": "lib.application" } + +// MVC package +{ "role": "lib.mvc" } + +// Domain package +{ "role": "lib.domain" } + +// EF Core package +{ "role": "lib.efcore" } +``` + +## Why Installer Projects Appear "Empty" + +Installer projects appear minimal because their primary function is infrastructure, not business logic: + +- **No Business Logic**: Business logic belongs in the actual module packages +- **Pure Infrastructure**: They only handle module installation and resource management +- **CLI Integration**: They enable automated module installation through the ABP CLI +- **Resource Management**: They package and distribute module metadata + +## Best Practices + +1. **Follow Naming Convention**: Use `{ModuleName}.Installer` for the project name +2. **Include Documentation**: Always provide `InstallationNotes.md` with module information +3. **Proper Dependencies**: Only depend on `Volo.Abp.VirtualFileSystem` +4. **Embed All Metadata**: Include both `.abpmdl` and `.abppkg` files +5. **Test Installation**: Verify your installer works with `abp add-module` command + +## Troubleshooting + +### Common Issues + +1. **Missing .abpmdl file**: Ensure the module definition file exists in the module root +2. **Missing .abppkg files**: Each package needs a definition file +3. **Incorrect roles**: Use appropriate roles for each package type +4. **CLI not finding module**: Verify the installer package is published to NuGet + +### Verification Steps + +1. Build the installer project: `dotnet build` +2. Check embedded resources: Verify `.abpmdl` and `.abppkg` files are embedded +3. Test CLI installation: `abp add-module YourModule` +4. Verify dependencies: Check that module dependencies are added correctly + +This installer system enables ABP's sophisticated module architecture, allowing for automated installation with proper dependency resolution and project type matching. \ No newline at end of file diff --git a/docs/en/framework/architecture/modularity/plugin-modules.md b/docs/en/framework/architecture/modularity/plugin-modules.md index c4c7c51d04..ce9e64f655 100644 --- a/docs/en/framework/architecture/modularity/plugin-modules.md +++ b/docs/en/framework/architecture/modularity/plugin-modules.md @@ -1,6 +1,6 @@ # Plug-In Modules -It is possible to load [modules](../modularity/basics.md) as plug-ins. That means you may not reference to a module's assembly in your solution, but you can load that module in the application startup just like any other module. +[Modules](../modularity/basics.md) can also be loaded as plug-ins. This means that you don’t need to reference a module’s assembly directly in your solution; instead, you can load it during application startup in the same way as any other module. ## Basic Usage @@ -28,7 +28,7 @@ That's all. ABP will discover the modules in the given folder, configure and ini options.PlugInSources.Add(new FolderPlugInSource(@"D:\Temp\MyPlugIns")); ```` -> `AddFolder()` only looks for the assembly file in the given folder, but not looks for the sub-folders. You can pass `SearchOption.AllDirectories` as a second parameter to explore plug-ins also from the sub-folders, recursively. +> `AddFolder()` only looks for the assembly file in the given folder, but doesn't look for the sub-folders. You can pass `SearchOption.AllDirectories` as a second parameter to explore plug-ins also from the sub-folders, recursively. There are two more built-in Plug-In Source implementations: @@ -124,13 +124,13 @@ Edit the `.csproj` file content: - net5.0 + net9.0 Library true - + diff --git a/docs/en/framework/architecture/multi-tenancy/index.md b/docs/en/framework/architecture/multi-tenancy/index.md index 98a4c9afb5..21bb054b6a 100644 --- a/docs/en/framework/architecture/multi-tenancy/index.md +++ b/docs/en/framework/architecture/multi-tenancy/index.md @@ -42,6 +42,8 @@ ABP supports all the following approaches to store the tenant data in the databa [Saas module (PRO)](../../../modules/saas.md) allows you to set a connection string for any tenant (as optional), so you can achieve any of the approaches. +> You can see the community article *[Multi-Tenancy with Separate Databases in .NET and ABP Framework](https://abp.io/community/articles/multitenancy-with-separate-databases-in-dotnet-and-abp-51nvl4u9)* for more details about different database architectures with practical implementation details. + ## Usage Multi-tenancy system is designed to **work seamlessly** and make your application code **multi-tenancy unaware** as much as possible. @@ -229,18 +231,20 @@ services.Configure(options => If you change the `TenantKey`, make sure to pass it to `provideAbpCore` via `withOptions` method in the Angular client as follows: ```js -@NgModule({ +// app.config.ts +// ... +export const appConfig: ApplicationConfig = { providers: [ + // ... provideAbpCore( withOptions({ // ... tenantKey: "MyTenantKey", }) ), + // ... ], - // ... -}) -export class AppModule {} +}; ``` If you need to access it, you can inject it as follows: @@ -456,8 +460,9 @@ The [Tenant Management module](../../../modules/tenant-management.md) provides a ### A note about separate database per tenant approach in open source version While ABP fully supports this option, managing connection strings of tenants from the UI is not available in open source version. You need to have [Saas module (PRO)](../../../modules/saas.md). -Alternatively you can implement this feature yourself by customizing the tenant management module and tenant application service to create and migrate the database on the fly. +Alternatively, you can implement this feature yourself by customizing the tenant management module and tenant application service to create and migrate the database on the fly. ## See Also * [Features](../../infrastructure/features.md) +* [Article: Multi-Tenancy with Separate Databases in .NET and ABP Framework](https://abp.io/community/articles/multitenancy-with-separate-databases-in-dotnet-and-abp-51nvl4u9) diff --git a/docs/en/framework/data/entity-framework-core/mysql.md b/docs/en/framework/data/entity-framework-core/mysql.md index 7760598414..8f299bdee4 100644 --- a/docs/en/framework/data/entity-framework-core/mysql.md +++ b/docs/en/framework/data/entity-framework-core/mysql.md @@ -17,10 +17,20 @@ Find ***YourProjectName*EntityFrameworkCoreModule** class inside the `.EntityFra Find `UseSqlServer()` calls in your solution. Check the following files: * *YourProjectName*EntityFrameworkCoreModule.cs inside the `.EntityFrameworkCore` project. Replace `UseSqlServer()` with `UseMySQL()`. -* *YourProjectName*DbContextFactory.cs inside the `.EntityFrameworkCore` project. Replace `UseSqlServer()` with `UseMySql()`. Then add a new parameter (`ServerVersion`) to `UseMySql()` method. Example: `.UseMySql(configuration.GetConnectionString("Default"), ServerVersion.Parse("8.0.21-mysql"))`. See [this issue](https://github.com/PomeloFoundation/Pomelo.EntityFrameworkCore.MySql/pull/1233) for more information about `ServerVersion`) +* *YourProjectName*DbContextFactory.cs inside the `.EntityFrameworkCore` project. Replace `UseSqlServer()` with `UseMySQL()`. > Depending on your solution structure, you may find more code files need to be changed. +## Use Pomelo Provider + +Alternatively, you can use the [Pomelo.EntityFrameworkCore.MySql](https://www.nuget.org/packages/Pomelo.EntityFrameworkCore.MySql) provider. Replace the [Volo.Abp.EntityFrameworkCore.MySQL](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.MySQL) package with the [Volo.Abp.EntityFrameworkCore.MySQL.Pomelo](https://www.nuget.org/packages/Volo.Abp.EntityFrameworkCore.MySQL.Pomelo) package in your `.EntityFrameworkCore` project. + +Find ***YourProjectName*EntityFrameworkCoreModule** class inside the `.EntityFrameworkCore` project, replace `typeof(AbpEntityFrameworkCoreMySQLModule)` with `typeof(AbpEntityFrameworkCoreMySQLPomeloModule)` in the `DependsOn` attribute. + +> Depending on your solution structure, you may find more code files need to be changed. + +The `UseMySQL()` method calls remain the same, no changes needed. + ## Change the Connection Strings MySQL connection strings are different than SQL Server connection strings. So, check all `appsettings.json` files in your solution and replace the connection strings inside them. See the [connectionstrings.com](https://www.connectionstrings.com/mysql) for details of MySQL connection string options. diff --git a/docs/en/framework/data/entity-framework-core/oracle-official.md b/docs/en/framework/data/entity-framework-core/oracle-official.md index d9942fe0e0..12509306db 100644 --- a/docs/en/framework/data/entity-framework-core/oracle-official.md +++ b/docs/en/framework/data/entity-framework-core/oracle-official.md @@ -59,6 +59,51 @@ Run the `.DbMigrator` project to create the database, apply the changes and seed Oracle limits strings to `NVARCHAR2(2000)` when the migration is created. Some of the entity properties may extend it. You can check the known and reported properties of ABP modules entities that can extend this limit. To prevent this problem, you need to convert the `string` type to `long` type first and generate a new migration. Then convert the `long` type to `clob` type with maximum length. +## ORA-00904: "FALSE": invalid identifier + +In the 23ai release, Oracle has added new `OracleSQLCompatibility` enumeration values: + +- `OracleSQLCompatibility.DatabaseVersion19` +- `OracleSQLCompatibility.DatabaseVersion21` +- `OracleSQLCompatibility.DatabaseVersion23` + +By default, the enumeration value will match the ODP.NET version number. In the case of ODP.NET 23, the enumeration is `OracleSQLCompatibility.DatabaseVersion23`. + +In DB 23ai, BOOLEAN columns and native JSON columns are supported. Thus, Oracle EF Core 8.23.40 use these in its SQL when `OracleSQLCompatibility.DatabaseVersion23` or no value is set. If you'd like to revert to 19c compatible behavior, use the `OracleSQLCompatibility.DatabaseVersion19` value. + +See the following issue for more details: +https://github.com/oracle/dotnet-db-samples/issues/377#issuecomment-2096419703 + + +If you want to change the `OracleSQLCompatibility` value, you need to change it at `AbpDbContextOptions` and `MyProjectNameDbContextFactory` files. + +```csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + Configure(options => + { + options.UseOracle(x => x.UseOracleSQLCompatibility(OracleSQLCompatibility.DatabaseVersion19)); + }); +} +``` + +```csharp +public MyProjectNameDbContext CreateDbContext(string[] args) +{ + var configuration = BuildConfiguration(); + + AbpSolution4EfCoreEntityExtensionMappings.Configure(); + + var builder = new DbContextOptionsBuilder() + .UseOracle(configuration.GetConnectionString("Default"), x => + { + x.UseOracleSQLCompatibility(OracleSQLCompatibility.DatabaseVersion19); + }); + + return new MyProjectNameDbContext(builder.Options); +} +``` + ### First Migration Update you application DbContext `OnModelCreating` method: diff --git a/docs/en/framework/fundamentals/localization.md b/docs/en/framework/fundamentals/localization.md index 6e8a3f678a..47c4ca564a 100644 --- a/docs/en/framework/fundamentals/localization.md +++ b/docs/en/framework/fundamentals/localization.md @@ -91,6 +91,34 @@ A JSON localization file content is shown below: > ABP will ignore (skip) the JSON file if the `culture` section is missing. +You can also use nesting or array in localization files, like this: + +````json +{ + "culture": "en", + "texts": { + "HelloWorld": "Hello World!", + "Hello": { + "World": "Hello World!" + }, + "Hi":[ + "Bye": "Bye World!" + "Hello": "Hello World!" + ] + } +} +```` + +Then you can use it like this: + +> The double underscore (`__`) is used to separate the parent key from the child key. + +````csharp +var str = L["Hello__World"]; // Hello World! +var str2 = L["Hi__0"]; // Bye World! +var str3 = L["Hi__1"]; // Hello World! +```` + ### Default Resource `AbpLocalizationOptions.DefaultResourceType` can be set to a resource type, so it is used when the localization resource was not specified: diff --git a/docs/en/framework/fundamentals/object-extensions.md b/docs/en/framework/fundamentals/object-extensions.md index b12be63dc0..87ece27f75 100644 --- a/docs/en/framework/fundamentals/object-extensions.md +++ b/docs/en/framework/fundamentals/object-extensions.md @@ -394,6 +394,22 @@ public class MyProfile : Profile It has the same parameters with the `MapExtraPropertiesTo` method. +#### Mapperly Integration + +If you're using the [Mapperly](https://github.com/riok/mapperly) library, the ABP also provides an extension method to utilize the `MapExtraPropertiesTo` method defined above. + +You can use the `MapExtraProperties` attribute to Mapperly class: + +````csharp +[Mapper] +[MapExtraProperties] +public partial class IdentityUserToProfileDtoMapper : MapperBase +{ + public override partial IdentityUserDto Map(IdentityUser source); + public override partial void Map(IdentityUser source, IdentityUserDto destination); +} +```` + ## Entity Framework Core Database Mapping If you're using the EF Core, you can map an extra property to a table field in the database. Example: diff --git a/docs/en/framework/infrastructure/audit-logging.md b/docs/en/framework/infrastructure/audit-logging.md index 3392f5ac35..2e6dc83940 100644 --- a/docs/en/framework/infrastructure/audit-logging.md +++ b/docs/en/framework/infrastructure/audit-logging.md @@ -230,6 +230,28 @@ public class MyUser : Entity } ```` +#### Ignore Update Audit Properties And Publish Entity Updated Event + +The `[DisableAuditing]` attribute supports additional configuration options when applied to **entity properties**. + +* **UpdateModificationProps** (default: `true`): When set to `false`, changes to this entity property will not update audit properties (like `LastModificationTime`. +* **PublishEntityEvent** (default: `true`): When set to `false`, changes to this entity property will not publish entity change events (`EntityUpdatedEvent`). + + +````csharp +public class MyUser : Entity +{ + public string Name { get; set; } + + [DisableAuditing(UpdateModificationProps = false, PublishEntityEvent = false)] + public string ReadCount { get; set; } +} +```` + +This example will ignore update audit properties and publish entity updated event when the `ReadCount` property is updated. + +> The `UpdateModificationProps` and `PublishEntityEvent` only work for [Entity Framework Core](../data/entity-framework-core). It will not work for [MongoDB](../data/mongodb). + ## IAuditingStore `IAuditingStore` is an interface that is used to save the audit log objects (explained below) by the ABP. If you need to save the audit log objects to a custom data store, you can implement the `IAuditingStore` in your own application and replace using the [dependency injection system](../fundamentals/dependency-injection.md). diff --git a/docs/en/framework/infrastructure/blob-storing/google.md b/docs/en/framework/infrastructure/blob-storing/google.md index a90152b4f0..bd7f7c1fbf 100644 --- a/docs/en/framework/infrastructure/blob-storing/google.md +++ b/docs/en/framework/infrastructure/blob-storing/google.md @@ -27,11 +27,11 @@ Configure(options => { container.UseGoogle(google => { - google.ClientEmail = "your coogle client email"; - google.ProjectId = "your coogle project id"; - google.PrivateKey = "your coogle private key"; - google.Scopes = "your coogle scopes"; - google.ContainerName = "your coogle container name"; + google.ClientEmail = "your google client email"; + google.ProjectId = "your google project id"; + google.PrivateKey = "your google private key"; + google.Scopes = "your google scopes"; + google.ContainerName = "your google container name"; google.CreateContainerIfNotExists = true; //google.UseApplicationDefaultCredentials = true; // If you want to use application default credentials }); diff --git a/docs/en/framework/infrastructure/entity-cache.md b/docs/en/framework/infrastructure/entity-cache.md index 64a03ad4c4..cba0cf8eaa 100644 --- a/docs/en/framework/infrastructure/entity-cache.md +++ b/docs/en/framework/infrastructure/entity-cache.md @@ -88,6 +88,17 @@ public class MyMapperProfile : Profile } ``` +If you are using [Mapperly](https://mapperly.riok.app/), you can create a new mapping class that implements the `MapperBase` class with the `[Mapper]` attribute as follows: + +```csharp +[Mapper] +public partial class ProductToProductDtoMapper : MapperBase +{ + public override partial ProductDto Map(Product source); + public override partial void Map(Product source, ProductDto destination); +} +``` + Now, you can inject the `IEntityCache` service wherever you want: ```csharp diff --git a/docs/en/framework/infrastructure/event-bus/distributed/index.md b/docs/en/framework/infrastructure/event-bus/distributed/index.md index d56215fa12..f379a44f40 100644 --- a/docs/en/framework/infrastructure/event-bus/distributed/index.md +++ b/docs/en/framework/infrastructure/event-bus/distributed/index.md @@ -643,6 +643,11 @@ Configure(options => * `InboxWaitingEventMaxCount`: The maximum number of events to query at once from the inbox in the database. Default value is 1000. * `OutboxWaitingEventMaxCount`: The maximum number of events to query at once from the outbox in the database. Default value is 1000. * `DistributedLockWaitDuration`: ABP uses [distributed locking](../../distributed-locking.md) to prevent concurrent access to the inbox and outbox messages in the database, when running multiple instance of the same application. If an instance of the application can not obtain the lock, it tries after a duration. This is the configuration of that duration. Default value is 15 seconds (`TimeSpan.FromSeconds(15)`). +* `InboxProcessorFailurePolicy`: The policy to handle the failure of the inbox processor. Default value is `Retry`. Possible values are: + * `Retry`: The current exception and subsequent events will continue to be processed in order in the next cycle. + * `RetryLater`: Skip the event that caused the exception and continue with the following events. The failed event will be retried after a delay that doubles with each retry, starting from the configured `InboxProcessorRetryBackoffFactor` (e.g., 10, 20, 40, 80 seconds). The default maximum retry count is 10 (configurable). Discard the event if it still fails after reaching the maximum retry count. + * `Discard`: The event that caused the exception will be discarded and will not be retried. +* `InboxProcessorRetryBackoffFactor`: The initial retry delay factor (double) used when `InboxProcessorFailurePolicy` is `RetryLater`. The retry delay is calculated as: `delay = InboxProcessorRetryBackoffFactor × 2^retryCount`. Default value is `10`. ### Skipping Outbox diff --git a/docs/en/framework/infrastructure/interceptors.md b/docs/en/framework/infrastructure/interceptors.md new file mode 100644 index 0000000000..d50de3ad84 --- /dev/null +++ b/docs/en/framework/infrastructure/interceptors.md @@ -0,0 +1,213 @@ +# Interceptors + +ABP provides a powerful interception system that allows you to execute custom logic before and after method calls without modifying the original method code. This is achieved through **dynamic proxying** and is extensively used throughout the ABP framework to implement cross-cutting concerns. ABP's interception is implemented on top of the [Castle DynamicProxy](https://www.castleproject.org/projects/dynamicproxy/) library. + +## What is Dynamic Proxying / Interception? + +**Interception** is a technique that allows executing additional logic before or after a method call without directly modifying the method's code. This is achieved through **dynamic proxying**, where the runtime generates proxy classes that wrap the original class. + +When a method is called on a proxied object: +1. The call is intercepted by the proxy +2. Custom behaviors (like logging, validation, or authorization) can be executed +3. The original method is called +4. Additional logic can be executed after the method completes + +This enables **cross-cutting concerns** (logic that applies across many parts of an application) to be handled in a clean, reusable way without code duplication. + +## Similarities and Differences with MVC Action/Page Filters + +If you are familiar with ASP.NET Core MVC, you've likely used **action filters** or **page filters**. Interceptors are conceptually similar but have some key differences: + +### Similarities + +* Both allow executing code before and after method execution +* Both are used to implement cross-cutting concerns like validation, logging, caching, or exception handling +* Both support asynchronous operations + +### Differences + +* **Scope**: Filters are tied to MVC's request pipeline, while interceptors can be applied to **any class or service** in the application +* **Configuration**: Filters are configured via attributes or middleware in MVC, while interceptors in ABP are applied through **dependency injection and dynamic proxies** +* **Target**: Interceptors can target application services, domain services, repositories, and virtually any service resolved from the IoC container—not just web controllers + +## How ABP Uses Interceptors + +ABP Framework extensively leverages interception to provide built-in features without requiring boilerplate code. Here are some key examples: + +### [Unit of Work (UOW)](../architecture/domain-driven-design/unit-of-work.md) + +Automatically begins and commits/rolls back a database transaction when entering or exiting an application service method. This ensures data consistency without manual transaction management. + +### [Input Validation](../fundamentals/validation.md) + +Input DTOs are automatically validated against data annotation attributes and custom validation rules before executing the service logic, providing consistent validation behavior across all services. + +### [Authorization](../fundamentals/authorization.md) + +Checks user permissions before allowing the execution of application service methods, ensuring security policies are enforced consistently. + +### [Feature](./features.md) & [Global Feature](./global-features.md) Checking + +Checks if a feature is enabled before executing the service logic, allowing you to conditionally enable or restrict functionality for tenants or the application. + +### [Auditing](./audit-logging.md) + +Automatically logs who performed an action, when it happened, what parameters were used, and what data was involved, providing comprehensive audit trails. + +## Building Your Own Interceptor + +You can create custom interceptors in ABP to implement your own cross-cutting concerns. + +### Creating an Interceptor + +Create a class that inherits from `AbpInterceptor`: + +````csharp +using System.Threading.Tasks; +using Volo.Abp.Aspects; +using Volo.Abp.DependencyInjection; +using Volo.Abp.DynamicProxy; + +public class ExecutionTimeLogInterceptor : AbpInterceptor, ITransientDependency +{ + private readonly ILogger _logger; + + public ExecutionTimeLogInterceptor(ILogger logger) + { + _logger = logger; + } + + public override async Task InterceptAsync(IAbpMethodInvocation invocation) + { + var sw = Stopwatch.StartNew(); + + _logger.LogInformation($"Executing {invocation.TargetObject.GetType().Name}.{invocation.Method.Name}"); + + // Proceed to the actual target method + await invocation.ProceedAsync(); + + sw.Stop(); + + _logger.LogInformation($"Executed {invocation.TargetObject.GetType().Name}.{invocation.Method.Name} in {sw.ElapsedMilliseconds} ms"); + } +} +```` + +### Register Interceptors + +Create a static class that contains the `RegisterIfNeeded` method and register the interceptor in the `PreConfigureServices` method of your module. + +The `ShouldIntercept` method is used to determine if the interceptor should be registered for the given type. You can add an `IExecutionTimeLogEnabled` interface and implement it in the classes that you want to intercept. + +> `DynamicProxyIgnoreTypes` is static class that contains the types that should be ignored by the interceptor. See [Performance Considerations](#performance-considerations) for more information. + +````csharp +// Define an interface to mark the classes that should be intercepted +public interface IExecutionTimeLogEnabled +{ +} +```` + +````csharp +using System.Threading.Tasks; +using Volo.Abp.DependencyInjection; + +// A simple service that added to the DI container and will be intercepted since it implements the `IExecutionTimeLogEnabled` interface +public class SampleExecutionTimeService : IExecutionTimeLogEnabled, ITransientDependency +{ + public virtual async Task DoWorkAsync() + { + // Simulate a long-running task to test the interceptor + await Task.Delay(1000); + } +} +```` + +````csharp +using System; +using Volo.Abp.DependencyInjection; +using Volo.Abp.DynamicProxy; + +public static class ExecutionTimeLogInterceptorRegistrar +{ + public static void RegisterIfNeeded(IOnServiceRegistredContext context) + { + if (ShouldIntercept(context.ImplementationType)) + { + context.Interceptors.TryAdd(); + } + } + + private static bool ShouldIntercept(Type type) + { + return !DynamicProxyIgnoreTypes.Contains(type) && typeof(IExecutionTimeLogEnabled).IsAssignableFrom(type); + } +} +```` + +````csharp +public override void PreConfigureServices(ServiceConfigurationContext context) +{ + context.Services.OnRegistered(ExecutionTimeLogInterceptorRegistrar.RegisterIfNeeded); +} +```` + +## Restrictions and Important Notes + +### Always use asynchronous methods + +For best performance and reliability, implement your service methods as asynchronous to avoid **async over sync**, that can cause unexpected problems, For more information, see [Should I expose synchronous wrappers for asynchronous methods?](https://devblogs.microsoft.com/dotnet/should-i-expose-synchronous-wrappers-for-asynchronous-methods/) + +### Virtual Methods Requirement + +For **class proxies**, methods need to be marked as `virtual` so that they can be overridden by the proxy. Otherwise, interception will not occur. + +````csharp +public class MyService : IExecutionTimeLogEnabled, ITransientDependency +{ + // This method CANNOT be intercepted (not virtual) + public void CannotBeIntercepted() + { + } + + // This method CAN be intercepted (virtual) + public virtual void CanBeIntercepted() + { + } +} +```` + +> This restriction does **not** apply to interface-based proxies. If your service implements an interface and is injected via that interface, all methods can be intercepted regardless of the `virtual` keyword. + +### Dependency Injection Scope + +Interceptors only work when services are resolved from the dependency injection container. Direct instantiation with `new` bypasses interception: + +````csharp +// This will NOT be intercepted +var service = new MyService(); +service.CannotBeIntercepted(); + +// This WILL be intercepted (if MyService is registered with DI) +var service = serviceProvider.GetService(); +service.CanBeIntercepted(); +```` + +### Performance Considerations + +Interceptors are generally efficient, but each one adds method-call overhead. Keep the number of interceptors minimal on hot paths. + +Castle DynamicProxy can negatively impact performance for certain components, notably ASP.NET Core MVC controllers. See the discussions in [castleproject/Core#486](https://github.com/castleproject/Core/issues/486) and [abpframework/abp#3180](https://github.com/abpframework/abp/issues/3180). + +ABP uses interceptors for features like UOW, auditing, and authorization, which rely on dynamic proxy classes. For controllers, prefer implementing cross-cutting concerns with middleware or MVC/Page filters instead of dynamic proxies. + +To avoid generating dynamic proxies for specific types, use the static class `DynamicProxyIgnoreTypes` and add the base classes of the types to the list. Subclasses of any listed base class are also ignored. ABP framework already adds some base classes to the list (`ComponentBase, ControllerBase, PageModel, ViewComponent`); you can add more base classes if needed. + +> Always use interface-based proxies instead of class-based proxies for better performance. + +## See Also + +* [Video tutorial: Interceptors in ABP Framework](https://abp.io/video-courses/essentials/interception) +* [Castle DynamicProxy](https://www.castleproject.org/projects/dynamicproxy/) +* [Castle.Core.AsyncInterceptor](https://github.com/JSkimming/Castle.Core.AsyncInterceptor) +* [ASP.NET Core Filters](https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/filters) diff --git a/docs/en/framework/infrastructure/json.md b/docs/en/framework/infrastructure/json.md index 67126edceb..6146436818 100644 --- a/docs/en/framework/infrastructure/json.md +++ b/docs/en/framework/infrastructure/json.md @@ -69,6 +69,19 @@ Add [Volo.Abp.Json.Newtonsoft](https://www.nuget.org/packages/Volo.Abp.Json.Newt ## Configuring JSON options in ASP.NET Core -You can change the JSON behavior in ASP.NET Core by configuring [JsonOptions](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.jsonoptions) or -[MvcNewtonsoftJsonOptions](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.mvcnewtonsoftjsonoptions)(if you use `Newtonsoft.Json`) +Configuring JSON options in ABP does not affect ASP.NET Core's JSON settings. To modify ASP.NET Core JSON behavior, configure [JsonOptions](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.jsonoptions) or [MvcNewtonsoftJsonOptions](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.mvcnewtonsoftjsonoptions) (if you're using `Newtonsoft.Json`) separately. +Example: + +```csharp +Configure(options => +{ + //options.SerializerOptions +}); + +// If you use Newtonsoft.Json +Configure(options => +{ + //options.SerializerSettings +}); +``` diff --git a/docs/en/framework/infrastructure/object-to-object-mapping.md b/docs/en/framework/infrastructure/object-to-object-mapping.md index 8961d804c7..8c9a0453df 100644 --- a/docs/en/framework/infrastructure/object-to-object-mapping.md +++ b/docs/en/framework/infrastructure/object-to-object-mapping.md @@ -84,7 +84,7 @@ public class UserAppService : ApplicationService } ```` -You should have defined the mappings before to be able to map objects. See the AutoMapper integration section to learn how to define mappings. +You should have defined the mappings before to be able to map objects. See the AutoMapper/Mapperly integration section to learn how to define mappings. ## AutoMapper Integration @@ -217,13 +217,298 @@ public class MyProfile : Profile } ```` +## Mapperly Integration + +[Mapperly](https://github.com/riok/mapperly) is a .NET source generator for generating object mappings. [Volo.Abp.Mapperly](https://www.nuget.org/packages/Volo.Abp.Mapperly) package defines the Mapperly integration for the `IObjectMapper`. + +Once you define mappings class as below, you can use the `IObjectMapper` interface just like explained before. + +### Define Mapping Classes + +You can define a mapper class by using the `Mapper` attribute. The class and methods must be `partial` to allow the Mapperly to generate the implementation during the build process: + +````csharp +[Mapper] +public partial class UserToUserDtoMapper : MapperBase +{ + public override partial UserDto Map(User source); + public override partial void Map(User source, UserDto destination); +} +```` + +If you also want to map `UserDto` to `User`, you can inherit from the `TwoWayMapperBase` class: + +````csharp +[Mapper] +public partial class UserToUserDtoMapper : TwoWayMapperBase +{ + public override partial UserDto Map(User source); + public override partial void Map(User source, UserDto destination); + + public override partial User ReverseMap(UserDto destination); + public override partial void ReverseMap(UserDto destination, User source); +} +```` + +### Before and After Mapping Methods + +The base class provides `BeforeMap` and `AfterMap` methods that can be overridden to perform actions before and after the mapping: + +````csharp +[Mapper] +public partial class UserToUserDtoMapper : TwoWayMapperBase +{ + public override partial UserDto Map(User source); + public override partial void Map(User source, UserDto destination); + + public override partial void BeforeMap(User source) + { + //TODO: Perform actions before the mapping + } + + public override partial void AfterMap(User source, UserDto destination) + { + //TODO: Perform actions after the mapping + } + + public override partial User ReverseMap(UserDto destination); + public override partial void ReverseMap(UserDto destination, User source); + + public override partial void BeforeReverseMap(UserDto destination) + { + //TODO: Perform actions before the reverse mapping + } + + public override partial void AfterReverseMap(UserDto destination, User source) + { + //TODO: Perform actions after the reverse mapping + } +} +```` + +### Mapping the Object Extensions + +[Object extension system](../fundamentals/object-extensions.md) allows to define extra properties for existing classes. ABP provides a mapping definition extension to properly map extra properties of two objects: + +````csharp +[Mapper] +[MapExtraProperties] +public partial class UserToUserDtoMapper : MapperBase +{ + public override partial UserDto Map(User source); + public override partial void Map(User source, UserDto destination); +} +```` + +It is suggested to use the `MapExtraPropertiesAttribute` attribute if both classes are extensible objects (implement the `IHasExtraProperties` interface). See the [object extension document](../fundamentals/object-extensions.md) for more. + +### Property Setter Method + +Mapperly requires that properties of both source and destination objects have `setter` methods. Otherwise, the property will be ignored. You can use `protected set` or `private set` to control the visibility of the `setter` method, but each property must have a `setter` method. + +### Deep Cloning + +By default, Mapperly does not create deep copies of objects to improve performance. If an object can be directly assigned to the target, it will do so (e.g., if the source and target type are both `List`, the list and its entries will not be cloned). To create deep copies, set the `UseDeepCloning` property on the `MapperAttribute` to `true`. + +````csharp +[Mapper(UseDeepCloning = true)] +public partial class UserToUserDtoMapper : MapperBase +{ + public override partial UserDto Map(User source); + public override partial void Map(User source, UserDto destination); +} +```` + +### Lists and Arrays Support + +ABP Mapperly integration also supports mapping lists and arrays as explained in the [IObjectMapper Interface](#iobjectmappertsource-tdestination-interface) section. + +**Example**: + +````csharp +[Mapper] +public partial class UserToUserDtoMapper : MapperBase +{ + public override partial UserDto Map(User source); + public override partial void Map(User source, UserDto destination); +} + +var users = await _userRepository.GetListAsync(); // returns List +var dtos = ObjectMapper.Map, List>(users); // creates List +```` + +> When mapping a collection property, if the source value is null Mapperly will keep the destination value as null. This is different from AutoMapper, which will map the destination field to an empty collection. + +### Nested Mapping + +When working with nested object mapping, there's an important limitation to be aware of. If you have separate mappers for nested types like in the example below, the parent mapper (`SourceTypeToDestinationTypeMapper`) will not automatically use the nested mapper (`SourceNestedTypeToDestinationNestedTypeMapper`) to handle the mapping of nested properties. This means that configurations like the `MapperIgnoreTarget` attribute on the nested mapper will be ignored during the parent mapping operation. + +````csharp +public class SourceNestedType +{ + public string Name { get; set; } + + public string Ignored { get; set; } +} + +public class SourceType +{ + public string Name { get; set; } + + public SourceNestedType Nested { get; set; } +} + +public class DestinationNestedType +{ + public string Name { get; set; } + + public string Ignored { get; set; } +} + +public class DestinationType +{ + public string Name { get; set; } + + public DestinationNestedType Nested { get; set; } +} + +[Mapper] +public partial class SourceTypeToDestinationTypeMapper : MapperBase +{ + public override partial DestinationType Map(SourceType source); + public override partial void Map(SourceType source, DestinationType destination); +} + +[Mapper] +public partial class SourceNestedTypeToDestinationNestedTypeMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(SourceNestedType.Ignored))] + public override partial DestinationNestedType Map(SourceNestedType source); + + [MapperIgnoreTarget(nameof(SourceNestedType.Ignored))] + public override partial void Map(SourceNestedType source, DestinationNestedType destination); +} +```` + +There are several ways to solve this nested mapping issue. Choose the approach that best fits your specific requirements: + +#### Solution 1: Multi-Interface Implementation + +Implement both mapping interfaces (`IAbpMapperlyMapper` and `IAbpMapperlyMapper`) in a single mapper class. This approach consolidates all related mapping logic into one class. + +**Important:** Remember to implement `ITransientDependency` to register the mapper class with the dependency injection container. + +````csharp +[Mapper] +public partial class SourceTypeToDestinationTypeMapper : IAbpMapperlyMapper, IAbpMapperlyMapper, ITransientDependency +{ + public partial DestinationType Map(SourceType source); + public partial void Map(SourceType source, DestinationType destination); + public void BeforeMap(SourceType source) + { + } + + public void AfterMap(SourceType source, DestinationType destination) + { + } + + [MapperIgnoreTarget(nameof(SourceNestedType.Ignored))] + public partial DestinationNestedType Map(SourceNestedType source); + + [MapperIgnoreTarget(nameof(SourceNestedType.Ignored))] + public partial void Map(SourceNestedType source, DestinationNestedType destination); + + public void BeforeMap(SourceNestedType source) + { + } + + public void AfterMap(SourceNestedType source, DestinationNestedType destination) + { + } +} +```` + +#### Solution 2: Consolidate Mapping Methods + +Copy the nested mapping methods from `SourceNestedTypeToDestinationNestedTypeMapper` to the parent `SourceTypeToDestinationTypeMapper` class. This ensures all mapping logic is contained within a single mapper. + +Example: + +````csharp +[Mapper] +public partial class SourceTypeToDestinationTypeMapper : MapperBase +{ + public override partial DestinationType Map(SourceType source); + public override partial void Map(SourceType source, DestinationType destination); + + [MapperIgnoreTarget(nameof(SourceNestedType.Ignored))] + public override partial DestinationNestedType Map(SourceNestedType source); + [MapperIgnoreTarget(nameof(SourceNestedType.Ignored))] + public override partial void Map(SourceNestedType source, DestinationNestedType destination); +} + +[Mapper] +public partial class SourceNestedTypeToDestinationNestedTypeMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(SourceNestedType.Ignored))] + public override partial DestinationNestedType Map(SourceNestedType source); + + [MapperIgnoreTarget(nameof(SourceNestedType.Ignored))] + public override partial void Map(SourceNestedType source, DestinationNestedType destination); +} +```` + +#### Solution 3: Dependency Injection Approach + +Inject the nested mapper as a dependency into the parent mapper and use it in the `AfterMap` method to handle nested object mapping manually. + +Example: + +````csharp +[Mapper] +public partial class SourceTypeToDestinationTypeMapper : MapperBase +{ + private readonly SourceNestedTypeToDestinationNestedTypeMapper _sourceNestedTypeToDestinationNestedTypeMapper; + + public SourceTypeToDestinationTypeMapper(SourceNestedTypeToDestinationNestedTypeMapper sourceNestedTypeToDestinationNestedTypeMapper) + { + _sourceNestedTypeToDestinationNestedTypeMapper = sourceNestedTypeToDestinationNestedTypeMapper; + } + + public override partial DestinationType Map(SourceType source); + public override partial void Map(SourceType source, DestinationType destination); + + public override void AfterMap(SourceType source, DestinationType destination) + { + if (source.Nested != null) + { + destination.Nested = _sourceNestedTypeToDestinationNestedTypeMapper.Map(source.Nested); + } + } +} +```` + +#### Choosing the Right Solution + +Each solution has its own advantages: + +- **Solution 1** consolidates all mapping logic in one place and works well when mappings are tightly related. +- **Solution 2** is simple but can lead to code duplication if you need the nested mapper elsewhere. +- **Solution 3** maintains separation of concerns and reusability but requires manual mapping in the `AfterMap` method. + +Choose the approach that best aligns with your application's architecture and maintainability requirements. + +### More Mapperly Features + +Most of Mapperly's features such as `Ignore` can be configured through its attributes. See the [Mapperly documentation](https://mapperly.riok.app/docs/intro/) for more details. + ## Advanced Topics ### IObjectMapper Interface -Assume that you have created a **reusable module** which defines AutoMapper profiles and uses `IObjectMapper` when it needs to map objects. Your module then can be used in different applications, by nature of the [modularity](../architecture/modularity/basics.md). +Assume that you have created a **reusable module** which defines AutoMapper/Mapperly profiles and uses `IObjectMapper` when it needs to map objects. Your module then can be used in different applications, by nature of the [modularity](../architecture/modularity/basics.md). -`IObjectMapper` is an abstraction and can be replaced by the final application to use another mapping library. The problem here that your reusable module is designed to use the AutoMapper library, because it only defines mappings for it. In such a case, you will want to guarantee that your module always uses AutoMapper even if the final application uses another default object mapping library. +`IObjectMapper` is an abstraction and can be replaced by the final application to use another mapping library. The problem here that your reusable module is designed to use the AutoMapper/Mapperly library, because it only defines mappings for it. In such a case, you will want to guarantee that your module always uses AutoMapper/Mapperly even if the final application uses another default object mapping library. `IObjectMapper` is used to contextualize the object mapper, so you can use different libraries for different modules/contexts. @@ -281,6 +566,8 @@ public class UserAppService : ApplicationService While using the contextualized object mapper is same as the normal object mapper, you should register the contextualized mapper in your module's `ConfigureServices` method: +When using AutoMapper: + ````csharp [DependsOn(typeof(AbpAutoMapperModule))] public class MyModule : AbpModule @@ -298,6 +585,20 @@ public class MyModule : AbpModule } ```` +When using Mapperly: + +````csharp +[DependsOn(typeof(AbpMapperlyModule))] +public class MyModule : AbpModule +{ + public override void ConfigureServices(ServiceConfigurationContext context) + { + //Use Mapperly for MyModule + context.Services.AddMapperlyObjectMapper(); + } +} +```` + `IObjectMapper` is an essential feature for a reusable module where it can be used in multiple applications each may use a different library for object to object mapping. All pre-built ABP modules are using it. But, for the final application, you can ignore this interface and always use the default `IObjectMapper` interface. ### IObjectMapper Interface diff --git a/docs/en/framework/ui/angular/account-module.md b/docs/en/framework/ui/angular/account-module.md index eb149af61e..05b670d4d2 100644 --- a/docs/en/framework/ui/angular/account-module.md +++ b/docs/en/framework/ui/angular/account-module.md @@ -17,36 +17,35 @@ npm install @abp/ng.account > Make sure v4.3 or higher version is installed. -Open the `app.module.ts` and add `provideAccountConfig()` to the providers array as shown below: +Open the `app.config.ts` and add `provideAccountConfig()` to the providers array as shown below: ```js -// app.module.ts +// app.config.ts import { provideAccountConfig } from "@abp/ng.account/config"; -//... +// ... -@NgModule({ +export const appConfig: ApplicationConfig = { providers: [ - //... + // ... provideAccountConfig(), + // ... ], - //... -}) -export class AppModule {} +}; ``` -Open the `app-routing.module.ts` and add the `account` route to `routes` array as follows: +Open the `app.routes.ts` and add the `account` route to `APP_ROUTES` array as follows: ```js -// app-routing.module.ts -const routes: Routes = [ +// app.routes.ts +export const APP_ROUTES: Routes = [ //... { path: 'account', - loadChildren: () => import('@abp/ng.account').then(m => m.AccountModule.forLazy()), + loadChildren: () => import('@abp/ng.account').then(c => c.createRoutes()), }, //... -export class AppRoutingModule {} +]; ``` ## Account Public Module Implementation for Commercial Templates @@ -59,42 +58,42 @@ npm install @volo/abp.ng.account > Make sure v4.3 or higher version is installed. -Open the `app.module.ts` and add `AccountPublicConfigModule.forRoot()` to the imports array as shown below: +Open the `app.config.ts` and add `provideAccountPublicConfig()` to the providers array as shown below: -> Ensure that the `Account Layout Module` has been added if you are using the Lepton X theme. If you miss the step, you will get an error message that says `Account layout not found. Please check your configuration. If you are using LeptonX, please make sure you have added "AccountLayoutModule.forRoot()" to your app.module configuration.` when you try to access the account pages. Otherwise, you can skip adding the `AccountLayoutModule` step. +> Ensure that the `Account Layout Provider` has been added if you are using the Lepton X theme. If you miss the step, you will get an error message that says `Account layout not found. Please check your configuration. If you are using LeptonX, please make sure you have added "provideAccountLayout()" to your app configuration.` Otherwise, you can skip adding the `provideAccountLayout()` step. ```js -// app.module.ts +// app.config.ts -import { AccountPublicConfigModule } from "@volo/abp.ng.account/public/config"; -// if you are using or want to use Lepton X, you should add AccountLayoutModule -// import { AccountLayoutModule } from '@volosoft/abp.ng.theme.lepton-x/account' +import { provideAccountPublicConfig } from "@volo/abp.ng.account/public/config"; +// if you are using or want to use Lepton X, you should add provideAccountLayout +// import { provideAccountLayout } from '@volosoft/abp.ng.theme.lepton-x/account' //... -@NgModule({ - imports: [ - //... - AccountPublicConfigModule.forRoot(), - // AccountLayoutModule.forRoot() // Only for Lepton X +export const appConfig: ApplicationConfig = { + providers: [ + // ... + provideAccountPublicConfig(), + provideAccountLayout() // Only for Lepton X + // ... ], - //... -}) -export class AppModule {} +}; ``` -Open the `app-routing.module.ts` and add the `account` route to `routes` array as follows: +Open the `app.routes.ts` and add the `account` route to `APP_ROUTES` array as follows: ```js -// app-routing.module.ts -const routes: Routes = [ +// app.routes.ts + +export const APP_ROUTES: Routes = [ //... { path: 'account', - loadChildren: () => import('@volo/abp.ng.account/public').then(m => m.AccountPublicModule.forLazy()), + loadChildren: () => import('@volo/abp.ng.account/public').then(c => c.createRoutes()), }, //... -export class AppRoutingModule {} +]; ``` ## My Account Page @@ -108,15 +107,15 @@ When the user changes their own data on the personal settings tab in My Account, If you want to disable these warning, You should set `isPersonalSettingsChangedConfirmationActive` false ```js -// app-routing.module.ts -const routes: Routes = [ +// app.routes.ts +export const APP_ROUTES: Routes = [ //... { path: 'account', - loadChildren: () => import('@volo/abp.ng.account/public').then(m => m.AccountPublicModule.forLazy({ isPersonalSettingsChangedConfirmationActive:false })), + loadChildren: () => import('@volo/abp.ng.account/public').then(c => c.create({ isPersonalSettingsChangedConfirmationActive:false })), }, //... -export class AppRoutingModule {} +]; ``` ## Security Logs Page [COMMERCIAL] diff --git a/docs/en/framework/ui/angular/authorization.md b/docs/en/framework/ui/angular/authorization.md index dcf6e0c67d..cef5dca7c4 100644 --- a/docs/en/framework/ui/angular/authorization.md +++ b/docs/en/framework/ui/angular/authorization.md @@ -61,15 +61,18 @@ The `AuthErrorFilterService` is an abstract service that needs to be replaced wi ### Usage -#### 1.Create an auth-filter.provider +#### 1.Create an auth filter provider ```js -import { APP_INITIALIZER, inject } from '@angular/core'; +//auth-filter.provider.ts +import { inject, provideAppInitializer } from '@angular/core'; import { AuthErrorFilter, AuthErrorEvent, AuthErrorFilterService } from '@abp/ng.core'; import { eCustomersAuthFilterNames } from '../enums'; export const CUSTOMERS_AUTH_FILTER_PROVIDER = [ - { provide: APP_INITIALIZER, useFactory: configureAuthFilter, multi: true }, + provideAppInitializer(() => { + configureAuthFilter() + }), ]; type Reason = object & { error: { grant_type: string | undefined } }; @@ -100,20 +103,17 @@ function configureAuthFilter() { - `executable:` a status for the filter object. If it's false then it won't work, yet it'll stay in the list - `execute:` a function that stores the skip logic -#### 2.Add to the FeatureConfigModule +#### 2.Add to the customer configuration provider ```js -import { ModuleWithProviders, NgModule } from "@angular/core"; -import { CUSTOMERS_AUTH_FILTER_PROVIDER } from "./providers/auth-filter.provider"; - -@NgModule() -export class CustomersConfigModule { - static forRoot(): ModuleWithProviders { - return { - ngModule: CustomersConfigModule, - providers: [CUSTOMERS_AUTH_FILTER_PROVIDER], - }; - } +// customer-config.provider.ts +import { EnvironmentProviders, makeEnvironmentProviders } from "@angular/core"; +import { CUSTOMERS_AUTH_FILTER_PROVIDER } from "./auth-filter.provider"; + +export function provideCustomerConfig(): EnvironmentProviders { + return makeEnvironmentProviders([ + CUSTOMERS_AUTH_FILTER_PROVIDER + ]) } ``` diff --git a/docs/en/framework/ui/angular/basic-theme.md b/docs/en/framework/ui/angular/basic-theme.md index 6e32d4e6be..2332091cf0 100644 --- a/docs/en/framework/ui/angular/basic-theme.md +++ b/docs/en/framework/ui/angular/basic-theme.md @@ -11,10 +11,9 @@ The Basic Theme is a theme implementation for the Angular UI. It is a minimalist If you need to manually this theme, follow the steps below: * Install the [@abp/ng.theme.basic](https://www.npmjs.com/package/@abp/ng.theme.basic) NPM package to your Angular project. -* Open the `src/app/app.module.ts` file, import `ThemeBasicModule`,`provideThemeBasicConfig` (it can be imported from `@abp/ng.theme.basic` package), and add `ThemeBasicModule` to the `imports` array and provide `provideThemeBasicConfig()` to the providers array. -* Open the `src/app/shared/shared.module` file, import `ThemeBasicModule` (it can be imported from `@abp/ng.theme.basic` package), and add `ThemeBasicModule` to the `imports` and `exports` array. +* Open the `src/app/app.config.ts` file, import `provideThemeBasicConfig` (it can be imported from `@abp/ng.theme.basic` package), and provide `provideThemeBasicConfig()` to the providers array. -The `ThemeBasicModule` is registered own layouts (`ApplicationLayoutComponent`, `AccountLayoutComponent`, `EmptyLayoutComponent`) to a service which is exposed by `@abp/ng.core` package on application initialization. +The `BASIC_THEME_STYLES_PROVIDERS` has registered three layouts being `ApplicationLayoutComponent`, `AccountLayoutComponent`, and `EmptyLayoutComponent`. These are provided inside `provideThemeBasicConfig()` function that is exposed by `@abp/ng.theme.basic` package on application initialization. ## Application Layout diff --git a/docs/en/framework/ui/angular/caps-lock-directive.md b/docs/en/framework/ui/angular/caps-lock-directive.md index f6a9c83931..179027022d 100644 --- a/docs/en/framework/ui/angular/caps-lock-directive.md +++ b/docs/en/framework/ui/angular/caps-lock-directive.md @@ -5,66 +5,27 @@ In password inputs, You may want to show if Caps Lock is on. To make this even e ## Getting Started -`TrackCapsLockDirective` is standalone. In order to use the `TrackCapsLockDirective` in an HTML template, import it to related module or your standalone component: - -**Importing to NgModule** -```ts -import { TrackCapsLockDirective } from '@abp/ng.core'; - -@NgModule({ - //... - declarations: [ - ..., - TestComponent - ], - imports: [ - ..., - TrackCapsLockDirective - ], -}) -export class MyFeatureModule {} -``` - -## Usage - -The `TrackCapsLockDirective` is very easy to use. The directive's selector is **`abpCapsLock`**. By adding the `abpCapsLock` event to an element, you can track the status of Caps Lock. You can use this to warn user. +`TrackCapsLockDirective` is standalone. In order to use the `TrackCapsLockDirective` in an HTML template, import it to related component. The selector of the directive is **`abpCapsLock`**. By adding the `abpCapsLock` event to an element, you can track the status of Caps Lock. You can use this to warn user. See an example usage: -**NgModule Component usage** -```ts -@Component({ - selector: 'test-component', - template: ` -
- - - icon -
- ` -}) -export class TestComponent{ - capsLock = false; -} -``` - -**Standalone Component usage** ```ts import { TrackCapsLockDirective } from '@abp/ng.core' @Component({ - selector: 'standalone-component', - standalone: true, + selector: 'sample-component', template: `
- icon + @if (capslock) { + icon + }
`, imports: [TrackCapsLockDirective] }) -export class StandaloneComponent{ +export class SampleComponent{ capsLock = false; } ``` diff --git a/docs/en/framework/ui/angular/card-component.md b/docs/en/framework/ui/angular/card-component.md index ca056853ff..e8f1ba4d8e 100644 --- a/docs/en/framework/ui/angular/card-component.md +++ b/docs/en/framework/ui/angular/card-component.md @@ -23,27 +23,7 @@ In addition to these components, the Card component provides directives like `Ca # Usage -ABP Card Component is a part of the `ThemeSharedModule` module. If you've imported that module into your module, you don't need to import it again. If not, first import it as shown below: - -```ts -// my-feature.module.ts - -import { ThemeSharedModule } from '@abp/ng.theme.shared'; -import { CardDemoComponent } from './card-demo.component'; - -@NgModule({ - imports: [ - ThemeSharedModule , - // ... - ], - declarations: [CardDemoComponent], - // ... -}) -export class MyFeatureModule {} - -``` - -Then, the `abp-card` component can be used. See the examples below: +ABP Card Component is a part of the `theme-shared` package. Once you import the necessary components, you can use them. See the examples below: ## CardBody @@ -51,16 +31,18 @@ Then, the `abp-card` component can be used. See the examples below: // card-demo.component.ts import { Component } from '@angular/core'; +import { CardComponent, CardBodyComponent } from '@abp/ng.theme.shared'; @Component({ selector: 'app-card-demo', + imports: [CardComponent, CardBodyComponent], template: ` This is some text within a card body `, }) -export class CardDemoComponent { } +export class CardDemoComponent {} ``` See the card body result below: @@ -72,22 +54,36 @@ See the card body result below: //card-demo.component.ts import { Component } from '@angular/core'; +import { + CardComponent, + CardBodyComponent, + CardTitleDirective, + CardSubtitleDirective +} from '@abp/ng.theme.shared'; @Component({ selector: 'app-card-demo', + imports: [ + CardComponent, + CardBodyComponent, + CardTitleDirective, + CardSubtitleDirective + ], template: `
Card Title
Card subtitle
-

Some quick example text to build on the card title and make up the bulk of the card's content.

- Card link - Another link +

+ Some quick example text to build on the card title and make up the bulk of the card's content. +

+ Card link + Another link
`, }) -export class CardDemoComponent { } +export class CardDemoComponent {} ``` See the card title, text and link result below: @@ -99,19 +95,23 @@ See the card title, text and link result below: //card-demo.component.ts import { Component } from '@angular/core'; +import { CardComponent, CardBodyComponent, CardImgTopDirective } from '@abp/ng.theme.shared'; @Component({ selector: 'app-card-demo', + imports: [CardComponent, CardBodyComponent, CardImgTopDirective], template: ` ... -

Some quick example text to build on the card title and make up the bulk of the card's content.

+

+ Some quick example text to build on the card title and make up the bulk of the card's content. +

`, }) -export class CardDemoComponent { } +export class CardDemoComponent {} ``` See the card image result below: @@ -123,9 +123,11 @@ See the card image result below: //card-demo.component.ts import { Component } from '@angular/core'; +import { CardComponent } from '@abp/ng.theme.shared'; @Component({ selector: 'app-card-demo', + imports: [CardComponent], template: `
    @@ -136,7 +138,7 @@ import { Component } from '@angular/core'; `, }) -export class CardDemoComponent { } +export class CardDemoComponent {} ``` See the group list result below: @@ -148,15 +150,29 @@ See the group list result below: //card-demo.component.ts import { Component } from '@angular/core'; +import { + CardComponent, + CardBodyComponent, + CardImgTopDirective, + CardTitleDirective +} from '@abp/ng.theme.shared'; @Component({ selector: 'app-card-demo', + imports: [ + CardComponent, + CardBodyComponent, + CardImgTopDirective, + CardTitleDirective + ], template: ` ...
    Card title
    -

    Some quick example text to build on the card title and make up the bulk of the card's content.

    +

    + Some quick example text to build on the card title and make up the bulk of the card's content. +

    • An item
    • @@ -170,7 +186,7 @@ import { Component } from '@angular/core'; `, }) -export class CardDemoComponent { } +export class CardDemoComponent {} ``` See kitchen sink result below: @@ -182,15 +198,31 @@ See kitchen sink result below: //card-demo.component.ts import { Component } from '@angular/core'; +import { + CardComponent, + CardHeaderComponent, + CardBodyComponent, + CardTitleDirective, + CardFooterComponent +} from '@abp/ng.theme.shared'; @Component({ selector: 'app-card-demo', + imports: [ + CardComponent, + CardHeaderComponent, + CardBodyComponent, + CardTitleDirective, + CardFooterComponent + ], template: ` Featured
      Special title treatment
      -

      With supporting text below as a natural lead-in to additional content.

      +

      + With supporting text below as a natural lead-in to additional content. +

      Go somewhere
      @@ -199,7 +231,7 @@ import { Component } from '@angular/core';
      `, }) -export class CardDemoComponent { } +export class CardDemoComponent {} ``` See the header and footer result below: diff --git a/docs/en/framework/ui/angular/chart-component.md b/docs/en/framework/ui/angular/chart-component.md index e2bf2c2e1e..44007eef5b 100644 --- a/docs/en/framework/ui/angular/chart-component.md +++ b/docs/en/framework/ui/angular/chart-component.md @@ -6,34 +6,16 @@ ABP Chart component exposed by `@abp/ng.components/chart.js` is based on [`chart ## How to Use -First of all, need to import the `ChartModule` to your feature module as follows: - -```ts -// your-feature.module.ts - -import { ChartModule } from "@abp/ng.components/chart.js"; -import { ChartDemoComponent } from "./chart-demo.component"; - -@NgModule({ - imports: [ - ChartModule, - // ... - ], - declarations: [ChartDemoComponent], - // ... -}) -export class YourFeatureModule {} -``` - -Then, `abp-chart` component can be used. See an example: +First of all, need to import the `ChartComponent` to your component. Then, `abp-chart` component can be used. See an example: ```ts // chart-demo.component.ts - import { Component } from "@angular/core"; +import { ChartComponent } from "@abp/ng.components/chart.js"; @Component({ selector: "app-chart-demo", + imports: [ChartComponent], template: ` `, }) export class ChartDemoComponent { @@ -62,9 +44,11 @@ See the result: ```ts import { Component } from "@angular/core"; +import { ChartComponent } from "@abp/ng.components/chart.js"; @Component({ selector: "app-chart-demo", + imports: [ChartComponent], template: ` + /> `, }) export class ChartDemoComponent { @@ -119,16 +103,18 @@ Result: ```ts import { Component } from "@angular/core"; +import { ChartComponent } from "@abp/ng.components/chart.js"; @Component({ selector: "app-chart-demo", + imports: [ChartComponent] template: ` + /> `, }) export class ChartDemoComponent { @@ -158,16 +144,18 @@ Result: ```ts import { Component } from "@angular/core"; +import { ChartComponent } from "@abp/ng.components/chart.js"; @Component({ selector: "app-chart-demo", + imports: [ChartComponent] template: ` + />
``` diff --git a/docs/en/framework/ui/angular/localization.md b/docs/en/framework/ui/angular/localization.md index 69d976be87..5d14eb0333 100644 --- a/docs/en/framework/ui/angular/localization.md +++ b/docs/en/framework/ui/angular/localization.md @@ -9,7 +9,7 @@ The Localization key format consists of 2 sections which are **Resource Name** a ```js const environment = { - //... + // ... localization: { defaultResourceName: "MyProjectName", }, @@ -49,7 +49,7 @@ Localization data is stored in key-value pairs: ```js { - //... + // ... AbpAccount: { // AbpAccount is the resource name Key: "Value", PagerInfo: "Showing {0} to {1} of {2} entries" @@ -122,12 +122,12 @@ See an example: ```ts import { provideAbpCore, withOptions } from '@abp/ng.core'; -@NgModule({ +export const appConfig: ApplicationConfig = { providers: [ // ... provideAbpCore( withOptions({ - ..., + // ..., localizations: [ { culture: 'en', @@ -156,22 +156,18 @@ import { provideAbpCore, withOptions } from '@abp/ng.core'; ], }), ), - ... ], -}) -export class AppModule {} +}; ``` -...or, you can determine the localizations in a feature module: +...or, you can determine the localizations in a feature provider configuration: ```ts -// your feature module +// your feature configuration -@NgModule({ - imports: [ - //...other imports - CoreModule.forChild({ - localizations: [ +export function provideFeatureConfiguration(): EnvironmentProviders{ + return provideAbpCoreChild({ + localizations: [ { culture: 'en', resources: [ @@ -197,9 +193,8 @@ export class AppModule {} ], }, ], - }), - ] -}) + }) +} ``` The localizations above can be used like this: @@ -268,8 +263,8 @@ import { Component } from "@angular/core"; @Component({ selector: "app-root", template: ` - - + + `, }) export class AppComponent {} @@ -277,7 +272,7 @@ export class AppComponent {} ## Registering a New Locale -Since ABP has more than one language, Angular locale files loads lazily using [Webpack's import function](https://webpack.js.org/api/module-methods/#import-1) to avoid increasing the bundle size and register to Angular core using the [`registerLocaleData`](https://angular.io/api/common/registerLocaleData) function. The chunks to be included in the bundle are specified by the [Webpack's magic comments](https://webpack.js.org/api/module-methods/#magic-comments) as hard-coded. Therefore a `registerLocale` function that returns Webpack `import` function must be passed to `CoreModule`. +Since ABP has more than one language, Angular locale files loads lazily using [Webpack's import function](https://webpack.js.org/api/module-methods/#import-1) to avoid increasing the bundle size and register to Angular core using the [`registerLocaleData`](https://angular.io/api/common/registerLocaleData) function. The chunks to be included in the bundle are specified by the [Webpack's magic comments](https://webpack.js.org/api/module-methods/#magic-comments) as hard-coded. Therefore a `registerLocale` function that returns Webpack `import` function must be passed to `provideAbpCore(withOptions({...}))`. ### registerLocaleFn @@ -287,11 +282,12 @@ Since ABP has more than one language, Angular locale files loads lazily using [W import { provideAbpCore, withOptions } from '@abp/ng.core'; import { registerLocale } from '@abp/ng.core/locale'; -@NgModule({ +export const appConfig: ApplicationConfig = { providers: [ + // ... provideAbpCore( withOptions({ - ..., + // ..., registerLocaleFn: registerLocale( // you can pass the cultureNameLocaleFileMap and errorHandlerFn as optionally { @@ -303,10 +299,9 @@ import { registerLocale } from '@abp/ng.core/locale'; ), }), ), - ... + // ... ], -}) -export class AppModule {} +}; ``` ### Mapping of Culture Name to Angular Locale File Name @@ -318,19 +313,18 @@ Some of the culture names defined in .NET do not match Angular locales. In such If you see an error like this, you should pass the `cultureNameLocaleFileMap` property like below to the `registerLocale` function. ```js -// app.module.ts +// app.config.ts import { registerLocale } from '@abp/ng.core/locale'; // if you have commercial license and the language management module, add the below import // import { registerLocale } from '@volo/abp.ng.language-management/locale'; - -@NgModule({ +export const appConfig: ApplicationConfig = { providers: [ // ... provideAbpCore( withOptions({ - ..., + // ..., registerLocaleFn: registerLocale( { cultureNameLocaleFileMap: { @@ -341,18 +335,18 @@ import { registerLocale } from '@abp/ng.core/locale'; ) }), ), - ] -}) + ], +}; ``` See [all locale files in Angular](https://github.com/angular/angular/tree/master/packages/common/locales). ### Adding a New Culture -Add the below code to the `app.module.ts` by replacing `your-locale` placeholder with a correct locale name. +Add the below code to the `app.config.ts` by replacing `your-locale` placeholder with a correct locale name. ```js -//app.module.ts +//app.config.ts import { storeLocaleData } from "@abp/ng.core/locale"; import( @@ -362,7 +356,7 @@ import( ).then((m) => storeLocaleData(m.default, "your-locale")); ``` -...or a custom `registerLocale` function can be passed to the `CoreModule`: +...or a custom `registerLocale` function can be passed to the abp core provider configuration options: ```js // register-locale.ts @@ -377,22 +371,22 @@ export function registerLocale(locale: string) { ) } -// app.module.ts +// app.config.ts import { registerLocale } from './register-locale'; -@NgModule({ +export const appConfig: ApplicationConfig = { providers: [ - // ... + // ... provideAbpCore( withOptions({ - ..., + // ..., registerLocaleFn: registerLocale, }), ), //... - ] -}) + ], +}; ``` After this custom `registerLocale` function, since the en and fr added to the `webpackInclude`, only en and fr locale files will be created as chunks: diff --git a/docs/en/framework/ui/angular/lookup-components.md b/docs/en/framework/ui/angular/lookup-components.md index ce1e32eefd..e63d2b3700 100644 --- a/docs/en/framework/ui/angular/lookup-components.md +++ b/docs/en/framework/ui/angular/lookup-components.md @@ -4,22 +4,7 @@ The Angular UI of ABP Commercial introduces some components with `abp-lookup-... ## Setup -The components are in the _@volo/abp.commercial.ng.ui_ package, which is included in the ABP templates. So, as long as your project is a product of these templates and unless you delete the package, you have access to the lookup components. Here is how you import them in your Angular module: - -```javascript -import { CommercialUiModule } from '@volo/abp.commercial.ng.ui'; - -@NgModule({ - imports: [ - // other imports - CommercialUiModule, - ], - // rest of the module metadata -}) -export class YourModule {} -``` - -Now you can use the lookup components in your components declared by this module. +The components are in the _@volo/abp.commercial.ng.ui_ package, which is included in the ABP templates. So, as long as your project is a product of these templates and unless you delete the package, you have access to the lookup components. You can import these in your standalone components in order to be able to use them. ## Lookup HTTP Requests @@ -48,7 +33,7 @@ Typeahead is a good choice when you have an unknown number of records for the re ![ABP Angular UI Typeahead Lookup](./images/angular-lookup-typeahead.gif) -Here is how it is used in the template. +Do not forget to import `LookupTypeaheadComponent` in your component, and here is how it is used in the template. ```html +/> ``` The available properties are as follows: @@ -77,7 +62,7 @@ Select is a good choice when you have a low (and usually fixed) number of record ![ABP Angular UI Select Lookup](./images/angular-lookup-select.gif) -Here is how it is used in the template. +Do not forget to import `LookupSelectComponent` in your component, and here is how it is used in the template. ```html +/> ``` The available properties are as follows: diff --git a/docs/en/framework/ui/angular/manage-profile-page-tabs.md b/docs/en/framework/ui/angular/manage-profile-page-tabs.md index 232575a419..fbb12c852e 100644 --- a/docs/en/framework/ui/angular/manage-profile-page-tabs.md +++ b/docs/en/framework/ui/angular/manage-profile-page-tabs.md @@ -9,7 +9,7 @@ See the example below, covers all features: ```ts // manage-profile-tabs.provider.ts -import { APP_INITIALIZER, Component } from "@angular/core"; +import { provideAppInitializer, Component } from "@angular/core"; import { TwoFactorTabComponent } from "@volo/abp.ng.account/public"; import { eAccountManageProfileTabNames, @@ -18,48 +18,47 @@ import { import { MyAwesomeTabComponent } from "./my-awesome-tab/my-awesome-tab.component"; @Component({ - standalone: true, selector: "abp-my-awesome-tab", template: `My Awesome Tab`, }) class MyAwesomeTabComponent {} export const MANAGE_PROFILE_TAB_PROVIDER = { - provide: APP_INITIALIZER, - useFactory: configureManageProfileTabs, - deps: [ManageProfileTabsService], - multi: true, + provideAppInitializer(()=>{ + configureManageProfileTabs(); + }), }; -export function configureManageProfileTabs(tabs: ManageProfileTabsService) { - return () => { - tabs.add([ - { - name: "::MyAwesomeTab", // supports localization keys - order: 5, - component: MyAwesomeTabComponent, - }, - ]); - - tabs.patch(eAccountManageProfileTabNames.TwoFactor, { - name: "Two factor authentication", - component: TwoFactorTabComponent, - }); - - tabs.remove([eAccountManageProfileTabNames.ProfilePicture]); - }; +export function configureManageProfileTabs() { + tabs = inject(ManageProfileTabsService); + tabs.add([ + { + name: "::MyAwesomeTab", // supports localization keys + order: 5, + component: MyAwesomeTabComponent, + }, + ]); + + tabs.patch(eAccountManageProfileTabNames.TwoFactor, { + name: "Two factor authentication", + component: TwoFactorTabComponent, + }); + + tabs.remove([eAccountManageProfileTabNames.ProfilePicture]); } ``` ```ts -//app.module.ts +//app.config.ts import { MANAGE_PROFILE_TAB_PROVIDER } from "./manage-profile-tabs.provider"; -@NgModule({ - providers: [MANAGE_PROFILE_TAB_PROVIDER], -}) -export class AppModule {} +export const appConfig: ApplicationConfig = { + providers: [ + // ... + MANAGE_PROFILE_TAB_PROVIDER + ], +}; ``` What we have done above; @@ -70,7 +69,7 @@ What we have done above; - Renamed the "Two factor" tab label. - Removed the "Profile picture" tab. - Determined the `MANAGE_PROFILE_TAB_PROVIDER` to be able to run the `configureManageProfileTabs` function on initialization. -- Registered the `MANAGE_PROFILE_TAB_PROVIDER` to the `AppModule` providers. +- Registered the `MANAGE_PROFILE_TAB_PROVIDER` to the `appConfig` providers. See the result: diff --git a/docs/en/framework/ui/angular/modal.md b/docs/en/framework/ui/angular/modal.md index 8c8efc2caf..7ff20d885a 100644 --- a/docs/en/framework/ui/angular/modal.md +++ b/docs/en/framework/ui/angular/modal.md @@ -15,27 +15,33 @@ The `abp-modal` provides some additional benefits: ## Getting Started -In order to use the `abp-modal` in an HTML template, the **`ThemeSharedModule`** should be imported into your module like this: +In order to use the `abp-modal` in an HTML template, the **`ModalComponent`** should be imported into your component like this: ```js +// sample.component.ts // ... -import { ThemeSharedModule } from '@abp/ng.theme.shared'; +import { ModalComponent, ModalCloseDirective } from '@abp/ng.theme.shared'; -@NgModule({ +@Component({ //... - imports: [..., ThemeSharedModule], + , + imports: [ + // ..., + ModalComponent, + ModalCloseDirective // if you use `abpClose` directive in the html template + ], }) -export class MyFeatureModule {} +export class SampleComponent { + isModalOpen = false; +} ``` -## Usage - -You can add the `abp-modal` to your component very quickly. See an example: - ```html - + @@ -43,24 +49,17 @@ You can add the `abp-modal` to your component very quickly. See an example: -

Modal content

+

Modal content

- +
``` -```js -// sample.component.ts - -@Component(/* component metadata */) -export class SampleComponent { - isModalOpen = false -} -``` - ![Example modal result](./images/modal-result-1.jpg) @@ -139,7 +138,6 @@ import { inject } from '@angular/core'; export class BookComponent { private fb = inject(FormBuilder); private service = inject(BookService); - form = this.fb.group({ author: [null, [Validators.required]], name: [null, [Validators.required]], @@ -152,7 +150,9 @@ export class BookComponent { isModalOpen: boolean; save() { - if (this.form.invalid) return; + if (this.form.invalid) { + return; + } this.inProgress = true; @@ -177,7 +177,41 @@ The modal with form looks like this: @Input() visible: boolean ``` -**`visible`** is a boolean input that determines whether the modal is open. It is also can be used two-way binding. +**`visible`** is a boolean input that controls whether the modal is open. + +Important details: + +- Default value: `false` (the modal is closed initially). +- Required binding: You must bind `visible` to a component property. If you omit it completely, the modal will never appear because it is only instantiated when `visible` becomes `true`. +- Two-way binding recommended: The component emits `visibleChange` when it needs to close (e.g., user presses the close button or backdrop). For this reason, using a constant like `[visible]="true"` or `visible="true"` is not supported—Angular cannot update a literal, so the modal cannot properly close and this may lead to an error. Always bind to a variable. +- Correct patterns: + - Preferred shorthand: `[(visible)]="isModalOpen"` + - Or explicit form: `[visible]="isModalOpen" (visibleChange)="isModalOpen = $event"` + +Example (already shown above): + +```html + + + +``` + +Programmatic control: + +```ts +// In your component class +isModalOpen = false; + +open() { this.isModalOpen = true; } +close() { this.isModalOpen = false; } +``` + +Avoid (incorrect): + +```html + + +``` #### busy @@ -258,19 +292,16 @@ export class NgbdModalOptions { **`suppressUnsavedChangesWarning`** is a boolean input that determines whether the confirmation popup triggering active or not. It can also be set globally as shown below: ```ts -//app.module.ts - -// app.module.ts +// app.config.ts import { SUPPRESS_UNSAVED_CHANGES_WARNING } from '@abp/ng.theme.shared'; -// ... - -@NgModule({ - // ... - providers: [{provide: SUPPRESS_UNSAVED_CHANGES_WARNING, useValue: true}] -}) -export class AppModule {} +export const appConfig: ApplicationConfig = { + providers: [ + // ... + { provide: SUPPRESS_UNSAVED_CHANGES_WARNING, useValue: true } + ], +}; ``` Note: The `suppressUnsavedChangesWarning` input of `abp-modal` value overrides the `SUPPRESS_UNSAVED_CHANGES_WARNING` injection token value. diff --git a/docs/en/framework/ui/angular/modifying-the-menu.md b/docs/en/framework/ui/angular/modifying-the-menu.md index d8e56a3bd6..087f022dc3 100644 --- a/docs/en/framework/ui/angular/modifying-the-menu.md +++ b/docs/en/framework/ui/angular/modifying-the-menu.md @@ -5,7 +5,7 @@ The menu is inside the `ApplicationLayoutComponent` in the @abp/ng.theme.basic p ## How to Add a Logo -The `logoUrl` property in the environment variables is the url of the logo. +The `logoUrl` property in the environment variables is the url of the logo. You can add your logo to `src/assets` folder and set the `logoUrl` as shown below: @@ -20,6 +20,25 @@ export const environment = { }; ``` +Then provide the logo at application startup using the Theme Shared provider. This makes the logo (and application name) available to all ABP/Theme components (including LeptonX brand component) via injection tokens. + +```ts +// app.config.ts +import { provideLogo, withEnvironmentOptions } from '@abp/ng.theme.shared'; +import { environment } from './environments/environment'; + +export const appConfig: ApplicationConfig = { + providers: [ + // ... other providers + provideLogo(withEnvironmentOptions(environment)), + ], +}; +``` + +Notes +- This approach works across themes. If you are using LeptonX, the brand logo component reads these values automatically; you don't need any theme-specific code. +- You can still override visuals with CSS variables if desired. See the LeptonX section for CSS overrides. + ## How to Add a Navigation Element ### Via `RoutesService` @@ -61,32 +80,33 @@ An alternative and probably cleaner way is to use a route provider. First create ```js // route.provider.ts import { RoutesService, eLayoutType } from '@abp/ng.core'; -import { APP_INITIALIZER } from '@angular/core'; +import { provideAppInitializer } from '@angular/core'; export const APP_ROUTE_PROVIDER = [ - { provide: APP_INITIALIZER, useFactory: configureRoutes, deps: [RoutesService], multi: true }, + provideAppInitializer(() => { + configureRoutes(); + }), ]; -function configureRoutes(routes: RoutesService) { - return () => { - routes.add([ - { - path: '/your-path', - name: 'Your navigation', - requiredPolicy: 'permission key here', - order: 101, - iconClass: 'fas fa-question-circle', - layout: eLayoutType.application, - }, - { - path: '/your-path/child', - name: 'Your child navigation', - parentName: 'Your navigation', - requiredPolicy: 'permission key here', - order: 1, - }, - ]); - }; +function configureRoutes() { + const routesService = inject(RoutesService); + routes.add([ + { + path: '/your-path', + name: 'Your navigation', + requiredPolicy: 'permission key here', + order: 101, + iconClass: 'fas fa-question-circle', + layout: eLayoutType.application, + }, + { + path: '/your-path/child', + name: 'Your child navigation', + parentName: 'Your navigation', + requiredPolicy: 'permission key here', + order: 1, + }, + ]); } ``` @@ -97,22 +117,21 @@ We can also define a group for navigation elements. It's an optional property // route.provider.ts import { RoutesService } from '@abp/ng.core'; -function configureRoutes(routes: RoutesService) { - return () => { - routes.add([ - { - //etc.. - group: 'ModuleName::GroupName' - }, - { - path: '/your-path/child', - name: 'Your child navigation', - parentName: 'Your navigation', - requiredPolicy: 'permission key here', - order: 1, - }, - ]); - }; +function configureRoutes() { + const routesService = inject(RoutesService); + routes.add([ + { + //etc.. + group: 'ModuleName::GroupName' + }, + { + path: '/your-path/child', + name: 'Your child navigation', + parentName: 'Your navigation', + requiredPolicy: 'permission key here', + order: 1, + }, + ]); } ``` @@ -134,25 +153,23 @@ export class AppComponent { } ``` -...and then in app.module.ts... +...and then in app.config.ts... - The `groupedVisible` method will return the `Others` group for ungrouped items, the default key is `AbpUi::OthersGroup`, we can change this `key` via the `OTHERS_GROUP` injection token ```js -import { NgModule } from '@angular/core'; import { OTHERS_GROUP } from '@abp/ng.core'; import { APP_ROUTE_PROVIDER } from './route.provider'; -@NgModule({ +export const appConfig: ApplicationConfig = { providers: [ + // ... APP_ROUTE_PROVIDER, { provide: OTHERS_GROUP, useValue: 'ModuleName::MyOthersGroupKey', }, ], - // imports, declarations, and bootstrap -}) -export class AppModule {} +}; ``` ### Singularize Route Item @@ -187,9 +204,9 @@ Here is what every property works as: - `invisible` makes the item invisible in the menu. (default: `false`) - `group` is an optional property that is used to group together related routes in an application. (type: `string`, default: `AbpUi::OthersGroup`) -### Via `routes` Property in `AppRoutingModule` +### Via `routes` Property in `APP_ROUTES` -You can define your routes by adding `routes` as a child property to `data` property of a route configuration in the `app-routing.module`. The `@abp/ng.core` package organizes your routes and stores them in the `RoutesService`. +You can define your routes by adding `routes` as a child property to `data` property of a route configuration in the `app.routes.ts`. The `@abp/ng.core` package organizes your routes and stores them in the `RoutesService`. You can add the `routes` property like below: @@ -276,7 +293,12 @@ this.routes.remove(['Your navigation']); // or this.routes.removeByParam({ name: 'Your navigation' }); ``` +**Method Parameters:** +- `remove(routeNames: string[])`: Takes an array of route names to remove. +- `removeByParam(routeProperty: Partial)`: Takes any route property (name, path, parentName, etc.) to match and remove routes. +
+**Results of the operations above:** - Moved the _Home_ navigation under the _Administration_ dropdown based on given `parentName`. - Added an icon to _Home_. - Specified the order and made _Home_ the first item in list. diff --git a/docs/en/framework/ui/angular/multi-tenancy.md b/docs/en/framework/ui/angular/multi-tenancy.md index bfb929cc68..c85ac976a3 100644 --- a/docs/en/framework/ui/angular/multi-tenancy.md +++ b/docs/en/framework/ui/angular/multi-tenancy.md @@ -133,12 +133,9 @@ Example: ```ts import { TENANT_NOT_FOUND_BY_NAME } from '@abp/ng.core'; -@NgModule({ - imports: [ - // removed for clarity - ], +export const appConfig: ApplicationConfig = { providers: [ - // removed for clarity + // removed for clarity { provide: TENANT_NOT_FOUND_BY_NAME, useFactory: function () { @@ -148,11 +145,7 @@ import { TENANT_NOT_FOUND_BY_NAME } from '@abp/ng.core'; }, }, ], - declarations: [AppComponent], - bootstrap: [AppComponent], -}) -export class AppModule {} - +}; ``` ## See Also diff --git a/docs/en/framework/ui/angular/oauth-module.md b/docs/en/framework/ui/angular/oauth-module.md index 0e67d44d07..8ec30ae4b4 100644 --- a/docs/en/framework/ui/angular/oauth-module.md +++ b/docs/en/framework/ui/angular/oauth-module.md @@ -2,8 +2,7 @@ The authentication functionality has been moved from @abp/ng.core to @abp/ng.ouath since v7.0. -If your app is version 8.3 or higher, you should include "provideAbpOAuth()" in your app.module.ts as an providers after "provideAbpCore() -". +If your app is version 8.3 or higher, you should include "provideAbpOAuth()" after "provideAbpCore()" in the `appConfig` array of your `app.config.ts`. Those abstractions can be found in the @abp/ng-core packages. diff --git a/docs/en/framework/ui/angular/page-component.md b/docs/en/framework/ui/angular/page-component.md index 3c56d3e658..86fd1b8aea 100644 --- a/docs/en/framework/ui/angular/page-component.md +++ b/docs/en/framework/ui/angular/page-component.md @@ -14,10 +14,10 @@ Let's look at the following example without `abp-page` component.

{%{{{ '::Dashboard' | abpLocalization }}}%}

- +
@@ -42,22 +42,19 @@ export enum PageParts { ## Usage -Firstly, you need to import `PageModule` from `@abp/ng.components/page` as follows: +Firstly, you need to import Page components from `@abp/ng.components/page` based on your usage. Here is an example: -`dashboard.module.ts` +`dashboard.component.ts` ```javascript -import { PageModule } from '@abp/ng.components/page'; -import { DashboardComponent } from './dashboard.component'; -@NgModule({ - declarations: [DashboardComponent], - imports: [PageModule] +@Component({ + imports: [ PageComponent, ... ] }) -export class DashboardModule {} +export class  DashboardComponent {} ``` -And change the template of `dashboard.component.ts` to the following: +And change the template of `dashboard.component.html` to the following: ```html @@ -75,35 +72,55 @@ And change the template of `dashboard.component.ts` to the following: ## Overriding template -If you need to replace the template of any part, you can use the following sub-components. +If you need to replace the template of any part, you can use the following sub-components. You will need to import these components and modify the html template accordingly. -```html - - -

Custom Title

-
- - - - - - - - -
+```javascript +import { + PageComponent, + PageTitleContainerComponent, + PageBreadcrumbContainerComponent, + PageToolbarContainerComponent +} from '@abp/ng.components/page'; + +@Component({ + selector: 'app-sample-component', + template: ` + + +

Custom Title

+
+ + + + + + + + +
+ ` + imports: [ + PageComponent, + PageTitleContainerComponent, + PageBreadcrumbContainerComponent, + MyBreadcrumbComponent, + PageToolbarContainerComponent + ] +}) +export class SampleCompnent {} ``` You do not have to provide them all. You can just use which one you need to replace. These components have priority over the inputs declared above. If you use these components, you can omit the inputs. ## PagePartDirective -`PageModule` provides a structural directive that is used internally within `PageComponent` and can also be used externally. +`Components` package provides a structural directive that is used internally within `PageComponent` and can also be used externally. `PageComponent` employs this directive internally as follows: ```html
- +
``` @@ -111,7 +128,7 @@ It also can take a context input as follows: ```html
- +
``` @@ -194,17 +211,14 @@ export class MyPageRenderStrategy implements PageRenderStrategy { }) export class DashboardComponent {} -@NgModule({ - imports: [PageModule], - declarations: [DashboardComponent], +export const appConfig: ApplicationConfig = { providers: [ { provide: PAGE_RENDER_STRATEGY, useClass: MyPageRenderStrategy, } - ] -}) -export class DashboardModule {} + ], +}; ``` ## See Also diff --git a/docs/en/framework/ui/angular/page-toolbar-extensions.md b/docs/en/framework/ui/angular/page-toolbar-extensions.md index 74f79178dd..5a741b3d2f 100644 --- a/docs/en/framework/ui/angular/page-toolbar-extensions.md +++ b/docs/en/framework/ui/angular/page-toolbar-extensions.md @@ -14,7 +14,7 @@ In this example, we will add a "Click Me!" action and log `userName` of all user ### Step 1. Create Toolbar Action Contributors -The following code prepares a constant named `identityToolbarActionContributors`, ready to be imported and used in your root module: +The following code prepares a constant named `identityToolbarActionContributors`, ready to be imported and used in your root application configuration: ```js // src/app/toolbar-action-contributors.ts @@ -53,22 +53,22 @@ The list of actions, conveniently named as `actionList`, is a **doubly linked li ### Step 2. Import and Use Toolbar Action Contributors -Import `identityToolbarActionContributors` in your routing module and pass it to the static `forLazy` method of `IdentityModule` as seen below: +Import `identityToolbarActionContributors` in your routing configuration and pass it to the static `createRoutes` method for `identity` route as seen below: ```js -// src/app/app-routing.module.ts +// src/app/app.routes.ts // other imports import { identityToolbarActionContributors } from './toolbar-action-contributors'; -const routes: Routes = [ +export const APP_ROUTES: Routes = [ // other routes { path: 'identity', loadChildren: () => - import('@abp/ng.identity').then(m => - m.IdentityModule.forLazy({ + import('@abp/ng.identity').then(c => + c.createRoutes({ toolbarActionContributors: identityToolbarActionContributors, }) ), @@ -78,7 +78,7 @@ const routes: Routes = [ ]; ``` -That is it, `logUserNames` toolbar action will be added as the first action on the page toolbar in the users page (`UsersComponent`) of the `IdentityModule`. +That is it, `logUserNames` toolbar action will be added as the first action on the page toolbar in the users page (`UsersComponent`) of the `identity` package. ## How to Add a Custom Component to Page Toolbar @@ -93,9 +93,9 @@ We need to have a component before we can pass it to the toolbar action contribu ```js // src/app/click-me-button.component.ts +import { Component, Inject } from '@angular/core'; import { IdentityUserDto } from '@abp/ng.identity/proxy'; import { ActionData, EXTENSIONS_ACTION_DATA } from '@abp/ng.components/extensible'; -import { Component, Inject } from '@angular/core'; @Component({ selector: 'app-click-me-button', @@ -120,7 +120,7 @@ Here, `EXTENSIONS_ACTION_DATA` token provides us the context from the page toolb ### Step 2. Create Toolbar Action Contributors -The following code prepares a constant named `identityToolbarActionContributors`, ready to be imported and used in your root module. When `ToolbarComponent` is used instead of `ToolbarAction`, we can pass a component in: +The following code prepares a constant named `identityToolbarActionContributors`, ready to be imported and used in your root application configuration. When `ToolbarComponent` is used instead of `ToolbarAction`, we can pass a component in: ```js // src/app/toolbar-action-contributors.ts @@ -156,22 +156,22 @@ The list of actions, conveniently named as `actionList`, is a **doubly linked li ### Step 3. Import and Use Toolbar Action Contributors -Import `identityToolbarActionContributors` in your routing module and pass it to the static `forLazy` method of `IdentityModule` as seen below. +Import `identityToolbarActionContributors` in your routing configuration and pass it to the static `createRoutes` method for `identity` route as seen below. ```js -// src/app/app-routing.module.ts +// src/app/app.routes.ts // other imports import { identityToolbarActionContributors } from './toolbar-action-contributors'; -const routes: Routes = [ +export const APP_ROUTES: Routes = [ // other routes { path: 'identity', loadChildren: () => - import('@abp/ng.identity').then(m => - m.IdentityModule.forLazy({ + import('@abp/ng.identity').then(c => + c.createRoutes({ toolbarActionContributors: identityToolbarActionContributors, }) ), @@ -181,7 +181,7 @@ const routes: Routes = [ ]; ``` -That is it, `logUserNames` toolbar action will be added as the first action on the page toolbar in the users page (`UsersComponent`) of the `IdentityModule` and it will be triggered by a custom button, i.e. `ClickMeButtonComponent`. Please note that **component projection is not limited to buttons** and you may use other UI components. +That is it, `logUserNames` toolbar action will be added as the first action on the page toolbar in the users page (`UsersComponent`) of the `identity` package and it will be triggered by a custom button, i.e. `ClickMeButtonComponent`. Please note that **component projection is not limited to buttons** and you may use other UI components. ## How to Place a Custom Modal and Trigger It by Toolbar Actions @@ -380,7 +380,7 @@ export const identityEntityActionContributors = { ### ToolbarActionContributorCallback\ -`ToolbarActionContributorCallback` is the type that you can pass as toolbar action contributor callbacks to static `forLazy` methods of the modules. +`ToolbarActionContributorCallback` is the type that you can pass as toolbar action contributor callbacks to static `createRoutes` methods of the packages. ```js // exportUsersContributor should have ToolbarActionContributorCallback type diff --git a/docs/en/framework/ui/angular/password-complexity-indicator-component.md b/docs/en/framework/ui/angular/password-complexity-indicator-component.md index 806b686ece..1733f9543e 100644 --- a/docs/en/framework/ui/angular/password-complexity-indicator-component.md +++ b/docs/en/framework/ui/angular/password-complexity-indicator-component.md @@ -42,7 +42,7 @@ The `PasswordComplexityIndicatorService` is for calculating the password complex It's easy, imagine you have a password input that you want to add the complexity indicator under. Put this component under the input ```ts - + ``` - Pass the password to the `validatePassword` method of the `PasswordComplexityIndicatorService`, and bind return the value to the `progressBar` property of the `abp-password-complexity-indicator` diff --git a/docs/en/framework/ui/angular/permission-management.md b/docs/en/framework/ui/angular/permission-management.md index d7ae48ec07..cb22b5863a 100644 --- a/docs/en/framework/ui/angular/permission-management.md +++ b/docs/en/framework/ui/angular/permission-management.md @@ -101,11 +101,10 @@ export class CustomPermissionService extends PermissionService { } ``` -- Then, in `app.module.ts`, provide this service as follows: +- Then, in `app.config.ts`, provide this service as follows: ```js -@NgModule({ - // ... +export const appConfig: ApplicationConfig = { providers: [ // ... { @@ -113,9 +112,7 @@ export class CustomPermissionService extends PermissionService { useExisting: CustomPermissionService, }, ], - // ... -}) -export class AppModule {} +}; ``` That's it. Now, when a directive/guard asks for `PermissionService` from angular, it will inject your service. diff --git a/docs/en/framework/ui/angular/pwa-configuration.md b/docs/en/framework/ui/angular/pwa-configuration.md index 918aa4970d..9825eb0a52 100644 --- a/docs/en/framework/ui/angular/pwa-configuration.md +++ b/docs/en/framework/ui/angular/pwa-configuration.md @@ -38,7 +38,7 @@ So, Angular CLI updates some files and add a few others: - `serviceWorker` is `true` in production build. - `ngswConfigPath` refers to _ngsw-config.json_. - **package.json** has _@angular/service-worker_ as a new dependency. -- **app.module.ts** imports `ServiceWorkerModule` and registers a service worker filename. +- **app.config.ts** imports `ServiceWorkerModule` and registers a service worker filename. - **index.html** has following modifications: - A `` element that refers to _manifest.webmanifest_. - A `` tag that sets a theme color. diff --git a/docs/en/framework/ui/angular/quick-start.md b/docs/en/framework/ui/angular/quick-start.md index dec3a23262..77080141ed 100644 --- a/docs/en/framework/ui/angular/quick-start.md +++ b/docs/en/framework/ui/angular/quick-start.md @@ -1,12 +1,12 @@ # ABP Angular Quick Start -**In this version ABP uses Angular [17.3.x](https://github.com/angular/angular/tree/17.3.x) version. You don't have to install Angular CLI globally** +**In this version ABP uses Angular [20.0.x](https://github.com/angular/angular/tree/20.0.x) version. You don't have to install Angular CLI globally** ## How to Prepare Development Environment Please follow the steps below to prepare your development environment for Angular. -1. **Install Node.js:** Please visit [Node.js downloads page](https://nodejs.org/en/download/) and download proper Node.js `v20.11+` installer for your OS. An alternative is to install [NVM](https://github.com/nvm-sh/nvm) and use it to have multiple versions of Node.js in your operating system. +1. **Install Node.js:** Please visit [Node.js downloads page](https://nodejs.org/en/download/) and download proper Node.js `v20.19+` installer for your OS. An alternative is to install [NVM](https://github.com/nvm-sh/nvm) and use it to have multiple versions of Node.js in your operating system. 2. **[Optional] Install Yarn:** You may install Yarn v1.22+ (not v2) following the instructions on [the installation page](https://classic.yarnpkg.com/en/docs/install). Yarn v1 delivers an arguably better developer experience compared to npm v10 and below. You may skip this step and work with npm, which is built-in in Node.js, instead. 3. **[Optional] Install VS Code:** [VS Code](https://code.visualstudio.com/) is a free, open-source IDE which works seamlessly with TypeScript. Although you can use any IDE including Visual Studio or Rider, VS Code will most likely deliver the best developer experience when it comes to Angular projects. ABP project templates even contain plugin recommendations for VS Code users, which VS Code will ask you to install when you open the Angular project folder. Here is a list of recommended extensions: - [Angular Language Service](https://marketplace.visualstudio.com/items?itemName=angular.ng-template) @@ -54,28 +54,27 @@ Here is what these folders and files are for: - **.vscode** has extension recommendations in it. - **e2e** is a separate app for possible end-to-end tests. - **src** is where the source files for your application are placed. We will have a closer look in a minute. -- **.browserlistrc** helps [configuring browser compatibility of your Angular app](https://angular.io/guide/build#configuring-browser-compatibility). +- **.browserlistrc** helps [configuring browser compatibility of your Angular app](https://angular.dev/tools/cli/build#configuring-browser-compatibility). - **.editorconfig** helps you have a shared coding style for separate editors and IDEs. Check [EditorConfig.org](https://editorconfig.org/) for details. - **.gitignore** defined which files and folders should not be tracked by git. Check [git documentation](https://git-scm.com/docs/gitignore) for details. - **.prettierrc** includes simple coding style choices for [Prettier](https://prettier.io/), an auto-formatter for TypeScript, HTML, CSS, and more. If you install recommended extensions to VS Code, you will never have to format your code anymore. -- **angular.json** is where Angular workspace is defined. It holds project configurations and workspace preferences. Please refer to [Angular workspace configuration](https://angular.io/guide/workspace-config) for details. +- **angular.json** is where Angular workspace is defined. It holds project configurations and workspace preferences. Please refer to [Angular workspace configuration](https://angular.dev/reference/configs/workspace-config) for details. - **karma.conf.js** holds [Karma test runner](https://karma-runner.github.io/) configurations. -- **package.json** is where your [package dependencies](https://angular.io/guide/npm-packages) are listed. It also includes some useful scripts for developing, testing, and building your application. +- **package.json** is where your [package dependencies](https://angular.dev/reference/configs/npm-packages) are listed. It also includes some useful scripts for developing, testing, and building your application. - **README.md** includes some of Angular CLI command examples. You either have to install Angular CLI globally or run these commands starting with `yarn` or `npx` to make them work. -- **start.ps1** is a simple PowerShell script to install dependencies and start a [development server via Angular CLI](https://angular.io/cli/serve), but you probably will not need that after reading this document. -- **tsconfig.json** and all other [tsconfig files](https://angular.io/guide/typescript-configuration) in general, include some TypeScript and Angular compile options. +- **start.ps1** is a simple PowerShell script to install dependencies and start a [development server via Angular CLI](https://angular.dev/cli/serve), but you probably will not need that after reading this document. +- **tsconfig.json** and all other [tsconfig files](https://angular.dev/reference/configs/angular-compiler-options) in general, include some TypeScript and Angular compile options. - **yarn.lock** enables installing consistent package versions across different devices so that working application build will not break because of a package update. Please read [Yarn documentation](https://classic.yarnpkg.com/en/docs/yarn-lock/) if you are interested in more information on the topic. If you have decided to use npm, please remove this file and keep the [package-lock.json](https://docs.npmjs.com/files/package-lock.json) instead. Now let us take a look at the contents of the source folder. Angular project source folder structure -- **app** is the main directory you put your application files in. Any module, component, directive, service, pipe, guard, interceptor, etc. should be placed here. You are free to choose any folder structure, but [organizing Angular applications based on modules](https://angular.io/guide/module-types) is generally a fine practice. -- **home** is a predefined module and acts as a welcome page. It also demonstrates how a feature-based folder structure may look like. More complex features will probably have sub-features, thus inner folders. You may change the home folder however you like. -- **shared** is spared for reusable code that works for several modules. Some, including yours truly, may disagree with using a single module for all shared code, so consider adding standalone sub-modules inside this folder instead of adding everything into **shared.module.ts**. -- **app-routing.module.ts** is where your top-level routes are defined. Angular is capable of [lazy loading feature modules](https://angular.io/guide/lazy-loading-ngmodules), so not all routes will be here. You may think of Angular routing as a tree and this file is the top of the tree. +- **app** is the main directory you put your application files in. Any component, directive, service, pipe, guard, interceptor, etc. should be placed here. You are free to choose any folder structure, but [organizing Angular applications using configuration-based structure](https://angular.dev/reference/configs/file-structure) is generally a fine practice, especially when using standalone APIs. This replaces the older convention of organizing strictly by NgModules. +- **home** is a predefined component and acts as a welcome page. It also demonstrates how a feature-based folder structure may look like. More complex features will probably have sub-features, thus inner folders. You may change the home folder however you like. +- **app.routes.ts** is where your top-level routes are defined. Angular is capable of [lazy loading routes now](https://angular.dev/reference/migrations/route-lazy-loading), so not all routes will be here. You may think of Angular routing as a tree and this file is the top of the tree. - **app.component.ts** is essentially the top component that holds the dynamic application layout. -- **app.module.ts** is the [root module](https://angular.io/guide/bootstrapping) that includes information about how parts of your application are related and what to run at the initiation of your application. +- **app.config.ts** is the [root configuration](https://angular.dev/api/platform-browser/bootstrapApplication) that includes information about how parts of your application are related and what to run at the initiation of your application. - **route.provider.ts** is used for [modifying the menu](../angular/modifying-the-menu.md). - **assets** is for static files. A file (e.g. an image) placed in this folder will be available as is when the application is served. - **environments** includes one file per environment configuration. There are two configurations by default, but you may always introduce another one. These files are directly referred to in _angular.json_ and help you have different builds and application variables. Please refer to [configuring Angular application environments](https://angular.io/guide/build#configuring-application-environments) for details. @@ -155,7 +154,7 @@ export const environment = { } as Config.Environment; ``` -When you run the development server, variables defined in _environment.ts_ take effect. Similarly, in production mode, the default environment is replaced by _environment.prod.ts_ and completely different variables become effective. You may even [create a new build configuration](https://angular.io/guide/workspace-config#build-configs) and set [file replacements](https://angular.io/guide/build#configure-target-specific-file-replacements) to use a completely new environment. For now, we will start a production build: +When you run the development server, variables defined in _environment.ts_ take effect. Similarly, in production mode, the default environment is replaced by _environment.prod.ts_ and completely different variables become effective. You may even [create a new build configuration](https://angular.dev/reference/configs/workspace-config#alternate-build-configurations) and set [file replacements](https://angular.io/guide/build#configure-target-specific-file-replacements) to use a completely new environment. For now, we will start a production build: 1. Open your terminal and navigate to the root Angular folder. 2. Run `yarn` or `npm install` if you have not installed dependencies already. diff --git a/docs/en/framework/ui/angular/show-password-directive.md b/docs/en/framework/ui/angular/show-password-directive.md index cacb2550c5..588aa3a60b 100644 --- a/docs/en/framework/ui/angular/show-password-directive.md +++ b/docs/en/framework/ui/angular/show-password-directive.md @@ -4,24 +4,20 @@ In password input, text can be shown easily via changing input type attribute to ## Getting Started -`ShowPasswordDirective` is standalone. In order to use the `ShowPasswordDirective` in an HTML template, import it to related module or your standalone component: +`ShowPasswordDirective` is standalone. In order to use the it in an HTML template, import it to your component: -**Importing to NgModule** +**Importing to Component** ```ts import { ShowPasswordDirective } from '@abp/ng.core'; -@NgModule({ +@Component({ //... - declarations: [ - ..., - TestComponent - ], imports: [ - ..., + // ..., ShowPasswordDirective ], }) -export class MyFeatureModule {} +export class TestComponent {} ``` ## Usage @@ -30,28 +26,10 @@ The `ShowPasswordDirective` is very easy to use. The directive's selector is **` See an example usage: -**NgModule Component usage** -```ts -@Component({ - selector: 'test-component', - template: ` -
- - - icon -
- ` -}) -export class TestComponent{ - showPassword = false; -} -``` -**Standalone Component usage** ```ts import { ShowPasswordDirective } from '@abp/ng.core'; @Component({ - selector: 'standalone-component', - standalone: true, + selector: 'sample-component', template: `
@@ -61,7 +39,7 @@ import { ShowPasswordDirective } from '@abp/ng.core'; `, imports: [ShowPasswordDirective] }) -export class StandaloneComponent{ +export class SampleComponent{ showPassword = false; } ``` diff --git a/docs/en/framework/ui/angular/sorting-navigation-elements.md b/docs/en/framework/ui/angular/sorting-navigation-elements.md index 7385a1681c..a5a9b3db89 100644 --- a/docs/en/framework/ui/angular/sorting-navigation-elements.md +++ b/docs/en/framework/ui/angular/sorting-navigation-elements.md @@ -19,19 +19,18 @@ This documentation describes how the navigation elements are sorted and how to c # How to Customize -**`in app.module.ts`** +**`in app.config.ts`** ```ts import { SORT_COMPARE_FUNC } from "@abp/ng.core"; -@NgModule({ +export const appConfig: ApplicationConfig = { providers: [ - ...{ + // ... + { provide: SORT_COMPARE_FUNC, useFactory: yourCompareFuncFactory, }, ], - // imports, declarations, and bootstrap -}) -export class AppModule {} +}; ``` diff --git a/docs/en/framework/ui/angular/toaster-service.md b/docs/en/framework/ui/angular/toaster-service.md index 41ca75949d..2737e1067e 100644 --- a/docs/en/framework/ui/angular/toaster-service.md +++ b/docs/en/framework/ui/angular/toaster-service.md @@ -4,7 +4,7 @@ You can use the `ToasterService` in @abp/ng.theme.shared package to display mess ## Getting Started -You do not have to provide the `ToasterService` at module or component level, because it is already **provided in root**. You can inject and start using it immediately in your components, directives, or services. +You do not have to provide the `ToasterService` at component level, because it is already **provided in root**. You can inject and start using it immediately in your components, directives, or services. ```js import { ToasterService } from '@abp/ng.theme.shared'; @@ -173,19 +173,19 @@ export class CustomToasterService implements Toaster.Service { ``` ```js -// app.module.ts +// app.config.ts import { ToasterService } from '@abp/ng.theme.shared'; -@NgModule({ +export const appConfig: ApplicationConfig = { providers: [ - // ... + // ... { provide: ToasterService, useClass: CustomToasterService, }, - ] -}) + ], +}; ``` ## API diff --git a/docs/en/framework/ui/blazor/layout-hooks.md b/docs/en/framework/ui/blazor/layout-hooks.md index 9e29ff69cd..d54b225c6d 100644 --- a/docs/en/framework/ui/blazor/layout-hooks.md +++ b/docs/en/framework/ui/blazor/layout-hooks.md @@ -2,7 +2,7 @@ ABP theming system places the page layout into the [theme](theming.md) NuGet packages. That means the final application doesn't include a layout, so you can't directly change the layout code to customize it. -> If you create a Blazor WASM project, the `index.html` file will be included within the template. You can also customize it to your needs. +> If you created your Blazor application with ABP templates, then you'll have an `App.razor` file in your project. You can customize it to your needs and even extend it with layout hooks. You can copy the theme code into your solution. In this case, you are completely free to customize it. However, then you won't be able to get automatic updates of the theme (by upgrading the theme NuGet package). @@ -101,11 +101,41 @@ See the *Layouts* section below to learn more about the layout system. There are some pre-defined layout hook points. The standard hook points are: +* `LayoutHooks.Head.First`: Used to add a component as the first item in the HTML head tag. +* `LayoutHooks.Head.Last`: Used to add a component as the last item in the HTML head tag. * `LayoutHooks.Body.First`: Used to add a component as the first item in the HTML body tag. * `LayoutHooks.Body.Last`: Used to add a component as the last item in the HTML body tag. > You (or the modules you are using) can add **multiple items to the same hook point**. All of them will be added to the layout in the order they were added. +### Render LayoutHooks.Head in App.razor + +In your Blazor application, there is an `App.razor` file, which acts as the entry point of your application. If you need to render layout hooks between the **head** tags, then you should manually register the layout hooks as below: + +```csharp +@using Volo.Abp.AspNetCore.Components.Web.Theming.Components.LayoutHooks; +@using Volo.Abp.Ui.LayoutHooks; +@using Volo.Abp.AspNetCore.Components.Web.Theming.Layout; + + + + + + + // Your head content + // ... + + + + + // Your body content + // ... + + +``` + +After registering the related layout hooks, you can define components to render in the specific place accordingly as mentioned above. + ## Layouts The layout system allows themes to define the standard named layouts and allows any page to select a proper layout for its purpose. There is one pre-defined layout: diff --git a/docs/en/framework/ui/maui/index.md b/docs/en/framework/ui/maui/index.md index 28f74aa6a3..94d9357fed 100644 --- a/docs/en/framework/ui/maui/index.md +++ b/docs/en/framework/ui/maui/index.md @@ -13,6 +13,8 @@ ABP Commercial platform provides a basic [MAUI](https://docs.microsoft.com/en-us Run the backend application described in the [getting started document](../../../get-started/index.md). +## Run the Mobile Application + Open the `appsettings.json` in the `MAUI` project: {{ if Tiered == "Yes" }} @@ -25,6 +27,12 @@ Open the `appsettings.json` in the `MAUI` project: {{ end }} +After ensuring the backend application is running and the `appsettings.json` is properly configured in the mobile application, you can proceed to run the mobile application. You can run the application either by using the `dotnet build` command (e.g. `dotnet build -t:Run -f net9.0-android` for Android or `dotnet build -t:Run -f net9.0-ios` for iOS) or by running it through Visual Studio or any other IDE that supports MAUI. + +> For more information about running the mobile application, please refer to the [Microsoft's documentation](https://learn.microsoft.com/en-us/dotnet/maui/?view=net-maui-9.0). + +You can examine the [Users Page](#users-page) or any other pre-defined page to see how to use CSharp Client Proxy to request backend API and consume the backend API in the same way in your application. Also, if you encounter any errors on specific platforms, you can refer to the following sections for each platform to find common issues and their solutions. + ### Android If you get the following error when connecting to the emulator or a physical phone, you need to set up port mapping. @@ -65,10 +73,11 @@ If you run the MAUI on a Mac agent, the remote iOS Simulator can't access the ba ## User Interface The MAUI template consists of four pages: -* **Homepage**: This is the welcome page of the application. -* **Users**: Management page for your application users. You can search, add, update, or delete users of your application. -* **Tenants**: Management page for your tenants. -* **Settings**: Management page for your application settings. On this page, you can change **the current language**, **the profile picture**, **the current password**, or/and **the current theme**. + +- **Homepage**: This is the welcome page of the application. +- **Users**: Management page for your application users. You can search, add, update, or delete users of your application. +- **Tenants**: Management page for your tenants. +- **Settings**: Management page for your application settings. On this page, you can change **the current language**, **the profile picture**, **the current password**, or/and **the current theme**. ### Homepage @@ -86,12 +95,6 @@ The MAUI template consists of four pages: ![Maui Settings Page](../../../images/maui-settings-page.png) -## Run the Mobile Application - -You can run the MAUI application through Visual Studio or any other IDE that supports MAUI. After the application is up and running, you can continue to develop your application based on this startup template. - -You can examine the [Users Page](#users-page) or any other pre-defined page to see how to use CSharp Client Proxy to request backend API and consume the backend API in the same way in your application. - ### Advanced #### Validation diff --git a/docs/en/get-started/images/abp-studio-created-new-solution_dark.png b/docs/en/get-started/images/abp-studio-created-new-solution_dark.png new file mode 100644 index 0000000000..1375f384c4 Binary files /dev/null and b/docs/en/get-started/images/abp-studio-created-new-solution_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-microservice-solution-runner-applications.png b/docs/en/get-started/images/abp-studio-microservice-solution-runner-applications.png index 0a4f7da5bd..2bd1a2fe40 100644 Binary files a/docs/en/get-started/images/abp-studio-microservice-solution-runner-applications.png and b/docs/en/get-started/images/abp-studio-microservice-solution-runner-applications.png differ diff --git a/docs/en/get-started/images/abp-studio-microservice-solution-runner.png b/docs/en/get-started/images/abp-studio-microservice-solution-runner.png index 2b5bc20678..3e06813a46 100644 Binary files a/docs/en/get-started/images/abp-studio-microservice-solution-runner.png and b/docs/en/get-started/images/abp-studio-microservice-solution-runner.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-additional-options_dark.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-additional-options_dark.png new file mode 100644 index 0000000000..aa483a8f02 Binary files /dev/null and b/docs/en/get-started/images/abp-studio-new-solution-dialog-additional-options_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-database-configurations-efcore_dark.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-database-configurations-efcore_dark.png new file mode 100644 index 0000000000..3bbb0cf2d3 Binary files /dev/null and b/docs/en/get-started/images/abp-studio-new-solution-dialog-database-configurations-efcore_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-database-configurations-mongo_dark.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-database-configurations-mongo_dark.png new file mode 100644 index 0000000000..53f65e05b2 Binary files /dev/null and b/docs/en/get-started/images/abp-studio-new-solution-dialog-database-configurations-mongo_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-database-provider-efcore_dark.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-database-provider-efcore_dark.png new file mode 100644 index 0000000000..1b101fe134 Binary files /dev/null and b/docs/en/get-started/images/abp-studio-new-solution-dialog-database-provider-efcore_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-database-provider-mongo_dark.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-database-provider-mongo_dark.png new file mode 100644 index 0000000000..51b9fb1687 Binary files /dev/null and b/docs/en/get-started/images/abp-studio-new-solution-dialog-database-provider-mongo_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-mobile-framework_dark.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-mobile-framework_dark.png new file mode 100644 index 0000000000..9e3b961b92 Binary files /dev/null and b/docs/en/get-started/images/abp-studio-new-solution-dialog-mobile-framework_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-modularity_dark.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-modularity_dark.png new file mode 100644 index 0000000000..4e03f136ed Binary files /dev/null and b/docs/en/get-started/images/abp-studio-new-solution-dialog-modularity_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-multi-tenancy_dark.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-multi-tenancy_dark.png new file mode 100644 index 0000000000..5cfa2fb425 Binary files /dev/null and b/docs/en/get-started/images/abp-studio-new-solution-dialog-multi-tenancy_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-optional-modules_dark.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-optional-modules_dark.png new file mode 100644 index 0000000000..f5921aeb64 Binary files /dev/null and b/docs/en/get-started/images/abp-studio-new-solution-dialog-optional-modules_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-public-website_dark.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-public-website_dark.png new file mode 100644 index 0000000000..7dc6796f21 Binary files /dev/null and b/docs/en/get-started/images/abp-studio-new-solution-dialog-public-website_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-solution-properties_dark.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-solution-properties_dark.png new file mode 100644 index 0000000000..7d9b180991 Binary files /dev/null and b/docs/en/get-started/images/abp-studio-new-solution-dialog-solution-properties_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-solution-structure-tiered.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-solution-structure-tiered.png new file mode 100644 index 0000000000..0264df8da8 Binary files /dev/null and b/docs/en/get-started/images/abp-studio-new-solution-dialog-solution-structure-tiered.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-solution-structure-tiered_dark.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-solution-structure-tiered_dark.png new file mode 100644 index 0000000000..abfad1996d Binary files /dev/null and b/docs/en/get-started/images/abp-studio-new-solution-dialog-solution-structure-tiered_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-solution-structure.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-solution-structure.png index 310488c1bc..b3ea308fa8 100644 Binary files a/docs/en/get-started/images/abp-studio-new-solution-dialog-solution-structure.png and b/docs/en/get-started/images/abp-studio-new-solution-dialog-solution-structure.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-solution-structure_dark.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-solution-structure_dark.png new file mode 100644 index 0000000000..e03f424b22 Binary files /dev/null and b/docs/en/get-started/images/abp-studio-new-solution-dialog-solution-structure_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-ui-framework-blazor-server_dark.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-ui-framework-blazor-server_dark.png new file mode 100644 index 0000000000..98849d3b62 Binary files /dev/null and b/docs/en/get-started/images/abp-studio-new-solution-dialog-ui-framework-blazor-server_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-ui-framework-blazor-wasm_dark.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-ui-framework-blazor-wasm_dark.png new file mode 100644 index 0000000000..b58698db00 Binary files /dev/null and b/docs/en/get-started/images/abp-studio-new-solution-dialog-ui-framework-blazor-wasm_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-ui-framework-blazor-webapp_dark.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-ui-framework-blazor-webapp_dark.png new file mode 100644 index 0000000000..394d049398 Binary files /dev/null and b/docs/en/get-started/images/abp-studio-new-solution-dialog-ui-framework-blazor-webapp_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-ui-framework-mvc_dark.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-ui-framework-mvc_dark.png new file mode 100644 index 0000000000..4dbac112cd Binary files /dev/null and b/docs/en/get-started/images/abp-studio-new-solution-dialog-ui-framework-mvc_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-ui-framework-ng_dark.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-ui-framework-ng_dark.png new file mode 100644 index 0000000000..cd804b5308 Binary files /dev/null and b/docs/en/get-started/images/abp-studio-new-solution-dialog-ui-framework-ng_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog-ui-theme_dark.png b/docs/en/get-started/images/abp-studio-new-solution-dialog-ui-theme_dark.png new file mode 100644 index 0000000000..544a3df51c Binary files /dev/null and b/docs/en/get-started/images/abp-studio-new-solution-dialog-ui-theme_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-dialog_dark.png b/docs/en/get-started/images/abp-studio-new-solution-dialog_dark.png new file mode 100644 index 0000000000..b131e76ed0 Binary files /dev/null and b/docs/en/get-started/images/abp-studio-new-solution-dialog_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-new-solution-language-selection_dark.png b/docs/en/get-started/images/abp-studio-new-solution-language-selection_dark.png new file mode 100644 index 0000000000..ac8315e116 Binary files /dev/null and b/docs/en/get-started/images/abp-studio-new-solution-language-selection_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-no-layers-created-new-solution_dark.png b/docs/en/get-started/images/abp-studio-no-layers-created-new-solution_dark.png new file mode 100644 index 0000000000..4939b13d12 Binary files /dev/null and b/docs/en/get-started/images/abp-studio-no-layers-created-new-solution_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-no-layers-new-solution-additional-options_dark.png b/docs/en/get-started/images/abp-studio-no-layers-new-solution-additional-options_dark.png new file mode 100644 index 0000000000..f34f6c415b Binary files /dev/null and b/docs/en/get-started/images/abp-studio-no-layers-new-solution-additional-options_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-no-layers-new-solution-dialog-database-configurations-efcore_dark.png b/docs/en/get-started/images/abp-studio-no-layers-new-solution-dialog-database-configurations-efcore_dark.png new file mode 100644 index 0000000000..075f9224c7 Binary files /dev/null and b/docs/en/get-started/images/abp-studio-no-layers-new-solution-dialog-database-configurations-efcore_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-no-layers-new-solution-dialog-database-configurations-mongo_dark.png b/docs/en/get-started/images/abp-studio-no-layers-new-solution-dialog-database-configurations-mongo_dark.png new file mode 100644 index 0000000000..973758b1a6 Binary files /dev/null and b/docs/en/get-started/images/abp-studio-no-layers-new-solution-dialog-database-configurations-mongo_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-no-layers-new-solution-dialog-database-provider-efcore_dark.png b/docs/en/get-started/images/abp-studio-no-layers-new-solution-dialog-database-provider-efcore_dark.png new file mode 100644 index 0000000000..c31eaee8e3 Binary files /dev/null and b/docs/en/get-started/images/abp-studio-no-layers-new-solution-dialog-database-provider-efcore_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-no-layers-new-solution-dialog-database-provider-mongo_dark.png b/docs/en/get-started/images/abp-studio-no-layers-new-solution-dialog-database-provider-mongo_dark.png new file mode 100644 index 0000000000..a3915f2ff9 Binary files /dev/null and b/docs/en/get-started/images/abp-studio-no-layers-new-solution-dialog-database-provider-mongo_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-no-layers-new-solution-dialog-multi-tenancy_dark.png b/docs/en/get-started/images/abp-studio-no-layers-new-solution-dialog-multi-tenancy_dark.png new file mode 100644 index 0000000000..a64debc8db Binary files /dev/null and b/docs/en/get-started/images/abp-studio-no-layers-new-solution-dialog-multi-tenancy_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-no-layers-new-solution-dialog-solution-properties_dark.png b/docs/en/get-started/images/abp-studio-no-layers-new-solution-dialog-solution-properties_dark.png new file mode 100644 index 0000000000..1f8351d656 Binary files /dev/null and b/docs/en/get-started/images/abp-studio-no-layers-new-solution-dialog-solution-properties_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-no-layers-new-solution-dialog-ui-framework-blazor-server_dark.png b/docs/en/get-started/images/abp-studio-no-layers-new-solution-dialog-ui-framework-blazor-server_dark.png new file mode 100644 index 0000000000..d4a126c5b8 Binary files /dev/null and b/docs/en/get-started/images/abp-studio-no-layers-new-solution-dialog-ui-framework-blazor-server_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-no-layers-new-solution-dialog-ui-framework-blazor-wasm_dark.png b/docs/en/get-started/images/abp-studio-no-layers-new-solution-dialog-ui-framework-blazor-wasm_dark.png new file mode 100644 index 0000000000..bf2cfb8c44 Binary files /dev/null and b/docs/en/get-started/images/abp-studio-no-layers-new-solution-dialog-ui-framework-blazor-wasm_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-no-layers-new-solution-dialog-ui-framework-blazor-webapp_dark.png b/docs/en/get-started/images/abp-studio-no-layers-new-solution-dialog-ui-framework-blazor-webapp_dark.png new file mode 100644 index 0000000000..4b3c75467e Binary files /dev/null and b/docs/en/get-started/images/abp-studio-no-layers-new-solution-dialog-ui-framework-blazor-webapp_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-no-layers-new-solution-dialog-ui-framework-mvc_dark.png b/docs/en/get-started/images/abp-studio-no-layers-new-solution-dialog-ui-framework-mvc_dark.png new file mode 100644 index 0000000000..45e0e0b89d Binary files /dev/null and b/docs/en/get-started/images/abp-studio-no-layers-new-solution-dialog-ui-framework-mvc_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-no-layers-new-solution-dialog-ui-framework-ng_dark.png b/docs/en/get-started/images/abp-studio-no-layers-new-solution-dialog-ui-framework-ng_dark.png new file mode 100644 index 0000000000..fbc97bbf6e Binary files /dev/null and b/docs/en/get-started/images/abp-studio-no-layers-new-solution-dialog-ui-framework-ng_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-no-layers-new-solution-dialog_dark.png b/docs/en/get-started/images/abp-studio-no-layers-new-solution-dialog_dark.png new file mode 100644 index 0000000000..3453bd976d Binary files /dev/null and b/docs/en/get-started/images/abp-studio-no-layers-new-solution-dialog_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-no-layers-new-solution-language-selection_dark.png b/docs/en/get-started/images/abp-studio-no-layers-new-solution-language-selection_dark.png new file mode 100644 index 0000000000..23da11bfbf Binary files /dev/null and b/docs/en/get-started/images/abp-studio-no-layers-new-solution-language-selection_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-no-layers-new-solution-modularity_dark.png b/docs/en/get-started/images/abp-studio-no-layers-new-solution-modularity_dark.png new file mode 100644 index 0000000000..accd5e6739 Binary files /dev/null and b/docs/en/get-started/images/abp-studio-no-layers-new-solution-modularity_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-no-layers-new-solution-optional-modules_dark.png b/docs/en/get-started/images/abp-studio-no-layers-new-solution-optional-modules_dark.png new file mode 100644 index 0000000000..9992d266c5 Binary files /dev/null and b/docs/en/get-started/images/abp-studio-no-layers-new-solution-optional-modules_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-no-layers-open-in-visual-studio_dark.png b/docs/en/get-started/images/abp-studio-no-layers-open-in-visual-studio_dark.png new file mode 100644 index 0000000000..f599c9bc8c Binary files /dev/null and b/docs/en/get-started/images/abp-studio-no-layers-open-in-visual-studio_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-no-layers-quick-start-application-solution-runner_dark.png b/docs/en/get-started/images/abp-studio-no-layers-quick-start-application-solution-runner_dark.png new file mode 100644 index 0000000000..5a0aba31b6 Binary files /dev/null and b/docs/en/get-started/images/abp-studio-no-layers-quick-start-application-solution-runner_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-no-layers-quick-start-browse-command_dark.png b/docs/en/get-started/images/abp-studio-no-layers-quick-start-browse-command_dark.png new file mode 100644 index 0000000000..a90b3fe57f Binary files /dev/null and b/docs/en/get-started/images/abp-studio-no-layers-quick-start-browse-command_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-no-layers-quick-start-browse-user-list_dark.png b/docs/en/get-started/images/abp-studio-no-layers-quick-start-browse-user-list_dark.png new file mode 100644 index 0000000000..fbdd1cb355 Binary files /dev/null and b/docs/en/get-started/images/abp-studio-no-layers-quick-start-browse-user-list_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-no-layers-quick-start-browse_dark.png b/docs/en/get-started/images/abp-studio-no-layers-quick-start-browse_dark.png new file mode 100644 index 0000000000..aa9e95bf4f Binary files /dev/null and b/docs/en/get-started/images/abp-studio-no-layers-quick-start-browse_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-no-layers-quick-start-example-applications-in-solution-runner_dark.png b/docs/en/get-started/images/abp-studio-no-layers-quick-start-example-applications-in-solution-runner_dark.png new file mode 100644 index 0000000000..f18fc2c646 Binary files /dev/null and b/docs/en/get-started/images/abp-studio-no-layers-quick-start-example-applications-in-solution-runner_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-nolayers-new-solution-dialog-ui-theme_dark.png b/docs/en/get-started/images/abp-studio-nolayers-new-solution-dialog-ui-theme_dark.png new file mode 100644 index 0000000000..64788b9b45 Binary files /dev/null and b/docs/en/get-started/images/abp-studio-nolayers-new-solution-dialog-ui-theme_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-open-in-visual-studio_dark.png b/docs/en/get-started/images/abp-studio-open-in-visual-studio_dark.png new file mode 100644 index 0000000000..59f8877e9f Binary files /dev/null and b/docs/en/get-started/images/abp-studio-open-in-visual-studio_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-quick-start-application-solution-runner_dark.png b/docs/en/get-started/images/abp-studio-quick-start-application-solution-runner_dark.png new file mode 100644 index 0000000000..382461de06 Binary files /dev/null and b/docs/en/get-started/images/abp-studio-quick-start-application-solution-runner_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-quick-start-browse-command_dark.png b/docs/en/get-started/images/abp-studio-quick-start-browse-command_dark.png new file mode 100644 index 0000000000..d0d3b3af06 Binary files /dev/null and b/docs/en/get-started/images/abp-studio-quick-start-browse-command_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-quick-start-browse-user-list_dark.png b/docs/en/get-started/images/abp-studio-quick-start-browse-user-list_dark.png new file mode 100644 index 0000000000..84b4bb8f4d Binary files /dev/null and b/docs/en/get-started/images/abp-studio-quick-start-browse-user-list_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-quick-start-browse_dark.png b/docs/en/get-started/images/abp-studio-quick-start-browse_dark.png new file mode 100644 index 0000000000..dab624fa75 Binary files /dev/null and b/docs/en/get-started/images/abp-studio-quick-start-browse_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-quick-start-example-applications-in-solution-runner_dark.png b/docs/en/get-started/images/abp-studio-quick-start-example-applications-in-solution-runner_dark.png new file mode 100644 index 0000000000..f06b2b4ca7 Binary files /dev/null and b/docs/en/get-started/images/abp-studio-quick-start-example-applications-in-solution-runner_dark.png differ diff --git a/docs/en/get-started/images/abp-studio-welcome-screen_dark.png b/docs/en/get-started/images/abp-studio-welcome-screen_dark.png new file mode 100644 index 0000000000..5aeabbce38 Binary files /dev/null and b/docs/en/get-started/images/abp-studio-welcome-screen_dark.png differ diff --git a/docs/en/get-started/images/bookstore-browser-users-page_dark.png b/docs/en/get-started/images/bookstore-browser-users-page_dark.png new file mode 100644 index 0000000000..85cd35c81d Binary files /dev/null and b/docs/en/get-started/images/bookstore-browser-users-page_dark.png differ diff --git a/docs/en/get-started/images/no-layers-bookstore-browser-users-page_dark.png b/docs/en/get-started/images/no-layers-bookstore-browser-users-page_dark.png new file mode 100644 index 0000000000..85cd35c81d Binary files /dev/null and b/docs/en/get-started/images/no-layers-bookstore-browser-users-page_dark.png differ diff --git a/docs/en/get-started/images/no-layers-visual-studio-bookstore-application_dark.png b/docs/en/get-started/images/no-layers-visual-studio-bookstore-application_dark.png new file mode 100644 index 0000000000..4f7bd3dd19 Binary files /dev/null and b/docs/en/get-started/images/no-layers-visual-studio-bookstore-application_dark.png differ diff --git a/docs/en/get-started/images/solution-runner-public-website.png b/docs/en/get-started/images/solution-runner-public-website.png index b1e76a4ce6..3b08bc3e9a 100644 Binary files a/docs/en/get-started/images/solution-runner-public-website.png and b/docs/en/get-started/images/solution-runner-public-website.png differ diff --git a/docs/en/get-started/images/visual-studio-bookstore-application_dark.png b/docs/en/get-started/images/visual-studio-bookstore-application_dark.png new file mode 100644 index 0000000000..a1530f0036 Binary files /dev/null and b/docs/en/get-started/images/visual-studio-bookstore-application_dark.png differ diff --git a/docs/en/get-started/layered-web-application.md b/docs/en/get-started/layered-web-application.md index 9e2a2ad6f2..3dcbb90f75 100644 --- a/docs/en/get-started/layered-web-application.md +++ b/docs/en/get-started/layered-web-application.md @@ -29,15 +29,15 @@ First things first! Let's setup your development environment before creating the Assuming that you have [installed and logged in](../studio/installation.md) to the application, you should see the following screen when you open ABP Studio: -![abp-studio-welcome-screen](images/abp-studio-welcome-screen.png) +![abp-studio-welcome-screen](images/abp-studio-welcome-screen_dark.png) Select the *File* -> *New Solution* in the main menu, or click the *New solution* button on the Welcome screen to open the *Create new solution* wizard: -![abp-studio-new-solution-dialog](images/abp-studio-new-solution-dialog.png) +![abp-studio-new-solution-dialog](images/abp-studio-new-solution-dialog_dark.png) We will use the *Application (Layered)* solution template for this tutorial, so pick it and click the *Next* button: -![abp-studio-new-solution-dialog-solution-properties](images/abp-studio-new-solution-dialog-solution-properties.png) +![abp-studio-new-solution-dialog-solution-properties](images/abp-studio-new-solution-dialog-solution-properties_dark.png) On that screen, you choose a name for your solution. You can use different levels of namespaces; e.g. `BookStore`, `Acme.BookStore` or `Acme.Retail.BookStore`. @@ -45,29 +45,61 @@ Then select an *output folder* to create your solution. The *Create solution fol Once your configuration is done, click the *Next* button to navigate to the *UI Framework* selection: -![abp-studio-new-solution-dialog-ui-framework](images/abp-studio-new-solution-dialog-ui-framework.png) +{{ if UI == "MVC" }} +![abp-studio-new-solution-dialog-ui-framework-mvc](images/abp-studio-new-solution-dialog-ui-framework-mvc_dark.png) +{{ else if UI == "Blazor" }} +![abp-studio-new-solution-dialog-ui-framework-blazor-wasm](images/abp-studio-new-solution-dialog-ui-framework-blazor-wasm_dark.png) +{{ else if UI == "BlazorServer" }} +![abp-studio-new-solution-dialog-ui-framework-blazor-server](images/abp-studio-new-solution-dialog-ui-framework-blazor-server_dark.png) +{{ else if UI == "BlazorWebApp" }} +![abp-studio-new-solution-dialog-ui-framework-blazor-webapp](images/abp-studio-new-solution-dialog-ui-framework-blazor-webapp_dark.png) +{{ else if UI == "NG" }} +![abp-studio-new-solution-dialog-ui-framework-ng](images/abp-studio-new-solution-dialog-ui-framework-ng_dark.png) +{{ end }} Here, you see all the possible UI options supported by that startup solution template. Pick the **{{ UI_Value }}**. -Notice that; Once you select a UI type, some additional options will be available under the UI Framework list. You can further configure the options or leave them as default and click the *Next* button for the *UI Theme* selection screen: +Notice that; Once you select a UI type, some additional options will be available under the UI Framework list. You can further configure the options or leave them as default and click the Next button for the *Database Provider* selection screen: + +{{ if DB == "EF" }} +![abp-studio-new-solution-dialog-database-provider](images/abp-studio-new-solution-dialog-database-provider-efcore_dark.png) +{{ else }} +![abp-studio-new-solution-dialog-database-provider](images/abp-studio-new-solution-dialog-database-provider-mongo_dark.png) +{{ end }} + +On that screen, you can decide on your database provider by selecting one of the provided options. There are some additional options for each database provider. Leave them as default or change them based on your preferences, then click the *Next* button for additional *Database Configurations*: + +{{ if DB == "EF" }} +![abp-studio-new-solution-dialog-database-configurations](images/abp-studio-new-solution-dialog-database-configurations-efcore_dark.png) +{{ else }} +![abp-studio-new-solution-dialog-database-configurations](images/abp-studio-new-solution-dialog-database-configurations-mongo_dark.png) +{{ end }} + +Here, you can select the database management systems (DBMS){{ if DB == "EF" }} and the connection string{{ end }}. Then, click the *Next* button for additional *Multi-Tenancy* selection: + +![abp-studio-new-solution-dialog-multi-tenancy](images/abp-studio-new-solution-dialog-multi-tenancy_dark.png) + +Here, you can enable or disable multi-tenancy for your solution. You can further configure the options or leave them as default and click the *Next* button for the *UI Theme* selection screen: + +![abp-studio-new-solution-dialog-ui-theme](images/abp-studio-new-solution-dialog-ui-theme_dark.png) -![abp-studio-new-solution-dialog-ui-theme](images/abp-studio-new-solution-dialog-ui-theme.png) +Notice that; Once you select a UI type, some additional options will be available under the UI Framework list. You can further configure the options or leave them as default and click the Next button for the *Database Provider* selection screen: LeptonX is the suggested UI theme that is proper for production usage. Select one of the themes, configure the additional options, and click the *Next* button for the *Mobile Framework* selection: -![abp-studio-new-solution-dialog-mobile-framework](images/abp-studio-new-solution-dialog-mobile-framework.png) +![abp-studio-new-solution-dialog-mobile-framework](images/abp-studio-new-solution-dialog-mobile-framework_dark.png) Here, you see all the mobile applications available in that startup solution template. These mobile applications are well-integrated into your solution and can use the same backend with your web application. They are simple (do not have pre-built features as much as the web application) but a very good starting point to build your mobile application. Pick the one best for you, or select the *None* if you don't want a mobile application in your solution, then click Next to navigate to the *Public website* screen: -![abp-studio-new-solution-dialog-public-website](images/abp-studio-new-solution-dialog-public-website.png) +![abp-studio-new-solution-dialog-public-website](images/abp-studio-new-solution-dialog-public-website_dark.png) That startup solution template also provides an option to create a second web application inside the solution. The second application is called the Public website, an ASP.NET Core MVC / Razor Page application. It can be used to create a public landing/promotion for your product. It is well integrated into the solution (can share the same services, entities, database, and the same authentication logic, for example). If you want, you can also include the [CMS Kit module](../modules/cms-kit) to your solution to add dynamic content features to your web application. So, either select the *Public website* or skip it and click the Next button for the *Optional Modules* selection: -![abp-studio-new-solution-dialog-optional-modules.png](images/abp-studio-new-solution-dialog-optional-modules.png) +![abp-studio-new-solution-dialog-optional-modules.png](images/abp-studio-new-solution-dialog-optional-modules_dark.png) Each item in that list is a pre-built application module. You can click the blue icon near to the module name to get more information about the module. You can leave the list as is (so, it installs the most common and used modules for you) or customize based on your preference. @@ -75,31 +107,37 @@ Installing a module after creating the solution may require manual steps. So, it Once you select the desired modules, click the *Next* button for the *Solution Structure* screen: -![abp-studio-new-solution-dialog-solution-structure](images/abp-studio-new-solution-dialog-solution-structure.png) +{{ if Tiered == "Yes" }} + +![abp-studio-new-solution-dialog-solution-structure-tiered](images/abp-studio-new-solution-dialog-solution-structure-tiered_dark.png) + +{{ else }} + +![abp-studio-new-solution-dialog-solution-structure](images/abp-studio-new-solution-dialog-solution-structure_dark.png) + +{{ end }} It creates a separate host application that only serves the HTTP (REST) APIs. The web application then performs remote HTTP calls to that application for every operation. If the *Tiered* option is not selected, then the web and HTTP APIs are hosted in a single application, and the calls from the UI layer to the API layer are performed in-process. The tiered architecture allows you to host the web (UI) application in a server that can not access to your database server. However, it brings a slight loss of performance (because of the HTTP calls between UI and HTTP API applications) and makes your architecture, development, and deployment more complex. If you don't understand the tiered structure, just skip it. -After making your *Tiered* selection, you can click the *Next* button for the *Database Provider* selection: +After making your *Tiered* selection, you can click the *Next* button for the *Language Selection* page: -{{ if DB == "EF" }} -![abp-studio-new-solution-dialog-database-provider](images/abp-studio-new-solution-dialog-database-provider-efcore.png) -{{ else }} -![abp-studio-new-solution-dialog-database-provider](images/abp-studio-new-solution-dialog-database-provider-mongo.png) -{{ end }} +![abp-studio-no-layers-new-solution-language-selection](images/abp-studio-new-solution-language-selection_dark.png) -On that screen, you can decide on your database provider by selecting one of the provided options. There are some additional options for each database provider. Leave them as default or change them based on your preferences, then click the *Next* button for additional *Database Configurations*: +In this step, you can choose which languages your application will support. -{{ if DB == "EF" }} -![abp-studio-new-solution-dialog-database-configurations](images/abp-studio-new-solution-dialog-database-configurations-efcore.png) -{{ else }} -![abp-studio-new-solution-dialog-database-configurations](images/abp-studio-new-solution-dialog-database-configurations-mongo.png) -{{ end }} +* Default Language: Select the main language for your app. + +* Localizable User Interface: Turn this on to support multiple languages. + +* Available Languages: Check the languages you want to include. + +* Click Add Custom Language if you want to add a language that is not listed. -Here, you can select the database management systems (DBMS){{ if DB == "EF" }} and the connection string{{ end }}. Click *Next* button to see the *Additional Options*. +You can change these settings later if needed. Thenk click the *Next* button for the *Additional Options* page: -![abp-studio-new-solution-dialog-additional-options](images/abp-studio-new-solution-dialog-additional-options.png) +![abp-studio-new-solution-dialog-additional-options](images/abp-studio-new-solution-dialog-additional-options_dark.png) If you uncheck the *Kubernetes Configuration* option, the solution will not include the Kubernetes configuration files, such as Helm charts and other Kubernetes-related files. You can also specify *Social Logins*; if you uncheck this option, the solution will not be configured for social login. Lastly, you can specify the *Include Tests* option to include or exclude the test projects from the solution. @@ -107,11 +145,11 @@ On the next screen, you can configure the modularity options for your solution: > If you select the *Setup as a modular solution* option, the solution is created more ready for [modular monolith development](../tutorials/modular-crm/index.md) and allows you to add sub-modules during the solution creation phase. -![abp-studio-new-solution-modularity](images/abp-studio-new-solution-dialog-modularity.png) +![abp-studio-new-solution-modularity](images/abp-studio-new-solution-dialog-modularity_dark.png) Now, we are ready to allow ABP Studio to create our solution. Just click the *Create* button and let the ABP Studio do the rest for you. After clicking the Create button, the dialog is closed and your solution is loaded into ABP Studio: -![abp-studio-created-new-solution](images/abp-studio-created-new-solution.png) +![abp-studio-created-new-solution](images/abp-studio-created-new-solution_dark.png) You can explore the solution, but you need to wait for background tasks to be completed before running any application in the solution. @@ -125,13 +163,13 @@ Open the [Solution Runner](../studio/running-applications.md) section on the lef > The solution runner structure can be different in your case based on the options you've selected. -![abp-studio-quick-start-application-solution-runner](images/abp-studio-quick-start-application-solution-runner.png) +![abp-studio-quick-start-application-solution-runner](images/abp-studio-quick-start-application-solution-runner_dark.png) Once you click the *Play* icon on the left side, the section is open in the same place as the Solution Explorer section. ABP Studio also opens the *Application Monitor* view on the main content area. *Application Monitor* shows useful insights for your applications (e.g. *HTTP Request*, *Events* and *Exceptions*) in real-time. You can use it to see the happenings in your applications, so you can easily track errors and many helpful details. In the Solution Runner section (on the left side) you can see all the runnable applications in the current solution. For the MVC with public website and MAUI mobile example, we have four applications: -![abp-studio-quick-start-example-applications-in-solution-runner](images/abp-studio-quick-start-example-applications-in-solution-runner.png) +![abp-studio-quick-start-example-applications-in-solution-runner](images/abp-studio-quick-start-example-applications-in-solution-runner_dark.png) You can run all the applications or start them one by one. To start an application, either click the *Play* icon near to the application or right-click and select the *Run* -> *Start* context menu item. @@ -164,17 +202,17 @@ You can start the following application(s): Once the `Acme.BookStore.{{ if UI == "NG" }}Angular{{ else if UI == "BlazorServer" || UI == "Blazor" || UI == "BlazorWebApp" }}Blazor{{ else }}Web{{ end }}` application started, you can right-click it and select the *Browse* command: -![abp-studio-quick-start-browse-command](images/abp-studio-quick-start-browse-command.png) +![abp-studio-quick-start-browse-command](images/abp-studio-quick-start-browse-command_dark.png) The *Browse* command opens the UI of the web application in the built-in browser: -![abp-studio-quick-start-browse](images/abp-studio-quick-start-browse.png) +![abp-studio-quick-start-browse](images/abp-studio-quick-start-browse_dark.png) You can browse your application in a full-featured web browser in ABP Studio. Click the *Login* button in the application UI, enter `admin` as username and `1q2w3E*` as password to login to the application. The following screenshot was taken from the *User Management* page of the [Identity module](../modules/identity.md) that is pre-installed in the application: -![abp-studio-quick-start-browse-user-list](images/abp-studio-quick-start-browse-user-list.png) +![abp-studio-quick-start-browse-user-list](images/abp-studio-quick-start-browse-user-list_dark.png) ## Open the Solution in Visual Studio @@ -184,7 +222,7 @@ First of all, we can stop the application(s) in ABP Studio, so it won't conflict You can use ABP Studio to open the solution with Visual Studio. Right-click to the `Acme.BookStore` [module](../modules), and select the *Open with* -> *Visual Studio* command: -![abp-studio-open-in-visual-studio](images/abp-studio-open-in-visual-studio.png) +![abp-studio-open-in-visual-studio](images/abp-studio-open-in-visual-studio_dark.png) If the *Visual Studio* command is not available, that means ABP Studio could not detect it on your computer. You can open the solution folder in your local file system (you can use the *Open with* -> *Explorer* as a shortcut) and manually open the solution in Visual Studio. @@ -192,11 +230,11 @@ Once the solution is opened in Visual Studio, you should see a screen like shown > The solution structure can be different in your case based on the options you've selected. -![visual-studio-bookstore-application](images/visual-studio-bookstore-application.png) +![visual-studio-bookstore-application](images/visual-studio-bookstore-application_dark.png) Right-click the `Acme.BookStore.{{ if UI == "NG" || UI == "Blazor" }}HttpApi.Host{{ else if UI == "BlazorServer" || UI == "BlazorWebApp" }}Blazor{{ else }}Web{{ end }}` project and select the *Set as Startup Project* command. You can then hit *F5* or *Ctrl + F5* to run the web application. It will run and open the application UI in your default browser: -![bookstore-browser-users-page](images/bookstore-browser-users-page.png) +![bookstore-browser-users-page](images/bookstore-browser-users-page_dark.png) You can use `admin` as username and `1q2w3E*` as default password to login to the application. @@ -230,4 +268,4 @@ Before starting the mobile application, ensure that you configure it for [react- > For example in non-tiered MVC with public website application: -![solution-runner-public-website](images/solution-runner-public-website.png) +![solution-runner-public-website](images/solution-runner-public-website.png) \ No newline at end of file diff --git a/docs/en/get-started/microservice.md b/docs/en/get-started/microservice.md index d4fae4fa62..c3d551a2a0 100644 --- a/docs/en/get-started/microservice.md +++ b/docs/en/get-started/microservice.md @@ -150,9 +150,9 @@ In the *Solution Runner* section (on the left side) you can see all the runnable ![abp-studio-microservice-solution-runner-applications](images/abp-studio-microservice-solution-runner-applications.png) -> A leaf item in the *Solution Runner* is called as an *Application* as it is an executable application. +> A leaf item in the *Solution Runner* is called as an *Application* as it is an executable application, excluding items under `Containers`. -As shown in the figure above, the executable applications are grouped into folders like `apps`, `gateways`, `infrastructure`, and `services`. You can start/stop them all, a group (folder) of them, or one by one. +As shown in the figure above, the executable applications are grouped into folders like `apps`, `gateways`, and `services`. You can start/stop them all, a group (folder) of them, or one by one. The `Containers` branch contains the needed docker containers for the applications. Before running the applications, you can run the all application by right-clicking the root item in the *Solution Runner* and select *Build* -> *Build All* action. However, you don't need to do that, because ABP Studio builds the applications before running them by default. diff --git a/docs/en/get-started/single-layer-web-application.md b/docs/en/get-started/single-layer-web-application.md index d75c3f2031..630c2a9046 100644 --- a/docs/en/get-started/single-layer-web-application.md +++ b/docs/en/get-started/single-layer-web-application.md @@ -29,15 +29,15 @@ First things first! Let's setup your development environment before creating the Assuming that you have [installed and logged in](../studio/installation.md) to the application, you should see the following screen when you open ABP Studio: -![abp-studio-welcome-screen](images/abp-studio-welcome-screen.png) +![abp-studio-welcome-screen](images/abp-studio-welcome-screen_dark.png) Select the *File* -> *New Solution* in the main menu, or click the *New solution* button on the *Welcome* screen to open the *Create new solution* wizard: -![abp-studio-new-solution-dialog](images/abp-studio-no-layers-new-solution-dialog-0.9.13.png) +![abp-studio-new-solution-dialog](images/abp-studio-no-layers-new-solution-dialog_dark.png) We will use the *Application (Single Layer)* solution template for this tutorial, so pick it and click the *Next* button: -![abp-studio-new-solution-dialog-solution-properties](images/abp-studio-no-layers-new-solution-dialog-solution-properties-0.9.13.png) +![abp-studio-new-solution-dialog-solution-properties](images/abp-studio-no-layers-new-solution-dialog-solution-properties_dark.png) On that screen, you choose a name for your solution. You can use different levels of namespaces; e.g. `BookStore`, `Acme.BookStore` or `Acme.Retail.BookStore`. @@ -45,45 +45,79 @@ Then select an *output folder* to create your solution. The *Create solution fol Once your configuration is done, click the *Next* button to navigate to the *UI Framework* selection: -![abp-studio-new-solution-dialog-ui-framework](images/abp-studio-no-layers-new-solution-dialog-ui-framework-0.9.13.png) - -Here, you see all the possible UI options supported by that startup solution template. Pick the **{{ UI_Value }}**. - -Notice that; Once you select a UI type, some additional options will be available under the UI Framework list. You can further configure the options or leave them as default and click the *Next* button for the *UI Theme* selection screen: +{{ if UI == "MVC" }} +![abp-studio-new-solution-dialog-ui-framework-mvc](images/abp-studio-no-layers-new-solution-dialog-ui-framework-mvc_dark.png) +{{ else if UI == "Blazor" }} +![abp-studio-new-solution-dialog-ui-framework-blazor-wasm](images/abp-studio-no-layers-new-solution-dialog-ui-framework-blazor-wasm_dark.png) +{{ else if UI == "BlazorServer" }} +![abp-studio-new-solution-dialog-ui-framework-blazor-server](images/abp-studio-no-layers-new-solution-dialog-ui-framework-blazor-server_dark.png) +{{ else if UI == "BlazorWebApp" }} +![abp-studio-new-solution-dialog-ui-framework-blazor-webapp](images/abp-studio-no-layers-new-solution-dialog-ui-framework-blazor-webapp_dark.png) +{{ else if UI == "NG" }} +![abp-studio-new-solution-dialog-ui-framework-ng](images/abp-studio-no-layers-new-solution-dialog-ui-framework-ng_dark.png) +{{ end }} -![abp-studio-new-solution-dialog-ui-theme](images/abp-studio-nolayers-new-solution-dialog-ui-theme-0.9.13.png) +Here, you see all the possible UI options supported by that startup solution template. Pick the **{{ UI_Value }}**. -LeptonX is the suggested UI theme that is proper for production usage. Select one of the themes, configure the additional options, and click the *Next* button for the *Database Provider* selection: +Notice that; Once you select a UI type, some additional options will be available under the UI Framework list. You can further configure the options or leave them as default and click the Next button for the *Database Provider* selection screen: {{ if DB == "EF" }} -![abp-studio-new-solution-dialog-database-provider](images/abp-studio-no-layers-new-solution-dialog-database-provider-efcore-0.9.13.png) +![abp-studio-new-solution-dialog-database-provider](images/abp-studio-no-layers-new-solution-dialog-database-provider-efcore_dark.png) {{ else }} -![abp-studio-new-solution-dialog-database-provider](images/abp-studio-no-layers-new-solution-dialog-database-provider-mongo-0.9.13.png) +![abp-studio-new-solution-dialog-database-provider](images/abp-studio-no-layers-new-solution-dialog-database-provider-mongo_dark.png) {{ end }} On that screen, you can decide on your database provider by selecting one of the provided options. There are some additional options for each database provider. Leave them as default or change them based on your preferences, then click the *Next* button for additional *Database Configurations*: {{ if DB == "EF" }} -![abp-studio-new-solution-dialog-database-configurations](images/abp-studio-no-layers-new-solution-dialog-database-configurations-efcore-0.9.13.png) +![abp-studio-new-solution-dialog-database-configurations](images/abp-studio-no-layers-new-solution-dialog-database-configurations-efcore_dark.png) {{ else }} -![abp-studio-new-solution-dialog-database-configurations](images/abp-studio-no-layers-new-solution-dialog-database-configurations-mongo-0.9.13.png) +![abp-studio-new-solution-dialog-database-configurations](images/abp-studio-no-layers-new-solution-dialog-database-configurations-mongo_dark.png) {{ end }} -Here, you can select the database management systems (DBMS){{ if DB == "EF" }} and the connection string{{ end }}. Then, you can select optional modules and enable additional options according to your preferences. +Here, you can select the database management systems (DBMS){{ if DB == "EF" }} and the connection string{{ end }}. Then, click the *Next* button for additional *Multi-Tenancy* selection: + +![abp-studio-no-layers-new-solution-dialog-multi-tenancy](images/abp-studio-no-layers-new-solution-dialog-multi-tenancy_dark.png) + +Here, you can enable or disable multi-tenancy for your solution. You can further configure the options or leave them as default and click the *Next* button for the *UI Theme* selection screen: + +![abp-studio-new-solution-dialog-ui-theme](images/abp-studio-nolayers-new-solution-dialog-ui-theme_dark.png) + +LeptonX is the suggested UI theme that is proper for production usage. Select one of the themes, configure the additional options, and click the *Next* button for the *Optional Modules* selection: + +Then, you can select optional modules according to your preferences. + +![abp-studio-no-layers-new-solution-additional-options](images/abp-studio-no-layers-new-solution-optional-modules_dark.png) + +Select the modules you want to use in your project; you can disable the ones you don’t need based on your preferences. Then, click the *Next* button for the *Language Selection* page: + +![abp-studio-no-layers-new-solution-language-selection](images/abp-studio-no-layers-new-solution-language-selection_dark.png) + +In this step, you can choose which languages your application will support. + +* Default Language: Select the main language for your app. + +* Localizable User Interface: Turn this on to support multiple languages. + +* Available Languages: Check the languages you want to include. + +* Click Add Custom Language if you want to add a language that is not listed. + +You can change these settings later if needed. Thenk click the *Next* button for the *Additional Options* page: -![abp-studio-no-layers-new-solution-additional-options](images/abp-studio-no-layers-new-solution-additional-options-0.9.13.png) +![abp-studio-no-layers-new-solution-additional-options](images/abp-studio-no-layers-new-solution-additional-options_dark.png) Configure any additional options as needed and click the *Next* button to continue. On the next screen, you can configure the modularity options for your solution: > If you select the *Setup as a modular solution* option, the solution is created more ready for [modular monolith development](../tutorials/modular-crm/index.md) and allows you to add sub-modules during the solution creation phase. -![abp-studio-no-layers-new-solution-modularity](images/abp-studio-no-layers-new-solution-modularity.png) +![abp-studio-no-layers-new-solution-modularity](images/abp-studio-no-layers-new-solution-modularity_dark.png) Now, we are ready to allow ABP Studio to create our solution. Just click the *Create* button and let the ABP Studio do the rest for you. After clicking the *Create* button, the dialog is closed and your solution is loaded into ABP Studio: -![abp-studio-created-new-solution](images/abp-studio-no-layers-created-new-solution.png) +![abp-studio-created-new-solution](images/abp-studio-no-layers-created-new-solution_dark.png) You can explore the solution, but you need to wait for background tasks to be completed before running any application in the solution. @@ -95,13 +129,13 @@ Open the [Solution Runner](../studio/running-applications.md) section on the lef > The solution runner structure can be different in your case based on the options you've selected. -![abp-studio-quick-start-application-solution-runner](images/abp-studio-no-layers-quick-start-application-solution-runner.png) +![abp-studio-quick-start-application-solution-runner](images/abp-studio-no-layers-quick-start-application-solution-runner_dark.png) Once you click the *Play* icon on the left side, the section is open in the same place as the Solution Explorer section. ABP Studio also opens the *Application Monitor* view on the main content area. *Application Monitor* shows useful insights for your applications (e.g. *HTTP Request*, *Events*, and *Exceptions*) in real-time. You can use it to see the happenings in your applications, so you can easily track errors and many helpful details. In the Solution Runner section (on the left side) you can see all the runnable applications in the current solution. For the MVC website example, we have only one application: -![abp-studio-quick-start-example-applications-in-solution-runner](images/abp-studio-no-layers-quick-start-example-applications-in-solution-runner.png) +![abp-studio-quick-start-example-applications-in-solution-runner](images/abp-studio-no-layers-quick-start-example-applications-in-solution-runner_dark.png) To start an application, either click the *Play* icon near to the application or right-click and select the *Run* -> *Start* context menu item. @@ -109,17 +143,17 @@ You can start the `Acme.BookStore`{{ if UI == "NG" }} and `Acme.BookStore.Angula Once the `Acme.BookStore{{ if UI == "NG" }}.Angular{{ end }}` application started, you can right-click it and select the *Browse* command: -![abp-studio-quick-start-browse-command](images/abp-studio-no-layers-quick-start-browse-command.png) +![abp-studio-quick-start-browse-command](images/abp-studio-no-layers-quick-start-browse-command_dark.png) The *Browse* command opens the UI of the web application in the built-in browser: -![abp-studio-quick-start-browse](images/abp-studio-no-layers-quick-start-browse.png) +![abp-studio-quick-start-browse](images/abp-studio-no-layers-quick-start-browse_dark.png) You can browse your application in a full-featured web browser in ABP Studio. Click the *Login* button in the application UI, enter `admin` as username and `1q2w3E*` as password to login to the application. The following screenshot was taken from the *User Management* page of the [Identity module](../modules/identity.md) that is pre-installed in the application: -![abp-studio-quick-start-browse-user-list](images/abp-studio-no-layers-quick-start-browse-user-list.png) +![abp-studio-quick-start-browse-user-list](images/abp-studio-no-layers-quick-start-browse-user-list_dark.png) ## Open the Solution in Visual Studio @@ -129,7 +163,7 @@ First of all, we can stop the application(s) in ABP Studio, so it won't conflict You can use ABP Studio to open the solution with Visual Studio. Right-click to the `Acme.BookStore` [module](../modules), and select the *Open with* -> *Visual Studio* command: -![abp-studio-open-in-visual-studio](images/abp-studio-no-layers-open-in-visual-studio.png) +![abp-studio-open-in-visual-studio](images/abp-studio-no-layers-open-in-visual-studio_dark.png) If the *Visual Studio* command is not available, that means ABP Studio could not detect it on your computer. You can open the solution folder in your local file system (you can use the *Open with* -> *Explorer* as a shortcut) and manually open the solution in Visual Studio. @@ -137,10 +171,10 @@ Once the solution is opened in Visual Studio, you should see a screen like shown > The solution structure can be different in your case based on the options you've selected. -![visual-studio-bookstore-application](images/no-layers-visual-studio-bookstore-application.png) +![visual-studio-bookstore-application](images/no-layers-visual-studio-bookstore-application_dark.png) You can then hit *F5* or *Ctrl + F5* to run the web application. It will run and open the application UI in your default browser: -![bookstore-browser-users-page](images/no-layers-bookstore-browser-users-page.png) +![bookstore-browser-users-page](images/no-layers-bookstore-browser-users-page_dark.png) You can use `admin` as username and `1q2w3E*` as default password to login to the application. diff --git a/docs/en/images/angular-folder-structure.png b/docs/en/images/angular-folder-structure.png index cfc5c6d0e2..f8b6ea656c 100644 Binary files a/docs/en/images/angular-folder-structure.png and b/docs/en/images/angular-folder-structure.png differ diff --git a/docs/en/images/cmskit-module-page-feedback-widget-2.png b/docs/en/images/cmskit-module-page-feedback-widget-2.png index 45510eebe0..b2702d9b69 100644 Binary files a/docs/en/images/cmskit-module-page-feedback-widget-2.png and b/docs/en/images/cmskit-module-page-feedback-widget-2.png differ diff --git a/docs/en/images/cmskit-module-page-feedback-widget.png b/docs/en/images/cmskit-module-page-feedback-widget.png index cd8264f7eb..b0bf6699ab 100644 Binary files a/docs/en/images/cmskit-module-page-feedback-widget.png and b/docs/en/images/cmskit-module-page-feedback-widget.png differ diff --git a/docs/en/modules/account-pro.md b/docs/en/modules/account-pro.md index c53a6f24ec..594d51da62 100644 --- a/docs/en/modules/account-pro.md +++ b/docs/en/modules/account-pro.md @@ -301,47 +301,41 @@ See the `AccountPermissions` class members for all permissions defined for this #### Installation -In order to configure the application to use the `AccountPublicModule` and the `AccountAdminModule`, you first need to import `AccountPublicConfigModule` from `@volo/abp.ng.account/public/config` and `AccountAdminConfigModule` from `@volo/abp.ng.account/admin/config` to root module. Config modules has a static `forRoot` method which you should call for a proper configuration. +In order to configure the application to use the public account module and the admin account module, you first need to import `provideAccountPublicConfig` from `@volo/abp.ng.account/public/config` and `provideAccountAdminConfig` from `@volo/abp.ng.account/admin/config`. Then, you will need to append them to the `appConfig` array. ```js -// app.module.ts -import { AccountAdminConfigModule } from '@volo/abp.ng.account/admin/config'; -import { AccountPublicConfigModule } from '@volo/abp.ng.account/public/config'; - -@NgModule({ - imports: [ - // other imports - AccountPublicConfigModule.forRoot(), - AccountAdminConfigModule.forRoot(), - // other imports +// app.config.ts +import { provideAccountPublicConfig } from '@volo/abp.ng.account/public/config'; +import { provideAccountAdminConfig } from '@volo/abp.ng.account/admin/config'; + +export const appConfig: ApplicationConfig = { + providers: [ + // ... + provideAccountAdminConfig(), + provideAccountPublicConfig(), ], - // ... -}) -export class AppModule {} +}; ``` -The `AccountPublicModule` should be imported and lazy-loaded in your routing module. It has a static `forLazy` method for configuration. Available options are listed below. It is available for import from `@volo/abp.ng.account/public`. +The account public package should be imported and lazy-loaded in your routing array. It has a static `createRoutes` method for configuration. Available options are listed below. It is available for import from `@volo/abp.ng.account/public`. ```js -// app-routing.module.ts -const routes: Routes = [ - // other route definitions +// app.routes.ts +export const APP_ROUTES: Routes = [ + // ... { path: 'account', - loadChildren: () => - import('@volo/abp.ng.account/public').then(m => m.AccountPublicModule.forLazy(/* options here */)), + loadChildren: () => import('@volo/abp.ng.account/public').then(c => c.createRoutes(/* options here */)), }, ]; -@NgModule(/* AppRoutingModule metadata */) -export class AppRoutingModule {} ``` -> If you have generated your project via the startup template, you do not have to do anything, because it already has the modules. +> If you have generated your project via the startup template, you do not have to do anything, because it already has the necessary configurations.

Options

-You can modify the look and behavior of the module pages by passing the following options to `AccountModule.forLazy` static method: +You can modify the look and behavior of the module pages by passing the following options to `createRoutes` static method: - **redirectUrl**: Default redirect URL after logging in. - **entityActionContributors:** Changes grid actions. Please check [Entity Action Extensions for Angular](../framework/ui/angular/entity-action-extensions.md) for details. diff --git a/docs/en/modules/audit-logging-pro.md b/docs/en/modules/audit-logging-pro.md index cb32ff52fe..607d67539c 100644 --- a/docs/en/modules/audit-logging-pro.md +++ b/docs/en/modules/audit-logging-pro.md @@ -131,7 +131,12 @@ To see `AbpAuditingOptions` properties, please see its [documentation](../framew Configure(options => { options.Period = (int)TimeSpan.FromSeconds(30).TotalMilliseconds; - options.CronExpression = "0 23 * * *"; // This Cron expression only works if Hangfire or Quartz is used for background workers. + + // This Cron expression only works if Hangfire or Quartz is used for background workers. + // The Hangfire Cron expression is different from the Quartz Cron expression, Please refer to the following links: + // https://www.quartz-scheduler.net/documentation/quartz-3.x/tutorial/crontriggers.html#cron-expressions + // https://docs.hangfire.io/en/latest/background-methods/performing-recurrent-tasks.html + options.ExcelFileCleanupOptions.CronExpression = "0 23 * * *"; // Quartz Cron expression is "0 23 * * * ?" }); ``` @@ -147,7 +152,12 @@ Configure(options => options.FileRetentionHours = 24; // How long to keep files before cleanup (default: 24 hours) options.DownloadBaseUrl = "https://yourdomain.com"; // Base URL for download links in emails options.ExcelFileCleanupOptions.Period = (int)TimeSpan.FromHours(24).TotalMilliseconds; // Interval of the cleanup worker (default: 24 hours) - options.ExcelFileCleanupOptions.CronExpression = "0 23 * * *"; // This Cron expression only works if Hangfire or Quartz is used for background workers. + + // This Cron expression only works if Hangfire or Quartz is used for background workers. + // The Hangfire Cron expression is different from the Quartz Cron expression, Please refer to the following links: + // https://www.quartz-scheduler.net/documentation/quartz-3.x/tutorial/crontriggers.html#cron-expressions + // https://docs.hangfire.io/en/latest/background-methods/performing-recurrent-tasks.html + options.ExcelFileCleanupOptions.CronExpression = "0 23 * * *"; // Quartz Cron expression is "0 23 * * * ?" }); ``` @@ -234,45 +244,39 @@ See the `AbpAuditLoggingPermissions` class members for all permissions defined f #### Installation -In order to configure the application to use the `AuditLoggingModule`, you first need to import `AuditLoggingConfigModule` from `@volo/abp.ng.audit-logging/config` to root module. `AuditLoggingConfigModule` has a static `forRoot` method which you should call for a proper configuration. +In order to configure the application to use the audit logging module, you first need to import `provideAuditLoggingConfig` from `@volo/abp.ng.audit-logging/config` to root configuration. Then, you will need to append it to the `appConfig` array. ```js -// app.module.ts -import { AuditLoggingConfigModule } from '@volo/abp.ng.audit-logging/config'; - -@NgModule({ - imports: [ - // other imports - AuditLoggingConfigModule.forRoot(), - // other imports +// app.config.ts +import { provideAuditLoggingConfig } from '@volo/abp.ng.audit-logging/config'; + +export const appConfig: ApplicationConfig = { + providers: [ + // ... + provideAuditLoggingConfig(), ], - // ... -}) -export class AppModule {} +}; ``` -The `AuditLoggingModule` should be imported and lazy-loaded in your routing module. It has a static `forLazy` method for configuration. Available options are listed below. It is available for import from `@volo/abp.ng.audit-logging`. +The audit logging module should be imported and lazy-loaded in your routing array. It has a static `createRoutes` method for configuration. Available options are listed below. It is available for import from `@volo/abp.ng.audit-logging`. ```js -// app-routing.module.ts -const routes: Routes = [ - // other route definitions +// app.routes.ts +export const APP_ROUTES: Routes = [ + // ... { path: 'audit-logs', - loadChildren: () => - import('@volo/abp.ng.audit-logging').then(m => m.AuditLoggingModule.forLazy(/* options here */)), + loadChildren: () => import('@volo/abp.ng.audit-logging').then(c => c.createRoutes(/* options here */)), }, ]; -@NgModule(/* AppRoutingModule metadata */) -export class AppRoutingModule {} ``` -> If you have generated your project via the startup template, you do not have to do anything, because it already has both `AuditLoggingConfigModule` and `AuditLoggingModule`. +> If you have generated your project via the startup template, you do not have to do anything, because it already has both files configured.

Options

-You can modify the look and behavior of the module pages by passing the following options to `AuditLoggingModule.forLazy` static method: +You can modify the look and behavior of the module pages by passing the following options to `createRoutes` static method: - **entityActionContributors:** Changes grid actions. Please check [Entity Action Extensions for Angular](../framework/ui/angular/entity-action-extensions.md) for details. - **toolbarActionContributors:** Changes page toolbar. Please check [Page Toolbar Extensions for Angular](../framework/ui/angular/page-toolbar-extensions.md) for details. diff --git a/docs/en/modules/chat.md b/docs/en/modules/chat.md index 589980ec98..42379b29bc 100644 --- a/docs/en/modules/chat.md +++ b/docs/en/modules/chat.md @@ -206,38 +206,33 @@ See the [connection strings](../framework/fundamentals/connection-strings.md) do #### Installation -In order to configure the application to use the `ChatModule`, you first need to import `ChatConfigModule` from `@volo/abp.ng.chat/config` to root module. `ChatConfigModule` has a static `forRoot` method which you should call for a proper configuration. +In order to configure the application to use the chat module, you first need to import `provideChatConfig` from `@volo/abp.ng.chat/config` to root application confiuration. Then, you will need to append it to the `appConfig` array. ```js -// app.module.ts -import { ChatConfigModule } from '@volo/abp.ng.chat/config'; - -@NgModule({ - imports: [ - // other imports - ChatConfigModule.forRoot(), - // other imports +// app.config.ts +import { provideChatConfig } from '@volo/abp.ng.chat/config'; + +export const appConfig: ApplicationConfig = { + providers: [ + // ... + provideChatConfig(), ], - // ... -}) -export class AppModule {} +}; + ``` -The `ChatModule` should be imported and lazy-loaded in your routing module. It has a static `forLazy` method for configuration. It is available for import from `@volo/abp.ng.chat`. +The chat module should be imported and lazy-loaded in your routing array. It has a static `createRoutes` method for configuration. It is available for import from `@volo/abp.ng.chat`. ```js -// app-routing.module.ts -const routes: Routes = [ - // other route definitions +// app.routes.ts +const APP_ROUTES: Routes = [ + // ... { path: 'chat', loadChildren: () => - import('@volo/abp.ng.chat').then(m => m.ChatModule.forLazy(/* options here */)), + import('@volo/abp.ng.chat').then(c => c.createRoutes(/* options here */)), }, ]; - -@NgModule(/* AppRoutingModule metadata */) -export class AppRoutingModule {} ``` #### Services / Models diff --git a/docs/en/modules/cms-kit-pro/page-feedback.md b/docs/en/modules/cms-kit-pro/page-feedback.md index e480e024fd..dd519eb15b 100644 --- a/docs/en/modules/cms-kit-pro/page-feedback.md +++ b/docs/en/modules/cms-kit-pro/page-feedback.md @@ -2,10 +2,9 @@ > You must have an ABP Team or a higher license to use CMS Kit Pro module's features. -The CMS Kit provides a **Page Feedback** system to collect feedback from users about pages. +The CMS Kit Pro module provides a comprehensive **Page Feedback** system that enables you to collect valuable user feedback about your website pages. This system allows visitors to quickly rate their experience and provide comments, helping you understand user satisfaction and identify areas for improvement. -| ![cmskit-module-page-feedback-widget](../../images/cmskit-module-page-feedback-widget.png) | ![cmskit-module-page-feedback-widget](../../images/cmskit-module-page-feedback-widget-2.png) | -| ------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------- | +![cmskit-module-page-feedback-widget](../../images/cmskit-module-page-feedback-widget.png) ## Enabling the Page Feedback System diff --git a/docs/en/modules/cms-kit/index.md b/docs/en/modules/cms-kit/index.md index 5c30806028..3744ce3590 100644 --- a/docs/en/modules/cms-kit/index.md +++ b/docs/en/modules/cms-kit/index.md @@ -72,6 +72,23 @@ CMS kit packages are designed for various usage scenarios. If you check the [CMS - `Volo.CmsKit.Public.*` packages contain the functionalities used in public websites where users read blog posts or leave comments. - `Volo.CmsKit.*` (without Admin/Public suffix) packages are called as unified packages. Unified packages are shortcuts for adding Admin & Public packages (of the related layer) separately. If you have a single application for administration and public web site, you can use these packages. +## Integrating Public and Admin Packages in a Unified Application + +If you are using a single application for both admin and public web site, it's important to configure the global layout settings appropriately. By default, the layout is set for a **Public Website**, which is suitable for public-facing pages. However, when your application serves both admin and public pages, you should explicitly set the global layout for all CMS Kit pages. + +To do this, add a `_ViewStart.cshtml` file to your web project at `/Pages/Public/CmsKit/_ViewStart.cshtml` and configure the layout as shown below: + +```html +@using Volo.Abp.AspNetCore.Mvc.UI.Theming +@inject IThemeManager ThemeManager +@{ + // default: GetPublicLayout() + Layout = ThemeManager.CurrentTheme.GetApplicationLayout(); +} +``` + +> The `_ViewStart.cshtml` file is used to set the layout for all pages in the `CmsKit` folder. + ## Internals ### Table / collection prefix & schema diff --git a/docs/en/modules/docs.md b/docs/en/modules/docs.md index b564980ba3..b9c3209e3f 100644 --- a/docs/en/modules/docs.md +++ b/docs/en/modules/docs.md @@ -148,11 +148,6 @@ An ABP module must declare `[DependsOn]` attribute if it has a dependency upon a { options.DefinitionProviders.Add(); }); - - Configure(options => - { - options.AddProfile(); - }); } } ``` diff --git a/docs/en/modules/gdpr.md b/docs/en/modules/gdpr.md index bef763a41a..43e38eb6b2 100644 --- a/docs/en/modules/gdpr.md +++ b/docs/en/modules/gdpr.md @@ -177,45 +177,46 @@ See the [connection strings](../framework/fundamentals/connection-strings.md) do ### Installation -In order to configure the application to use the `GdprModule`, you first need to import `GdprConfigModule` from `@volo/abp.ng.gdpr/config` to the root module. `GdprConfigModule` has a static `forRoot` method which you should call for a proper configuration. +In order to configure the application to use the gdpr module, you first need to import `provideGdprConfig` from `@volo/abp.ng.gdpr/config` to the root configuration. Then, you will need to append it to the `appConfig` array. ```js -// app.module.ts -import { GdprConfigModule } from '@volo/abp.ng.gdpr/config'; - -@NgModule({ - imports: [ - // other imports - GdprConfigModule.forRoot(), - // other imports +// app.config.ts +import { + provideGdprConfig, + withCookieConsentOptions, +} from '@volo/abp.ng.gdpr/config'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideGdprConfig( + withCookieConsentOptions({ + cookiePolicyUrl: '/gdpr-cookie-consent/cookie', + privacyPolicyUrl: '/gdpr-cookie-consent/privacy', + }), + ), ], - // ... -}) -export class AppModule {} +}; ``` -The `GdprModule` should be imported and lazy-loaded in your routing module. It has a static `forLazy` method for configuration. Available options are listed below. It is available for import from `@volo/abp.ng.gdpr`. +The gdpr module should be imported and lazy-loaded in your routing array. It has a static `createRoutes` method for configuration. Available options are listed below. It is available for import from `@volo/abp.ng.gdpr`. ```js -// app-routing.module.ts -const routes: Routes = [ +// app.routes.ts +const APP_ROUTES: Routes = [ // other route definitions { path: 'gdpr', loadChildren: () => - import('@volo/abp.ng.gdpr').then(m => m.GdprModule.forLazy(/* options here */)), + import('@volo/abp.ng.gdpr').then(c => c.createRoutes(/* options here */)), }, ]; - -@NgModule(/* AppRoutingModule metadata */) -export class AppRoutingModule {} ``` -> If you have generated your project via the startup template, you do not have to do anything, because it already has both `GdprConfigModule` and `GdprModule`. +> If you have generated your project via the startup template, you do not have to do anything, because it already has both files configured.

Options

-You can modify the look and behavior of the module pages by passing the following options to the `GdprModule.forLazy` static method: +You can modify the look and behavior of the module pages by passing the following options to the `createRoutes` static method: - **entityActionContributors:** Changes the grid actions. Please check [Entity Action Extensions for Angular](../framework/ui/angular/entity-action-extensions.md) for details. - **toolbarActionContributors:** Changes the page toolbar. Please check [Page Toolbar Extensions for Angular](../framework/ui/angular/page-toolbar-extensions.md) for details. diff --git a/docs/en/modules/identity-pro.md b/docs/en/modules/identity-pro.md index ab25a8d5c8..6649a335e3 100644 --- a/docs/en/modules/identity-pro.md +++ b/docs/en/modules/identity-pro.md @@ -343,45 +343,39 @@ See the `IdentityPermissions` class members for all permissions defined for this #### Installation -In order to configure the application to use the `IdentityModule`, you first need to import `IdentityConfigModule` from `@volo/abp.ng.identity/config` to root module. `IdentityConfigModule` has a static `forRoot` method which you should call for a proper configuration. +In order to configure the application to use the identity module, you first need to import `provideIdentityConfig` from `@volo/abp.ng.identity/config` to root configuration. Then, you will need to append it to the `appConfig` array. ```js -// app.module.ts -import { IdentityConfigModule } from '@volo/abp.ng.identity/config'; - -@NgModule({ - imports: [ - // other imports - IdentityConfigModule.forRoot(), - // other imports +// app.config.ts +import { provideIdentityConfig } from '@volo/abp.ng.identity/config'; + +export const appConfig: ApplicationConfig = { + providers: [ + // ... + provideIdentityConfig(), ], - // ... -}) -export class AppModule {} +}; ``` -The `IdentityModule` should be imported and lazy-loaded in your routing module. It has a static `forLazy` method for configuration. Available options are listed below. It is available for import from `@volo/abp.ng.identity`. +The identity module should be imported and lazy-loaded in your routing configuration. It has a static `createRoutes` method for configuration. Available options are listed below. It is available for import from `@volo/abp.ng.identity`. ```js -// app-routing.module.ts -const routes: Routes = [ - // other route definitions +// app.routes.ts +const APP_ROUTES: Routes = [ + // ... { path: 'identity', loadChildren: () => - import('@volo/abp.ng.identity').then(m => m.IdentityModule.forLazy(/* options here */)), + import('@volo/abp.ng.identity').then(c => c.createRoutes(/* options here */)), }, ]; - -@NgModule(/* AppRoutingModule metadata */) -export class AppRoutingModule {} ``` -> If you have generated your project via the startup template, you do not have to do anything, because it already has both `IdentityConfigModule` and `IdentityModule`. +> If you have generated your project via the startup template, you do not have to do anything, because it already has both configurations added.

Options

-You can modify the look and behavior of the module pages by passing the following options to `IdentityModule.forLazy` static method: +You can modify the look and behavior of the module pages by passing the following options to `createRoutes` static method: - **entityActionContributors:** Changes grid actions. Please check [Entity Action Extensions for Angular](../framework/ui/angular/entity-action-extensions.md) for details. - **toolbarActionContributors:** Changes page toolbar. Please check [Page Toolbar Extensions for Angular](../framework/ui/angular/page-toolbar-extensions.md) for details. diff --git a/docs/en/modules/identity-server-pro.md b/docs/en/modules/identity-server-pro.md index ce25823ee8..ad197e8a7d 100644 --- a/docs/en/modules/identity-server-pro.md +++ b/docs/en/modules/identity-server-pro.md @@ -245,45 +245,39 @@ See the `AbpIdentityServerPermissions` class members for all permissions defined #### Installation -In order to configure the application to use the `IdentityServerModule`, you first need to import `IdentityServerConfigModule` from `@volo/abp.ng.identity-server/config` to root module. `IdentityServerConfigModule` has a static `forRoot` method which you should call for a proper configuration. +In order to configure the application to use the identity server, you first need to import `provideIdentityServerConfig` from `@volo/abp.ng.identity-server/config` to root configuration. Then, you will need to append it to the `appConfig` array. ```js -// app.module.ts -import { IdentityServerConfigModule } from '@volo/abp.ng.identity-server/config'; - -@NgModule({ - imports: [ - // other imports - IdentityServerConfigModule.forRoot(), - // other imports +// app.config.ts +import { provideIdentityServerConfig } from '@volo/abp.ng.identity-server/config'; + +export const appConfig: ApplicationConfig = { + providers: [ + // ... + provideIdentityServerConfig() ], - // ... -}) -export class AppModule {} +}; ``` -The `IdentityServerModule` should be imported and lazy-loaded in your routing module. It has a static `forLazy` method for configuration. Available options are listed below. It is available for import from `@volo/abp.ng.identity-server`. +The identity server module should be imported and lazy-loaded in your routing module. It has a static `creatRoutes` method for configuration. Available options are listed below. It is available for import from `@volo/abp.ng.identity-server`. ```js -// app-routing.module.ts -const routes: Routes = [ +// app.routes.ts +const APP_ROUTES: Routes = [ // other route definitions { path: 'identity-server', loadChildren: () => - import('@volo/abp.ng.identity-server').then(m => m.IdentityServerModule.forLazy(/* options here */)), + import('@volo/abp.ng.identity-server').then(c => c.createRoutes(/* options here */)), }, ]; - -@NgModule(/* AppRoutingModule metadata */) -export class AppRoutingModule {} ``` -> If you have generated your project via the startup template, you do not have to do anything, because it already has both `IdentityServerConfigModule` and `IdentityServerModule`. +> If you have generated your project via the startup template, you do not have to do anything, because it already has both files configured.

Options

-You can modify the look and behavior of the module pages by passing the following options to `IdentityServerModule.forLazy` static method: +You can modify the look and behavior of the module pages by passing the following options to `createRoutes` static method: - **entityActionContributors:** Changes grid actions. Please check [Entity Action Extensions for Angular](../framework/ui/angular/entity-action-extensions.md) for details. - **toolbarActionContributors:** Changes page toolbar. Please check [Page Toolbar Extensions for Angular](../framework/ui/angular/page-toolbar-extensions.md) for details. diff --git a/docs/en/modules/language-management.md b/docs/en/modules/language-management.md index 62327378a4..536abc7ab3 100644 --- a/docs/en/modules/language-management.md +++ b/docs/en/modules/language-management.md @@ -149,45 +149,41 @@ See the `LanguageManagementPermissions` class members for all permissions define #### Installation -To configure the application to use the `LanguageManagementModule`, you first need to import `LanguageManagementConfigModule` from `@volo/abp.ng.language-management/config` to root module. `LanguageManagementConfigModule` has a static `forRoot` method which you should call for a proper configuration. +To configure the application to use the language management module, you first need to import `provideLanguageManagementConfig` from `@volo/abp.ng.language-management/config` to root configuration. Then, you will need to append it to the `appConfig` array. ```js -// app.module.ts -import { LanguageManagementConfigModule } from '@volo/abp.ng.language-management/config'; - -@NgModule({ - imports: [ - // other imports - LanguageManagementConfigModule.forRoot(), - // other imports +// app.config.ts +import { provideLanguageManagementConfig } from '@volo/abp.ng.language-management/config'; + +export const appConfig: ApplicationConfig = { + providers: [ + // ... + provideLanguageManagementConfig() ], - // ... -}) -export class AppModule {} +}; + ``` -The `LanguageManagementModule` should be imported and lazy-loaded in your routing module. It has a static `forLazy` method for configuration. Available options are listed below. It is available for import from `@volo/abp.ng.language-management`. +The language management module should be imported and lazy-loaded in your routing array. It has a static `createRoutes` method for configuration. Available options are listed below. It is available for import from `@volo/abp.ng.language-management`. ```js -// app-routing.module.ts -const routes: Routes = [ - // other route definitions +// app.routes.ts + +const APP_ROUTES: Routes = [ + // ... { path: 'language-management', loadChildren: () => - import('@volo/abp.ng.language-management').then(m => m.LanguageManagementModule.forLazy(/* options here */)), + import('@volo/abp.ng.language-management').then(c => c.createRoutes(/* options here */)), }, ]; - -@NgModule(/* AppRoutingModule metadata */) -export class AppRoutingModule {} ``` -> If you have generated your project via the startup template, you do not have to do anything because it already has both `LanguageManagementConfigModule` and `LanguageManagementModule`. +> If you have generated your project via the startup template, you do not have to do anything because it already has both configurations implemented.

Options

-You can modify the look and behavior of the module pages by passing the following options to `LanguageManagementModule.forLazy` static method: +You can modify the look and behavior of the module pages by passing the following options to `createRoutes` static method: - **entityActionContributors:** Changes grid actions. Please check [Entity Action Extensions for Angular](../framework/ui/angular/entity-action-extensions.md) for details. - **toolbarActionContributors:** Changes page toolbar. Please check [Page Toolbar Extensions for Angular](../framework/ui/angular/page-toolbar-extensions.md) for details. diff --git a/docs/en/modules/payment.md b/docs/en/modules/payment.md index b3fb6de84b..6a55cc83ae 100644 --- a/docs/en/modules/payment.md +++ b/docs/en/modules/payment.md @@ -81,38 +81,36 @@ This page is used to send Name, Surname and Email Address of user to PayU. #### Installation -In order to configure the application to use the `PaymentModule`, you first need to import `PaymentAdminConfigModule` from `@volo/abp.ng.payment/admin/config` to the root module. `PaymentAdminConfigModule` has a static `forRoot` method which you should call for a proper configuration: +In order to configure the application to use the payment module, you first need to import `PaymentAdminConfigModule` from `@volo/abp.ng.payment/admin/config` to the root configuration. `PaymentAdminConfigModule` has a static `forRoot` method which you should call for a proper configuration: ```js -// app.module.ts +// app.config.ts +import { ApplicationConfig, importProvidersFrom } from '@angular/core'; import { PaymentAdminConfigModule } from '@volo/abp.ng.payment/admin/config'; -@NgModule({ - imports: [ - // other imports - PaymentAdminConfigModule.forRoot(), - // other imports +export const appConfig: ApplicationConfig = { + providers: [ + // ... + importProvidersFrom([ + PaymentAdminConfigModule.forRoot() + ]), ], - // ... -}) -export class AppModule {} +}; + ``` -The `PaymentAdminModule` should be imported and lazy-loaded in your routing module as below: +The payment admin module should be imported and lazy-loaded in your routing array as below: ```js -// app-routing.module.ts -const routes: Routes = [ - // other route definitions +// app.routes.ts +const APP_ROUTES: Routes = [ + // ... { path: 'payment', loadChildren: () => - import('@volo/abp.ng.payment/admin').then(m => m.PaymentAdminModule.forLazy()), + import('@volo/abp.ng.payment/admin').then(c => c.createRoutes()), }, ]; - -@NgModule(/* AppRoutingModule metadata */) -export class AppRoutingModule {} ``` #### Payment plans page diff --git a/docs/en/modules/saas.md b/docs/en/modules/saas.md index bfd0115853..f1e85d6ceb 100644 --- a/docs/en/modules/saas.md +++ b/docs/en/modules/saas.md @@ -244,45 +244,39 @@ See the `SaasHostPermissions` class members for all permissions defined for this #### Installation -In order to configure the application to use the `SaasModule`, you first need to import `SaasConfigModule` from `@volo/abp.ng.saas/config` to root module. `SaasConfigModule` has a static `forRoot` method which you should call for a proper configuration. +In order to configure the application to use the saas module, you first need to import `provideSaasConfig` from `@volo/abp.ng.saas/config` to root module. Then, you will need to append it to the `appConfig` array. ```js -// app.module.ts -import { SaasConfigModule } from '@volo/abp.ng.saas/config'; - -@NgModule({ - imports: [ - // other imports - SaasConfigModule.forRoot(), - // other imports +// app.config.ts +import { provideSaasConfig } from '@volo/abp.ng.saas/config'; + +export const appConfig: ApplicationConfig = { + providers: [ + // ... + provideSaasConfig(), ], - // ... -}) -export class AppModule {} +}; ``` -The `SaasModule` should be imported and lazy-loaded in your routing module. It has a static `forLazy` method for configuration. Available options are listed below. It is available for import from `@volo/abp.ng.saas`. +The saas module should be imported and lazy-loaded in your routing configuration. It has a static `createRoutes` method for configuration. Available options are listed below. It is available for import from `@volo/abp.ng.saas`. ```js -// app-routing.module.ts -const routes: Routes = [ - // other route definitions +// app.routes.ts +const APP_ROUTES: Routes = [ + // ... { path: 'saas', loadChildren: () => - import('@volo/abp.ng.saas').then(m => m.SaasModule.forLazy(/* options here */)), + import('@volo/abp.ng.saas').then(c => c.createRoutes(/* options here */)), }, ]; - -@NgModule(/* AppRoutingModule metadata */) -export class AppRoutingModule {} ``` -> If you have generated your project via the startup template, you do not have to do anything, because it already has both `SaasConfigModule` and `SaasModule`. +> If you have generated your project via the startup template, you do not have to do anything, because it already has both configurations implemented.

Options

-You can modify the look and behavior of the module pages by passing the following options to `SaasModule.forLazy` static method: +You can modify the look and behavior of the module pages by passing the following options to `createRoutes` static method: - **entityActionContributors:** Changes grid actions. Please check [Entity Action Extensions for Angular](../framework/ui/angular/entity-action-extensions.md) for details. - **toolbarActionContributors:** Changes page toolbar. Please check [Page Toolbar Extensions for Angular](../framework/ui/angular/page-toolbar-extensions.md) for details. diff --git a/docs/en/modules/text-template-management.md b/docs/en/modules/text-template-management.md index 9b2235314f..7f045cd8e1 100644 --- a/docs/en/modules/text-template-management.md +++ b/docs/en/modules/text-template-management.md @@ -162,45 +162,39 @@ See the `TextTemplateManagementPermissions` class members for all permissions de #### Installation -In order to configure the application to use the `TextTemplateManagementModule`, you first need to import `TextTemplateManagementConfigModule` from `@volo/abp.ng.text-template-management/config` to root module. `TextTemplateManagementConfigModule` has a static `forRoot` method which you should call for a proper configuration. +In order to configure the application to use the text template management module, you first need to import `provideTextTemplateManagementConfig` from `@volo/abp.ng.text-template-management/config` to root configuration. Then, you will need to append it to the `appConfig` array. ```js -// app.module.ts -import { TextTemplateManagementConfigModule } from '@volo/abp.ng.text-template-management/config'; - -@NgModule({ - imports: [ - // other imports - TextTemplateManagementConfigModule.forRoot(), - // other imports +// app.config.ts +import { provideTextTemplateManagementConfig } from '@volo/abp.ng.text-template-management/config'; + +export const appConfig: ApplicationConfig = { + providers: [ + // ... + provideTextTemplateManagementConfig() ], - // ... -}) -export class AppModule {} +}; ``` -The `TextTemplateManagementModule` should be imported and lazy-loaded in your routing module. It has a static `forLazy` method for configuration. Available options are listed below. It is available for import from `@volo/abp.ng.text-template-management`. +The text template management module should be imported and lazy-loaded in your routing array. It has a static `createRoutes` method for configuration. Available options are listed below. It is available for import from `@volo/abp.ng.text-template-management`. ```js -// app-routing.module.ts -const routes: Routes = [ - // other route definitions +// app.routes.ts +const APP_ROUTES: Routes = [ + // ... { path: 'text-template-management', loadChildren: () => - import('@volo/abp.ng.text-template-management').then(m => m.TextTemplateManagementModule.forLazy(/* options here */)), + import('@volo/abp.ng.text-template-management').then(c => c.createRoutes(/* options here */)), }, ]; - -@NgModule(/* AppRoutingModule metadata */) -export class AppRoutingModule {} ``` -> If you have generated your project via the startup template, you do not have to do anything, because it already has both `TextTemplateManagementConfigModule` and `TextTemplateManagementModule`. +> If you have generated your project via the startup template, you do not have to do anything, because it already has both configurations implemented.

Options

-You can modify the look and behavior of the module pages by passing the following options to `TextTemplateManagementModule.forLazy` static method: +You can modify the look and behavior of the module pages by passing the following options to `createRoutes` static method: - **entityActionContributors:** Changes grid actions. Please check [Entity Action Extensions for Angular](../framework/ui/angular/entity-action-extensions.md) for details. - **toolbarActionContributors:** Changes page toolbar. Please check [Page Toolbar Extensions for Angular](../framework/ui/angular/page-toolbar-extensions.md) for details. diff --git a/docs/en/others/why-abp-platform.md b/docs/en/others/why-abp-platform.md index c13631546b..61756d2654 100644 --- a/docs/en/others/why-abp-platform.md +++ b/docs/en/others/why-abp-platform.md @@ -169,7 +169,7 @@ The learning curve is much lower than not using the ABP. That may sound surprisi ABP creates a full stack, production-ready, working solution for you in seconds. Many of the real-life problems are already solved and many fine tune configurations are already applied for the ASP.NET Core and the other used libraries. If you start from scratch, you will experience and learn all these details yourself to truly implement your solution. -ABP uses the industry standard frameworks, libraries and systems you already know (or need to learn to build a real-world product) like Angular, Blazor, MAUI, EF Core, AutoMapper, OpenIddict, Bootstrap, Redis, SignalR... etc. So, all your knowledge is directly re-usable with the ABP. ABP even simplifies using these libraries and systems and solves the integration problems. If you don't know these tools now, learning them will be easier within the ABP. +ABP uses the industry standard frameworks, libraries and systems you already know (or need to learn to build a real-world product) like Angular, Blazor, MAUI, EF Core, AutoMapper (switched to Mapperly due to licensing concerns), OpenIddict, Bootstrap, Redis, SignalR... etc. So, all your knowledge is directly re-usable with the ABP. ABP even simplifies using these libraries and systems and solves the integration problems. If you don't know these tools now, learning them will be easier within the ABP. ABP provides an excellent infrastructure to apply DDD principles and other best practices. It provides a lot of sweet abstractions and automation to reduce the repeating code. However, it doesn't force you to use or apply all these. A common mistake is to see that ABP has a lot of features, and it is hard to learn all of them. Having a lot of features is an advantage when you come to the point that you need them. However, you don't need to know a feature until you need it, and you can continue with the development approach you are used to. You can still write code as you are used to as if ABP doesn't provide all these benefits. Learning the ABP infrastructure is progressive. You will love it whenever you learn a new feature but can continue developing without knowing its existence. diff --git a/docs/en/release-info/migration-guides/AutoMapper-To-Mapperly.md b/docs/en/release-info/migration-guides/AutoMapper-To-Mapperly.md new file mode 100644 index 0000000000..7356a6930b --- /dev/null +++ b/docs/en/release-info/migration-guides/AutoMapper-To-Mapperly.md @@ -0,0 +1,376 @@ +# Migrating from AutoMapper to Mapperly + +## Introduction + +The AutoMapper library is **no longer free for commercial use**. For more details, you can refer to [this announcement post](https://www.jimmybogard.com/automapper-and-mediatr-going-commercial/). + +ABP Framework provides both AutoMapper and Mapperly integrations. If your project currently uses AutoMapper and you don't have a commercial license, you can switch to Mapperly by following the steps outlined below. + +## Migration Steps + +Please open your project in an IDE(`Visual Studio`, `VS Code` or `JetBrains Rider`), then perform the following global search and replace operations: + +1. Replace `Volo.Abp.AutoMapper` with `Volo.Abp.Mapperly` in all `*.csproj` files. +2. Replace `using Volo.Abp.AutoMapper;` with `using Volo.Abp.Mapperly;` in all `*.cs` files. +3. Replace `AbpAutoMapperModule` with `AbpMapperlyModule` in all `*.cs` files. +4. Replace `AddAutoMapperObjectMapper` with `AddMapperlyObjectMapper` in all `*.cs` files. +5. Remove any code sections that configure `Configure`. +6. Review any existing AutoMapper `Profile` classes and ensure all newly created Mapperly mapping classes are registered appropriately. (You can refer to the example below for guidance) + +**Example:** + +Here is an AutoMapper's `Profile` class: + +```csharp +public class ExampleAutoMapper : Profile +{ + public AbpIdentityApplicationModuleAutoMapperProfile() + { + CreateMap() + .MapExtraProperties() + .Ignore(x => x.IsLockedOut) + .Ignore(x => x.SupportTwoFactor) + .Ignore(x => x.RoleNames); + + CreateMap(); + + CreateMap() + .MapExtraProperties(); + + CreateMap() + .ReverseMap(); + + CreateMap() + .ForMember(dest => dest.RoleId, src => src.MapFrom(r => r.Id)); + + CreateMap() + .ForMember(dest => dest.Active, src => src.MapFrom(r => r.IsActive ? "Yes" : "No")) + .ForMember(dest => dest.EmailConfirmed, src => src.MapFrom(r => r.EmailConfirmed ? "Yes" : "No")) + .ForMember(dest => dest.TwoFactorEnabled, src => src.MapFrom(r => r.TwoFactorEnabled ? "Yes" : "No")) + .ForMember(dest => dest.AccountLookout, src => src.MapFrom(r => r.LockoutEnd != null && r.LockoutEnd > DateTime.UtcNow ? "Yes" : "No")) + .Ignore(x => x.Roles); + } +} +``` + +And the Mapperly mapping class: + +```csharp +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class IdentityUserToIdentityUserDtoMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(IdentityUserDto.IsLockedOut))] + [MapperIgnoreTarget(nameof(IdentityUserDto.SupportTwoFactor))] + [MapperIgnoreTarget(nameof(IdentityUserDto.RoleNames))] + public override partial IdentityUserDto Map(IdentityUser source); + + [MapperIgnoreTarget(nameof(IdentityUserDto.IsLockedOut))] + [MapperIgnoreTarget(nameof(IdentityUserDto.SupportTwoFactor))] + [MapperIgnoreTarget(nameof(IdentityUserDto.RoleNames))] + public override partial void Map(IdentityUser source, IdentityUserDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class IdentityUserClaimToIdentityUserClaimDtoMapper : MapperBase +{ + public override partial IdentityUserClaimDto Map(IdentityUserClaim source); + + public override partial void Map(IdentityUserClaim source, IdentityUserClaimDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class OrganizationUnitToOrganizationUnitDtoMapper : MapperBase +{ + public override partial OrganizationUnitDto Map(OrganizationUnit source); + public override partial void Map(OrganizationUnit source, OrganizationUnitDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class OrganizationUnitRoleToOrganizationUnitRoleDtoMapper : TwoWayMapperBase +{ + public override partial OrganizationUnitRoleDto Map(OrganizationUnitRole source); + public override partial void Map(OrganizationUnitRole source, OrganizationUnitRoleDto destination); + + public override partial OrganizationUnitRole ReverseMap(OrganizationUnitRoleDto destination); + public override partial void ReverseMap(OrganizationUnitRoleDto destination, OrganizationUnitRole source); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class OrganizationUnitToOrganizationUnitWithDetailsDtoMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(OrganizationUnitWithDetailsDto.Roles))] + [MapperIgnoreTarget(nameof(OrganizationUnitWithDetailsDto.UserCount))] + public override partial OrganizationUnitWithDetailsDto Map(OrganizationUnit source); + + [MapperIgnoreTarget(nameof(OrganizationUnitWithDetailsDto.Roles))] + [MapperIgnoreTarget(nameof(OrganizationUnitWithDetailsDto.UserCount))] + public override partial void Map(OrganizationUnit source, OrganizationUnitWithDetailsDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class IdentityRoleToOrganizationUnitRoleDtoMapper : MapperBase +{ + [MapProperty(nameof(IdentityRole.Id), nameof(OrganizationUnitRoleDto.RoleId))] + public override partial OrganizationUnitRoleDto Map(IdentityRole source); + + [MapProperty(nameof(IdentityRole.Id), nameof(OrganizationUnitRoleDto.RoleId))] + public override partial void Map(IdentityRole source, OrganizationUnitRoleDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class IdentityUserToIdentityUserExportDtoMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(IdentityUserExportDto.Roles))] + public override partial IdentityUserExportDto Map(IdentityUser source); + + [MapperIgnoreTarget(nameof(IdentityUserExportDto.Roles))] + public override partial void Map(IdentityUser source, IdentityUserExportDto destination); + + public override void AfterMap(IdentityUser source, IdentityUserExportDto destination) + { + destination.Active = source.IsActive ? "Yes" : "No"; + destination.EmailConfirmed = source.EmailConfirmed ? "Yes" : "No"; + destination.TwoFactorEnabled = source.TwoFactorEnabled ? "Yes" : "No"; + destination.AccountLookout = source.LockoutEnd != null && source.LockoutEnd > DateTime.UtcNow ? "Yes" : "No"; + } +} +``` + +## Mapperly Mapping Class + +To use Mapperly, you'll need to create a dedicated mapping class for each source and destination types. + +* Use the `[Mapper]` attribute to designate the class as a Mapperly mapper. The `RequiredMappingStrategy` is set to `Target` by default. +* Replace AutoMapper's `Ignore()` method with the `[MapperIgnoreTarget]` attribute. +* Replace the `MapExtraProperties()` method with the `[MapExtraProperties]` attribute. +* Use the `TwoWayMapperBase` class as an alternative to AutoMapper’s `ReverseMap()` functionality. +* Implement the `AfterMap()` method to execute logic after the mapping is completed. + +### Dependency Injection in Mapper Class + +All Mapperly mapping classes automatically registered in the [dependency injection (DI)](../../framework/fundamentals/dependency-injection.md) container. To use a service within a Mapper class, simply add it to the constructor; Mapperly will inject it automatically. + +**Example:** + +```csharp +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class IdentityUserToIdentityUserDtoMapper : MapperBase +{ + public IdentityUserToIdentityUserDtoMapper(MyService myService) + { + _myService = myService; + } + + public override partial IdentityUserDto Map(IdentityUser source); + public override partial void Map(IdentityUser source, IdentityUserDto destination); + + public override void AfterMap(IdentityUser source, IdentityUserDto destination) + { + destination.MyProperty = _myService.GetMyProperty(source.MyProperty); + } +} +``` + +## AI Prompt for Migrating AutoMapper to Mapperly + +If you have AI tools like Cursor, you can use the following prompt to migrate your AutoMapper mappings to Mapperly automatically: + +> AI may generate some code that is not correct. Please check the code carefully. + +``` +Please help me migrate AutoMapper Profile classes to Mapperly. I have AutoMapper Profile files in my current workspace/context that need to be converted. + +**Conversion Requirements:** + +1. **Convert AutoMapper Profile to Mapperly Mappers**: Transform each `CreateMap` into a separate Mapperly mapper class +2. **Rename the file**: Change from `XXXAutoMapperProfile.cs` to `XXXMappers.cs` +3. **Use proper Mapperly attributes**: + - `[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)]` for each mapper class + - `[MapExtraProperties]` for classes that need extra properties mapping + - `[MapperIgnoreTarget]` for ignored properties + - `[MapProperty]` for custom property mappings +4. **Inherit from appropriate base classes**: + - `MapperBase` for one-way mapping + - `TwoWayMapperBase` for reverse mapping +5. **Handle complex mappings**: Use `AfterMap` method for complex transformations + +**Note:** The code below contains two parts - both are reference examples for you to understand the conversion pattern: +1. **AutoMapper Profile example** - shows the original AutoMapper code structure +2. **Mapperly Mappers example** - shows the expected converted Mapperly code structure + +Please convert the actual AutoMapper Profile files that exist in your current context/workspace, following the same conversion pattern as shown in these examples. + +**Reference Examples:** + +**1. AutoMapper Profile (original code):** + +using System; +using AutoMapper; +using System.Linq; +using Volo.Abp.AutoMapper; + +namespace Volo.Abp.Identity; + +public class ExampleAutoMapperProfile : Profile +{ + public ExampleAutoMapperProfile() + { + CreateMap() + .MapExtraProperties() + .Ignore(x => x.IsLockedOut) + .Ignore(x => x.SupportTwoFactor) + .Ignore(x => x.RoleNames); + + CreateMap(); + + CreateMap() + .MapExtraProperties(); + + CreateMap() + .ReverseMap(); + + CreateMap() + .ForMember(dest => dest.RoleId, src => src.MapFrom(r => r.Id)); + + CreateMap() + .ForMember(dest => dest.Active, src => src.MapFrom(r => r.IsActive ? "Yes" : "No")) + .ForMember(dest => dest.EmailConfirmed, src => src.MapFrom(r => r.EmailConfirmed ? "Yes" : "No")) + .ForMember(dest => dest.TwoFactorEnabled, src => src.MapFrom(r => r.TwoFactorEnabled ? "Yes" : "No")) + .ForMember(dest => dest.AccountLookout, src => src.MapFrom(r => r.LockoutEnd != null && r.LockoutEnd > DateTime.UtcNow ? "Yes" : "No")) + .Ignore(x => x.Roles); + } +} + +--- + +**2. Mapperly Mappers (converted code):** + +using System; +using System.Linq; +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; + +namespace Volo.Abp.Identity; + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class IdentityUserToIdentityUserDtoMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(IdentityUserDto.IsLockedOut))] + [MapperIgnoreTarget(nameof(IdentityUserDto.SupportTwoFactor))] + [MapperIgnoreTarget(nameof(IdentityUserDto.RoleNames))] + public override partial IdentityUserDto Map(IdentityUser source); + + [MapperIgnoreTarget(nameof(IdentityUserDto.IsLockedOut))] + [MapperIgnoreTarget(nameof(IdentityUserDto.SupportTwoFactor))] + [MapperIgnoreTarget(nameof(IdentityUserDto.RoleNames))] + public override partial void Map(IdentityUser source, IdentityUserDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class IdentityUserClaimToIdentityUserClaimDtoMapper : MapperBase +{ + public override partial IdentityUserClaimDto Map(IdentityUserClaim source); + + public override partial void Map(IdentityUserClaim source, IdentityUserClaimDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class OrganizationUnitToOrganizationUnitDtoMapper : MapperBase +{ + public override partial OrganizationUnitDto Map(OrganizationUnit source); + public override partial void Map(OrganizationUnit source, OrganizationUnitDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class OrganizationUnitRoleToOrganizationUnitRoleDtoMapper : TwoWayMapperBase +{ + public override partial OrganizationUnitRoleDto Map(OrganizationUnitRole source); + public override partial void Map(OrganizationUnitRole source, OrganizationUnitRoleDto destination); + + public override partial OrganizationUnitRole ReverseMap(OrganizationUnitRoleDto destination); + public override partial void ReverseMap(OrganizationUnitRoleDto destination, OrganizationUnitRole source); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +[MapExtraProperties] +public partial class OrganizationUnitToOrganizationUnitWithDetailsDtoMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(OrganizationUnitWithDetailsDto.Roles))] + [MapperIgnoreTarget(nameof(OrganizationUnitWithDetailsDto.UserCount))] + public override partial OrganizationUnitWithDetailsDto Map(OrganizationUnit source); + + [MapperIgnoreTarget(nameof(OrganizationUnitWithDetailsDto.Roles))] + [MapperIgnoreTarget(nameof(OrganizationUnitWithDetailsDto.UserCount))] + public override partial void Map(OrganizationUnit source, OrganizationUnitWithDetailsDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class IdentityRoleToOrganizationUnitRoleDtoMapper : MapperBase +{ + [MapProperty(nameof(IdentityRole.Id), nameof(OrganizationUnitRoleDto.RoleId))] + public override partial OrganizationUnitRoleDto Map(IdentityRole source); + + [MapProperty(nameof(IdentityRole.Id), nameof(OrganizationUnitRoleDto.RoleId))] + public override partial void Map(IdentityRole source, OrganizationUnitRoleDto destination); +} + +[Mapper(RequiredMappingStrategy = RequiredMappingStrategy.Target)] +public partial class IdentityUserToIdentityUserExportDtoMapper : MapperBase +{ + [MapperIgnoreTarget(nameof(IdentityUserExportDto.Roles))] + public override partial IdentityUserExportDto Map(IdentityUser source); + + [MapperIgnoreTarget(nameof(IdentityUserExportDto.Roles))] + public override partial void Map(IdentityUser source, IdentityUserExportDto destination); + + public override void AfterMap(IdentityUser source, IdentityUserExportDto destination) + { + destination.Active = source.IsActive ? "Yes" : "No"; + destination.EmailConfirmed = source.EmailConfirmed ? "Yes" : "No"; + destination.TwoFactorEnabled = source.TwoFactorEnabled ? "Yes" : "No"; + destination.AccountLookout = source.LockoutEnd != null && source.LockoutEnd > DateTime.UtcNow ? "Yes" : "No"; + } +} +``` + +## Mapperly Documentation + +Please refer to the [Mapperly documentation](https://mapperly.riok.app/docs/intro/) for more details. + +**Key points:** + +- [Mapperly Configuration](https://mapperly.riok.app/docs/configuration/mapper/) +- [Mapperly Enums](https://mapperly.riok.app/docs/configuration/enum/) +- [Mapperly Flattening and unflattening](https://mapperly.riok.app/docs/configuration/flattening/) + + +## Set Default Mapping Provider + +When your project contains modules using both AutoMapper and Mapperly, you may need to explicitly set the default `IAutoObjectMappingProvider` to ensure consistent behavior across your application. + +If your application uses `AutoMapper`: + +```csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + context.Services.AddAutoMapperObjectMapper(); +} +``` + +If your application uses `Mapperly`: + +```csharp +public override void ConfigureServices(ServiceConfigurationContext context) +{ + context.Services.AddMapperlyObjectMapper(); +} +``` + +### Why Set Default Mapping Provider? + +When your project contains modules using both AutoMapper and Mapperly, both `AbpAutoMapperModule` and `AbpMapperlyModule` will be loaded. Their dependency order may vary based on your project's module structure, and the last loaded module will override the `IAutoObjectMappingProvider` implementation. This could lead to unexpected behavior. Setting an explicit default ensures predictable mapping behavior throughout your application. diff --git a/docs/en/release-info/migration-guides/abp-9-3.md b/docs/en/release-info/migration-guides/abp-9-3.md index 3d085eba7d..7da446beee 100644 --- a/docs/en/release-info/migration-guides/abp-9-3.md +++ b/docs/en/release-info/migration-guides/abp-9-3.md @@ -2,6 +2,14 @@ This document is a guide for upgrading ABP v9.2 solutions to ABP v9.3. There are some changes in this version that may affect your applications, please read it carefully and apply the necessary changes to your application. +## Switched to `MySql.EntityFrameworkCore` for EF Core MySQL Provider + +In this version, we switched the EF Core MySQL provider from `Pomelo.EntityFrameworkCore.MySql` to `MySql.EntityFrameworkCore`. + +If you want to use the `Pomelo.EntityFrameworkCore.MySql` provider, then you can follow the [Use Pomelo provider documentation](https://abp.io/docs/latest/framework/data/entity-framework-core/mysql#use-pomelo-provider) to migrate your application. + +> See the internal changes we made in [#23392](https://github.com/abpframework/abp/pull/23392), for implementation details. + ## Updated `RabbitMQ.Client` to `7.x` In this version, we updated `RabbitMQ.Client` to `7.1.2`. [This is a major version update](https://github.com/rabbitmq/rabbitmq-dotnet-client/blob/main/v7-MIGRATION.md) that brings significant improvements to the library: @@ -43,4 +51,19 @@ The main changes include: - Added support for standalone components in ABP Suite code generation - Updated schematics to support both module-based and standalone templates -For detailed migration steps and best practices, please refer to our upcoming documentation and/or blog post. The migration is optional, and you can continue using the module-based approach if you prefer. \ No newline at end of file +For detailed migration steps and best practices, please refer to our upcoming documentation and/or blog post. The migration is optional, and you can continue using the module-based approach if you prefer. + +## Angular UI: Migrating Version to v20 + +In this version, we've updated our Angular version to v20. This update brings our Angular user interface to the latest and most powerful version + +Key Updates: +- Updated Angular packages (core, common, forms, router, etc.) to ~20.0.0 +- Updated @angular/cli and @angular-devkit/* to ~20.0.0 +- Updated Typescript to ~5.8.0 +- Updated RxJS to ~7.8.0 +- Updated third-party libraries to their latest versions compatible with Angular v20 + +Breaking Changes: +- Minimum required Node.js version is now v20.19.0 +- @angular/platform-browser no longer includes deprecated APIs like DOCUMENT token globally; ensure you're importing from @angular/common diff --git a/docs/en/release-info/migration-guides/pro/openiddict-microservice.md b/docs/en/release-info/migration-guides/pro/openiddict-microservice.md index 818a8c368a..33c319111b 100644 --- a/docs/en/release-info/migration-guides/pro/openiddict-microservice.md +++ b/docs/en/release-info/migration-guides/pro/openiddict-microservice.md @@ -472,17 +472,12 @@ In `appsettings.json` replace **IdentityServer** section with **OpenIddict** and typeof(AbpOpenIddictProWebModule), ``` -- In **IdentityServiceWebModule.cs** add object mapping configurations: +- In **IdentityServiceWebModule.cs** add object mapping configurations for [Mapperly](https://mapperly.riok.app/) (if you are using an another mapping providers, see the [Object to Object Mapping](../../../framework/infrastructure/object-to-object-mapping.md) documentation): ```csharp - context.Services.AddAutoMapperObjectMapper(); - Configure(options => - { - options.AddMaps(validate: true); - }); + context.Services.AddMapperlyObjectMapper(); ``` - ### Shared Hosting Module - In **MyApplicationSharedHostingModule** replace the **database configuration**: diff --git a/docs/en/release-info/release-notes.md b/docs/en/release-info/release-notes.md index 685b1dcb72..284995d04d 100644 --- a/docs/en/release-info/release-notes.md +++ b/docs/en/release-info/release-notes.md @@ -16,6 +16,7 @@ This is currently a RC (release-candidate) and you can see the detailed **[blog * Angular UI: Standalone Package Structure * Upgraded to `Blazorise` **v1.7.7** * Audit Logging Module: Excel Export +* Angular UI: Version Upgrade to **v20** ## 9.2 (2025-06-02) diff --git a/docs/en/solution-templates/application-module/index.md b/docs/en/solution-templates/application-module/index.md index 2b6de263fc..48013d2d90 100644 --- a/docs/en/solution-templates/application-module/index.md +++ b/docs/en/solution-templates/application-module/index.md @@ -183,45 +183,29 @@ The issue management page is empty in the beginning. You may change the content Now, let's have a closer look at some key elements of your project. -### The Main Module +### The Main Component -`IssueManagementModule` at the _angular/projects/issue-management/src/lib/issue-management.module.ts_ path is the main module of your module project. There are a few things worth mentioning in it: +`IssueManagementComponent` at the _angular/projects/issue-management/src/lib/issue-management.routes.ts_ path is the main component of your module project. There are a few things worth mentioning in it: -- Essential ABP modules, i.e. `CoreModule` and `ThemeSharedModule`, are imported. -- `IssueManagementRoutingModule` is imported. -- `IssueManagementComponent` is declared. -- It is prepared for configurability. The `forLazy` static method enables [a configuration to be passed to the module when it is loaded by the router](https://volosoft.com/blog/how-to-configure-angular-modules-loaded-by-the-router). - - -### The Main Routing Module - -`IssueManagementRoutingModule` at the _angular/projects/issue-management/src/lib/issue-management-routing.module.ts_ path is the main routing module of your module project. It currently does two things: - -- Loads `DynamicLayoutComponent` at base path it is given. -- Loads `IssueManagementComponent` as child to the layout, again at the given base path. - -You can rearrange this module to load more than one component at different routes, but you need to update the route provider at _angular/projects/issue-management/config/src/providers/route.provider.ts_ to match the new routing structure with the routes in the menu. Please check [Modifying the Menu](../../framework/ui/angular/modifying-the-menu.md) to see how route providers work. +- `IssueManagementComponent` is declared as standalone within the latest migration. +- `ISSUE_MANAGEMENT_ROUTES` is configured to be lazy-loaded. ### The Config Module -There is a config module at the _angular/projects/issue-management/config/src/issue-management-config.module.ts_ path. The static `forRoot` method of this module is supposed to be called at the route level. So, you may assume the following will take place: +There is a config module at the _angular/projects/issue-management/config/src/providers/route.provider.ts_ path. The static `provideIssueManagement` method of this module is supposed to be called at the route level. So, you may assume the following will take place: ```js -@NgModule({ - imports: [ - /* other imports */ - - IssueManagementConfigModule.forRoot(), +export const appConfig: ApplicationConfig = { + providers: [ + // ... + provideIssueManagement(), + // ... ], - - /* rest of the module meta data */ -}) -export class AppModule {} +}; ``` You can use this static method to configure an application that uses your module project. An example of such configuration is already implemented and the `ISSUE_MANAGEMENT_ROUTE_PROVIDERS` token is provided here. The method can take options which enables further configuration possibilities. -The difference between the `forRoot` method of the config module and the `forLazy` method of the main module is that, for smallest bundle size, the former should only be used when you have to configure an app before your module is even loaded. ### Testing Angular UI diff --git a/docs/en/solution-templates/guide.md b/docs/en/solution-templates/guide.md index cb14f57ef3..ded0c49f20 100644 --- a/docs/en/solution-templates/guide.md +++ b/docs/en/solution-templates/guide.md @@ -27,7 +27,7 @@ Besides the overall solution structure, the internals of each project in a solut ### Library Integrations & Configurations -When you use ABP startup solution templates to create a new solution, some **fundamental library installations** ([Serilog](https://serilog.net/), [Autofac](https://autofac.org/), [AutoMapper](https://automapper.org/), [Swagger](https://swagger.io/), [HealthCheck](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks) and others..) and their fine-tuned configurations are already prepared for you. Also, required **[ABP packages](https://abp.io/packages)** are just installed based on your preferences and configured for **development and production environments**. +When you use ABP startup solution templates to create a new solution, some **fundamental library installations** ([Serilog](https://serilog.net/), [Autofac](https://autofac.org/), [Mapperly](https://mapperly.riok.app/), [Swagger](https://swagger.io/), [HealthCheck](https://github.com/Xabaril/AspNetCore.Diagnostics.HealthChecks) and others..) and their fine-tuned configurations are already prepared for you. Also, required **[ABP packages](https://abp.io/packages)** are just installed based on your preferences and configured for **development and production environments**. ### Development Ready diff --git a/docs/en/solution-templates/layered-web-application/images/angular-folder-structure.png b/docs/en/solution-templates/layered-web-application/images/angular-folder-structure.png index 95bfc8986f..1ee4f87081 100644 Binary files a/docs/en/solution-templates/layered-web-application/images/angular-folder-structure.png and b/docs/en/solution-templates/layered-web-application/images/angular-folder-structure.png differ diff --git a/docs/en/solution-templates/layered-web-application/images/angular-template-structure-diagram.png b/docs/en/solution-templates/layered-web-application/images/angular-template-structure-diagram.png index dd7a4e5cc7..10841cccd0 100644 Binary files a/docs/en/solution-templates/layered-web-application/images/angular-template-structure-diagram.png and b/docs/en/solution-templates/layered-web-application/images/angular-template-structure-diagram.png differ diff --git a/docs/en/solution-templates/layered-web-application/web-applications.md b/docs/en/solution-templates/layered-web-application/web-applications.md index 8a1b71fda7..ce67866815 100644 --- a/docs/en/solution-templates/layered-web-application/web-applications.md +++ b/docs/en/solution-templates/layered-web-application/web-applications.md @@ -49,25 +49,25 @@ The Angular application runs as a client-side SPA in the user's browser and comm ![angular-folder-structure](images/angular-folder-structure.png) -Each of ABP modules is an NPM package. Some ABP modules are added as a dependency in `package.json`. These modules install with their dependencies. To see all ABP packages, you can run the following command in the `angular` folder: +Each of ABP module is an NPM package. Some ABP modules are added as a dependency in `package.json`. These modules are installed with their dependencies. To see all ABP packages, you can run the following command in the `angular` folder: ```bash yarn list --pattern abp ``` -Angular application module structure: +Angular application structure: ![Angular template structure diagram](images/angular-template-structure-diagram.png) -### AppModule +### Application Config -`AppModule` is the root module of the application. Some of the ABP modules and some essential modules are imported to `AppModule`. +Application config is the root configuration of the application. Some of the ABP modules and some essential providers are imported to `appConfig`. -ABP Config modules have also been imported to `AppModule` for initial requirements of the lazy-loadable ABP modules. +ABP Config modules have also been provided in `appConfig` for initial requirements of the lazy-loadable ABP modules. -### AppRoutingModule +### APP_ROUTES -There are lazy-loadable ABP modules in the `AppRoutingModule` as routes. +There are lazy-loadable ABP modules in the `APP_ROUTES` as routes. > Paths of ABP Modules should not be changed. @@ -76,7 +76,7 @@ You should add `routes` property in the `data` object to add a link on the menu ```js { path: 'dashboard', - loadChildren: () => import('./dashboard/dashboard.module').then(m => m.DashboardModule), + loadComponent: () => import('./dashboard/dashboard.component').then(c => c.DashboardComponent), canActivate: [authGuard, permissionGuard], data: { routes: { @@ -97,19 +97,13 @@ In the above example; After the above `routes` definition, if the user is authorized, the dashboard link will appear on the menu. -### Shared Module - -The modules that may be required for all modules have been imported to the `SharedModule`. You should import `SharedModule` to all modules. - -See the [Sharing Modules](https://angular.io/guide/sharing-ngmodules) document. - ### Environments The files under the `src/environments` folder have the essential configuration of the application. -### Home Module +### Home Component -Home module is an example lazy-loadable module that loads on the root address of the application. +Home component is an example lazy-loadable component that loads on the root address of the application. ### Styles diff --git a/docs/en/solution-templates/microservice/index.md b/docs/en/solution-templates/microservice/index.md index 7650ccb46d..542f9e6021 100644 --- a/docs/en/solution-templates/microservice/index.md +++ b/docs/en/solution-templates/microservice/index.md @@ -1,5 +1,15 @@ # ABP Studio: Microservice Solution Template +````json +//[doc-nav] +{ + "Next": { + "Name": "Microservice Solution: Overview", + "Path": "solution-templates/microservice/overview" + } +} +```` + > You must have an ABP Business or a higher license to use this startup template. ABP Studio provides pre-architected and production-ready templates to jump start a new solution. One of them is the Microservice solution template. You can use it to build distributed systems with common microservice patterns. It includes multiple services, API gateways and applications that are well integrated to each other and ready to be a great base solution for your microservice system. diff --git a/docs/en/solution-templates/microservice/localization-system.md b/docs/en/solution-templates/microservice/localization-system.md index a350de6848..e11232236e 100644 --- a/docs/en/solution-templates/microservice/localization-system.md +++ b/docs/en/solution-templates/microservice/localization-system.md @@ -92,12 +92,12 @@ You can define new localization entries in the language files under the **Locali Angular UI gets the localization resources from the [`application-localization`](../../framework/api-development/standard-apis/localization.md) API's response and merges these resources in the `ConfigStateService` for the localization entries/resources coming from the backend side. -In addition, you may need to define some localization entries and only use them on the UI side. ABP already provides the related configuration for you, so you don't need to make any configurations related to that and instead you can directly define localization entries in the `app.-module.ts` file of your angular application as follows: +In addition, you may need to define some localization entries and only use them on the UI side. ABP already provides the related configuration for you, so you don't need to make any configurations related to that and instead you can directly define localization entries in the `app.config.ts` file of your angular application as follows: ```ts import { provideAbpCore, withOptions } from '@abp/ng.core'; -@NgModule({ +export const appConfig: ApplicationConfig = { providers: [ // ... provideAbpCore( @@ -119,11 +119,8 @@ import { provideAbpCore, withOptions } from '@abp/ng.core'; ] }), ), - ... ], -}) -export class AppModule {} - +}; ``` After defining the localization entries, it can be used as below: diff --git a/docs/en/solution-templates/microservice/main-components.md b/docs/en/solution-templates/microservice/main-components.md index a83dc9cd29..e2a0f1aaa9 100644 --- a/docs/en/solution-templates/microservice/main-components.md +++ b/docs/en/solution-templates/microservice/main-components.md @@ -1,5 +1,15 @@ # Microservice Solution: Main Components +```json +//[doc-nav] +{ + "Next": { + "Name": "Microservices", + "Path": "solution-templates/microservice/microservices" + } +} +``` + > You must have an ABP Business or a higher license to be able to create a microservice solution. The solution consists of various applications, API gateways and services. The following documents explains these components in details: diff --git a/docs/en/studio/images/monitoring-applications/monitoring-window-hint.png b/docs/en/studio/images/monitoring-applications/monitoring-window-hint.png new file mode 100644 index 0000000000..cb7b689283 Binary files /dev/null and b/docs/en/studio/images/monitoring-applications/monitoring-window-hint.png differ diff --git a/docs/en/studio/images/monitoring-applications/monitoring.png b/docs/en/studio/images/monitoring-applications/monitoring.png index 5f31d1a88f..580d47c72b 100644 Binary files a/docs/en/studio/images/monitoring-applications/monitoring.png and b/docs/en/studio/images/monitoring-applications/monitoring.png differ diff --git a/docs/en/studio/images/monitoring-applications/tools-create.png b/docs/en/studio/images/monitoring-applications/tools-create.png new file mode 100644 index 0000000000..ac473d50da Binary files /dev/null and b/docs/en/studio/images/monitoring-applications/tools-create.png differ diff --git a/docs/en/studio/images/monitoring-applications/tools-overview.png b/docs/en/studio/images/monitoring-applications/tools-overview.png new file mode 100644 index 0000000000..7c376fbdd1 Binary files /dev/null and b/docs/en/studio/images/monitoring-applications/tools-overview.png differ diff --git a/docs/en/studio/monitoring-applications.md b/docs/en/studio/monitoring-applications.md index 5ef2dc0f91..be0387c5bb 100644 --- a/docs/en/studio/monitoring-applications.md +++ b/docs/en/studio/monitoring-applications.md @@ -10,10 +10,14 @@ } ```` -ABP Studio offers a comprehensive centralized monitoring solution, enabling you to oversee all applications from a single interface. To see the monitoring tabs you can select the [Solution Runner](./running-applications.md) or *Kubernetes* from the left menu, monitoring tabs are automatically opened in the center. You can start the applications for monitoring. Various monitoring options are available, including [Overall](#overall), [Browse](#browse), [HTTP Requests](#http-requests), [Events](#events), [Exceptions](#exceptions), [Logs](#logs). +ABP Studio offers a comprehensive centralized monitoring solution, enabling you to oversee all applications from a single interface. To see the monitoring tabs you can select the [Solution Runner](./running-applications.md) or *Kubernetes* from the left menu, monitoring tabs are automatically opened in the center. You can start the applications for monitoring. Various monitoring options are available, including [Overall](#overall), [Browse](#browse), [HTTP Requests](#http-requests), [Events](#events), [Exceptions](#exceptions), [Logs](#logs), [Tools](#tools). ![monitoring](./images/monitoring-applications/monitoring.png) +If you want to open any of these tabs in separate window, just drag it from the header a little bit and it will pop-up in a new window. In this way you can monitor multiple tabs at once: + +![monitoring-window-hint](./images/monitoring-applications/monitoring-window-hint.png) + ## Collecting Telemetry Information There are two application [types](./running-applications.md#abp-studio-running-applications): C# and CLI. Only C# applications can establish a connection with ABP Studio and transmit telemetry information via the `Volo.Abp.Studio.Client.AspNetCore` package. However, we can view the *Logs* and *Browse* (if there is a *Launch URL*) for both CLI and C# application types. Upon starting C# applications, they attempt to establish a connection with ABP Studio. When connection successful, you should see a chain icon next to the application name in [Solution Runner](./running-applications.md#run-1). Applications can connect the ABP Studio with *Solution Runner* -> *C# Application* -> *Run* -> *Start* or from an outside environment such as debugging with Visual Studio. Additionally, they can establish a connection from a Kubernetes Cluster through the ABP Studio [Kubernetes Integration: Connecting to the Cluster](../get-started/microservice.md#kubernetes-integration-connecting-to-the-cluster). @@ -130,3 +134,16 @@ Click on a row to inspect the details of each exception; `Application`, `Excepti The *Logs* tab allows you to view all logs for both CLI and C# applications. To access logs, simply select an application. You can also apply filters using the search textbox by log text or by selecting a specific *Log Level*. When you select a *Log Level* it shows selected log level and higher log levels. For example, if you select *Warning* it shows *Warning*, *Error* and *Critical* logs. To clear selected application logs, use the *Clear Logs* button. If *Auto Scroll* is checked, the display automatically scrolls when new logs are received. ![logs](./images/monitoring-applications/logs.png) + +## Tools + +The *Tools* tab allows you to easily access to the user interfaces of the tools you are using. A *tool* may be related with a docker container, or independent. If it is related with a container (ex: *grafana*), the tool is opened when the container is up. If the tool is independent, it will be always opened. + +![tools](./images/monitoring-applications/tools-overview.png) + +The microservice template comes with pre-defined tools to display related container user interfaces. You can edit existing tools, add new tools or delete existing tools. + +In the example below, a new tool named `My Application Status` will be added to the tools and it will display the URL in the input: + +![tools-create](./images/monitoring-applications/tools-create.png) + diff --git a/docs/en/studio/release-notes.md b/docs/en/studio/release-notes.md index d2db328fbf..d36500ec3b 100644 --- a/docs/en/studio/release-notes.md +++ b/docs/en/studio/release-notes.md @@ -2,6 +2,42 @@ This document contains **brief release notes** for each ABP Studio release. Release notes only include **major features** and **visible enhancements**. Therefore, they don't include all the development done in the related version. +## 1.2.1 (2025-08-14) + +* Upgraded template dependencies for ABP Framework and LeptonX. (targeting ABP `9.3.1`) +* Migrated templates to standalone structure for Angular UI. +* Allowed relating tools with Kubernetes Services (allows to browse tools dashboard those are running in k8s). +* Made several enhancements for Solution Runner. +* Added test projects to **Application (Single Layer)** template (optional). + + +## 1.1.2 (2025-07-31) + +* Upgraded template dependencies for ABP Framework and LeptonX. (targeting ABP `9.2.3`) +* Configured LeptonX Lite logos in the templates. +* Added browser tab memory feature to remember previously selected tabs. +* Enhanced tools section with default credentials display for first-time tool usage. +* Improved module and package loading with better error handling. + +## 1.1.1 (2025-07-22) + +* Enhanced tools section with clear cookies option. +* Fixed language management module name display for imported modules. +* Improved update window messaging with "Skip this version" option. +* Fixed Docker Compose file issues in microservice template. +* Resolved RabbitMQ tool cookie problems. + +## 1.1.0 (2025-07-16) + +* Upgraded template dependencies for ABP Framework and LeptonX. (targeting ABP `9.2.2`) +* Enhanced UI scaling for all windows and improved user experience. +* Added tools section in solution runner main area with basic Grafana dashboard for microservice template. +* Improved container management during application building. +* Enhanced background task exception handling. +* Added public account module package reference to Blazor WebApp client. +* Fixed tenant database context updating errors. +* Improved optional module selection UI with better documentation integration. + ## 1.0.2 (2025-06-24) * Enhanced the ABP NuGet package installation experience. diff --git a/docs/en/studio/version-mapping.md b/docs/en/studio/version-mapping.md index 51e0de5bbd..eaf6dc2313 100644 --- a/docs/en/studio/version-mapping.md +++ b/docs/en/studio/version-mapping.md @@ -4,6 +4,9 @@ This document provides a general overview of the relationship between various ve | **ABP Studio Version** | **ABP Version of Startup Template** | |------------------------|---------------------------| +| 1.2.1 | 9.3.1 | +| 1.1.2 | 9.2.3 | +| 1.1.0 - 1.1.1 | 9.2.2 | | 1.0.2 | 9.2.1 | | 1.0.1 | 9.2.0 | | 1.0.0 | 9.2.0 | diff --git a/docs/en/suite/solution-structure.md b/docs/en/suite/solution-structure.md index b1949a137a..5abd50d751 100644 --- a/docs/en/suite/solution-structure.md +++ b/docs/en/suite/solution-structure.md @@ -226,25 +226,21 @@ Angular application folder structure looks like below: ![angular-folder-structure](../images/angular-folder-structure.png) -Each of ABP modules is an NPM package. Some ABP modules are added as a dependency in `package.json`. These modules install with their dependencies. To see all ABP packages, you can run the following command in the `angular` folder: +Each of ABP module is an NPM package. Some ABP modules are added as a dependency in `package.json`. These modules are installed with their dependencies. To see all ABP packages, you can run the following command in the `angular` folder: ```bash yarn list --pattern abp ``` -Angular application module structure: +### Application Config -![Angular template structure diagram](../images/angular-template-structure-diagram.png) +Application config is the root module of the application. Some of ABP modules and some essential modules are imported to the `appConfig`. -### AppModule +ABP Config modules also have been imported to `appConfig`  for initially requirements of lazy-loadable ABP modules. -`AppModule` is the root module of the application. Some of ABP modules and some essential modules imported to the `AppModule`. +### APP_ROUTES -ABP Config modules also have imported to `AppModule`  for initially requirements of lazy-loadable ABP modules. - -### AppRoutingModule - -There are lazy-loadable ABP modules in the `AppRoutingModule` as routes. +There are lazy-loadable ABP modules in the `APP_ROUTES` as routes. > Paths of ABP Modules should not be changed. @@ -253,7 +249,7 @@ You should add `routes` property in the `data` object to add a link on the menu ```js { path: 'dashboard', - loadChildren: () => import('./dashboard/dashboard.module').then(m => m.DashboardModule), + loadComponent: () => import('./dashboard/dashboard.component').then(c => c.DashboardComponent), canActivate: [authGuard, permissionGuard], data: { routes: { @@ -274,25 +270,19 @@ In the above example; After the above `routes` definition, if the user is authorized, the dashboard link will appear on the menu. -### Shared Module - -The modules that may be required for all modules have imported to the `SharedModule`. You should import the `SharedModule` to all modules. - -See the [Sharing Modules](https://angular.io/guide/sharing-ngmodules) document. - ### Environments The files under the `src/environments` folder has the essential configuration of the application. -### Home Module +### Home Component -Home module is an example lazy-loadable module that loads on the root address of the application. +Home component is an example lazy-loadable standalone component that loads on the root address of the application. -### Dashboard Module +### Dashboard Component -Dashboard module is a lazy-loadable module. `HostDashboardComponent` and `TenantDashboardComponent` declared to this module. One of these components is shown according to the user's authorization. +Dashboard component is a lazy-loadable component. `HostDashboardComponent` and `TenantDashboardComponent` are declared within this root component. One of these components is shown according to authorization of the user. -There are four widgets in the `HostDashboardComponent` which declared in ABP modules. +There are four widgets in the `HostDashboardComponent` which are declared in ABP modules. ### Styles diff --git a/docs/en/tutorials/book-store/part-01.md b/docs/en/tutorials/book-store/part-01.md index 6b856d9687..4634a68c67 100644 --- a/docs/en/tutorials/book-store/part-01.md +++ b/docs/en/tutorials/book-store/part-01.md @@ -291,22 +291,17 @@ public class BookDto : AuditedEntityDto * The `BookDto` is used to transfer the book data to the presentation layer in order to show the book information on the UI. * The `BookDto` is derived from the `AuditedEntityDto` which has audit properties just like the `Book` entity defined above. -It will be needed to map the `Book` entities to the `BookDto` objects while returning books to the presentation layer. [AutoMapper](https://automapper.org) library can automate this conversion when you define the proper mapping. The startup template comes with AutoMapper pre-configured. So, you can just define the mapping in the `BookStoreApplicationAutoMapperProfile` class in the `Acme.BookStore.Application` project: +It will be needed to map the `Book` entities to the `BookDto` objects while returning books to the presentation layer. [Mapperly](https://mapperly.riok.app/) library can automate this conversion when you define the proper mapping. The startup template comes with Mapperly pre-configured. So, you can just define the mapping in the `BookStoreApplicationMappers` class in the `Acme.BookStore.Application` project: -````csharp -using Acme.BookStore.Books; -using AutoMapper; - -namespace Acme.BookStore; - -public class BookStoreApplicationAutoMapperProfile : Profile +```csharp +[Mapper] +public partial class BookToBookDtoMapper : MapperBase { - public BookStoreApplicationAutoMapperProfile() - { - CreateMap(); - } + public override partial BookDto Map(Book source); + + public override partial void Map(Book source, BookDto destination); } -```` +``` > See the [object to object mapping](../../framework/infrastructure/object-to-object-mapping.md) document for details. @@ -343,21 +338,23 @@ public class CreateUpdateBookDto As done to the `BookDto` above, we should define the mapping from the `CreateUpdateBookDto` object to the `Book` entity. The final class will be as shown below: -````csharp -using Acme.BookStore.Books; -using AutoMapper; +```csharp +[Mapper] +public partial class BookToBookDtoMapper : MapperBase +{ + public override partial BookDto Map(Book source); -namespace Acme.BookStore; + public override partial void Map(Book source, BookDto destination); +} -public class BookStoreApplicationAutoMapperProfile : Profile +[Mapper] +public partial class CreateUpdateBookDtoToBookMapper : MapperBase { - public BookStoreApplicationAutoMapperProfile() - { - CreateMap(); - CreateMap(); - } + public override partial Book Map(CreateUpdateBookDto source); + + public override partial void Map(CreateUpdateBookDto source, Book destination); } -```` +``` ### IBookAppService @@ -416,7 +413,7 @@ public class BookAppService : * `BookAppService` is derived from `CrudAppService<...>` which implements all the CRUD (create, read, update, delete) methods defined by the `ICrudAppService`. * `BookAppService` injects `IRepository` which is the default repository for the `Book` entity. ABP automatically creates default repositories for each aggregate root (or entity). See the [repository document](../../framework/architecture/domain-driven-design/repositories.md). -* `BookAppService` uses `IObjectMapper` service ([see](../../framework/infrastructure/object-to-object-mapping.md)) to map the `Book` objects to the `BookDto` objects and `CreateUpdateBookDto` objects to the `Book` objects. The Startup template uses the [AutoMapper](http://automapper.org/) library as the object mapping provider. We have defined the mappings before, so it will work as expected. +* `BookAppService` uses `IObjectMapper` service ([see](../../framework/infrastructure/object-to-object-mapping.md)) to map the `Book` objects to the `BookDto` objects and `CreateUpdateBookDto` objects to the `Book` objects. The Startup template uses the [Mapperly](https://mapperly.riok.app/) library as the object mapping provider. We have defined the mappings before, so it will work as expected. ## Auto API Controllers diff --git a/docs/en/tutorials/book-store/part-03.md b/docs/en/tutorials/book-store/part-03.md index 0c6fed1238..ae96f8a8e0 100644 --- a/docs/en/tutorials/book-store/part-03.md +++ b/docs/en/tutorials/book-store/part-03.md @@ -298,23 +298,17 @@ public class EditModalModel : BookStorePageModel ### Mapping from BookDto to CreateUpdateBookDto -To be able to map the `BookDto` to `CreateUpdateBookDto`, configure a new mapping. To do this, open the `BookStoreWebAutoMapperProfile.cs` file in the `Acme.BookStore.Web` project and change it as shown below: +To be able to map the `BookDto` to `CreateUpdateBookDto`, configure a new mapping. To do this, open the `BookStoreWebMappers.cs` file in the `Acme.BookStore.Web` project and change it as shown below: -````csharp -using AutoMapper; - -namespace Acme.BookStore.Web; - -public class BookStoreWebAutoMapperProfile : Profile +```csharp +[Mapper] +public partial class BookDtoToCreateUpdateBookDtoMapper : MapperBase { - public BookStoreWebAutoMapperProfile() - { - CreateMap(); - } -} -```` + public override partial CreateUpdateBookDto Map(BookDto source); -* We have just added `CreateMap();` to define this mapping. + public override partial void Map(BookDto source, CreateUpdateBookDto destination); +} +``` > Notice that we do the mapping definition in the web layer as a best practice since it is only needed in this layer. @@ -1290,28 +1284,26 @@ We can now define a modal to edit the book. Add the following code to the end of ```` -### AutoMapper Configuration +### Mapperly Configuration The base `AbpCrudPageBase` uses the [object to object mapping](../../framework/infrastructure/object-to-object-mapping.md) system to convert an incoming `BookDto` object to a `CreateUpdateBookDto` object. So, we need to define the mapping. -Open the `BookStoreBlazorAutoMapperProfile` inside the {{ if UI == "BlazorServer" }}`Acme.BookStore.Blazor` {{ else if UI == "MAUIBlazor" }}`Acme.BookStore.MauiBlazor` {{ else }}`Acme.BookStore.Blazor.Client`{{ end }} project and change the content as the following: +Open the `BookStoreBlazorMappers` inside the {{ if UI == "BlazorServer" }}`Acme.BookStore.Blazor` {{ else if UI == "MAUIBlazor" }}`Acme.BookStore.MauiBlazor` {{ else }}`Acme.BookStore.Blazor.Client`{{ end }} project and change the content as the following: -````csharp -using Acme.BookStore.Books; -using AutoMapper; +```csharp +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; {{ if UI == "BlazorServer" }}namespace Acme.BookStore.Blazor; {{ else if UI == "MAUIBlazor" }}namespace Acme.BookStore.MauiBlazor; {{ else }}namespace Acme.BookStore.Blazor.Client;{{ end }} -public class BookStoreBlazorAutoMapperProfile : Profile +[Mapper] +public partial class BookDtoToCreateUpdateBookDtoMapper : MapperBase { - public BookStoreBlazorAutoMapperProfile() - { - CreateMap(); - } -} -```` + public override partial CreateUpdateBookDto Map(BookDto source); -* We've just added the `CreateMap();` line to define the mapping. + public override partial void Map(BookDto source, CreateUpdateBookDto destination); +} +``` ### Test the Editing Modal diff --git a/docs/en/tutorials/book-store/part-05.md b/docs/en/tutorials/book-store/part-05.md index 0acc5cac9f..0f84a945d3 100644 --- a/docs/en/tutorials/book-store/part-05.md +++ b/docs/en/tutorials/book-store/part-05.md @@ -72,7 +72,12 @@ public class BookStorePermissionDefinitionProvider : PermissionDefinitionProvide public override void Define(IPermissionDefinitionContext context) { var bookStoreGroup = context.AddGroup(BookStorePermissions.GroupName, L("Permission:BookStore")); + + //Dashboard permissions + bookStoreGroup.AddPermission(BookStorePermissions.Dashboard.Host, L("Permission:Dashboard"), MultiTenancySides.Host); + bookStoreGroup.AddPermission(BookStorePermissions.Dashboard.Tenant, L("Permission:Dashboard"), MultiTenancySides.Tenant); + //Books permissions var booksPermission = bookStoreGroup.AddPermission(BookStorePermissions.Books.Default, L("Permission:Books")); booksPermission.AddChild(BookStorePermissions.Books.Create, L("Permission:Books.Create")); booksPermission.AddChild(BookStorePermissions.Books.Edit, L("Permission:Books.Edit")); diff --git a/docs/en/tutorials/book-store/part-08.md b/docs/en/tutorials/book-store/part-08.md index a7d73f0209..ac49c6aee4 100644 --- a/docs/en/tutorials/book-store/part-08.md +++ b/docs/en/tutorials/book-store/part-08.md @@ -193,7 +193,7 @@ public async Task GetAsync(Guid id) } ```` -This method simply gets the `Author` entity by its `Id`, converts to the `AuthorDto` using the [object to object mapper](../../framework/infrastructure/object-to-object-mapping.md). This requires to configure the AutoMapper, which will be explained later. +This method simply gets the `Author` entity by its `Id`, converts to the `AuthorDto` using the [object to object mapper](../../framework/infrastructure/object-to-object-mapping.md). This requires to configure the Mapperly, which will be explained later. ### GetListAsync @@ -350,12 +350,18 @@ Finally, add the following entries to the `Localization/BookStore/en.json` insid ## Object to Object Mapping -`AuthorAppService` is using the `ObjectMapper` to convert the `Author` objects to `AuthorDto` objects. So, we need to define this mapping in the AutoMapper configuration. +`AuthorAppService` is using the `ObjectMapper` to convert the `Author` objects to `AuthorDto` objects. So, we need to define this mapping in the Mapperly configuration. -Open the `BookStoreApplicationAutoMapperProfile` class inside the `Acme.BookStore.Application` project and add the following line to the constructor: +Open the `BookStoreApplicationMappers` class inside the `Acme.BookStore.Application` project and define the following mapping class: ````csharp -CreateMap(); +[Mapper] +public partial class AuthorToAuthorDtoMapper : MapperBase +{ + public override partial AuthorDto Map(Author source); + + public override partial void Map(Author source, AuthorDto destination); +} ```` ## Data Seeder diff --git a/docs/en/tutorials/book-store/part-09.md b/docs/en/tutorials/book-store/part-09.md index 93d68295c8..95de6ccd61 100644 --- a/docs/en/tutorials/book-store/part-09.md +++ b/docs/en/tutorials/book-store/part-09.md @@ -335,27 +335,16 @@ The main reason of this decision was to show you how to use a different model cl * Added `[DataType(DataType.Date)]` attribute to the `BirthDate` which shows a date picker on the UI for this property. * Added `[TextArea]` attribute to the `ShortBio` which shows a multi-line text area instead of a standard textbox. -In this way, you can specialize the view model class based on your UI requirements without touching to the DTO. As a result of this decision, we have used `ObjectMapper` to map `CreateAuthorViewModel` to `CreateAuthorDto`. To be able to do that, you need to add a new mapping code to the `BookStoreWebAutoMapperProfile` constructor: +In this way, you can specialize the view model class based on your UI requirements without touching to the DTO. As a result of this decision, we have used `ObjectMapper` to map `CreateAuthorViewModel` to `CreateAuthorDto`. To be able to do that, you need to define a new mapping configuration in the `BookStoreWebMappers` class: -````csharp -using Acme.BookStore.Authors; // ADDED NAMESPACE IMPORT -using Acme.BookStore.Books; -using AutoMapper; - -namespace Acme.BookStore.Web; - -public class BookStoreWebAutoMapperProfile : Profile +```csharp +[Mapper] +public partial class CreateAuthorViewModelToCreateAuthorDtoMapper : MapperBase { - public BookStoreWebAutoMapperProfile() - { - CreateMap(); - - // ADD a NEW MAPPING - CreateMap(); - } + public override partial CreateAuthorDto Map(Pages.Authors.CreateModalModel.CreateAuthorViewModel source); + public override partial void Map(Pages.Authors.CreateModalModel.CreateAuthorViewModel source, CreateAuthorDto destination); } -```` +``` "New author" button will work as expected and open a new model when you run the application again: @@ -456,29 +445,22 @@ This class is similar to the `CreateModal.cshtml.cs` while there are some main d * Uses the `IAuthorAppService.GetAsync(...)` method to get the editing author from the application layer. * `EditAuthorViewModel` has an additional `Id` property which is marked with the `[HiddenInput]` attribute that creates a hidden input for this property. -This class requires to add two object mapping declarations to the `BookStoreWebAutoMapperProfile` class: +This class requires to add two object mapping declarations, so open the `BookStoreWebMappers` class and add the following mappings: ```csharp -using Acme.BookStore.Authors; -using Acme.BookStore.Books; -using AutoMapper; - -namespace Acme.BookStore.Web; - -public class BookStoreWebAutoMapperProfile : Profile +[Mapper] +public partial class AuthorDtoToEditAuthorViewModelMapper : MapperBase { - public BookStoreWebAutoMapperProfile() - { - CreateMap(); + public override partial EditAuthorViewModel Map(AuthorDto source); - CreateMap(); + public override partial void Map(AuthorDto source, EditAuthorViewModel destination); +} - // ADD THESE NEW MAPPINGS - CreateMap(); - CreateMap(); - } +[Mapper] +public partial class EditAuthorViewModelToUpdateAuthorDtoMapper : MapperBase +{ + public override partial UpdateAuthorDto Map(Pages.Authors.EditModalModel.EditAuthorViewModel source); + public override partial void Map(Pages.Authors.EditModalModel.EditAuthorViewModel source, UpdateAuthorDto destination); } ``` @@ -1218,13 +1200,23 @@ This class typically defines the properties and methods used by the `Authors.raz `Authors` class uses the `IObjectMapper` in the `OpenEditAuthorModal` method. So, we need to define this mapping. -Open the `BookStoreBlazorAutoMapperProfile.cs` in the {{ if UI == "BlazorServer" }}`Acme.BookStore.Blazor`{{ else if UI == "MAUIBlazor" }}`Acme.BookStore.MauiBlazor`{{ else }}`Acme.BookStore.Blazor.Client`{{ end }} project and add the following mapping code in the constructor: +Open the `BookStoreBlazorMappers.cs` in the {{ if UI == "BlazorServer" }}`Acme.BookStore.Blazor`{{ else if UI == "MAUIBlazor" }}`Acme.BookStore.MauiBlazor`{{ else }}`Acme.BookStore.Blazor.Client`{{ end }} project and add the following mappings in the class: -````csharp -CreateMap(); -```` +```csharp +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; +using Acme.BookStore.Authors; + +//... + +[Mapper] +public partial class AuthorDtoToUpdateAuthorDtoMapper : MapperBase +{ + public override partial UpdateAuthorDto Map(AuthorDto source); -You will need to declare a `using Acme.BookStore.Authors;` statement to the beginning of the file. + public override partial void Map(AuthorDto source, UpdateAuthorDto destination); +} +``` ### Add to the Main Menu diff --git a/docs/en/tutorials/book-store/part-10.md b/docs/en/tutorials/book-store/part-10.md index 990acd9aad..ccb6755468 100644 --- a/docs/en/tutorials/book-store/part-10.md +++ b/docs/en/tutorials/book-store/part-10.md @@ -578,11 +578,17 @@ Let's see the changes we've done: ### Object to Object Mapping Configuration -Introduced the `AuthorLookupDto` class and used object mapping inside the `GetAuthorLookupAsync` method. So, we need to add a new mapping definition inside the `BookStoreApplicationAutoMapperProfile.cs` file of the `Acme.BookStore.Application` project: +Introduced the `AuthorLookupDto` class and used object mapping inside the `GetAuthorLookupAsync` method. So, we need to add a new mapping definition inside the `BookStoreApplicationMappers.cs` file of the `Acme.BookStore.Application` project: -````csharp -CreateMap(); -```` +```csharp +[Mapper] +public partial class AuthorToAuthorLookupDtoMapper : MapperBase +{ + public override partial AuthorLookupDto Map(Author source); + + public override partial void Map(Author source, AuthorLookupDto destination); +} +``` ## Unit Tests @@ -898,12 +904,37 @@ These changes require a small change in the `EditModal.cshtml`. Remove the `(); -CreateMap(); -CreateMap(); +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; + +//... + +[Mapper] +public partial class CreateBookViewModelToCreateUpdateBookDtoMapper : MapperBase +{ + public override partial CreateUpdateBookDto Map(Pages.Books.CreateModalModel.CreateBookViewModel source); + + public override partial void Map(Pages.Books.CreateModalModel.CreateBookViewModel source, CreateUpdateBookDto destination); +} + +[Mapper] +public partial class BookDtoToEditBookViewModelMapper : MapperBase +{ + public override partial Pages.Books.EditModalModel.EditBookViewModel Map(BookDto source); + + public override partial void Map(BookDto source, Pages.Books.EditModalModel.EditBookViewModel destination); +} + +[Mapper] +public partial class EditBookViewModelToCreateUpdateBookDtoMapper : MapperBase +{ + public override partial CreateUpdateBookDto Map(Pages.Books.EditModalModel.EditBookViewModel source); + + public override partial void Map(Pages.Books.EditModalModel.EditBookViewModel source, CreateUpdateBookDto destination); +} ``` You can run the application and try to create a new book or update an existing book. You will see a drop down list on the create/update form to select the author of the book: diff --git a/docs/en/tutorials/microservice/part-05.md b/docs/en/tutorials/microservice/part-05.md index 1aadd92778..26bd3e3306 100644 --- a/docs/en/tutorials/microservice/part-05.md +++ b/docs/en/tutorials/microservice/part-05.md @@ -255,21 +255,20 @@ public class OrderAppService : ApplicationService, IOrderAppService In this code snippet, we inject the `IRepository` into the `OrderAppService` class. We use this repository to interact with the `Order` entity. The `GetListAsync` method retrieves a list of orders from the database and maps them to the `OrderDto` class. The `CreateAsync` method creates a new order entity and inserts it into the database. -Afterward, we need to configure the *AutoMapper* object to map the `Order` entity to the `OrderDto` class. Open the `OrderingServiceApplicationAutoMapperProfile` class in the `CloudCrm.OrderingService` project, located in the `ObjectMapping` folder, and add the following code: +Afterward, we need to configure the *Mapperly* object to map the `Order` entity to the `OrderDto` class. Open the `OrderingServiceApplicationMappers` class in the `CloudCrm.OrderingService` project, located in the `ObjectMapping` folder, and add the following code: ```csharp -using AutoMapper; -using CloudCrm.OrderingService.Entities; -using CloudCrm.OrderingService.Services; +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; namespace CloudCrm.OrderingService.ObjectMapping; -public class OrderingServiceApplicationAutoMapperProfile : Profile +[Mapper] +public partial class OrderingServiceApplicationMappers : MapperBase { - public OrderingServiceApplicationAutoMapperProfile() - { - CreateMap(); - } + public override partial OrderDto Map(Order source); + + public override partial void Map(Order source, OrderDto destination); } ``` @@ -516,14 +515,6 @@ abp generate-proxy -t ng -m ordering -u http://localhost:44311 --target ordering For more information, please refer to the [Service Proxies](https://abp.io/docs/latest/framework/ui/angular/service-proxies) documentation. -### Create Order Module - -Run the following command line to create a new module, named `OrderModule` in the root folder of the angular application: - -```bash -yarn ng generate module order --module ordering-service --project ordering-service --routing --route orders -``` - ### Add Order Route * Create `order-base.routes.ts` file under the `projects/ordering-service/config/src/providers` folder and add the following code: @@ -565,24 +556,86 @@ function configureRoutes() { routesService.add(routes); } ``` +* Open the `projects/ordering-service/config/src/providers/route.provider.ts` file and add `ORDERS_ORDER_ROUTE_PROVIDER` to the `ORDER_SERVICE_PROVIDERS` array as following code: -* Open the `projects/ordering-service/config/src/ordering-service-config.module.ts` file and add `ORDERS_ORDER_ROUTE_PROVIDER` to the `providers` array as following code: - -*ordering-service-config.module.ts* +*route.provider.ts* ```typescript -import { ModuleWithProviders, NgModule } from '@angular/core'; -import { ORDERING_SERVICE_ROUTE_PROVIDERS } from './providers/route.provider'; -import { ORDERS_ORDER_ROUTE_PROVIDER } from './providers/order-route.provider'; - -@NgModule() -export class OrderingServiceConfigModule { - static forRoot(): ModuleWithProviders { - return { - ngModule: OrderingServiceConfigModule, - providers: [ORDERING_SERVICE_ROUTE_PROVIDERS, ORDERS_ORDER_ROUTE_PROVIDER], - }; - } +import { eLayoutType, RoutesService } from '@abp/ng.core'; +import { + EnvironmentProviders, + inject, + makeEnvironmentProviders, + provideAppInitializer, +} from '@angular/core'; +import { eOrderingServiceRouteNames } from '../enums/route-names'; +import { ORDERS_ORDER_ROUTE_PROVIDER } from './order-route.provider'; + + +export const ORDER_SERVICE_ROUTE_PROVIDERS = [ + provideAppInitializer(() => { + configureRoutes(); + }), +]; + +export function configureRoutes() { + const routesService = inject(RoutesService); + routesService.add([ + { + path: '/order-service', + name: eOrderingServiceRouteNames.OrderService, + iconClass: 'fas fa-book', + layout: eLayoutType.application, + order: 3, + }, + ]); } + +const ORDER_SERVICE_PROVIDERS: EnvironmentProviders[] = [ + ...ORDER_SERVICE_ROUTE_PROVIDERS, + ...ORDERS_ORDER_ROUTE_PROVIDER +]; + +export function provideOrderService() { + return makeEnvironmentProviders(ORDER_SERVICE_PROVIDERS); +} +``` + +* Do not forget adding `provideOrderService()` to the providers inside `app.config.ts` as follows: + +```typescript +import { provideOrderService } from '@order-service/config'; + +export const appConfig: ApplicationConfig = { + providers: [ + // ... + provideOrderService() + ], +}; +``` + +* Lastly, you need to update the `APP_ROUTES` array in `app.routes.ts` file as follows: + +```typescript +// app.routes.ts +export const APP_ROUTES: Routes = [ + // ... + { + path: 'order-service', + children: ORDER_SERVICE_ROUTES, + }, +]; +``` + +```typescript +// order-service.routes.ts +export const ORDER_SERVICE_ROUTES: Routes = [ + { + path: '', + pathMatch: 'full', + component: RouterOutletComponent, + }, + { path: 'orders', children: ORDER_ROUTES }, +]; ``` ### Create Order Page @@ -591,13 +644,14 @@ export class OrderingServiceConfigModule { ```typescript import { Component, inject } from '@angular/core'; +import { CommonModule } from '@angular/common'; import { OrderDto, OrderService } from './proxy/ordering-service/services'; @Component({ selector: 'lib-order', - standalone: false, templateUrl: './order.component.html', styleUrl: './order.component.css' + imports: [CommonModule] }) export class OrderComponent { diff --git a/docs/en/tutorials/microservice/part-06.md b/docs/en/tutorials/microservice/part-06.md index e5a8f53fe5..f268ba6fff 100644 --- a/docs/en/tutorials/microservice/part-06.md +++ b/docs/en/tutorials/microservice/part-06.md @@ -216,25 +216,25 @@ public class OrderDto } ``` -Lastly, open the `OrderingServiceApplicationAutoMapperProfile` class (the `OrderingServiceApplicationAutoMapperProfile.cs` file under the `ObjectMapping` folder of the `CloudCrm.OrderingService` project of the `CloudCrm.OrderingService` .NET solution) and ignore the `ProductName` property in the mapping configuration: +Lastly, open the `OrderingServiceApplicationMappers` class (the `OrderingServiceApplicationMappers.cs` file under the `ObjectMapping` folder of the `CloudCrm.OrderingService` project of the `CloudCrm.OrderingService` .NET solution) and ignore the `ProductName` property in the mapping configuration: ```csharp -using AutoMapper; -using CloudCrm.OrderingService.Entities; -using CloudCrm.OrderingService.Services; -using Volo.Abp.AutoMapper; +using Riok.Mapperly.Abstractions; +using Volo.Abp.Mapperly; namespace CloudCrm.OrderingService.ObjectMapping; -public class OrderingServiceApplicationAutoMapperProfile : Profile +[Mapper] +public partial class OrderingServiceApplicationMappers : MapperBase { - public OrderingServiceApplicationAutoMapperProfile() - { - CreateMap() - .Ignore(x => x.ProductName); // New line - } + [MapperIgnoreTarget(nameof(OrderDto.ProductName))] + public override partial OrderDto Map(Order source); + + [MapperIgnoreTarget(nameof(OrderDto.ProductName))] + public override partial void Map(Order source, OrderDto destination); } ``` + Let's explain the changes we made: - We added a new property named `ProductName` to the `OrderDto` class. This property will hold the product name. diff --git a/docs/en/tutorials/modular-crm/images/visual-studio-product-integration-service-implementation.png b/docs/en/tutorials/modular-crm/images/visual-studio-product-integration-service-implementation.png index 48ea4cec66..b1fc399dec 100644 Binary files a/docs/en/tutorials/modular-crm/images/visual-studio-product-integration-service-implementation.png and b/docs/en/tutorials/modular-crm/images/visual-studio-product-integration-service-implementation.png differ diff --git a/docs/en/tutorials/modular-crm/part-03.md b/docs/en/tutorials/modular-crm/part-03.md index fb19277895..bbb0339b50 100644 --- a/docs/en/tutorials/modular-crm/part-03.md +++ b/docs/en/tutorials/modular-crm/part-03.md @@ -323,23 +323,17 @@ Notice that `ProductAppService` class implements the `IProductAppService` and al #### Object Mapping -`ProductAppService.GetListAsync` method uses the `ObjectMapper` service to convert `Product` entities to `ProductDto` objects. The mapping should be configured. Open the `CatalogAutoMapperProfile` class in the `ModularCrm.Catalog` project and change it to the following code block: +`ProductAppService.GetListAsync` method uses the `ObjectMapper` service to convert `Product` entities to `ProductDto` objects. The mapping should be configured. So, create a new mapping class in the `ModularCrm.Catalog` project that implements the `MapperBase` class with the `[Mapper]` attribute as follows: -````csharp -using AutoMapper; - -namespace ModularCrm.Catalog; - -public class CatalogAutoMapperProfile : Profile +```csharp +[Mapper] +public partial class ProductToProductDtoMapper : MapperBase { - public CatalogAutoMapperProfile() - { - CreateMap(); - } -} -```` + public override partial ProductDto Map(Product source); -We've added the `CreateMap();` line to define the mapping. + public override partial void Map(Product source, ProductDto destination); +} +``` ### Exposing Application Services as HTTP API Controllers diff --git a/docs/en/tutorials/modular-crm/part-05.md b/docs/en/tutorials/modular-crm/part-05.md index 139c4a9a8b..46a693a963 100644 --- a/docs/en/tutorials/modular-crm/part-05.md +++ b/docs/en/tutorials/modular-crm/part-05.md @@ -167,7 +167,7 @@ The `ReplaceDbContext` attribute allows the use of the `ModularCrmDbContext` cla ````csharp public class ModularCrmDbContext : AbpDbContext, - IProductsDbContext, + ICatalogDbContext, IOrderingDbContext //NEW: IMPLEMENT THE INTERFACE { public DbSet Products { get; set; } @@ -283,21 +283,17 @@ The new files under the `ModularCrm.Ordering.Contracts` project should be like t ### Implementing the Application Service -First we configure the *AutoMapper* to map the `Order` entity to the `OrderDto` object, because we will need it later. Open the `OrderingAutoMapperProfile` under the `ModularCrm.Ordering` project: +First, create a new mapping class (under the `ModularCrm.Ordering` project) that implements the `MapperBase` class with the `[Mapper]` attribute to map `Order` entities to `OrderDto` objects as follows, because we will need it later: -````csharp -using AutoMapper; - -namespace ModularCrm.Ordering; - -public class OrderingAutoMapperProfile : Profile +```csharp +[Mapper] +public partial class OrderToOrderDtoMapper : MapperBase { - public OrderingAutoMapperProfile() - { - CreateMap(); - } + public override partial OrderDto Map(Order source); + + public override partial void Map(Order source, OrderDto destination); } -```` +``` Now, you can implement the `IOrderAppService` interface. Create an `OrderAppService` class under the `ModularCrm.Ordering` project: diff --git a/docs/en/tutorials/modular-crm/part-06.md b/docs/en/tutorials/modular-crm/part-06.md index 75af841312..1c04cd56d1 100644 --- a/docs/en/tutorials/modular-crm/part-06.md +++ b/docs/en/tutorials/modular-crm/part-06.md @@ -19,7 +19,7 @@ You have created two modules so far: the **Catalog** module to store and manage In this part and next two pars, you will learn to implement three common patterns for integrating these modules: 1. The Order module will make a request to the Catalog module to get product information when needed. -2. The Product module will listen to events from the Ordering module, so it can decrease a product's stock count when an order is placed. +2. The Catalog module will listen to events from the Ordering module, so it can decrease a product's stock count when an order is placed. 3. Finally, you will execute a database query that includes product and order data. Let's begin from the first one: The Integration Services. @@ -217,21 +217,17 @@ public class OrderDto } ```` -Lastly, open the `OrderingAutoMapperProfile` class (the `OrderingAutoMapperProfile.cs` file under the `Services` folder of the `ModularCrm.Ordering` project of the `ModularCrm.Ordering` .NET solution) and ignore the `ProductName` property in the mapping configuration: +Lastly, open the `OrderingApplicationMappers` class (the `OrderingApplicationMappers.cs` file under the `Services` folder of the `ModularCrm.Ordering` project of the `ModularCrm.Ordering` .NET solution) and add the following mapping class: ````csharp -using AutoMapper; -using Volo.Abp.AutoMapper; - -namespace ModularCrm.Ordering; - -public class OrderingApplicationAutoMapperProfile : Profile +[Mapper] +public partial class OrderToOrderDtoMapper : MapperBase { - public OrderingApplicationAutoMapperProfile() - { - CreateMap() - .Ignore(x => x.ProductName); // New line - } + [MapperIgnoreTarget(nameof(OrderDto.ProductName))] + public override partial OrderDto Map(Order source); + + [MapperIgnoreTarget(nameof(OrderDto.ProductName))] + public override partial void Map(Order source, OrderDto destination); } ```` diff --git a/docs/en/tutorials/todo/layered/index.md b/docs/en/tutorials/todo/layered/index.md index 783d1d8854..b990704c7a 100644 --- a/docs/en/tutorials/todo/layered/index.md +++ b/docs/en/tutorials/todo/layered/index.md @@ -171,13 +171,13 @@ This application has a single [entity](../../../framework/architecture/domain-dr using System; using Volo.Abp.Domain.Entities; -namespace TodoApp +namespace TodoApp; + +public class TodoItem : BasicAggregateRoot { - public class TodoItem : BasicAggregateRoot - { - public string Text { get; set; } = string.Empty; - } + public string Text { get; set; } = string.Empty; } + ```` `BasicAggregateRoot` is the simplest base class to create root entities, and `Guid` is the primary key (`Id`) of the entity here. @@ -281,15 +281,15 @@ using System.Collections.Generic; using System.Threading.Tasks; using Volo.Abp.Application.Services; -namespace TodoApp +namespace TodoApp; + +public interface ITodoAppService : IApplicationService { - public interface ITodoAppService : IApplicationService - { - Task> GetListAsync(); - Task CreateAsync(string text); - Task DeleteAsync(Guid id); - } + Task> GetListAsync(); + Task CreateAsync(string text); + Task DeleteAsync(Guid id); } + ```` ### Data Transfer Object @@ -299,14 +299,14 @@ namespace TodoApp ````csharp using System; -namespace TodoApp +namespace TodoApp; + +public class TodoItemDto { - public class TodoItemDto - { - public Guid Id { get; set; } - public string Text { get; set; } = string.Empty; - } + public Guid Id { get; set; } + public string Text { get; set; } = string.Empty; } + ```` This is a very simple DTO class that matches our `TodoItem` entity. We are ready to implement the `ITodoAppService`. @@ -323,19 +323,18 @@ using System.Threading.Tasks; using Volo.Abp.Application.Services; using Volo.Abp.Domain.Repositories; -namespace TodoApp +namespace TodoApp; + +public class TodoAppService : ApplicationService, ITodoAppService { - public class TodoAppService : ApplicationService, ITodoAppService - { - private readonly IRepository _todoItemRepository; + private readonly IRepository _todoItemRepository; - public TodoAppService(IRepository todoItemRepository) - { - _todoItemRepository = todoItemRepository; - } - - // TODO: Implement the methods here... + public TodoAppService(IRepository todoItemRepository) + { + _todoItemRepository = todoItemRepository; } + + // TODO: Implement the methods here... } ```` @@ -412,8 +411,8 @@ Open the `Index.cshtml.cs` file in the `Pages` folder of the *TodoApp.Web* proje using System.Collections.Generic; using System.Threading.Tasks; -namespace TodoApp.Web.Pages -{ +namespace TodoApp.Web.Pages; + public class IndexModel : TodoAppPageModel { public List TodoItems { get; set; } @@ -430,7 +429,7 @@ namespace TodoApp.Web.Pages TodoItems = await _todoAppService.GetListAsync(); } } -} + ```` This class uses the `ITodoAppService` to get the list of todo items and assign the `TodoItems` property. We will use it to render the todo items on the razor page. @@ -578,34 +577,39 @@ using Microsoft.AspNetCore.Components; using System.Collections.Generic; using System.Threading.Tasks; -namespace TodoApp.Blazor.Pages +{{if UI=="Blazor" || UI=="BlazorWebApp"}} +namespace TodoApp.Blazor.Client.Pages; +{{else if UI=="BlazorServer"}} +namespace TodoApp.Blazor.Pages; +{{else if UI=="MAUIBlazor"}} +namespace TodoApp.MauiBlazor.Pages; +{{end}} + +public partial class Index { - public partial class Index - { - [Inject] - private ITodoAppService TodoAppService { get; set; } + [Inject] + private ITodoAppService TodoAppService { get; set; } - private List TodoItems { get; set; } = new List(); - private string NewTodoText { get; set; } = string.Empty; + private List TodoItems { get; set; } = new List(); + private string NewTodoText { get; set; } = string.Empty; - protected override async Task OnInitializedAsync() - { - TodoItems = await TodoAppService.GetListAsync(); - } - - private async Task Create() - { - var result = await TodoAppService.CreateAsync(NewTodoText); - TodoItems.Add(result); - NewTodoText = null; - } + protected override async Task OnInitializedAsync() + { + TodoItems = await TodoAppService.GetListAsync(); + } + + private async Task Create() + { + var result = await TodoAppService.CreateAsync(NewTodoText); + TodoItems.Add(result); + NewTodoText = null; + } - private async Task Delete(TodoItemDto todoItem) - { - await TodoAppService.DeleteAsync(todoItem.Id); - await Notify.Info("Deleted the todo item."); - TodoItems.Remove(todoItem); - } + private async Task Delete(TodoItemDto todoItem) + { + await TodoAppService.DeleteAsync(todoItem.Id); + await Notify.Info("Deleted the todo item."); + TodoItems.Remove(todoItem); } } ``` diff --git a/docs/en/ui-themes/lepton-x-lite/angular.md b/docs/en/ui-themes/lepton-x-lite/angular.md index 88fd0627b6..4abe6c86ff 100644 --- a/docs/en/ui-themes/lepton-x-lite/angular.md +++ b/docs/en/ui-themes/lepton-x-lite/angular.md @@ -31,45 +31,68 @@ yarn add bootstrap-icons Note: You should remove the old theme styles from "angular.json" if you are switching from "ThemeBasic" or "Lepton." Look at the [Theme Configurations](../../framework/ui/angular/theme-configurations.md) list of styles. Depending on your theme, you can alter your styles in angular.json. -- Finally, remove `ThemeBasicModule`, `provideThemeBasicConfig` from `app.module.ts`, and import the related modules in `app.module.ts` +- Finally, remove `provideThemeBasicConfig` from `app.config.ts`, and import the related providers in `app.config.ts` ```js -import { ThemeLeptonXModule } from "@abp/ng.theme.lepton-x"; +import { provideThemeLeptonX } from "@abp/ng.theme.lepton-x"; +import { provideSideMenuLayout } from "@abp/ng.theme.lepton-x/layouts"; -@NgModule({ - imports: [ - // ... - // do not forget to remove ThemeBasicModule or other old theme module - // ThemeBasicModule - ThemeLeptonXModule.forRoot() - ], +export const appConfig: ApplicationConfig = { providers: [ - // do not forget to remove provideThemeBasicConfig or other old theme providers - // provideThemeBasicConfig + // ... + provideSideMenuLayout(), + provideThemeLeptonX(), ], - // ... -}) -export class AppModule {} +}; ``` -Note: If you employ [Resource Owner Password Flow](../../framework/ui/angular/authorization.md#resource-owner-password-flow) for authorization, you should import the following module as well: +Note: If you employ [Resource Owner Password Flow](../../framework/ui/angular/authorization.md#resource-owner-password-flow) for authorization, you should provide the following provider as well: ```js -import { AccountLayoutModule } from "@abp/ng.theme.lepton-x/account"; +import { provideAccountLayout } from "@abp/ng.theme.lepton-x/account"; -@NgModule({ - // ... - imports: [ +export const appConfig: ApplicationConfig = { + providers: [ // ... - AccountLayoutModule.forRoot(), + provideAccountLayout() + ], +}; + +``` + +To change the logos and brand color of `LeptonX`, you have two options: + +1) Provide logo and application name via the Theme Shared provider (recommended) + +```ts +// app.config.ts +import { provideLogo, withEnvironmentOptions } from '@abp/ng.theme.shared'; +import { environment } from './environments/environment'; + +export const appConfig: ApplicationConfig = { + providers: [ // ... + provideLogo(withEnvironmentOptions(environment)), ], +}; +``` + +Ensure your environment contains the logo url and app name: + +```ts +// environment.ts +export const environment = { // ... -}) -export class AppModule {} + application: { + name: 'MyProjectName', + logoUrl: '/assets/images/logo.png', + }, +}; ``` -To change the logos and brand color of `LeptonX`, simply add the following CSS to the `styles.scss` +The LeptonX brand component reads these values automatically from `@abp/ng.theme.shared`. + +2) Or override via CSS variables in `styles.scss` ```css :root { @@ -83,6 +106,8 @@ To change the logos and brand color of `LeptonX`, simply add the following CSS t - `--lpx-logo-icon` is a square icon used when the menu is collapsed. - `--lpx-brand` is a color used throughout the application, especially on active elements. +Tip: You can combine both approaches. For example, provide the main logo via `provideLogo(...)` and still fine-tune visuals (sizes, colors) with CSS. + ### Server Side In order to migrate to LeptonX on your server side projects (Host and/or AuthServer projects), please follow the [Server Side Migration](asp-net-core.md) document. diff --git a/docs/en/ui-themes/lepton-x/angular.md b/docs/en/ui-themes/lepton-x/angular.md index 60ad62b5d9..af1713cec8 100644 --- a/docs/en/ui-themes/lepton-x/angular.md +++ b/docs/en/ui-themes/lepton-x/angular.md @@ -9,51 +9,36 @@ To add `LeptonX` into your existing projects, follow the steps below. Add theme-specific styles into the `styles` array of the file. Check the [Theme Configurations](../../framework/ui/angular/theme-configurations.md#lepton-x-commercial) documentation for more information. -Importing a CSS file as an ECMA module is not supported in Angular 14. Therefore, we need to add the styles in the angular.json file. -- At last, remove `ThemeLeptonModule` from `app.module.ts` and `shared.module.ts`, and import the following modules in `app.module.ts` +- At last, remove `provideThemeLepton` from `app.config.ts`, and add the following providers in `app.config.ts` + ```ts -import { - HttpErrorComponent, - ThemeLeptonXModule, -} from "@volosoft/abp.ng.theme.lepton-x"; -import { SideMenuLayoutModule } from "@volosoft/abp.ng.theme.lepton-x/layouts"; - -@NgModule({ - // ... - imports: [ - // ... - // ThemeLeptonModule.forRoot(), -> remove this line. - ThemeLeptonXModule.forRoot(), - SideMenuLayoutModule.forRoot(), // depends on which layout you choose - // ... +import { provideThemeLeptonX } from '@volosoft/abp.ng.theme.lepton-x'; +import { provideSideMenuLayout } from '@volosoft/abp.ng.theme.lepton-x/layouts'; +// import { provideThemeLepton } from '@volo/abp.ng.theme.lepton'; + +export const appConfig: ApplicationConfig = { + providers: [ + // provideThemeLepton() delete this + provideSideMenuLayout(), // depends on which layout you choose + provideThemeLeptonX(), ], - // ... -}) -export class AppModule {} +}; ``` -If you want to use the **`Top Menu`** instead of the **`Side Menu`**, add TopMenuLayoutModule as below,and [this style imports](https://docs.abp.io/en/abp/7.4/UI/Angular/Theme-Configurations#lepton-x-commercial) +If you want to use the **`Top Menu`** instead of the **`Side Menu`**, add `provideTopMenuLayout` as below,and [this style imports](https://docs.abp.io/en/abp/7.4/UI/Angular/Theme-Configurations#lepton-x-commercial) ```ts -import { - HttpErrorComponent, - ThemeLeptonXModule, -} from "@volosoft/abp.ng.theme.lepton-x"; -import { TopMenuLayoutModule } from "@volosoft/abp.ng.theme.lepton-x/layouts"; - -@NgModule({ - // ... - imports: [ - // ... - // ThemeLeptonModule.forRoot(), -> remove this line. - ThemeLeptonXModule.forRoot(), - TopMenuLayoutModule.forRoot(), +import { provideThemeLeptonX } from '@volosoft/abp.ng.theme.lepton-x'; +import { provideTopMenuLayout } from '@volosoft/abp.ng.theme.lepton-x/layouts'; + +export const appConfig: ApplicationConfig = { + providers: [ + provideTopMenuLayout(), + provideThemeLeptonX(), ], - // ... -}) -export class AppModule {} +}; ``` - At this point, `LeptonX` theme should be up and running within your application. However, you may need to overwrite some css variables based your needs for every theme available as follows: diff --git a/docs/en/ui-themes/lepton-x/angular/how-to-add-a-new-variation-to-lepton-x-for-angular.md b/docs/en/ui-themes/lepton-x/angular/how-to-add-a-new-variation-to-lepton-x-for-angular.md index afea2fd228..dc858715d0 100644 --- a/docs/en/ui-themes/lepton-x/angular/how-to-add-a-new-variation-to-lepton-x-for-angular.md +++ b/docs/en/ui-themes/lepton-x/angular/how-to-add-a-new-variation-to-lepton-x-for-angular.md @@ -37,26 +37,20 @@ export const NEW_THEME_PROVIDER = [ In this code snippet, we create a new instance of the LpxTheme class called myNewThemeDefinition. We specify the bundles that make up the theme (e.g., "bootstrap-custom" and "custom-theme"), the style name for the theme, a label to display in the UI, and an icon (using Bootstrap icons in this example). ### Step 2: Registering the New Theme Provider -Now that we have defined the new theme appearance, we need to register it as a provider in our Angular project. Open the app.module.ts file (or the module where LeptonX is configured), and add the following code: +Now that we have defined the new theme appearance, we need to register it as a provider in our Angular project. Open the `app.config.ts` file (or the configuration where LeptonX is configured), and add the following code: ```js import { NEW_THEME_PROVIDER } from './new-theme.provider.ts'; -@NgModule({ - imports: [ - // ... - ], +export const appConfig: ApplicationConfig = { providers: [ // ... - NEW_THEME_PROVIDER, + NEW_THEME_PROVIDER ], - // ... -}) -export class AppModule { } - +}; ``` -By importing the `NEW_THEME_PROVIDER` from the file where we defined our theme, we can add it to the providers array of our Angular module. This makes the new theme appearance available throughout the application. +By importing the `NEW_THEME_PROVIDER` from the file where we defined our theme, we can add it to the providers array of our Angular application configuration. This makes the new theme appearance available throughout the application. ### Step 3: Adding the Styles Path to angular.json diff --git a/docs/en/ui-themes/lepton-x/angular/how-to-change-default-theme-option.md b/docs/en/ui-themes/lepton-x/angular/how-to-change-default-theme-option.md index 4b58c4b106..264091b561 100644 --- a/docs/en/ui-themes/lepton-x/angular/how-to-change-default-theme-option.md +++ b/docs/en/ui-themes/lepton-x/angular/how-to-change-default-theme-option.md @@ -1,26 +1,25 @@ # Configuring the Default Theme for LeptonX -The LeptonX theme offers multiple appearances to suit your application's visual style. You can easily configure the default theme for your application using the ThemeLeptonXModule provided by LeptonX. +The LeptonX theme offers multiple appearances to suit your application's visual style. You can easily configure the default theme for your application using the `provideThemeLeptonX` provided by LeptonX. ### Configuration Code -To set the default theme, you need to configure the ThemeLeptonXModule using the forRoot() function in your application's main module (often referred to as AppModule). Here's an example: +To set the default theme, you need to configure the `provideThemeLeptonX` using the `withThemeLeptonXOptions({...})` function in the main configuration of your application (often referred to as appConfig). Here's an example: + ```js -import { ThemeLeptonXModule } from 'leptonx'; // Import the LeptonX theme module +import { provideThemeLeptonX, withThemeLeptonXOptions } from '@volosoft/abp.ng.theme.lepton-x'; -@NgModule({ - // ... Other module configurations - imports: [ - // ... Other imported modules - ThemeLeptonXModule.forRoot({ - defaultTheme: 'light', // Set the default theme to 'light' - }), +export const appConfig: ApplicationConfig = { + providers: [ + // ... + provideThemeLeptonX( + withThemeLeptonXOptions({ + defaultTheme: 'light' + }) + ), ], - bootstrap: [AppComponent], -}) -export class AppModule {} +}; ``` - -In the example above, we've imported the ThemeLeptonXModule and configured it using the forRoot() function. By providing the defaultTheme parameter and setting its value to 'light', +In the example above, we've imported the `provideThemeLeptonX` and `withThemeLeptonXOptions`, then configured it using the option parameters. By providing the `defaultTheme` parameter and setting its value to 'light'. If you delete the defaultTheme parameter in the configuration object, the LeptonX theme will use the default value of "System" as the default theme appearance. diff --git a/docs/en/ui-themes/lepton-x/how-to-use-lepton-x-components-with-angular-custom-layout.md b/docs/en/ui-themes/lepton-x/how-to-use-lepton-x-components-with-angular-custom-layout.md index f68f3bd90f..57cdcd0ec9 100644 --- a/docs/en/ui-themes/lepton-x/how-to-use-lepton-x-components-with-angular-custom-layout.md +++ b/docs/en/ui-themes/lepton-x/how-to-use-lepton-x-components-with-angular-custom-layout.md @@ -5,25 +5,22 @@ First, The custom layout component should be created and implemented for the Ang Related content can be found in the [Component Replacement Document](../../framework/ui/angular/component-replacement.md#how-to-replace-a-layout) - -After creating a custom layout, these imports should be imported in the `app.module.ts` file because the modules contain definitions of the Lepton X components. +After creating a custom layout, these imports should be imported in the `app.config.ts` file because the modules contain definitions of the Lepton X components. ```javascript -// app.module.ts +// app.config.ts import { LpxSideMenuLayoutModule } from '@volosoft/ngx-lepton-x/layouts'; import { LpxResponsiveModule } from '@volo/ngx-lepton-x.core';// optional. Only, if you are using lpxResponsive directive - @NgModule({ - //... removed for clearity - imports: [ - //... removed for clearity - LpxSideMenuLayoutModule, - LpxResponsiveModule // <-- Optional - ] -}) -export class AppModule {} - +export const appConfig: ApplicationConfig = { + providers: [ + importProvidersFrom([ + LpxSideMenuLayoutModule, + LpxResponsiveModule // <-- Optional + ]) + ], +}; ``` Here is the simplified version of the `side-menu-layout.ts` file. Only the ABP Component Replacement code has been removed. diff --git a/docs/en/ui-themes/lepton/customizing-lepton-theme.md b/docs/en/ui-themes/lepton/customizing-lepton-theme.md index fb88742c5b..d626fd2769 100644 --- a/docs/en/ui-themes/lepton/customizing-lepton-theme.md +++ b/docs/en/ui-themes/lepton/customizing-lepton-theme.md @@ -12,19 +12,28 @@ You may want to change certain aspects of your website’s appearance with a cu ## Adding Custom Style -There is a `customStyle` boolean configuration in `ThemeLeptonModule`'s `forRoot` method. If this configuration is true, the style selection box is not included in the theme settings form and `ThemeLeptonModule` does not load its own styles. In this case, a custom style file must be added to the styles array in `angular.json` or must be imported by `style.scss`. +There is a `customStyle` boolean configuration in `provideThemeLepton(withLeptonOptions({...}))` method. If this configuration is true, the style selection box is not included in the theme settings form and `theme-lepton` does not load its own styles. In this case, a custom style file must be added to the styles array in `angular.json` or must be imported by `style.scss`. > Only angular project styles can be changed in this way. If the authorization flow is authorization code flow, MVC pages (login, profile, etc) are not affected by this change. Custom style implementation can be done with the following steps -Set `customStyle` property to `true` where is `ThemeLeptonModule` imported with `forRoot` method. +Set `customStyle` property to `true` where `provideThemeLepton(withLeptonOptions({...}))` method is called. ```javascript -// app.module.ts -ThemeLeptonModule.forRoot({ - customStyle: true -}) +// app.config.ts +import { provideThemeLepton, withOptions as withLeptonOptions } from '@volo/abp.ng.theme.lepton'; + +export const appConfig: ApplicationConfig = { + providers: [ + // ... + provideThemeLepton( + withLeptonOptions({ + customStyle: true + }) + ) + ], +}; ``` Import your style file to `src/style.scss` @@ -68,7 +77,7 @@ Or add your style file to the `styles` arrays which in `angular.json` file ## Inserting Custom Content To Lepton Menu -Lepton menu can take custom content both before and after the menu items displayed. In order to achieve this, pass a component as content through the parameters of `ThemeLeptonModule.forRoot` when you import the module in your root module, i.e. `AppModule`. Let's take a look at some examples. +Lepton menu can take custom content both before and after the menu items displayed. In order to achieve this, pass a component as content through the parameters of `provideThemeLepton(withLeptonOptions({...}))` when you import the provider in your root app configuration, i.e. `appConfig`. Let's take a look at some examples. ### Placing Custom Content Before & After Menu Items @@ -76,7 +85,10 @@ Lepton menu can take custom content both before and after the menu items display First step is to create a component which will serve as the custom content. ```js +// ... @Component({ + // ... + imports: [AsyncPipe], template: ` Support Issues @@ -88,27 +100,23 @@ First step is to create a component which will serve as the custom content. export class SupportLinkComponent { issueCount$ = of(26); // dummy count, replace this with an actual service } - -@NgModule({ - declarations: [SupportLinkComponent], - imports: [CommonModule], -}) -export class SupportLinkModule {} ``` -Now, pass this component as `contentAfterRoutes` option to `ThemeLeptonModule`. +Now, pass this component as `contentAfterRoutes` option to `provideThemeLepton(withLeptonOptions({...}))`. ```js -@NgModule({ - imports: [ - // other imports are removed for sake of brevity - SupportLinkModule, - ThemeLeptonModule.forRoot({ - contentAfterRoutes: [SupportLinkComponent], - }) +import { provideThemeLepton, withOptions as withLeptonOptions } from '@volo/abp.ng.theme.lepton'; + +export const appConfig: ApplicationConfig = { + providers: [ + // ... + provideThemeLepton( + withLeptonOptions({ + contentAfterRoutes: [SupportLinkComponent], + }) + ) ], -}) -export class AppModule {} +}; ``` If you start the dev server, you must see the inserted content as follows: @@ -124,24 +132,25 @@ Placing the content before menu items is straightforward: Just replace `contentA ### Placing a Search Input Before Menu Items -The Lepton package has a search component designed to work with the routes in the menu. You can simply import the module and pass the component as `contentBeforeRoutes` option to `ThemeLeptonModule`. +The Lepton package has a search component designed to work with the routes in the menu. You can simply import the provider and pass the component as `contentBeforeRoutes` option to `provideThemeLepton(withLeptonOptions({...}))`. ```js -import { MenuSearchComponent, MenuSearchModule } from '@volo/abp.ng.theme.lepton/extensions'; - -@NgModule({ - imports: [ - // other imports are removed for sake of brevity - - MenuSearchModule.forRoot({ +import { provideThemeLepton, withOptions as withLeptonOptions } from '@volo/abp.ng.theme.lepton'; +import { MenuSearchComponent, provideMenuSearch } from '@volo/abp.ng.theme.lepton/extensions'; + +export const appConfig: ApplicationConfig = { + providers: [ + // ... + provideThemeLepton( + withLeptonOptions({ + contentBeforeRoutes: [MenuSearchComponent], + }) + ), + provideMenuSearch({ limit: 3 // search result limit (default: Infinity) - }), - ThemeLeptonModule.forRoot({ - contentBeforeRoutes: [MenuSearchComponent], }) ], -}) -export class AppModule {} +}; ``` Here is how the search input works: diff --git a/framework/Volo.Abp.sln b/framework/Volo.Abp.sln index 78f7ae0011..e304510e48 100644 --- a/framework/Volo.Abp.sln +++ b/framework/Volo.Abp.sln @@ -491,6 +491,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.BlobStoring.Bunny. EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Timing.Tests", "test\Volo.Abp.Timing.Tests\Volo.Abp.Timing.Tests.csproj", "{58FCF22D-E8DB-4EB8-B586-9BB6E9899D64}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Mapperly", "src\Volo.Abp.Mapperly\Volo.Abp.Mapperly.csproj", "{AF556046-54CD-48BC-9740-3E926DB8B510}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.Mapperly.Tests", "test\Volo.Abp.Mapperly.Tests\Volo.Abp.Mapperly.Tests.csproj", "{C38926D5-C1E7-47D6-BD0B-D36BE4C19AE7}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Volo.Abp.EntityFrameworkCore.MySQL.Pomelo", "src\Volo.Abp.EntityFrameworkCore.MySQL.Pomelo\Volo.Abp.EntityFrameworkCore.MySQL.Pomelo.csproj", "{5B49FE47-A4C5-45BE-A903-8215CF5E2FAF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -1465,6 +1471,18 @@ Global {58FCF22D-E8DB-4EB8-B586-9BB6E9899D64}.Debug|Any CPU.Build.0 = Debug|Any CPU {58FCF22D-E8DB-4EB8-B586-9BB6E9899D64}.Release|Any CPU.ActiveCfg = Release|Any CPU {58FCF22D-E8DB-4EB8-B586-9BB6E9899D64}.Release|Any CPU.Build.0 = Release|Any CPU + {AF556046-54CD-48BC-9740-3E926DB8B510}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AF556046-54CD-48BC-9740-3E926DB8B510}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AF556046-54CD-48BC-9740-3E926DB8B510}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AF556046-54CD-48BC-9740-3E926DB8B510}.Release|Any CPU.Build.0 = Release|Any CPU + {C38926D5-C1E7-47D6-BD0B-D36BE4C19AE7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C38926D5-C1E7-47D6-BD0B-D36BE4C19AE7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C38926D5-C1E7-47D6-BD0B-D36BE4C19AE7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C38926D5-C1E7-47D6-BD0B-D36BE4C19AE7}.Release|Any CPU.Build.0 = Release|Any CPU + {5B49FE47-A4C5-45BE-A903-8215CF5E2FAF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5B49FE47-A4C5-45BE-A903-8215CF5E2FAF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5B49FE47-A4C5-45BE-A903-8215CF5E2FAF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5B49FE47-A4C5-45BE-A903-8215CF5E2FAF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -1712,6 +1730,9 @@ Global {1BBCBA72-CDB6-4882-96EE-D4CD149433A2} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} {BC4BB2D6-DFD8-4190-AAC3-32C0A7A8E915} = {447C8A77-E5F0-4538-8687-7383196D04EA} {58FCF22D-E8DB-4EB8-B586-9BB6E9899D64} = {447C8A77-E5F0-4538-8687-7383196D04EA} + {AF556046-54CD-48BC-9740-3E926DB8B510} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} + {C38926D5-C1E7-47D6-BD0B-D36BE4C19AE7} = {447C8A77-E5F0-4538-8687-7383196D04EA} + {5B49FE47-A4C5-45BE-A903-8215CF5E2FAF} = {5DF0E140-0513-4D0D-BE2E-3D4D85CD70E6} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {BB97ECF4-9A84-433F-A80B-2A3285BDD1D5} diff --git a/framework/src/Volo.Abp.AspNetCore.Authentication.JwtBearer/Volo.Abp.AspNetCore.Authentication.JwtBearer.csproj b/framework/src/Volo.Abp.AspNetCore.Authentication.JwtBearer/Volo.Abp.AspNetCore.Authentication.JwtBearer.csproj index 2e22d5194f..ced3191585 100644 --- a/framework/src/Volo.Abp.AspNetCore.Authentication.JwtBearer/Volo.Abp.AspNetCore.Authentication.JwtBearer.csproj +++ b/framework/src/Volo.Abp.AspNetCore.Authentication.JwtBearer/Volo.Abp.AspNetCore.Authentication.JwtBearer.csproj @@ -27,7 +27,7 @@ - + diff --git a/framework/src/Volo.Abp.AspNetCore.Authentication.JwtBearer/Volo/Abp/AspNetCore/Authentication/JwtBearer/DynamicClaims/WebRemoteDynamicClaimsPrincipalContributorCache.cs b/framework/src/Volo.Abp.AspNetCore.Authentication.JwtBearer/Volo/Abp/AspNetCore/Authentication/JwtBearer/DynamicClaims/WebRemoteDynamicClaimsPrincipalContributorCache.cs index 3a3b16131d..63c2d6d082 100644 --- a/framework/src/Volo.Abp.AspNetCore.Authentication.JwtBearer/Volo/Abp/AspNetCore/Authentication/JwtBearer/DynamicClaims/WebRemoteDynamicClaimsPrincipalContributorCache.cs +++ b/framework/src/Volo.Abp.AspNetCore.Authentication.JwtBearer/Volo/Abp/AspNetCore/Authentication/JwtBearer/DynamicClaims/WebRemoteDynamicClaimsPrincipalContributorCache.cs @@ -1,7 +1,7 @@ using System; using System.Net.Http; using System.Threading.Tasks; -using IdentityModel.Client; +using Duende.IdentityModel.Client; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; diff --git a/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling/MauiStyleContributor.cs b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling/MauiStyleContributor.cs index e5ae6947de..32d1880c55 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling/MauiStyleContributor.cs +++ b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming.Bundling/MauiStyleContributor.cs @@ -14,5 +14,6 @@ public class MauiStyleContributor : BundleContributor context.Files.AddIfNotContains("_content/Blazorise/blazorise.css"); context.Files.AddIfNotContains("_content/Blazorise.Bootstrap5/blazorise.bootstrap5.css"); context.Files.AddIfNotContains("_content/Blazorise.Snackbar/blazorise.snackbar.css"); + context.Files.AddIfNotContains("_content/Volo.Abp.BlazoriseUI/volo.abp.blazoriseui.css"); } } diff --git a/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming/wwwroot/libs/bootstrap/css/bootstrap.min.css b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming/wwwroot/libs/bootstrap/css/bootstrap.min.css index 02ae65b5fe..955575adcb 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming/wwwroot/libs/bootstrap/css/bootstrap.min.css +++ b/framework/src/Volo.Abp.AspNetCore.Components.MauiBlazor.Theming/wwwroot/libs/bootstrap/css/bootstrap.min.css @@ -1,7 +1,6 @@ @charset "UTF-8";/*! - * Bootstrap v5.1.0 (https://getbootstrap.com/) - * Copyright 2011-2021 The Bootstrap Authors - * Copyright 2011-2021 Twitter, Inc. + * Bootstrap v5.3.7 (https://getbootstrap.com/) + * Copyright 2011-2025 The Bootstrap Authors * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) - */:root{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-body-rgb:33,37,41;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans","Liberation Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-bg:#fff}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;background-color:currentColor;border:0;opacity:.25}hr:not([size]){height:1px}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[data-bs-original-title],abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.2em;background-color:#fcf8e3}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:#0d6efd;text-decoration:underline}a:hover{color:#0a58ca}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em;direction:ltr;unicode-bidi:bidi-override}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:#d63384;word-wrap:break-word}a>code{color:inherit}kbd{padding:.2rem .4rem;font-size:.875em;color:#fff;background-color:#212529;border-radius:.2rem}kbd kbd{padding:0;font-size:1em;font-weight:700}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:#6c757d;text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]::-webkit-calendar-picker-indicator{display:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;font-size:calc(1.275rem + .3vw);line-height:inherit}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:textfield}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::file-selector-button{font:inherit}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-size:calc(1.625rem + 4.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-size:calc(1.575rem + 3.9vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-size:calc(1.525rem + 3.3vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-size:calc(1.475rem + 2.7vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-size:calc(1.425rem + 2.1vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-size:calc(1.375rem + 1.5vw);font-weight:300;line-height:1.2}@media (min-width:1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:#fff;border:1px solid #dee2e6;border-radius:.25rem;max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:#6c757d}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{width:100%;padding-right:var(--bs-gutter-x,.75rem);padding-left:var(--bs-gutter-x,.75rem);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(var(--bs-gutter-y) * -1);margin-right:calc(var(--bs-gutter-x) * -.5);margin-left:calc(var(--bs-gutter-x) * -.5)}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0%}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.6666666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm{flex:1 0 0%}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.6666666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md{flex:1 0 0%}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.6666666667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg{flex:1 0 0%}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.6666666667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl{flex:1 0 0%}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl{flex:1 0 0%}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.3333333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.6666666667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.table{--bs-table-bg:transparent;--bs-table-accent-bg:transparent;--bs-table-striped-color:#212529;--bs-table-striped-bg:rgba(0, 0, 0, 0.05);--bs-table-active-color:#212529;--bs-table-active-bg:rgba(0, 0, 0, 0.1);--bs-table-hover-color:#212529;--bs-table-hover-bg:rgba(0, 0, 0, 0.075);width:100%;margin-bottom:1rem;color:#212529;vertical-align:top;border-color:#dee2e6}.table>:not(caption)>*>*{padding:.5rem .5rem;background-color:var(--bs-table-bg);border-bottom-width:1px;box-shadow:inset 0 0 0 9999px var(--bs-table-accent-bg)}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table>:not(:last-child)>:last-child>*{border-bottom-color:currentColor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:1px 0}.table-bordered>:not(caption)>*>*{border-width:0 1px}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-striped>tbody>tr:nth-of-type(odd){--bs-table-accent-bg:var(--bs-table-striped-bg);color:var(--bs-table-striped-color)}.table-active{--bs-table-accent-bg:var(--bs-table-active-bg);color:var(--bs-table-active-color)}.table-hover>tbody>tr:hover{--bs-table-accent-bg:var(--bs-table-hover-bg);color:var(--bs-table-hover-color)}.table-primary{--bs-table-bg:#cfe2ff;--bs-table-striped-bg:#c5d7f2;--bs-table-striped-color:#000;--bs-table-active-bg:#bacbe6;--bs-table-active-color:#000;--bs-table-hover-bg:#bfd1ec;--bs-table-hover-color:#000;color:#000;border-color:#bacbe6}.table-secondary{--bs-table-bg:#e2e3e5;--bs-table-striped-bg:#d7d8da;--bs-table-striped-color:#000;--bs-table-active-bg:#cbccce;--bs-table-active-color:#000;--bs-table-hover-bg:#d1d2d4;--bs-table-hover-color:#000;color:#000;border-color:#cbccce}.table-success{--bs-table-bg:#d1e7dd;--bs-table-striped-bg:#c7dbd2;--bs-table-striped-color:#000;--bs-table-active-bg:#bcd0c7;--bs-table-active-color:#000;--bs-table-hover-bg:#c1d6cc;--bs-table-hover-color:#000;color:#000;border-color:#bcd0c7}.table-info{--bs-table-bg:#cff4fc;--bs-table-striped-bg:#c5e8ef;--bs-table-striped-color:#000;--bs-table-active-bg:#badce3;--bs-table-active-color:#000;--bs-table-hover-bg:#bfe2e9;--bs-table-hover-color:#000;color:#000;border-color:#badce3}.table-warning{--bs-table-bg:#fff3cd;--bs-table-striped-bg:#f2e7c3;--bs-table-striped-color:#000;--bs-table-active-bg:#e6dbb9;--bs-table-active-color:#000;--bs-table-hover-bg:#ece1be;--bs-table-hover-color:#000;color:#000;border-color:#e6dbb9}.table-danger{--bs-table-bg:#f8d7da;--bs-table-striped-bg:#eccccf;--bs-table-striped-color:#000;--bs-table-active-bg:#dfc2c4;--bs-table-active-color:#000;--bs-table-hover-bg:#e5c7ca;--bs-table-hover-color:#000;color:#000;border-color:#dfc2c4}.table-light{--bs-table-bg:#f8f9fa;--bs-table-striped-bg:#ecedee;--bs-table-striped-color:#000;--bs-table-active-bg:#dfe0e1;--bs-table-active-color:#000;--bs-table-hover-bg:#e5e6e7;--bs-table-hover-color:#000;color:#000;border-color:#dfe0e1}.table-dark{--bs-table-bg:#212529;--bs-table-striped-bg:#2c3034;--bs-table-striped-color:#fff;--bs-table-active-bg:#373b3e;--bs-table-active-color:#fff;--bs-table-hover-bg:#323539;--bs-table-hover-color:#fff;color:#fff;border-color:#373b3e}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(.375rem + 1px);padding-bottom:calc(.375rem + 1px);margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + 1px);padding-bottom:calc(.5rem + 1px);font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + 1px);padding-bottom:calc(.25rem + 1px);font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:#6c757d}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;-webkit-appearance:none;-moz-appearance:none;appearance:none;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:#212529;background-color:#fff;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-control::-webkit-date-and-time-value{height:1.5em}.form-control::-moz-placeholder{color:#6c757d;opacity:1}.form-control::placeholder{color:#6c757d;opacity:1}.form-control:disabled,.form-control[readonly]{background-color:#e9ecef;opacity:1}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:#dde0e3}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:#212529;background-color:#e9ecef;pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:1px;border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:#dde0e3}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:#212529;background-color:transparent;border:solid transparent;border-width:1px 0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + .5rem + 2px);padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + 2px);padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + .75rem + 2px)}textarea.form-control-sm{min-height:calc(1.5em + .5rem + 2px)}textarea.form-control-lg{min-height:calc(1.5em + 1rem + 2px)}.form-control-color{width:3rem;height:auto;padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{height:1.5em;border-radius:.25rem}.form-control-color::-webkit-color-swatch{height:1.5em;border-radius:.25rem}.form-select{display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;-moz-padding-start:calc(0.75rem - 3px);font-size:1rem;font-weight:400;line-height:1.5;color:#212529;background-color:#fff;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:1px solid #ced4da;border-radius:.25rem;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-select{transition:none}}.form-select:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:#e9ecef}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 #212529}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-input{width:1em;height:1em;margin-top:.25em;vertical-align:top;background-color:#fff;background-repeat:no-repeat;background-position:center;background-size:contain;border:1px solid rgba(0,0,0,.25);-webkit-appearance:none;-moz-appearance:none;appearance:none;-webkit-print-color-adjust:exact;color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-check-input:checked{background-color:#0d6efd;border-color:#0d6efd}.form-check-input:checked[type=checkbox]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10l3 3l6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#0d6efd;border-color:#0d6efd;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{width:2em;margin-left:-2.5em;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");background-position:left center;border-radius:2em;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;filter:none;opacity:.65}.form-range{width:100%;height:1.5rem;padding:0;background-color:transparent;-webkit-appearance:none;-moz-appearance:none;appearance:none}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;background-color:#0d6efd;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-webkit-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#b6d4fe}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;background-color:#0d6efd;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;-moz-appearance:none;appearance:none}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#b6d4fe}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:#dee2e6;border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:#adb5bd}.form-range:disabled::-moz-range-thumb{background-color:#adb5bd}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-select{height:calc(3.5rem + 2px);line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;height:100%;padding:1rem .75rem;pointer-events:none;border:1px solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media (prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control{padding:1rem .75rem}.form-floating>.form-control::-moz-placeholder{color:transparent}.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control:not(:-moz-placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control:not(:-moz-placeholder-shown)~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:-webkit-autofill~label{opacity:.65;transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-select:focus{z-index:3}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:3}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:#212529;text-align:center;white-space:nowrap;background-color:#e9ecef;border:1px solid #ced4da;border-radius:.25rem}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#198754}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(25,135,84,.9);border-radius:.25rem}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:#198754;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:#198754}.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"],.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73L.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1z'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:#198754;box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:#198754}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:#198754}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:#198754}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group .form-control.is-valid,.input-group .form-select.is-valid,.was-validated .input-group .form-control:valid,.was-validated .input-group .form-select:valid{z-index:1}.input-group .form-control.is-valid:focus,.input-group .form-select.is-valid:focus,.was-validated .input-group .form-control:valid:focus,.was-validated .input-group .form-select:valid:focus{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:#dc3545}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:rgba(220,53,69,.9);border-radius:.25rem}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:#dc3545;padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:#dc3545}.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"]{padding-right:4.125rem;background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e"),url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:#dc3545;box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:#dc3545}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:#dc3545}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:#dc3545}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group .form-control.is-invalid,.input-group .form-select.is-invalid,.was-validated .input-group .form-control:invalid,.was-validated .input-group .form-select:invalid{z-index:2}.input-group .form-control.is-invalid:focus,.input-group .form-select.is-invalid:focus,.was-validated .input-group .form-control:invalid:focus,.was-validated .input-group .form-select:invalid:focus{z-index:3}.btn{display:inline-block;font-weight:400;line-height:1.5;color:#212529;text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.375rem .75rem;font-size:1rem;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:#212529}.btn-check:focus+.btn,.btn:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{pointer-events:none;opacity:.65}.btn-primary{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-primary:hover{color:#fff;background-color:#0b5ed7;border-color:#0a58ca}.btn-check:focus+.btn-primary,.btn-primary:focus{color:#fff;background-color:#0b5ed7;border-color:#0a58ca;box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.btn-check:active+.btn-primary,.btn-check:checked+.btn-primary,.btn-primary.active,.btn-primary:active,.show>.btn-primary.dropdown-toggle{color:#fff;background-color:#0a58ca;border-color:#0a53be}.btn-check:active+.btn-primary:focus,.btn-check:checked+.btn-primary:focus,.btn-primary.active:focus,.btn-primary:active:focus,.show>.btn-primary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(49,132,253,.5)}.btn-primary.disabled,.btn-primary:disabled{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-secondary{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-secondary:hover{color:#fff;background-color:#5c636a;border-color:#565e64}.btn-check:focus+.btn-secondary,.btn-secondary:focus{color:#fff;background-color:#5c636a;border-color:#565e64;box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-check:active+.btn-secondary,.btn-check:checked+.btn-secondary,.btn-secondary.active,.btn-secondary:active,.show>.btn-secondary.dropdown-toggle{color:#fff;background-color:#565e64;border-color:#51585e}.btn-check:active+.btn-secondary:focus,.btn-check:checked+.btn-secondary:focus,.btn-secondary.active:focus,.btn-secondary:active:focus,.show>.btn-secondary.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(130,138,145,.5)}.btn-secondary.disabled,.btn-secondary:disabled{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-success{color:#fff;background-color:#198754;border-color:#198754}.btn-success:hover{color:#fff;background-color:#157347;border-color:#146c43}.btn-check:focus+.btn-success,.btn-success:focus{color:#fff;background-color:#157347;border-color:#146c43;box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-check:active+.btn-success,.btn-check:checked+.btn-success,.btn-success.active,.btn-success:active,.show>.btn-success.dropdown-toggle{color:#fff;background-color:#146c43;border-color:#13653f}.btn-check:active+.btn-success:focus,.btn-check:checked+.btn-success:focus,.btn-success.active:focus,.btn-success:active:focus,.show>.btn-success.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(60,153,110,.5)}.btn-success.disabled,.btn-success:disabled{color:#fff;background-color:#198754;border-color:#198754}.btn-info{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-info:hover{color:#000;background-color:#31d2f2;border-color:#25cff2}.btn-check:focus+.btn-info,.btn-info:focus{color:#000;background-color:#31d2f2;border-color:#25cff2;box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-check:active+.btn-info,.btn-check:checked+.btn-info,.btn-info.active,.btn-info:active,.show>.btn-info.dropdown-toggle{color:#000;background-color:#3dd5f3;border-color:#25cff2}.btn-check:active+.btn-info:focus,.btn-check:checked+.btn-info:focus,.btn-info.active:focus,.btn-info:active:focus,.show>.btn-info.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(11,172,204,.5)}.btn-info.disabled,.btn-info:disabled{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-warning{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-warning:hover{color:#000;background-color:#ffca2c;border-color:#ffc720}.btn-check:focus+.btn-warning,.btn-warning:focus{color:#000;background-color:#ffca2c;border-color:#ffc720;box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-check:active+.btn-warning,.btn-check:checked+.btn-warning,.btn-warning.active,.btn-warning:active,.show>.btn-warning.dropdown-toggle{color:#000;background-color:#ffcd39;border-color:#ffc720}.btn-check:active+.btn-warning:focus,.btn-check:checked+.btn-warning:focus,.btn-warning.active:focus,.btn-warning:active:focus,.show>.btn-warning.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(217,164,6,.5)}.btn-warning.disabled,.btn-warning:disabled{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-danger{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-danger:hover{color:#fff;background-color:#bb2d3b;border-color:#b02a37}.btn-check:focus+.btn-danger,.btn-danger:focus{color:#fff;background-color:#bb2d3b;border-color:#b02a37;box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-check:active+.btn-danger,.btn-check:checked+.btn-danger,.btn-danger.active,.btn-danger:active,.show>.btn-danger.dropdown-toggle{color:#fff;background-color:#b02a37;border-color:#a52834}.btn-check:active+.btn-danger:focus,.btn-check:checked+.btn-danger:focus,.btn-danger.active:focus,.btn-danger:active:focus,.show>.btn-danger.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(225,83,97,.5)}.btn-danger.disabled,.btn-danger:disabled{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-light{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-light:hover{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:focus+.btn-light,.btn-light:focus{color:#000;background-color:#f9fafb;border-color:#f9fafb;box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-check:active+.btn-light,.btn-check:checked+.btn-light,.btn-light.active,.btn-light:active,.show>.btn-light.dropdown-toggle{color:#000;background-color:#f9fafb;border-color:#f9fafb}.btn-check:active+.btn-light:focus,.btn-check:checked+.btn-light:focus,.btn-light.active:focus,.btn-light:active:focus,.show>.btn-light.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(211,212,213,.5)}.btn-light.disabled,.btn-light:disabled{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-dark{color:#fff;background-color:#212529;border-color:#212529}.btn-dark:hover{color:#fff;background-color:#1c1f23;border-color:#1a1e21}.btn-check:focus+.btn-dark,.btn-dark:focus{color:#fff;background-color:#1c1f23;border-color:#1a1e21;box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-check:active+.btn-dark,.btn-check:checked+.btn-dark,.btn-dark.active,.btn-dark:active,.show>.btn-dark.dropdown-toggle{color:#fff;background-color:#1a1e21;border-color:#191c1f}.btn-check:active+.btn-dark:focus,.btn-check:checked+.btn-dark:focus,.btn-dark.active:focus,.btn-dark:active:focus,.show>.btn-dark.dropdown-toggle:focus{box-shadow:0 0 0 .25rem rgba(66,70,73,.5)}.btn-dark.disabled,.btn-dark:disabled{color:#fff;background-color:#212529;border-color:#212529}.btn-outline-primary{color:#0d6efd;border-color:#0d6efd}.btn-outline-primary:hover{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:focus+.btn-outline-primary,.btn-outline-primary:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)}.btn-check:active+.btn-outline-primary,.btn-check:checked+.btn-outline-primary,.btn-outline-primary.active,.btn-outline-primary.dropdown-toggle.show,.btn-outline-primary:active{color:#fff;background-color:#0d6efd;border-color:#0d6efd}.btn-check:active+.btn-outline-primary:focus,.btn-check:checked+.btn-outline-primary:focus,.btn-outline-primary.active:focus,.btn-outline-primary.dropdown-toggle.show:focus,.btn-outline-primary:active:focus{box-shadow:0 0 0 .25rem rgba(13,110,253,.5)}.btn-outline-primary.disabled,.btn-outline-primary:disabled{color:#0d6efd;background-color:transparent}.btn-outline-secondary{color:#6c757d;border-color:#6c757d}.btn-outline-secondary:hover{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:focus+.btn-outline-secondary,.btn-outline-secondary:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-check:active+.btn-outline-secondary,.btn-check:checked+.btn-outline-secondary,.btn-outline-secondary.active,.btn-outline-secondary.dropdown-toggle.show,.btn-outline-secondary:active{color:#fff;background-color:#6c757d;border-color:#6c757d}.btn-check:active+.btn-outline-secondary:focus,.btn-check:checked+.btn-outline-secondary:focus,.btn-outline-secondary.active:focus,.btn-outline-secondary.dropdown-toggle.show:focus,.btn-outline-secondary:active:focus{box-shadow:0 0 0 .25rem rgba(108,117,125,.5)}.btn-outline-secondary.disabled,.btn-outline-secondary:disabled{color:#6c757d;background-color:transparent}.btn-outline-success{color:#198754;border-color:#198754}.btn-outline-success:hover{color:#fff;background-color:#198754;border-color:#198754}.btn-check:focus+.btn-outline-success,.btn-outline-success:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-check:active+.btn-outline-success,.btn-check:checked+.btn-outline-success,.btn-outline-success.active,.btn-outline-success.dropdown-toggle.show,.btn-outline-success:active{color:#fff;background-color:#198754;border-color:#198754}.btn-check:active+.btn-outline-success:focus,.btn-check:checked+.btn-outline-success:focus,.btn-outline-success.active:focus,.btn-outline-success.dropdown-toggle.show:focus,.btn-outline-success:active:focus{box-shadow:0 0 0 .25rem rgba(25,135,84,.5)}.btn-outline-success.disabled,.btn-outline-success:disabled{color:#198754;background-color:transparent}.btn-outline-info{color:#0dcaf0;border-color:#0dcaf0}.btn-outline-info:hover{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:focus+.btn-outline-info,.btn-outline-info:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-check:active+.btn-outline-info,.btn-check:checked+.btn-outline-info,.btn-outline-info.active,.btn-outline-info.dropdown-toggle.show,.btn-outline-info:active{color:#000;background-color:#0dcaf0;border-color:#0dcaf0}.btn-check:active+.btn-outline-info:focus,.btn-check:checked+.btn-outline-info:focus,.btn-outline-info.active:focus,.btn-outline-info.dropdown-toggle.show:focus,.btn-outline-info:active:focus{box-shadow:0 0 0 .25rem rgba(13,202,240,.5)}.btn-outline-info.disabled,.btn-outline-info:disabled{color:#0dcaf0;background-color:transparent}.btn-outline-warning{color:#ffc107;border-color:#ffc107}.btn-outline-warning:hover{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:focus+.btn-outline-warning,.btn-outline-warning:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-check:active+.btn-outline-warning,.btn-check:checked+.btn-outline-warning,.btn-outline-warning.active,.btn-outline-warning.dropdown-toggle.show,.btn-outline-warning:active{color:#000;background-color:#ffc107;border-color:#ffc107}.btn-check:active+.btn-outline-warning:focus,.btn-check:checked+.btn-outline-warning:focus,.btn-outline-warning.active:focus,.btn-outline-warning.dropdown-toggle.show:focus,.btn-outline-warning:active:focus{box-shadow:0 0 0 .25rem rgba(255,193,7,.5)}.btn-outline-warning.disabled,.btn-outline-warning:disabled{color:#ffc107;background-color:transparent}.btn-outline-danger{color:#dc3545;border-color:#dc3545}.btn-outline-danger:hover{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:focus+.btn-outline-danger,.btn-outline-danger:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-check:active+.btn-outline-danger,.btn-check:checked+.btn-outline-danger,.btn-outline-danger.active,.btn-outline-danger.dropdown-toggle.show,.btn-outline-danger:active{color:#fff;background-color:#dc3545;border-color:#dc3545}.btn-check:active+.btn-outline-danger:focus,.btn-check:checked+.btn-outline-danger:focus,.btn-outline-danger.active:focus,.btn-outline-danger.dropdown-toggle.show:focus,.btn-outline-danger:active:focus{box-shadow:0 0 0 .25rem rgba(220,53,69,.5)}.btn-outline-danger.disabled,.btn-outline-danger:disabled{color:#dc3545;background-color:transparent}.btn-outline-light{color:#f8f9fa;border-color:#f8f9fa}.btn-outline-light:hover{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:focus+.btn-outline-light,.btn-outline-light:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-check:active+.btn-outline-light,.btn-check:checked+.btn-outline-light,.btn-outline-light.active,.btn-outline-light.dropdown-toggle.show,.btn-outline-light:active{color:#000;background-color:#f8f9fa;border-color:#f8f9fa}.btn-check:active+.btn-outline-light:focus,.btn-check:checked+.btn-outline-light:focus,.btn-outline-light.active:focus,.btn-outline-light.dropdown-toggle.show:focus,.btn-outline-light:active:focus{box-shadow:0 0 0 .25rem rgba(248,249,250,.5)}.btn-outline-light.disabled,.btn-outline-light:disabled{color:#f8f9fa;background-color:transparent}.btn-outline-dark{color:#212529;border-color:#212529}.btn-outline-dark:hover{color:#fff;background-color:#212529;border-color:#212529}.btn-check:focus+.btn-outline-dark,.btn-outline-dark:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-check:active+.btn-outline-dark,.btn-check:checked+.btn-outline-dark,.btn-outline-dark.active,.btn-outline-dark.dropdown-toggle.show,.btn-outline-dark:active{color:#fff;background-color:#212529;border-color:#212529}.btn-check:active+.btn-outline-dark:focus,.btn-check:checked+.btn-outline-dark:focus,.btn-outline-dark.active:focus,.btn-outline-dark.dropdown-toggle.show:focus,.btn-outline-dark:active:focus{box-shadow:0 0 0 .25rem rgba(33,37,41,.5)}.btn-outline-dark.disabled,.btn-outline-dark:disabled{color:#212529;background-color:transparent}.btn-link{font-weight:400;color:#0d6efd;text-decoration:underline}.btn-link:hover{color:#0a58ca}.btn-link.disabled,.btn-link:disabled{color:#6c757d}.btn-group-lg>.btn,.btn-lg{padding:.5rem 1rem;font-size:1.25rem;border-radius:.3rem}.btn-group-sm>.btn,.btn-sm{padding:.25rem .5rem;font-size:.875rem;border-radius:.2rem}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media (prefers-reduced-motion:reduce){.collapsing.collapse-horizontal{transition:none}}.dropdown,.dropend,.dropstart,.dropup{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{position:absolute;z-index:1000;display:none;min-width:10rem;padding:.5rem 0;margin:0;font-size:1rem;color:#212529;text-align:left;list-style:none;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.15);border-radius:.25rem}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:.125rem}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:.125rem}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:.125rem}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:.125rem}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:.5rem 0;overflow:hidden;border-top:1px solid rgba(0,0,0,.15)}.dropdown-item{display:block;width:100%;padding:.25rem 1rem;clear:both;font-weight:400;color:#212529;text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0}.dropdown-item:focus,.dropdown-item:hover{color:#1e2125;background-color:#e9ecef}.dropdown-item.active,.dropdown-item:active{color:#fff;text-decoration:none;background-color:#0d6efd}.dropdown-item.disabled,.dropdown-item:disabled{color:#adb5bd;pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:.5rem 1rem;margin-bottom:0;font-size:.875rem;color:#6c757d;white-space:nowrap}.dropdown-item-text{display:block;padding:.25rem 1rem;color:#212529}.dropdown-menu-dark{color:#dee2e6;background-color:#343a40;border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item{color:#dee2e6}.dropdown-menu-dark .dropdown-item:focus,.dropdown-menu-dark .dropdown-item:hover{color:#fff;background-color:rgba(255,255,255,.15)}.dropdown-menu-dark .dropdown-item.active,.dropdown-menu-dark .dropdown-item:active{color:#fff;background-color:#0d6efd}.dropdown-menu-dark .dropdown-item.disabled,.dropdown-menu-dark .dropdown-item:disabled{color:#adb5bd}.dropdown-menu-dark .dropdown-divider{border-color:rgba(0,0,0,.15)}.dropdown-menu-dark .dropdown-item-text{color:#dee2e6}.dropdown-menu-dark .dropdown-header{color:#adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group>.btn-group:not(:first-child),.btn-group>.btn:not(:first-child){margin-left:-1px}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:-1px}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn~.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:.5rem 1rem;color:#0d6efd;text-decoration:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:#0a58ca}.nav-link.disabled{color:#6c757d;pointer-events:none;cursor:default}.nav-tabs{border-bottom:1px solid #dee2e6}.nav-tabs .nav-link{margin-bottom:-1px;background:0 0;border:1px solid transparent;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{border-color:#e9ecef #e9ecef #dee2e6;isolation:isolate}.nav-tabs .nav-link.disabled{color:#6c757d;background-color:transparent;border-color:transparent}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:#495057;background-color:#fff;border-color:#dee2e6 #dee2e6 #fff}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-left-radius:0;border-top-right-radius:0}.nav-pills .nav-link{background:0 0;border:0;border-radius:.25rem}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#0d6efd}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-basis:0;flex-grow:1;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding-top:.5rem;padding-bottom:.5rem}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:.3125rem;padding-bottom:.3125rem;margin-right:1rem;font-size:1.25rem;text-decoration:none;white-space:nowrap}.navbar-nav{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link{padding-right:0;padding-left:0}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem}.navbar-collapse{flex-basis:100%;flex-grow:1;align-items:center}.navbar-toggler{padding:.25rem .75rem;font-size:1.25rem;line-height:1;background-color:transparent;border:1px solid transparent;border-radius:.25rem;transition:box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 .25rem}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height,75vh);overflow-y:auto}@media (min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas-header{display:none}.navbar-expand-sm .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-sm .offcanvas-bottom,.navbar-expand-sm .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-sm .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas-header{display:none}.navbar-expand-md .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-md .offcanvas-bottom,.navbar-expand-md .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-md .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas-header{display:none}.navbar-expand-lg .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-lg .offcanvas-bottom,.navbar-expand-lg .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-lg .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas-header{display:none}.navbar-expand-xl .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-xl .offcanvas-bottom,.navbar-expand-xl .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-xl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand-xxl .offcanvas-bottom,.navbar-expand-xxl .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand-xxl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:.5rem;padding-left:.5rem}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas-header{display:none}.navbar-expand .offcanvas{position:inherit;bottom:0;z-index:1000;flex-grow:1;visibility:visible!important;background-color:transparent;border-right:0;border-left:0;transition:none;transform:none}.navbar-expand .offcanvas-bottom,.navbar-expand .offcanvas-top{height:auto;border-top:0;border-bottom:0}.navbar-expand .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}.navbar-light .navbar-brand{color:rgba(0,0,0,.9)}.navbar-light .navbar-brand:focus,.navbar-light .navbar-brand:hover{color:rgba(0,0,0,.9)}.navbar-light .navbar-nav .nav-link{color:rgba(0,0,0,.55)}.navbar-light .navbar-nav .nav-link:focus,.navbar-light .navbar-nav .nav-link:hover{color:rgba(0,0,0,.7)}.navbar-light .navbar-nav .nav-link.disabled{color:rgba(0,0,0,.3)}.navbar-light .navbar-nav .nav-link.active,.navbar-light .navbar-nav .show>.nav-link{color:rgba(0,0,0,.9)}.navbar-light .navbar-toggler{color:rgba(0,0,0,.55);border-color:rgba(0,0,0,.1)}.navbar-light .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%280, 0, 0, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-light .navbar-text{color:rgba(0,0,0,.55)}.navbar-light .navbar-text a,.navbar-light .navbar-text a:focus,.navbar-light .navbar-text a:hover{color:rgba(0,0,0,.9)}.navbar-dark .navbar-brand{color:#fff}.navbar-dark .navbar-brand:focus,.navbar-dark .navbar-brand:hover{color:#fff}.navbar-dark .navbar-nav .nav-link{color:rgba(255,255,255,.55)}.navbar-dark .navbar-nav .nav-link:focus,.navbar-dark .navbar-nav .nav-link:hover{color:rgba(255,255,255,.75)}.navbar-dark .navbar-nav .nav-link.disabled{color:rgba(255,255,255,.25)}.navbar-dark .navbar-nav .nav-link.active,.navbar-dark .navbar-nav .show>.nav-link{color:#fff}.navbar-dark .navbar-toggler{color:rgba(255,255,255,.55);border-color:rgba(255,255,255,.1)}.navbar-dark .navbar-toggler-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.navbar-dark .navbar-text{color:rgba(255,255,255,.55)}.navbar-dark .navbar-text a,.navbar-dark .navbar-text a:focus,.navbar-dark .navbar-text a:hover{color:#fff}.card{position:relative;display:flex;flex-direction:column;min-width:0;word-wrap:break-word;background-color:#fff;background-clip:border-box;border:1px solid rgba(0,0,0,.125);border-radius:.25rem}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:1rem 1rem}.card-title{margin-bottom:.5rem}.card-subtitle{margin-top:-.25rem;margin-bottom:0}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:1rem}.card-header{padding:.5rem 1rem;margin-bottom:0;background-color:rgba(0,0,0,.03);border-bottom:1px solid rgba(0,0,0,.125)}.card-header:first-child{border-radius:calc(.25rem - 1px) calc(.25rem - 1px) 0 0}.card-footer{padding:.5rem 1rem;background-color:rgba(0,0,0,.03);border-top:1px solid rgba(0,0,0,.125)}.card-footer:last-child{border-radius:0 0 calc(.25rem - 1px) calc(.25rem - 1px)}.card-header-tabs{margin-right:-.5rem;margin-bottom:-.5rem;margin-left:-.5rem;border-bottom:0}.card-header-pills{margin-right:-.5rem;margin-left:-.5rem}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:1rem;border-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.card-img,.card-img-bottom{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.card-group>.card{margin-bottom:.75rem}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0%;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child) .card-header,.card-group>.card:not(:last-child) .card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child) .card-footer,.card-group>.card:not(:last-child) .card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child) .card-header,.card-group>.card:not(:first-child) .card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child) .card-footer,.card-group>.card:not(:first-child) .card-img-bottom{border-bottom-left-radius:0}}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:1rem 1.25rem;font-size:1rem;color:#212529;text-align:left;background-color:#fff;border:0;border-radius:0;overflow-anchor:none;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out,border-radius .15s ease}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:#0c63e4;background-color:#e7f1ff;box-shadow:inset 0 -1px 0 rgba(0,0,0,.125)}.accordion-button:not(.collapsed)::after{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%230c63e4'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");transform:rotate(-180deg)}.accordion-button::after{flex-shrink:0;width:1.25rem;height:1.25rem;margin-left:auto;content:"";background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23212529'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");background-repeat:no-repeat;background-size:1.25rem;transition:transform .2s ease-in-out}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.accordion-header{margin-bottom:0}.accordion-item{background-color:#fff;border:1px solid rgba(0,0,0,.125)}.accordion-item:first-of-type{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.accordion-item:first-of-type .accordion-button{border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-item:last-of-type .accordion-button.collapsed{border-bottom-right-radius:calc(.25rem - 1px);border-bottom-left-radius:calc(.25rem - 1px)}.accordion-item:last-of-type .accordion-collapse{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.accordion-body{padding:1rem 1.25rem}.accordion-flush .accordion-collapse{border-width:0}.accordion-flush .accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush .accordion-item:first-child{border-top:0}.accordion-flush .accordion-item:last-child{border-bottom:0}.accordion-flush .accordion-item .accordion-button{border-radius:0}.breadcrumb{display:flex;flex-wrap:wrap;padding:0 0;margin-bottom:1rem;list-style:none}.breadcrumb-item+.breadcrumb-item{padding-left:.5rem}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:.5rem;color:#6c757d;content:var(--bs-breadcrumb-divider, "/")}.breadcrumb-item.active{color:#6c757d}.pagination{display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;color:#0d6efd;text-decoration:none;background-color:#fff;border:1px solid #dee2e6;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:#0a58ca;background-color:#e9ecef;border-color:#dee2e6}.page-link:focus{z-index:3;color:#0a58ca;background-color:#e9ecef;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.page-item:not(:first-child) .page-link{margin-left:-1px}.page-item.active .page-link{z-index:3;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.page-item.disabled .page-link{color:#6c757d;pointer-events:none;background-color:#fff;border-color:#dee2e6}.page-link{padding:.375rem .75rem}.page-item:first-child .page-link{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.page-item:last-child .page-link{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.pagination-lg .page-link{padding:.75rem 1.5rem;font-size:1.25rem}.pagination-lg .page-item:first-child .page-link{border-top-left-radius:.3rem;border-bottom-left-radius:.3rem}.pagination-lg .page-item:last-child .page-link{border-top-right-radius:.3rem;border-bottom-right-radius:.3rem}.pagination-sm .page-link{padding:.25rem .5rem;font-size:.875rem}.pagination-sm .page-item:first-child .page-link{border-top-left-radius:.2rem;border-bottom-left-radius:.2rem}.pagination-sm .page-item:last-child .page-link{border-top-right-radius:.2rem;border-bottom-right-radius:.2rem}.badge{display:inline-block;padding:.35em .65em;font-size:.75em;font-weight:700;line-height:1;color:#fff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25rem}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{position:relative;padding:1rem 1rem;margin-bottom:1rem;border:1px solid transparent;border-radius:.25rem}.alert-heading{color:inherit}.alert-link{font-weight:700}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{color:#084298;background-color:#cfe2ff;border-color:#b6d4fe}.alert-primary .alert-link{color:#06357a}.alert-secondary{color:#41464b;background-color:#e2e3e5;border-color:#d3d6d8}.alert-secondary .alert-link{color:#34383c}.alert-success{color:#0f5132;background-color:#d1e7dd;border-color:#badbcc}.alert-success .alert-link{color:#0c4128}.alert-info{color:#055160;background-color:#cff4fc;border-color:#b6effb}.alert-info .alert-link{color:#04414d}.alert-warning{color:#664d03;background-color:#fff3cd;border-color:#ffecb5}.alert-warning .alert-link{color:#523e02}.alert-danger{color:#842029;background-color:#f8d7da;border-color:#f5c2c7}.alert-danger .alert-link{color:#6a1a21}.alert-light{color:#636464;background-color:#fefefe;border-color:#fdfdfe}.alert-light .alert-link{color:#4f5050}.alert-dark{color:#141619;background-color:#d3d3d4;border-color:#bcbebf}.alert-dark .alert-link{color:#101214}@-webkit-keyframes progress-bar-stripes{0%{background-position-x:1rem}}@keyframes progress-bar-stripes{0%{background-position-x:1rem}}.progress{display:flex;height:1rem;overflow:hidden;font-size:.75rem;background-color:#e9ecef;border-radius:.25rem}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:#fff;text-align:center;white-space:nowrap;background-color:#0d6efd;transition:width .6s ease}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:1rem 1rem}.progress-bar-animated{-webkit-animation:1s linear infinite progress-bar-stripes;animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{-webkit-animation:none;animation:none}}.list-group{display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:.25rem}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>li::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item-action{width:100%;color:#495057;text-align:inherit}.list-group-item-action:focus,.list-group-item-action:hover{z-index:1;color:#495057;text-decoration:none;background-color:#f8f9fa}.list-group-item-action:active{color:#212529;background-color:#e9ecef}.list-group-item{position:relative;display:block;padding:.5rem 1rem;color:#212529;text-decoration:none;background-color:#fff;border:1px solid rgba(0,0,0,.125)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:#6c757d;pointer-events:none;background-color:#fff}.list-group-item.active{z-index:2;color:#fff;background-color:#0d6efd;border-color:#0d6efd}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:-1px;border-top-width:1px}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child{border-bottom-left-radius:.25rem;border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child{border-top-right-radius:.25rem;border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:1px;border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:-1px;border-left-width:1px}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 1px}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{color:#084298;background-color:#cfe2ff}.list-group-item-primary.list-group-item-action:focus,.list-group-item-primary.list-group-item-action:hover{color:#084298;background-color:#bacbe6}.list-group-item-primary.list-group-item-action.active{color:#fff;background-color:#084298;border-color:#084298}.list-group-item-secondary{color:#41464b;background-color:#e2e3e5}.list-group-item-secondary.list-group-item-action:focus,.list-group-item-secondary.list-group-item-action:hover{color:#41464b;background-color:#cbccce}.list-group-item-secondary.list-group-item-action.active{color:#fff;background-color:#41464b;border-color:#41464b}.list-group-item-success{color:#0f5132;background-color:#d1e7dd}.list-group-item-success.list-group-item-action:focus,.list-group-item-success.list-group-item-action:hover{color:#0f5132;background-color:#bcd0c7}.list-group-item-success.list-group-item-action.active{color:#fff;background-color:#0f5132;border-color:#0f5132}.list-group-item-info{color:#055160;background-color:#cff4fc}.list-group-item-info.list-group-item-action:focus,.list-group-item-info.list-group-item-action:hover{color:#055160;background-color:#badce3}.list-group-item-info.list-group-item-action.active{color:#fff;background-color:#055160;border-color:#055160}.list-group-item-warning{color:#664d03;background-color:#fff3cd}.list-group-item-warning.list-group-item-action:focus,.list-group-item-warning.list-group-item-action:hover{color:#664d03;background-color:#e6dbb9}.list-group-item-warning.list-group-item-action.active{color:#fff;background-color:#664d03;border-color:#664d03}.list-group-item-danger{color:#842029;background-color:#f8d7da}.list-group-item-danger.list-group-item-action:focus,.list-group-item-danger.list-group-item-action:hover{color:#842029;background-color:#dfc2c4}.list-group-item-danger.list-group-item-action.active{color:#fff;background-color:#842029;border-color:#842029}.list-group-item-light{color:#636464;background-color:#fefefe}.list-group-item-light.list-group-item-action:focus,.list-group-item-light.list-group-item-action:hover{color:#636464;background-color:#e5e5e5}.list-group-item-light.list-group-item-action.active{color:#fff;background-color:#636464;border-color:#636464}.list-group-item-dark{color:#141619;background-color:#d3d3d4}.list-group-item-dark.list-group-item-action:focus,.list-group-item-dark.list-group-item-action:hover{color:#141619;background-color:#bebebf}.list-group-item-dark.list-group-item-action.active{color:#fff;background-color:#141619;border-color:#141619}.btn-close{box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:#000;background:transparent url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 011.414 0L8 6.586 14.293.293a1 1 0 111.414 1.414L9.414 8l6.293 6.293a1 1 0 01-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 01-1.414-1.414L6.586 8 .293 1.707a1 1 0 010-1.414z'/%3e%3c/svg%3e") center/1em auto no-repeat;border:0;border-radius:.25rem;opacity:.5}.btn-close:hover{color:#000;text-decoration:none;opacity:.75}.btn-close:focus{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25);opacity:1}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:.25}.btn-close-white{filter:invert(1) grayscale(100%) brightness(200%)}.toast{width:350px;max-width:100%;font-size:.875rem;pointer-events:auto;background-color:rgba(255,255,255,.85);background-clip:padding-box;border:1px solid rgba(0,0,0,.1);box-shadow:0 .5rem 1rem rgba(0,0,0,.15);border-radius:.25rem}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:.75rem}.toast-header{display:flex;align-items:center;padding:.5rem .75rem;color:#6c757d;background-color:rgba(255,255,255,.85);background-clip:padding-box;border-bottom:1px solid rgba(0,0,0,.05);border-top-left-radius:calc(.25rem - 1px);border-top-right-radius:calc(.25rem - 1px)}.toast-header .btn-close{margin-right:-.375rem;margin-left:.75rem}.toast-body{padding:.75rem;word-wrap:break-word}.modal{position:fixed;top:0;left:0;z-index:1055;display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:.5rem;pointer-events:none}.modal.fade .modal-dialog{transition:transform .3s ease-out;transform:translate(0,-50px)}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - 1rem)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - 1rem)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;pointer-events:auto;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem;outline:0}.modal-backdrop{position:fixed;top:0;left:0;z-index:1050;width:100vw;height:100vh;background-color:#000}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:.5}.modal-header{display:flex;flex-shrink:0;align-items:center;justify-content:space-between;padding:1rem 1rem;border-bottom:1px solid #dee2e6;border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.modal-header .btn-close{padding:.5rem .5rem;margin:-.5rem -.5rem -.5rem auto}.modal-title{margin-bottom:0;line-height:1.5}.modal-body{position:relative;flex:1 1 auto;padding:1rem}.modal-footer{display:flex;flex-wrap:wrap;flex-shrink:0;align-items:center;justify-content:flex-end;padding:.75rem;border-top:1px solid #dee2e6;border-bottom-right-radius:calc(.3rem - 1px);border-bottom-left-radius:calc(.3rem - 1px)}.modal-footer>*{margin:.25rem}@media (min-width:576px){.modal-dialog{max-width:500px;margin:1.75rem auto}.modal-dialog-scrollable{height:calc(100% - 3.5rem)}.modal-dialog-centered{min-height:calc(100% - 3.5rem)}.modal-sm{max-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{max-width:800px}}@media (min-width:1200px){.modal-xl{max-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}.modal-fullscreen .modal-footer{border-radius:0}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}.modal-fullscreen-sm-down .modal-footer{border-radius:0}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}.modal-fullscreen-md-down .modal-footer{border-radius:0}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}.modal-fullscreen-lg-down .modal-footer{border-radius:0}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}.modal-fullscreen-xl-down .modal-footer{border-radius:0}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}.modal-fullscreen-xxl-down .modal-footer{border-radius:0}}.tooltip{position:absolute;z-index:1080;display:block;margin:0;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;opacity:0}.tooltip.show{opacity:.9}.tooltip .tooltip-arrow{position:absolute;display:block;width:.8rem;height:.4rem}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top],.bs-tooltip-top{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:0}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:.4rem .4rem 0;border-top-color:#000}.bs-tooltip-auto[data-popper-placement^=right],.bs-tooltip-end{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:.4rem .4rem .4rem 0;border-right-color:#000}.bs-tooltip-auto[data-popper-placement^=bottom],.bs-tooltip-bottom{padding:.4rem 0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:0}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 .4rem .4rem;border-bottom-color:#000}.bs-tooltip-auto[data-popper-placement^=left],.bs-tooltip-start{padding:0 .4rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:0;width:.4rem;height:.8rem}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:.4rem 0 .4rem .4rem;border-left-color:#000}.tooltip-inner{max-width:200px;padding:.25rem .5rem;color:#fff;text-align:center;background-color:#000;border-radius:.25rem}.popover{position:absolute;top:0;left:0;z-index:1070;display:block;max-width:276px;font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;word-spacing:normal;white-space:normal;line-break:auto;font-size:.875rem;word-wrap:break-word;background-color:#fff;background-clip:padding-box;border:1px solid rgba(0,0,0,.2);border-radius:.3rem}.popover .popover-arrow{position:absolute;display:block;width:1rem;height:.5rem}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-width:.5rem .5rem 0;border-top-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:1px;border-width:.5rem .5rem 0;border-top-color:#fff}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-width:.5rem .5rem .5rem 0;border-right-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:1px;border-width:.5rem .5rem .5rem 0;border-right-color:#fff}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-.5rem - 1px)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-width:0 .5rem .5rem .5rem;border-bottom-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:1px;border-width:0 .5rem .5rem .5rem;border-bottom-color:#fff}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:1rem;margin-left:-.5rem;content:"";border-bottom:1px solid #f0f0f0}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-.5rem - 1px);width:.5rem;height:1rem}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-width:.5rem 0 .5rem .5rem;border-left-color:rgba(0,0,0,.25)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:1px;border-width:.5rem 0 .5rem .5rem;border-left-color:#fff}.popover-header{padding:.5rem 1rem;margin-bottom:0;font-size:1rem;background-color:#f0f0f0;border-bottom:1px solid rgba(0,0,0,.2);border-top-left-radius:calc(.3rem - 1px);border-top-right-radius:calc(.3rem - 1px)}.popover-header:empty{display:none}.popover-body{padding:1rem 1rem;color:#212529}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0z'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%;list-style:none}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:#fff;background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:#fff;text-align:center}.carousel-dark .carousel-control-next-icon,.carousel-dark .carousel-control-prev-icon{filter:invert(1) grayscale(100)}.carousel-dark .carousel-indicators [data-bs-target]{background-color:#000}.carousel-dark .carousel-caption{color:#000}@-webkit-keyframes spinner-border{to{transform:rotate(360deg)}}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;border:.25em solid currentColor;border-right-color:transparent;border-radius:50%;-webkit-animation:.75s linear infinite spinner-border;animation:.75s linear infinite spinner-border}.spinner-border-sm{width:1rem;height:1rem;border-width:.2em}@-webkit-keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;background-color:currentColor;border-radius:50%;opacity:0;-webkit-animation:.75s linear infinite spinner-grow;animation:.75s linear infinite spinner-grow}.spinner-grow-sm{width:1rem;height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{-webkit-animation-duration:1.5s;animation-duration:1.5s}}.offcanvas{position:fixed;bottom:0;z-index:1045;display:flex;flex-direction:column;max-width:100%;visibility:hidden;background-color:#fff;background-clip:padding-box;outline:0;transition:transform .3s ease-in-out}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;align-items:center;justify-content:space-between;padding:1rem 1rem}.offcanvas-header .btn-close{padding:.5rem .5rem;margin-top:-.5rem;margin-right:-.5rem;margin-bottom:-.5rem}.offcanvas-title{margin-bottom:0;line-height:1.5}.offcanvas-body{flex-grow:1;padding:1rem 1rem;overflow-y:auto}.offcanvas-start{top:0;left:0;width:400px;border-right:1px solid rgba(0,0,0,.2);transform:translateX(-100%)}.offcanvas-end{top:0;right:0;width:400px;border-left:1px solid rgba(0,0,0,.2);transform:translateX(100%)}.offcanvas-top{top:0;right:0;left:0;height:30vh;max-height:100%;border-bottom:1px solid rgba(0,0,0,.2);transform:translateY(-100%)}.offcanvas-bottom{right:0;left:0;height:30vh;max-height:100%;border-top:1px solid rgba(0,0,0,.2);transform:translateY(100%)}.offcanvas.show{transform:none}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentColor;opacity:.5}.placeholder.btn::before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{-webkit-animation:placeholder-glow 2s ease-in-out infinite;animation:placeholder-glow 2s ease-in-out infinite}@-webkit-keyframes placeholder-glow{50%{opacity:.2}}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{-webkit-mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);-webkit-mask-size:200% 100%;mask-size:200% 100%;-webkit-animation:placeholder-wave 2s linear infinite;animation:placeholder-wave 2s linear infinite}@-webkit-keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}@keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}.clearfix::after{display:block;clear:both;content:""}.link-primary{color:#0d6efd}.link-primary:focus,.link-primary:hover{color:#0a58ca}.link-secondary{color:#6c757d}.link-secondary:focus,.link-secondary:hover{color:#565e64}.link-success{color:#198754}.link-success:focus,.link-success:hover{color:#146c43}.link-info{color:#0dcaf0}.link-info:focus,.link-info:hover{color:#3dd5f3}.link-warning{color:#ffc107}.link-warning:focus,.link-warning:hover{color:#ffcd39}.link-danger{color:#dc3545}.link-danger:focus,.link-danger:hover{color:#b02a37}.link-light{color:#f8f9fa}.link-light:focus,.link-light:hover{color:#f9fafb}.link-dark{color:#212529}.link-dark:focus,.link-dark:hover{color:#1a1e21}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio:100%}.ratio-4x3{--bs-aspect-ratio:calc(3 / 4 * 100%)}.ratio-16x9{--bs-aspect-ratio:calc(9 / 16 * 100%)}.ratio-21x9{--bs-aspect-ratio:calc(9 / 21 * 100%)}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}}.hstack{display:flex;flex-direction:row;align-items:center;align-self:stretch}.vstack{display:flex;flex:1 1 auto;flex-direction:column;align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){position:absolute!important;width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;align-self:stretch;width:1px;min-height:1em;background-color:currentColor;opacity:.25}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.opacity-0{opacity:0!important}.opacity-25{opacity:.25!important}.opacity-50{opacity:.5!important}.opacity-75{opacity:.75!important}.opacity-100{opacity:1!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:0 .5rem 1rem rgba(0,0,0,.15)!important}.shadow-sm{box-shadow:0 .125rem .25rem rgba(0,0,0,.075)!important}.shadow-lg{box-shadow:0 1rem 3rem rgba(0,0,0,.175)!important}.shadow-none{box-shadow:none!important}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:1px solid #dee2e6!important}.border-0{border:0!important}.border-top{border-top:1px solid #dee2e6!important}.border-top-0{border-top:0!important}.border-end{border-right:1px solid #dee2e6!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:1px solid #dee2e6!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:1px solid #dee2e6!important}.border-start-0{border-left:0!important}.border-primary{border-color:#0d6efd!important}.border-secondary{border-color:#6c757d!important}.border-success{border-color:#198754!important}.border-info{border-color:#0dcaf0!important}.border-warning{border-color:#ffc107!important}.border-danger{border-color:#dc3545!important}.border-light{border-color:#f8f9fa!important}.border-dark{border-color:#212529!important}.border-white{border-color:#fff!important}.border-1{border-width:1px!important}.border-2{border-width:2px!important}.border-3{border-width:3px!important}.border-4{border-width:4px!important}.border-5{border-width:5px!important}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.375rem + 1.5vw)!important}.fs-2{font-size:calc(1.325rem + .9vw)!important}.fs-3{font-size:calc(1.3rem + .6vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-light{font-weight:300!important}.fw-lighter{font-weight:lighter!important}.fw-normal{font-weight:400!important}.fw-bold{font-weight:700!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-primary{--bs-text-opacity:1;color:rgba(var(--bs-primary-rgb),var(--bs-text-opacity))!important}.text-secondary{--bs-text-opacity:1;color:rgba(var(--bs-secondary-rgb),var(--bs-text-opacity))!important}.text-success{--bs-text-opacity:1;color:rgba(var(--bs-success-rgb),var(--bs-text-opacity))!important}.text-info{--bs-text-opacity:1;color:rgba(var(--bs-info-rgb),var(--bs-text-opacity))!important}.text-warning{--bs-text-opacity:1;color:rgba(var(--bs-warning-rgb),var(--bs-text-opacity))!important}.text-danger{--bs-text-opacity:1;color:rgba(var(--bs-danger-rgb),var(--bs-text-opacity))!important}.text-light{--bs-text-opacity:1;color:rgba(var(--bs-light-rgb),var(--bs-text-opacity))!important}.text-dark{--bs-text-opacity:1;color:rgba(var(--bs-dark-rgb),var(--bs-text-opacity))!important}.text-black{--bs-text-opacity:1;color:rgba(var(--bs-black-rgb),var(--bs-text-opacity))!important}.text-white{--bs-text-opacity:1;color:rgba(var(--bs-white-rgb),var(--bs-text-opacity))!important}.text-body{--bs-text-opacity:1;color:rgba(var(--bs-body-rgb),var(--bs-text-opacity))!important}.text-muted{--bs-text-opacity:1;color:#6c757d!important}.text-black-50{--bs-text-opacity:1;color:rgba(0,0,0,.5)!important}.text-white-50{--bs-text-opacity:1;color:rgba(255,255,255,.5)!important}.text-reset{--bs-text-opacity:1;color:inherit!important}.text-opacity-25{--bs-text-opacity:0.25}.text-opacity-50{--bs-text-opacity:0.5}.text-opacity-75{--bs-text-opacity:0.75}.text-opacity-100{--bs-text-opacity:1}.bg-primary{--bs-bg-opacity:1;background-color:rgba(var(--bs-primary-rgb),var(--bs-bg-opacity))!important}.bg-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-rgb),var(--bs-bg-opacity))!important}.bg-success{--bs-bg-opacity:1;background-color:rgba(var(--bs-success-rgb),var(--bs-bg-opacity))!important}.bg-info{--bs-bg-opacity:1;background-color:rgba(var(--bs-info-rgb),var(--bs-bg-opacity))!important}.bg-warning{--bs-bg-opacity:1;background-color:rgba(var(--bs-warning-rgb),var(--bs-bg-opacity))!important}.bg-danger{--bs-bg-opacity:1;background-color:rgba(var(--bs-danger-rgb),var(--bs-bg-opacity))!important}.bg-light{--bs-bg-opacity:1;background-color:rgba(var(--bs-light-rgb),var(--bs-bg-opacity))!important}.bg-dark{--bs-bg-opacity:1;background-color:rgba(var(--bs-dark-rgb),var(--bs-bg-opacity))!important}.bg-black{--bs-bg-opacity:1;background-color:rgba(var(--bs-black-rgb),var(--bs-bg-opacity))!important}.bg-white{--bs-bg-opacity:1;background-color:rgba(var(--bs-white-rgb),var(--bs-bg-opacity))!important}.bg-body{--bs-bg-opacity:1;background-color:rgba(var(--bs-body-rgb),var(--bs-bg-opacity))!important}.bg-transparent{--bs-bg-opacity:1;background-color:transparent!important}.bg-opacity-10{--bs-bg-opacity:0.1}.bg-opacity-25{--bs-bg-opacity:0.25}.bg-opacity-50{--bs-bg-opacity:0.5}.bg-opacity-75{--bs-bg-opacity:0.75}.bg-opacity-100{--bs-bg-opacity:1}.bg-gradient{background-image:var(--bs-gradient)!important}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:.25rem!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:.2rem!important}.rounded-2{border-radius:.25rem!important}.rounded-3{border-radius:.3rem!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:50rem!important}.rounded-top{border-top-left-radius:.25rem!important;border-top-right-radius:.25rem!important}.rounded-end{border-top-right-radius:.25rem!important;border-bottom-right-radius:.25rem!important}.rounded-bottom{border-bottom-right-radius:.25rem!important;border-bottom-left-radius:.25rem!important}.rounded-start{border-bottom-left-radius:.25rem!important;border-top-left-radius:.25rem!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:1rem!important}.gap-sm-4{gap:1.5rem!important}.gap-sm-5{gap:3rem!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:1rem!important}.gap-md-4{gap:1.5rem!important}.gap-md-5{gap:3rem!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:1rem!important}.gap-lg-4{gap:1.5rem!important}.gap-lg-5{gap:3rem!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:1rem!important}.gap-xl-4{gap:1.5rem!important}.gap-xl-5{gap:3rem!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.25rem!important}.gap-xxl-2{gap:.5rem!important}.gap-xxl-3{gap:1rem!important}.gap-xxl-4{gap:1.5rem!important}.gap-xxl-5{gap:3rem!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:3rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:3rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media (min-width:1200px){.fs-1{font-size:2.5rem!important}.fs-2{font-size:2rem!important}.fs-3{font-size:1.75rem!important}.fs-4{font-size:1.5rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}} + */:root,[data-bs-theme=light]{--bs-blue:#0d6efd;--bs-indigo:#6610f2;--bs-purple:#6f42c1;--bs-pink:#d63384;--bs-red:#dc3545;--bs-orange:#fd7e14;--bs-yellow:#ffc107;--bs-green:#198754;--bs-teal:#20c997;--bs-cyan:#0dcaf0;--bs-black:#000;--bs-white:#fff;--bs-gray:#6c757d;--bs-gray-dark:#343a40;--bs-gray-100:#f8f9fa;--bs-gray-200:#e9ecef;--bs-gray-300:#dee2e6;--bs-gray-400:#ced4da;--bs-gray-500:#adb5bd;--bs-gray-600:#6c757d;--bs-gray-700:#495057;--bs-gray-800:#343a40;--bs-gray-900:#212529;--bs-primary:#0d6efd;--bs-secondary:#6c757d;--bs-success:#198754;--bs-info:#0dcaf0;--bs-warning:#ffc107;--bs-danger:#dc3545;--bs-light:#f8f9fa;--bs-dark:#212529;--bs-primary-rgb:13,110,253;--bs-secondary-rgb:108,117,125;--bs-success-rgb:25,135,84;--bs-info-rgb:13,202,240;--bs-warning-rgb:255,193,7;--bs-danger-rgb:220,53,69;--bs-light-rgb:248,249,250;--bs-dark-rgb:33,37,41;--bs-primary-text-emphasis:#052c65;--bs-secondary-text-emphasis:#2b2f32;--bs-success-text-emphasis:#0a3622;--bs-info-text-emphasis:#055160;--bs-warning-text-emphasis:#664d03;--bs-danger-text-emphasis:#58151c;--bs-light-text-emphasis:#495057;--bs-dark-text-emphasis:#495057;--bs-primary-bg-subtle:#cfe2ff;--bs-secondary-bg-subtle:#e2e3e5;--bs-success-bg-subtle:#d1e7dd;--bs-info-bg-subtle:#cff4fc;--bs-warning-bg-subtle:#fff3cd;--bs-danger-bg-subtle:#f8d7da;--bs-light-bg-subtle:#fcfcfd;--bs-dark-bg-subtle:#ced4da;--bs-primary-border-subtle:#9ec5fe;--bs-secondary-border-subtle:#c4c8cb;--bs-success-border-subtle:#a3cfbb;--bs-info-border-subtle:#9eeaf9;--bs-warning-border-subtle:#ffe69c;--bs-danger-border-subtle:#f1aeb5;--bs-light-border-subtle:#e9ecef;--bs-dark-border-subtle:#adb5bd;--bs-white-rgb:255,255,255;--bs-black-rgb:0,0,0;--bs-font-sans-serif:system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";--bs-font-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--bs-gradient:linear-gradient(180deg, rgba(255, 255, 255, 0.15), rgba(255, 255, 255, 0));--bs-body-font-family:var(--bs-font-sans-serif);--bs-body-font-size:1rem;--bs-body-font-weight:400;--bs-body-line-height:1.5;--bs-body-color:#212529;--bs-body-color-rgb:33,37,41;--bs-body-bg:#fff;--bs-body-bg-rgb:255,255,255;--bs-emphasis-color:#000;--bs-emphasis-color-rgb:0,0,0;--bs-secondary-color:rgba(33, 37, 41, 0.75);--bs-secondary-color-rgb:33,37,41;--bs-secondary-bg:#e9ecef;--bs-secondary-bg-rgb:233,236,239;--bs-tertiary-color:rgba(33, 37, 41, 0.5);--bs-tertiary-color-rgb:33,37,41;--bs-tertiary-bg:#f8f9fa;--bs-tertiary-bg-rgb:248,249,250;--bs-heading-color:inherit;--bs-link-color:#0d6efd;--bs-link-color-rgb:13,110,253;--bs-link-decoration:underline;--bs-link-hover-color:#0a58ca;--bs-link-hover-color-rgb:10,88,202;--bs-code-color:#d63384;--bs-highlight-color:#212529;--bs-highlight-bg:#fff3cd;--bs-border-width:1px;--bs-border-style:solid;--bs-border-color:#dee2e6;--bs-border-color-translucent:rgba(0, 0, 0, 0.175);--bs-border-radius:0.375rem;--bs-border-radius-sm:0.25rem;--bs-border-radius-lg:0.5rem;--bs-border-radius-xl:1rem;--bs-border-radius-xxl:2rem;--bs-border-radius-2xl:var(--bs-border-radius-xxl);--bs-border-radius-pill:50rem;--bs-box-shadow:0 0.5rem 1rem rgba(0, 0, 0, 0.15);--bs-box-shadow-sm:0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);--bs-box-shadow-lg:0 1rem 3rem rgba(0, 0, 0, 0.175);--bs-box-shadow-inset:inset 0 1px 2px rgba(0, 0, 0, 0.075);--bs-focus-ring-width:0.25rem;--bs-focus-ring-opacity:0.25;--bs-focus-ring-color:rgba(13, 110, 253, 0.25);--bs-form-valid-color:#198754;--bs-form-valid-border-color:#198754;--bs-form-invalid-color:#dc3545;--bs-form-invalid-border-color:#dc3545}[data-bs-theme=dark]{color-scheme:dark;--bs-body-color:#dee2e6;--bs-body-color-rgb:222,226,230;--bs-body-bg:#212529;--bs-body-bg-rgb:33,37,41;--bs-emphasis-color:#fff;--bs-emphasis-color-rgb:255,255,255;--bs-secondary-color:rgba(222, 226, 230, 0.75);--bs-secondary-color-rgb:222,226,230;--bs-secondary-bg:#343a40;--bs-secondary-bg-rgb:52,58,64;--bs-tertiary-color:rgba(222, 226, 230, 0.5);--bs-tertiary-color-rgb:222,226,230;--bs-tertiary-bg:#2b3035;--bs-tertiary-bg-rgb:43,48,53;--bs-primary-text-emphasis:#6ea8fe;--bs-secondary-text-emphasis:#a7acb1;--bs-success-text-emphasis:#75b798;--bs-info-text-emphasis:#6edff6;--bs-warning-text-emphasis:#ffda6a;--bs-danger-text-emphasis:#ea868f;--bs-light-text-emphasis:#f8f9fa;--bs-dark-text-emphasis:#dee2e6;--bs-primary-bg-subtle:#031633;--bs-secondary-bg-subtle:#161719;--bs-success-bg-subtle:#051b11;--bs-info-bg-subtle:#032830;--bs-warning-bg-subtle:#332701;--bs-danger-bg-subtle:#2c0b0e;--bs-light-bg-subtle:#343a40;--bs-dark-bg-subtle:#1a1d20;--bs-primary-border-subtle:#084298;--bs-secondary-border-subtle:#41464b;--bs-success-border-subtle:#0f5132;--bs-info-border-subtle:#087990;--bs-warning-border-subtle:#997404;--bs-danger-border-subtle:#842029;--bs-light-border-subtle:#495057;--bs-dark-border-subtle:#343a40;--bs-heading-color:inherit;--bs-link-color:#6ea8fe;--bs-link-hover-color:#8bb9fe;--bs-link-color-rgb:110,168,254;--bs-link-hover-color-rgb:139,185,254;--bs-code-color:#e685b5;--bs-highlight-color:#dee2e6;--bs-highlight-bg:#664d03;--bs-border-color:#495057;--bs-border-color-translucent:rgba(255, 255, 255, 0.15);--bs-form-valid-color:#75b798;--bs-form-valid-border-color:#75b798;--bs-form-invalid-color:#ea868f;--bs-form-invalid-border-color:#ea868f}*,::after,::before{box-sizing:border-box}@media (prefers-reduced-motion:no-preference){:root{scroll-behavior:smooth}}body{margin:0;font-family:var(--bs-body-font-family);font-size:var(--bs-body-font-size);font-weight:var(--bs-body-font-weight);line-height:var(--bs-body-line-height);color:var(--bs-body-color);text-align:var(--bs-body-text-align);background-color:var(--bs-body-bg);-webkit-text-size-adjust:100%;-webkit-tap-highlight-color:transparent}hr{margin:1rem 0;color:inherit;border:0;border-top:var(--bs-border-width) solid;opacity:.25}.h1,.h2,.h3,.h4,.h5,.h6,h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5rem;font-weight:500;line-height:1.2;color:var(--bs-heading-color)}.h1,h1{font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.h1,h1{font-size:2.5rem}}.h2,h2{font-size:calc(1.325rem + .9vw)}@media (min-width:1200px){.h2,h2{font-size:2rem}}.h3,h3{font-size:calc(1.3rem + .6vw)}@media (min-width:1200px){.h3,h3{font-size:1.75rem}}.h4,h4{font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){.h4,h4{font-size:1.5rem}}.h5,h5{font-size:1.25rem}.h6,h6{font-size:1rem}p{margin-top:0;margin-bottom:1rem}abbr[title]{-webkit-text-decoration:underline dotted;text-decoration:underline dotted;cursor:help;-webkit-text-decoration-skip-ink:none;text-decoration-skip-ink:none}address{margin-bottom:1rem;font-style:normal;line-height:inherit}ol,ul{padding-left:2rem}dl,ol,ul{margin-top:0;margin-bottom:1rem}ol ol,ol ul,ul ol,ul ul{margin-bottom:0}dt{font-weight:700}dd{margin-bottom:.5rem;margin-left:0}blockquote{margin:0 0 1rem}b,strong{font-weight:bolder}.small,small{font-size:.875em}.mark,mark{padding:.1875em;color:var(--bs-highlight-color);background-color:var(--bs-highlight-bg)}sub,sup{position:relative;font-size:.75em;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}a{color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,1));text-decoration:underline}a:hover{--bs-link-color-rgb:var(--bs-link-hover-color-rgb)}a:not([href]):not([class]),a:not([href]):not([class]):hover{color:inherit;text-decoration:none}code,kbd,pre,samp{font-family:var(--bs-font-monospace);font-size:1em}pre{display:block;margin-top:0;margin-bottom:1rem;overflow:auto;font-size:.875em}pre code{font-size:inherit;color:inherit;word-break:normal}code{font-size:.875em;color:var(--bs-code-color);word-wrap:break-word}a>code{color:inherit}kbd{padding:.1875rem .375rem;font-size:.875em;color:var(--bs-body-bg);background-color:var(--bs-body-color);border-radius:.25rem}kbd kbd{padding:0;font-size:1em}figure{margin:0 0 1rem}img,svg{vertical-align:middle}table{caption-side:bottom;border-collapse:collapse}caption{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-secondary-color);text-align:left}th{text-align:inherit;text-align:-webkit-match-parent}tbody,td,tfoot,th,thead,tr{border-color:inherit;border-style:solid;border-width:0}label{display:inline-block}button{border-radius:0}button:focus:not(:focus-visible){outline:0}button,input,optgroup,select,textarea{margin:0;font-family:inherit;font-size:inherit;line-height:inherit}button,select{text-transform:none}[role=button]{cursor:pointer}select{word-wrap:normal}select:disabled{opacity:1}[list]:not([type=date]):not([type=datetime-local]):not([type=month]):not([type=week]):not([type=time])::-webkit-calendar-picker-indicator{display:none!important}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}[type=button]:not(:disabled),[type=reset]:not(:disabled),[type=submit]:not(:disabled),button:not(:disabled){cursor:pointer}::-moz-focus-inner{padding:0;border-style:none}textarea{resize:vertical}fieldset{min-width:0;padding:0;margin:0;border:0}legend{float:left;width:100%;padding:0;margin-bottom:.5rem;line-height:inherit;font-size:calc(1.275rem + .3vw)}@media (min-width:1200px){legend{font-size:1.5rem}}legend+*{clear:left}::-webkit-datetime-edit-day-field,::-webkit-datetime-edit-fields-wrapper,::-webkit-datetime-edit-hour-field,::-webkit-datetime-edit-minute,::-webkit-datetime-edit-month-field,::-webkit-datetime-edit-text,::-webkit-datetime-edit-year-field{padding:0}::-webkit-inner-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-color-swatch-wrapper{padding:0}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}::file-selector-button{font:inherit;-webkit-appearance:button}output{display:inline-block}iframe{border:0}summary{display:list-item;cursor:pointer}progress{vertical-align:baseline}[hidden]{display:none!important}.lead{font-size:1.25rem;font-weight:300}.display-1{font-weight:300;line-height:1.2;font-size:calc(1.625rem + 4.5vw)}@media (min-width:1200px){.display-1{font-size:5rem}}.display-2{font-weight:300;line-height:1.2;font-size:calc(1.575rem + 3.9vw)}@media (min-width:1200px){.display-2{font-size:4.5rem}}.display-3{font-weight:300;line-height:1.2;font-size:calc(1.525rem + 3.3vw)}@media (min-width:1200px){.display-3{font-size:4rem}}.display-4{font-weight:300;line-height:1.2;font-size:calc(1.475rem + 2.7vw)}@media (min-width:1200px){.display-4{font-size:3.5rem}}.display-5{font-weight:300;line-height:1.2;font-size:calc(1.425rem + 2.1vw)}@media (min-width:1200px){.display-5{font-size:3rem}}.display-6{font-weight:300;line-height:1.2;font-size:calc(1.375rem + 1.5vw)}@media (min-width:1200px){.display-6{font-size:2.5rem}}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none}.list-inline-item{display:inline-block}.list-inline-item:not(:last-child){margin-right:.5rem}.initialism{font-size:.875em;text-transform:uppercase}.blockquote{margin-bottom:1rem;font-size:1.25rem}.blockquote>:last-child{margin-bottom:0}.blockquote-footer{margin-top:-1rem;margin-bottom:1rem;font-size:.875em;color:#6c757d}.blockquote-footer::before{content:"— "}.img-fluid{max-width:100%;height:auto}.img-thumbnail{padding:.25rem;background-color:var(--bs-body-bg);border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius);max-width:100%;height:auto}.figure{display:inline-block}.figure-img{margin-bottom:.5rem;line-height:1}.figure-caption{font-size:.875em;color:var(--bs-secondary-color)}.container,.container-fluid,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{--bs-gutter-x:1.5rem;--bs-gutter-y:0;width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-right:auto;margin-left:auto}@media (min-width:576px){.container,.container-sm{max-width:540px}}@media (min-width:768px){.container,.container-md,.container-sm{max-width:720px}}@media (min-width:992px){.container,.container-lg,.container-md,.container-sm{max-width:960px}}@media (min-width:1200px){.container,.container-lg,.container-md,.container-sm,.container-xl{max-width:1140px}}@media (min-width:1400px){.container,.container-lg,.container-md,.container-sm,.container-xl,.container-xxl{max-width:1320px}}:root{--bs-breakpoint-xs:0;--bs-breakpoint-sm:576px;--bs-breakpoint-md:768px;--bs-breakpoint-lg:992px;--bs-breakpoint-xl:1200px;--bs-breakpoint-xxl:1400px}.row{--bs-gutter-x:1.5rem;--bs-gutter-y:0;display:flex;flex-wrap:wrap;margin-top:calc(-1 * var(--bs-gutter-y));margin-right:calc(-.5 * var(--bs-gutter-x));margin-left:calc(-.5 * var(--bs-gutter-x))}.row>*{flex-shrink:0;width:100%;max-width:100%;padding-right:calc(var(--bs-gutter-x) * .5);padding-left:calc(var(--bs-gutter-x) * .5);margin-top:var(--bs-gutter-y)}.col{flex:1 0 0}.row-cols-auto>*{flex:0 0 auto;width:auto}.row-cols-1>*{flex:0 0 auto;width:100%}.row-cols-2>*{flex:0 0 auto;width:50%}.row-cols-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-4>*{flex:0 0 auto;width:25%}.row-cols-5>*{flex:0 0 auto;width:20%}.row-cols-6>*{flex:0 0 auto;width:16.66666667%}.col-auto{flex:0 0 auto;width:auto}.col-1{flex:0 0 auto;width:8.33333333%}.col-2{flex:0 0 auto;width:16.66666667%}.col-3{flex:0 0 auto;width:25%}.col-4{flex:0 0 auto;width:33.33333333%}.col-5{flex:0 0 auto;width:41.66666667%}.col-6{flex:0 0 auto;width:50%}.col-7{flex:0 0 auto;width:58.33333333%}.col-8{flex:0 0 auto;width:66.66666667%}.col-9{flex:0 0 auto;width:75%}.col-10{flex:0 0 auto;width:83.33333333%}.col-11{flex:0 0 auto;width:91.66666667%}.col-12{flex:0 0 auto;width:100%}.offset-1{margin-left:8.33333333%}.offset-2{margin-left:16.66666667%}.offset-3{margin-left:25%}.offset-4{margin-left:33.33333333%}.offset-5{margin-left:41.66666667%}.offset-6{margin-left:50%}.offset-7{margin-left:58.33333333%}.offset-8{margin-left:66.66666667%}.offset-9{margin-left:75%}.offset-10{margin-left:83.33333333%}.offset-11{margin-left:91.66666667%}.g-0,.gx-0{--bs-gutter-x:0}.g-0,.gy-0{--bs-gutter-y:0}.g-1,.gx-1{--bs-gutter-x:0.25rem}.g-1,.gy-1{--bs-gutter-y:0.25rem}.g-2,.gx-2{--bs-gutter-x:0.5rem}.g-2,.gy-2{--bs-gutter-y:0.5rem}.g-3,.gx-3{--bs-gutter-x:1rem}.g-3,.gy-3{--bs-gutter-y:1rem}.g-4,.gx-4{--bs-gutter-x:1.5rem}.g-4,.gy-4{--bs-gutter-y:1.5rem}.g-5,.gx-5{--bs-gutter-x:3rem}.g-5,.gy-5{--bs-gutter-y:3rem}@media (min-width:576px){.col-sm{flex:1 0 0}.row-cols-sm-auto>*{flex:0 0 auto;width:auto}.row-cols-sm-1>*{flex:0 0 auto;width:100%}.row-cols-sm-2>*{flex:0 0 auto;width:50%}.row-cols-sm-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-sm-4>*{flex:0 0 auto;width:25%}.row-cols-sm-5>*{flex:0 0 auto;width:20%}.row-cols-sm-6>*{flex:0 0 auto;width:16.66666667%}.col-sm-auto{flex:0 0 auto;width:auto}.col-sm-1{flex:0 0 auto;width:8.33333333%}.col-sm-2{flex:0 0 auto;width:16.66666667%}.col-sm-3{flex:0 0 auto;width:25%}.col-sm-4{flex:0 0 auto;width:33.33333333%}.col-sm-5{flex:0 0 auto;width:41.66666667%}.col-sm-6{flex:0 0 auto;width:50%}.col-sm-7{flex:0 0 auto;width:58.33333333%}.col-sm-8{flex:0 0 auto;width:66.66666667%}.col-sm-9{flex:0 0 auto;width:75%}.col-sm-10{flex:0 0 auto;width:83.33333333%}.col-sm-11{flex:0 0 auto;width:91.66666667%}.col-sm-12{flex:0 0 auto;width:100%}.offset-sm-0{margin-left:0}.offset-sm-1{margin-left:8.33333333%}.offset-sm-2{margin-left:16.66666667%}.offset-sm-3{margin-left:25%}.offset-sm-4{margin-left:33.33333333%}.offset-sm-5{margin-left:41.66666667%}.offset-sm-6{margin-left:50%}.offset-sm-7{margin-left:58.33333333%}.offset-sm-8{margin-left:66.66666667%}.offset-sm-9{margin-left:75%}.offset-sm-10{margin-left:83.33333333%}.offset-sm-11{margin-left:91.66666667%}.g-sm-0,.gx-sm-0{--bs-gutter-x:0}.g-sm-0,.gy-sm-0{--bs-gutter-y:0}.g-sm-1,.gx-sm-1{--bs-gutter-x:0.25rem}.g-sm-1,.gy-sm-1{--bs-gutter-y:0.25rem}.g-sm-2,.gx-sm-2{--bs-gutter-x:0.5rem}.g-sm-2,.gy-sm-2{--bs-gutter-y:0.5rem}.g-sm-3,.gx-sm-3{--bs-gutter-x:1rem}.g-sm-3,.gy-sm-3{--bs-gutter-y:1rem}.g-sm-4,.gx-sm-4{--bs-gutter-x:1.5rem}.g-sm-4,.gy-sm-4{--bs-gutter-y:1.5rem}.g-sm-5,.gx-sm-5{--bs-gutter-x:3rem}.g-sm-5,.gy-sm-5{--bs-gutter-y:3rem}}@media (min-width:768px){.col-md{flex:1 0 0}.row-cols-md-auto>*{flex:0 0 auto;width:auto}.row-cols-md-1>*{flex:0 0 auto;width:100%}.row-cols-md-2>*{flex:0 0 auto;width:50%}.row-cols-md-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-md-4>*{flex:0 0 auto;width:25%}.row-cols-md-5>*{flex:0 0 auto;width:20%}.row-cols-md-6>*{flex:0 0 auto;width:16.66666667%}.col-md-auto{flex:0 0 auto;width:auto}.col-md-1{flex:0 0 auto;width:8.33333333%}.col-md-2{flex:0 0 auto;width:16.66666667%}.col-md-3{flex:0 0 auto;width:25%}.col-md-4{flex:0 0 auto;width:33.33333333%}.col-md-5{flex:0 0 auto;width:41.66666667%}.col-md-6{flex:0 0 auto;width:50%}.col-md-7{flex:0 0 auto;width:58.33333333%}.col-md-8{flex:0 0 auto;width:66.66666667%}.col-md-9{flex:0 0 auto;width:75%}.col-md-10{flex:0 0 auto;width:83.33333333%}.col-md-11{flex:0 0 auto;width:91.66666667%}.col-md-12{flex:0 0 auto;width:100%}.offset-md-0{margin-left:0}.offset-md-1{margin-left:8.33333333%}.offset-md-2{margin-left:16.66666667%}.offset-md-3{margin-left:25%}.offset-md-4{margin-left:33.33333333%}.offset-md-5{margin-left:41.66666667%}.offset-md-6{margin-left:50%}.offset-md-7{margin-left:58.33333333%}.offset-md-8{margin-left:66.66666667%}.offset-md-9{margin-left:75%}.offset-md-10{margin-left:83.33333333%}.offset-md-11{margin-left:91.66666667%}.g-md-0,.gx-md-0{--bs-gutter-x:0}.g-md-0,.gy-md-0{--bs-gutter-y:0}.g-md-1,.gx-md-1{--bs-gutter-x:0.25rem}.g-md-1,.gy-md-1{--bs-gutter-y:0.25rem}.g-md-2,.gx-md-2{--bs-gutter-x:0.5rem}.g-md-2,.gy-md-2{--bs-gutter-y:0.5rem}.g-md-3,.gx-md-3{--bs-gutter-x:1rem}.g-md-3,.gy-md-3{--bs-gutter-y:1rem}.g-md-4,.gx-md-4{--bs-gutter-x:1.5rem}.g-md-4,.gy-md-4{--bs-gutter-y:1.5rem}.g-md-5,.gx-md-5{--bs-gutter-x:3rem}.g-md-5,.gy-md-5{--bs-gutter-y:3rem}}@media (min-width:992px){.col-lg{flex:1 0 0}.row-cols-lg-auto>*{flex:0 0 auto;width:auto}.row-cols-lg-1>*{flex:0 0 auto;width:100%}.row-cols-lg-2>*{flex:0 0 auto;width:50%}.row-cols-lg-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-lg-4>*{flex:0 0 auto;width:25%}.row-cols-lg-5>*{flex:0 0 auto;width:20%}.row-cols-lg-6>*{flex:0 0 auto;width:16.66666667%}.col-lg-auto{flex:0 0 auto;width:auto}.col-lg-1{flex:0 0 auto;width:8.33333333%}.col-lg-2{flex:0 0 auto;width:16.66666667%}.col-lg-3{flex:0 0 auto;width:25%}.col-lg-4{flex:0 0 auto;width:33.33333333%}.col-lg-5{flex:0 0 auto;width:41.66666667%}.col-lg-6{flex:0 0 auto;width:50%}.col-lg-7{flex:0 0 auto;width:58.33333333%}.col-lg-8{flex:0 0 auto;width:66.66666667%}.col-lg-9{flex:0 0 auto;width:75%}.col-lg-10{flex:0 0 auto;width:83.33333333%}.col-lg-11{flex:0 0 auto;width:91.66666667%}.col-lg-12{flex:0 0 auto;width:100%}.offset-lg-0{margin-left:0}.offset-lg-1{margin-left:8.33333333%}.offset-lg-2{margin-left:16.66666667%}.offset-lg-3{margin-left:25%}.offset-lg-4{margin-left:33.33333333%}.offset-lg-5{margin-left:41.66666667%}.offset-lg-6{margin-left:50%}.offset-lg-7{margin-left:58.33333333%}.offset-lg-8{margin-left:66.66666667%}.offset-lg-9{margin-left:75%}.offset-lg-10{margin-left:83.33333333%}.offset-lg-11{margin-left:91.66666667%}.g-lg-0,.gx-lg-0{--bs-gutter-x:0}.g-lg-0,.gy-lg-0{--bs-gutter-y:0}.g-lg-1,.gx-lg-1{--bs-gutter-x:0.25rem}.g-lg-1,.gy-lg-1{--bs-gutter-y:0.25rem}.g-lg-2,.gx-lg-2{--bs-gutter-x:0.5rem}.g-lg-2,.gy-lg-2{--bs-gutter-y:0.5rem}.g-lg-3,.gx-lg-3{--bs-gutter-x:1rem}.g-lg-3,.gy-lg-3{--bs-gutter-y:1rem}.g-lg-4,.gx-lg-4{--bs-gutter-x:1.5rem}.g-lg-4,.gy-lg-4{--bs-gutter-y:1.5rem}.g-lg-5,.gx-lg-5{--bs-gutter-x:3rem}.g-lg-5,.gy-lg-5{--bs-gutter-y:3rem}}@media (min-width:1200px){.col-xl{flex:1 0 0}.row-cols-xl-auto>*{flex:0 0 auto;width:auto}.row-cols-xl-1>*{flex:0 0 auto;width:100%}.row-cols-xl-2>*{flex:0 0 auto;width:50%}.row-cols-xl-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-xl-4>*{flex:0 0 auto;width:25%}.row-cols-xl-5>*{flex:0 0 auto;width:20%}.row-cols-xl-6>*{flex:0 0 auto;width:16.66666667%}.col-xl-auto{flex:0 0 auto;width:auto}.col-xl-1{flex:0 0 auto;width:8.33333333%}.col-xl-2{flex:0 0 auto;width:16.66666667%}.col-xl-3{flex:0 0 auto;width:25%}.col-xl-4{flex:0 0 auto;width:33.33333333%}.col-xl-5{flex:0 0 auto;width:41.66666667%}.col-xl-6{flex:0 0 auto;width:50%}.col-xl-7{flex:0 0 auto;width:58.33333333%}.col-xl-8{flex:0 0 auto;width:66.66666667%}.col-xl-9{flex:0 0 auto;width:75%}.col-xl-10{flex:0 0 auto;width:83.33333333%}.col-xl-11{flex:0 0 auto;width:91.66666667%}.col-xl-12{flex:0 0 auto;width:100%}.offset-xl-0{margin-left:0}.offset-xl-1{margin-left:8.33333333%}.offset-xl-2{margin-left:16.66666667%}.offset-xl-3{margin-left:25%}.offset-xl-4{margin-left:33.33333333%}.offset-xl-5{margin-left:41.66666667%}.offset-xl-6{margin-left:50%}.offset-xl-7{margin-left:58.33333333%}.offset-xl-8{margin-left:66.66666667%}.offset-xl-9{margin-left:75%}.offset-xl-10{margin-left:83.33333333%}.offset-xl-11{margin-left:91.66666667%}.g-xl-0,.gx-xl-0{--bs-gutter-x:0}.g-xl-0,.gy-xl-0{--bs-gutter-y:0}.g-xl-1,.gx-xl-1{--bs-gutter-x:0.25rem}.g-xl-1,.gy-xl-1{--bs-gutter-y:0.25rem}.g-xl-2,.gx-xl-2{--bs-gutter-x:0.5rem}.g-xl-2,.gy-xl-2{--bs-gutter-y:0.5rem}.g-xl-3,.gx-xl-3{--bs-gutter-x:1rem}.g-xl-3,.gy-xl-3{--bs-gutter-y:1rem}.g-xl-4,.gx-xl-4{--bs-gutter-x:1.5rem}.g-xl-4,.gy-xl-4{--bs-gutter-y:1.5rem}.g-xl-5,.gx-xl-5{--bs-gutter-x:3rem}.g-xl-5,.gy-xl-5{--bs-gutter-y:3rem}}@media (min-width:1400px){.col-xxl{flex:1 0 0}.row-cols-xxl-auto>*{flex:0 0 auto;width:auto}.row-cols-xxl-1>*{flex:0 0 auto;width:100%}.row-cols-xxl-2>*{flex:0 0 auto;width:50%}.row-cols-xxl-3>*{flex:0 0 auto;width:33.33333333%}.row-cols-xxl-4>*{flex:0 0 auto;width:25%}.row-cols-xxl-5>*{flex:0 0 auto;width:20%}.row-cols-xxl-6>*{flex:0 0 auto;width:16.66666667%}.col-xxl-auto{flex:0 0 auto;width:auto}.col-xxl-1{flex:0 0 auto;width:8.33333333%}.col-xxl-2{flex:0 0 auto;width:16.66666667%}.col-xxl-3{flex:0 0 auto;width:25%}.col-xxl-4{flex:0 0 auto;width:33.33333333%}.col-xxl-5{flex:0 0 auto;width:41.66666667%}.col-xxl-6{flex:0 0 auto;width:50%}.col-xxl-7{flex:0 0 auto;width:58.33333333%}.col-xxl-8{flex:0 0 auto;width:66.66666667%}.col-xxl-9{flex:0 0 auto;width:75%}.col-xxl-10{flex:0 0 auto;width:83.33333333%}.col-xxl-11{flex:0 0 auto;width:91.66666667%}.col-xxl-12{flex:0 0 auto;width:100%}.offset-xxl-0{margin-left:0}.offset-xxl-1{margin-left:8.33333333%}.offset-xxl-2{margin-left:16.66666667%}.offset-xxl-3{margin-left:25%}.offset-xxl-4{margin-left:33.33333333%}.offset-xxl-5{margin-left:41.66666667%}.offset-xxl-6{margin-left:50%}.offset-xxl-7{margin-left:58.33333333%}.offset-xxl-8{margin-left:66.66666667%}.offset-xxl-9{margin-left:75%}.offset-xxl-10{margin-left:83.33333333%}.offset-xxl-11{margin-left:91.66666667%}.g-xxl-0,.gx-xxl-0{--bs-gutter-x:0}.g-xxl-0,.gy-xxl-0{--bs-gutter-y:0}.g-xxl-1,.gx-xxl-1{--bs-gutter-x:0.25rem}.g-xxl-1,.gy-xxl-1{--bs-gutter-y:0.25rem}.g-xxl-2,.gx-xxl-2{--bs-gutter-x:0.5rem}.g-xxl-2,.gy-xxl-2{--bs-gutter-y:0.5rem}.g-xxl-3,.gx-xxl-3{--bs-gutter-x:1rem}.g-xxl-3,.gy-xxl-3{--bs-gutter-y:1rem}.g-xxl-4,.gx-xxl-4{--bs-gutter-x:1.5rem}.g-xxl-4,.gy-xxl-4{--bs-gutter-y:1.5rem}.g-xxl-5,.gx-xxl-5{--bs-gutter-x:3rem}.g-xxl-5,.gy-xxl-5{--bs-gutter-y:3rem}}.table{--bs-table-color-type:initial;--bs-table-bg-type:initial;--bs-table-color-state:initial;--bs-table-bg-state:initial;--bs-table-color:var(--bs-emphasis-color);--bs-table-bg:var(--bs-body-bg);--bs-table-border-color:var(--bs-border-color);--bs-table-accent-bg:transparent;--bs-table-striped-color:var(--bs-emphasis-color);--bs-table-striped-bg:rgba(var(--bs-emphasis-color-rgb), 0.05);--bs-table-active-color:var(--bs-emphasis-color);--bs-table-active-bg:rgba(var(--bs-emphasis-color-rgb), 0.1);--bs-table-hover-color:var(--bs-emphasis-color);--bs-table-hover-bg:rgba(var(--bs-emphasis-color-rgb), 0.075);width:100%;margin-bottom:1rem;vertical-align:top;border-color:var(--bs-table-border-color)}.table>:not(caption)>*>*{padding:.5rem .5rem;color:var(--bs-table-color-state,var(--bs-table-color-type,var(--bs-table-color)));background-color:var(--bs-table-bg);border-bottom-width:var(--bs-border-width);box-shadow:inset 0 0 0 9999px var(--bs-table-bg-state,var(--bs-table-bg-type,var(--bs-table-accent-bg)))}.table>tbody{vertical-align:inherit}.table>thead{vertical-align:bottom}.table-group-divider{border-top:calc(var(--bs-border-width) * 2) solid currentcolor}.caption-top{caption-side:top}.table-sm>:not(caption)>*>*{padding:.25rem .25rem}.table-bordered>:not(caption)>*{border-width:var(--bs-border-width) 0}.table-bordered>:not(caption)>*>*{border-width:0 var(--bs-border-width)}.table-borderless>:not(caption)>*>*{border-bottom-width:0}.table-borderless>:not(:first-child){border-top-width:0}.table-striped>tbody>tr:nth-of-type(odd)>*{--bs-table-color-type:var(--bs-table-striped-color);--bs-table-bg-type:var(--bs-table-striped-bg)}.table-striped-columns>:not(caption)>tr>:nth-child(2n){--bs-table-color-type:var(--bs-table-striped-color);--bs-table-bg-type:var(--bs-table-striped-bg)}.table-active{--bs-table-color-state:var(--bs-table-active-color);--bs-table-bg-state:var(--bs-table-active-bg)}.table-hover>tbody>tr:hover>*{--bs-table-color-state:var(--bs-table-hover-color);--bs-table-bg-state:var(--bs-table-hover-bg)}.table-primary{--bs-table-color:#000;--bs-table-bg:#cfe2ff;--bs-table-border-color:#a6b5cc;--bs-table-striped-bg:#c5d7f2;--bs-table-striped-color:#000;--bs-table-active-bg:#bacbe6;--bs-table-active-color:#000;--bs-table-hover-bg:#bfd1ec;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-secondary{--bs-table-color:#000;--bs-table-bg:#e2e3e5;--bs-table-border-color:#b5b6b7;--bs-table-striped-bg:#d7d8da;--bs-table-striped-color:#000;--bs-table-active-bg:#cbccce;--bs-table-active-color:#000;--bs-table-hover-bg:#d1d2d4;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-success{--bs-table-color:#000;--bs-table-bg:#d1e7dd;--bs-table-border-color:#a7b9b1;--bs-table-striped-bg:#c7dbd2;--bs-table-striped-color:#000;--bs-table-active-bg:#bcd0c7;--bs-table-active-color:#000;--bs-table-hover-bg:#c1d6cc;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-info{--bs-table-color:#000;--bs-table-bg:#cff4fc;--bs-table-border-color:#a6c3ca;--bs-table-striped-bg:#c5e8ef;--bs-table-striped-color:#000;--bs-table-active-bg:#badce3;--bs-table-active-color:#000;--bs-table-hover-bg:#bfe2e9;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-warning{--bs-table-color:#000;--bs-table-bg:#fff3cd;--bs-table-border-color:#ccc2a4;--bs-table-striped-bg:#f2e7c3;--bs-table-striped-color:#000;--bs-table-active-bg:#e6dbb9;--bs-table-active-color:#000;--bs-table-hover-bg:#ece1be;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-danger{--bs-table-color:#000;--bs-table-bg:#f8d7da;--bs-table-border-color:#c6acae;--bs-table-striped-bg:#eccccf;--bs-table-striped-color:#000;--bs-table-active-bg:#dfc2c4;--bs-table-active-color:#000;--bs-table-hover-bg:#e5c7ca;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-light{--bs-table-color:#000;--bs-table-bg:#f8f9fa;--bs-table-border-color:#c6c7c8;--bs-table-striped-bg:#ecedee;--bs-table-striped-color:#000;--bs-table-active-bg:#dfe0e1;--bs-table-active-color:#000;--bs-table-hover-bg:#e5e6e7;--bs-table-hover-color:#000;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-dark{--bs-table-color:#fff;--bs-table-bg:#212529;--bs-table-border-color:#4d5154;--bs-table-striped-bg:#2c3034;--bs-table-striped-color:#fff;--bs-table-active-bg:#373b3e;--bs-table-active-color:#fff;--bs-table-hover-bg:#323539;--bs-table-hover-color:#fff;color:var(--bs-table-color);border-color:var(--bs-table-border-color)}.table-responsive{overflow-x:auto;-webkit-overflow-scrolling:touch}@media (max-width:575.98px){.table-responsive-sm{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:767.98px){.table-responsive-md{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:991.98px){.table-responsive-lg{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1199.98px){.table-responsive-xl{overflow-x:auto;-webkit-overflow-scrolling:touch}}@media (max-width:1399.98px){.table-responsive-xxl{overflow-x:auto;-webkit-overflow-scrolling:touch}}.form-label{margin-bottom:.5rem}.col-form-label{padding-top:calc(.375rem + var(--bs-border-width));padding-bottom:calc(.375rem + var(--bs-border-width));margin-bottom:0;font-size:inherit;line-height:1.5}.col-form-label-lg{padding-top:calc(.5rem + var(--bs-border-width));padding-bottom:calc(.5rem + var(--bs-border-width));font-size:1.25rem}.col-form-label-sm{padding-top:calc(.25rem + var(--bs-border-width));padding-bottom:calc(.25rem + var(--bs-border-width));font-size:.875rem}.form-text{margin-top:.25rem;font-size:.875em;color:var(--bs-secondary-color)}.form-control{display:block;width:100%;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-body-bg);background-clip:padding-box;border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius);transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control{transition:none}}.form-control[type=file]{overflow:hidden}.form-control[type=file]:not(:disabled):not([readonly]){cursor:pointer}.form-control:focus{color:var(--bs-body-color);background-color:var(--bs-body-bg);border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-control::-webkit-date-and-time-value{min-width:85px;height:1.5em;margin:0}.form-control::-webkit-datetime-edit{display:block;padding:0}.form-control::placeholder{color:var(--bs-secondary-color);opacity:1}.form-control:disabled{background-color:var(--bs-secondary-bg);opacity:1}.form-control::-webkit-file-upload-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:var(--bs-body-color);background-color:var(--bs-tertiary-bg);pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:var(--bs-border-width);border-radius:0;-webkit-transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}.form-control::file-selector-button{padding:.375rem .75rem;margin:-.375rem -.75rem;-webkit-margin-end:.75rem;margin-inline-end:.75rem;color:var(--bs-body-color);background-color:var(--bs-tertiary-bg);pointer-events:none;border-color:inherit;border-style:solid;border-width:0;border-inline-end-width:var(--bs-border-width);border-radius:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-control::-webkit-file-upload-button{-webkit-transition:none;transition:none}.form-control::file-selector-button{transition:none}}.form-control:hover:not(:disabled):not([readonly])::-webkit-file-upload-button{background-color:var(--bs-secondary-bg)}.form-control:hover:not(:disabled):not([readonly])::file-selector-button{background-color:var(--bs-secondary-bg)}.form-control-plaintext{display:block;width:100%;padding:.375rem 0;margin-bottom:0;line-height:1.5;color:var(--bs-body-color);background-color:transparent;border:solid transparent;border-width:var(--bs-border-width) 0}.form-control-plaintext:focus{outline:0}.form-control-plaintext.form-control-lg,.form-control-plaintext.form-control-sm{padding-right:0;padding-left:0}.form-control-sm{min-height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2));padding:.25rem .5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.form-control-sm::-webkit-file-upload-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-sm::file-selector-button{padding:.25rem .5rem;margin:-.25rem -.5rem;-webkit-margin-end:.5rem;margin-inline-end:.5rem}.form-control-lg{min-height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2));padding:.5rem 1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}.form-control-lg::-webkit-file-upload-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}.form-control-lg::file-selector-button{padding:.5rem 1rem;margin:-.5rem -1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}textarea.form-control{min-height:calc(1.5em + .75rem + calc(var(--bs-border-width) * 2))}textarea.form-control-sm{min-height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2))}textarea.form-control-lg{min-height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2))}.form-control-color{width:3rem;height:calc(1.5em + .75rem + calc(var(--bs-border-width) * 2));padding:.375rem}.form-control-color:not(:disabled):not([readonly]){cursor:pointer}.form-control-color::-moz-color-swatch{border:0!important;border-radius:var(--bs-border-radius)}.form-control-color::-webkit-color-swatch{border:0!important;border-radius:var(--bs-border-radius)}.form-control-color.form-control-sm{height:calc(1.5em + .5rem + calc(var(--bs-border-width) * 2))}.form-control-color.form-control-lg{height:calc(1.5em + 1rem + calc(var(--bs-border-width) * 2))}.form-select{--bs-form-select-bg-img:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23343a40' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e");display:block;width:100%;padding:.375rem 2.25rem .375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-body-bg);background-image:var(--bs-form-select-bg-img),var(--bs-form-select-bg-icon,none);background-repeat:no-repeat;background-position:right .75rem center;background-size:16px 12px;border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius);transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-select{transition:none}}.form-select:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-select[multiple],.form-select[size]:not([size="1"]){padding-right:.75rem;background-image:none}.form-select:disabled{background-color:var(--bs-secondary-bg)}.form-select:-moz-focusring{color:transparent;text-shadow:0 0 0 var(--bs-body-color)}.form-select-sm{padding-top:.25rem;padding-bottom:.25rem;padding-left:.5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.form-select-lg{padding-top:.5rem;padding-bottom:.5rem;padding-left:1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}[data-bs-theme=dark] .form-select{--bs-form-select-bg-img:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23dee2e6' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e")}.form-check{display:block;min-height:1.5rem;padding-left:1.5em;margin-bottom:.125rem}.form-check .form-check-input{float:left;margin-left:-1.5em}.form-check-reverse{padding-right:1.5em;padding-left:0;text-align:right}.form-check-reverse .form-check-input{float:right;margin-right:-1.5em;margin-left:0}.form-check-input{--bs-form-check-bg:var(--bs-body-bg);flex-shrink:0;width:1em;height:1em;margin-top:.25em;vertical-align:top;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:var(--bs-form-check-bg);background-image:var(--bs-form-check-bg-image);background-repeat:no-repeat;background-position:center;background-size:contain;border:var(--bs-border-width) solid var(--bs-border-color);-webkit-print-color-adjust:exact;color-adjust:exact;print-color-adjust:exact}.form-check-input[type=checkbox]{border-radius:.25em}.form-check-input[type=radio]{border-radius:50%}.form-check-input:active{filter:brightness(90%)}.form-check-input:focus{border-color:#86b7fe;outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.form-check-input:checked{background-color:#0d6efd;border-color:#0d6efd}.form-check-input:checked[type=checkbox]{--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='m6 10 3 3 6-6'/%3e%3c/svg%3e")}.form-check-input:checked[type=radio]{--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='2' fill='%23fff'/%3e%3c/svg%3e")}.form-check-input[type=checkbox]:indeterminate{background-color:#0d6efd;border-color:#0d6efd;--bs-form-check-bg-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'%3e%3cpath fill='none' stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='3' d='M6 10h8'/%3e%3c/svg%3e")}.form-check-input:disabled{pointer-events:none;filter:none;opacity:.5}.form-check-input:disabled~.form-check-label,.form-check-input[disabled]~.form-check-label{cursor:default;opacity:.5}.form-switch{padding-left:2.5em}.form-switch .form-check-input{--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%280, 0, 0, 0.25%29'/%3e%3c/svg%3e");width:2em;margin-left:-2.5em;background-image:var(--bs-form-switch-bg);background-position:left center;border-radius:2em;transition:background-position .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-switch .form-check-input{transition:none}}.form-switch .form-check-input:focus{--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%2386b7fe'/%3e%3c/svg%3e")}.form-switch .form-check-input:checked{background-position:right center;--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e")}.form-switch.form-check-reverse{padding-right:2.5em;padding-left:0}.form-switch.form-check-reverse .form-check-input{margin-right:-2.5em;margin-left:0}.form-check-inline{display:inline-block;margin-right:1rem}.btn-check{position:absolute;clip:rect(0,0,0,0);pointer-events:none}.btn-check:disabled+.btn,.btn-check[disabled]+.btn{pointer-events:none;filter:none;opacity:.65}[data-bs-theme=dark] .form-switch .form-check-input:not(:checked):not(:focus){--bs-form-switch-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='rgba%28255, 255, 255, 0.25%29'/%3e%3c/svg%3e")}.form-range{width:100%;height:1.5rem;padding:0;-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:transparent}.form-range:focus{outline:0}.form-range:focus::-webkit-slider-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range:focus::-moz-range-thumb{box-shadow:0 0 0 1px #fff,0 0 0 .25rem rgba(13,110,253,.25)}.form-range::-moz-focus-outer{border:0}.form-range::-webkit-slider-thumb{width:1rem;height:1rem;margin-top:-.25rem;-webkit-appearance:none;appearance:none;background-color:#0d6efd;border:0;border-radius:1rem;-webkit-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-range::-webkit-slider-thumb{-webkit-transition:none;transition:none}}.form-range::-webkit-slider-thumb:active{background-color:#b6d4fe}.form-range::-webkit-slider-runnable-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:var(--bs-secondary-bg);border-color:transparent;border-radius:1rem}.form-range::-moz-range-thumb{width:1rem;height:1rem;-moz-appearance:none;appearance:none;background-color:#0d6efd;border:0;border-radius:1rem;-moz-transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out;transition:background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.form-range::-moz-range-thumb{-moz-transition:none;transition:none}}.form-range::-moz-range-thumb:active{background-color:#b6d4fe}.form-range::-moz-range-track{width:100%;height:.5rem;color:transparent;cursor:pointer;background-color:var(--bs-secondary-bg);border-color:transparent;border-radius:1rem}.form-range:disabled{pointer-events:none}.form-range:disabled::-webkit-slider-thumb{background-color:var(--bs-secondary-color)}.form-range:disabled::-moz-range-thumb{background-color:var(--bs-secondary-color)}.form-floating{position:relative}.form-floating>.form-control,.form-floating>.form-control-plaintext,.form-floating>.form-select{height:calc(3.5rem + calc(var(--bs-border-width) * 2));min-height:calc(3.5rem + calc(var(--bs-border-width) * 2));line-height:1.25}.form-floating>label{position:absolute;top:0;left:0;z-index:2;max-width:100%;height:100%;padding:1rem .75rem;overflow:hidden;color:rgba(var(--bs-body-color-rgb),.65);text-align:start;text-overflow:ellipsis;white-space:nowrap;pointer-events:none;border:var(--bs-border-width) solid transparent;transform-origin:0 0;transition:opacity .1s ease-in-out,transform .1s ease-in-out}@media (prefers-reduced-motion:reduce){.form-floating>label{transition:none}}.form-floating>.form-control,.form-floating>.form-control-plaintext{padding:1rem .75rem}.form-floating>.form-control-plaintext::placeholder,.form-floating>.form-control::placeholder{color:transparent}.form-floating>.form-control-plaintext:focus,.form-floating>.form-control-plaintext:not(:placeholder-shown),.form-floating>.form-control:focus,.form-floating>.form-control:not(:placeholder-shown){padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-control-plaintext:-webkit-autofill,.form-floating>.form-control:-webkit-autofill{padding-top:1.625rem;padding-bottom:.625rem}.form-floating>.form-select{padding-top:1.625rem;padding-bottom:.625rem;padding-left:.75rem}.form-floating>.form-control-plaintext~label,.form-floating>.form-control:focus~label,.form-floating>.form-control:not(:placeholder-shown)~label,.form-floating>.form-select~label{transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>.form-control:-webkit-autofill~label{transform:scale(.85) translateY(-.5rem) translateX(.15rem)}.form-floating>textarea:focus~label::after,.form-floating>textarea:not(:placeholder-shown)~label::after{position:absolute;inset:1rem 0.375rem;z-index:-1;height:1.5em;content:"";background-color:var(--bs-body-bg);border-radius:var(--bs-border-radius)}.form-floating>textarea:disabled~label::after{background-color:var(--bs-secondary-bg)}.form-floating>.form-control-plaintext~label{border-width:var(--bs-border-width) 0}.form-floating>.form-control:disabled~label,.form-floating>:disabled~label{color:#6c757d}.input-group{position:relative;display:flex;flex-wrap:wrap;align-items:stretch;width:100%}.input-group>.form-control,.input-group>.form-floating,.input-group>.form-select{position:relative;flex:1 1 auto;width:1%;min-width:0}.input-group>.form-control:focus,.input-group>.form-floating:focus-within,.input-group>.form-select:focus{z-index:5}.input-group .btn{position:relative;z-index:2}.input-group .btn:focus{z-index:5}.input-group-text{display:flex;align-items:center;padding:.375rem .75rem;font-size:1rem;font-weight:400;line-height:1.5;color:var(--bs-body-color);text-align:center;white-space:nowrap;background-color:var(--bs-tertiary-bg);border:var(--bs-border-width) solid var(--bs-border-color);border-radius:var(--bs-border-radius)}.input-group-lg>.btn,.input-group-lg>.form-control,.input-group-lg>.form-select,.input-group-lg>.input-group-text{padding:.5rem 1rem;font-size:1.25rem;border-radius:var(--bs-border-radius-lg)}.input-group-sm>.btn,.input-group-sm>.form-control,.input-group-sm>.form-select,.input-group-sm>.input-group-text{padding:.25rem .5rem;font-size:.875rem;border-radius:var(--bs-border-radius-sm)}.input-group-lg>.form-select,.input-group-sm>.form-select{padding-right:3rem}.input-group:not(.has-validation)>.dropdown-toggle:nth-last-child(n+3),.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-control,.input-group:not(.has-validation)>.form-floating:not(:last-child)>.form-select,.input-group:not(.has-validation)>:not(:last-child):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){border-top-right-radius:0;border-bottom-right-radius:0}.input-group.has-validation>.dropdown-toggle:nth-last-child(n+4),.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-control,.input-group.has-validation>.form-floating:nth-last-child(n+3)>.form-select,.input-group.has-validation>:nth-last-child(n+3):not(.dropdown-toggle):not(.dropdown-menu):not(.form-floating){border-top-right-radius:0;border-bottom-right-radius:0}.input-group>:not(:first-child):not(.dropdown-menu):not(.valid-tooltip):not(.valid-feedback):not(.invalid-tooltip):not(.invalid-feedback){margin-left:calc(-1 * var(--bs-border-width));border-top-left-radius:0;border-bottom-left-radius:0}.input-group>.form-floating:not(:first-child)>.form-control,.input-group>.form-floating:not(:first-child)>.form-select{border-top-left-radius:0;border-bottom-left-radius:0}.valid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:var(--bs-form-valid-color)}.valid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:var(--bs-success);border-radius:var(--bs-border-radius)}.is-valid~.valid-feedback,.is-valid~.valid-tooltip,.was-validated :valid~.valid-feedback,.was-validated :valid~.valid-tooltip{display:block}.form-control.is-valid,.was-validated .form-control:valid{border-color:var(--bs-form-valid-border-color);padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-valid:focus,.was-validated .form-control:valid:focus{border-color:var(--bs-form-valid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.was-validated textarea.form-control:valid,textarea.form-control.is-valid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-valid,.was-validated .form-select:valid{border-color:var(--bs-form-valid-border-color)}.form-select.is-valid:not([multiple]):not([size]),.form-select.is-valid:not([multiple])[size="1"],.was-validated .form-select:valid:not([multiple]):not([size]),.was-validated .form-select:valid:not([multiple])[size="1"]{--bs-form-select-bg-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 8 8'%3e%3cpath fill='%23198754' d='M2.3 6.73.6 4.53c-.4-1.04.46-1.4 1.1-.8l1.1 1.4 3.4-3.8c.6-.63 1.6-.27 1.2.7l-4 4.6c-.43.5-.8.4-1.1.1'/%3e%3c/svg%3e");padding-right:4.125rem;background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-valid:focus,.was-validated .form-select:valid:focus{border-color:var(--bs-form-valid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.form-control-color.is-valid,.was-validated .form-control-color:valid{width:calc(3rem + calc(1.5em + .75rem))}.form-check-input.is-valid,.was-validated .form-check-input:valid{border-color:var(--bs-form-valid-border-color)}.form-check-input.is-valid:checked,.was-validated .form-check-input:valid:checked{background-color:var(--bs-form-valid-color)}.form-check-input.is-valid:focus,.was-validated .form-check-input:valid:focus{box-shadow:0 0 0 .25rem rgba(var(--bs-success-rgb),.25)}.form-check-input.is-valid~.form-check-label,.was-validated .form-check-input:valid~.form-check-label{color:var(--bs-form-valid-color)}.form-check-inline .form-check-input~.valid-feedback{margin-left:.5em}.input-group>.form-control:not(:focus).is-valid,.input-group>.form-floating:not(:focus-within).is-valid,.input-group>.form-select:not(:focus).is-valid,.was-validated .input-group>.form-control:not(:focus):valid,.was-validated .input-group>.form-floating:not(:focus-within):valid,.was-validated .input-group>.form-select:not(:focus):valid{z-index:3}.invalid-feedback{display:none;width:100%;margin-top:.25rem;font-size:.875em;color:var(--bs-form-invalid-color)}.invalid-tooltip{position:absolute;top:100%;z-index:5;display:none;max-width:100%;padding:.25rem .5rem;margin-top:.1rem;font-size:.875rem;color:#fff;background-color:var(--bs-danger);border-radius:var(--bs-border-radius)}.is-invalid~.invalid-feedback,.is-invalid~.invalid-tooltip,.was-validated :invalid~.invalid-feedback,.was-validated :invalid~.invalid-tooltip{display:block}.form-control.is-invalid,.was-validated .form-control:invalid{border-color:var(--bs-form-invalid-border-color);padding-right:calc(1.5em + .75rem);background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");background-repeat:no-repeat;background-position:right calc(.375em + .1875rem) center;background-size:calc(.75em + .375rem) calc(.75em + .375rem)}.form-control.is-invalid:focus,.was-validated .form-control:invalid:focus{border-color:var(--bs-form-invalid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.was-validated textarea.form-control:invalid,textarea.form-control.is-invalid{padding-right:calc(1.5em + .75rem);background-position:top calc(.375em + .1875rem) right calc(.375em + .1875rem)}.form-select.is-invalid,.was-validated .form-select:invalid{border-color:var(--bs-form-invalid-border-color)}.form-select.is-invalid:not([multiple]):not([size]),.form-select.is-invalid:not([multiple])[size="1"],.was-validated .form-select:invalid:not([multiple]):not([size]),.was-validated .form-select:invalid:not([multiple])[size="1"]{--bs-form-select-bg-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12' width='12' height='12' fill='none' stroke='%23dc3545'%3e%3ccircle cx='6' cy='6' r='4.5'/%3e%3cpath stroke-linejoin='round' d='M5.8 3.6h.4L6 6.5z'/%3e%3ccircle cx='6' cy='8.2' r='.6' fill='%23dc3545' stroke='none'/%3e%3c/svg%3e");padding-right:4.125rem;background-position:right .75rem center,center right 2.25rem;background-size:16px 12px,calc(.75em + .375rem) calc(.75em + .375rem)}.form-select.is-invalid:focus,.was-validated .form-select:invalid:focus{border-color:var(--bs-form-invalid-border-color);box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.form-control-color.is-invalid,.was-validated .form-control-color:invalid{width:calc(3rem + calc(1.5em + .75rem))}.form-check-input.is-invalid,.was-validated .form-check-input:invalid{border-color:var(--bs-form-invalid-border-color)}.form-check-input.is-invalid:checked,.was-validated .form-check-input:invalid:checked{background-color:var(--bs-form-invalid-color)}.form-check-input.is-invalid:focus,.was-validated .form-check-input:invalid:focus{box-shadow:0 0 0 .25rem rgba(var(--bs-danger-rgb),.25)}.form-check-input.is-invalid~.form-check-label,.was-validated .form-check-input:invalid~.form-check-label{color:var(--bs-form-invalid-color)}.form-check-inline .form-check-input~.invalid-feedback{margin-left:.5em}.input-group>.form-control:not(:focus).is-invalid,.input-group>.form-floating:not(:focus-within).is-invalid,.input-group>.form-select:not(:focus).is-invalid,.was-validated .input-group>.form-control:not(:focus):invalid,.was-validated .input-group>.form-floating:not(:focus-within):invalid,.was-validated .input-group>.form-select:not(:focus):invalid{z-index:4}.btn{--bs-btn-padding-x:0.75rem;--bs-btn-padding-y:0.375rem;--bs-btn-font-family: ;--bs-btn-font-size:1rem;--bs-btn-font-weight:400;--bs-btn-line-height:1.5;--bs-btn-color:var(--bs-body-color);--bs-btn-bg:transparent;--bs-btn-border-width:var(--bs-border-width);--bs-btn-border-color:transparent;--bs-btn-border-radius:var(--bs-border-radius);--bs-btn-hover-border-color:transparent;--bs-btn-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.15),0 1px 1px rgba(0, 0, 0, 0.075);--bs-btn-disabled-opacity:0.65;--bs-btn-focus-box-shadow:0 0 0 0.25rem rgba(var(--bs-btn-focus-shadow-rgb), .5);display:inline-block;padding:var(--bs-btn-padding-y) var(--bs-btn-padding-x);font-family:var(--bs-btn-font-family);font-size:var(--bs-btn-font-size);font-weight:var(--bs-btn-font-weight);line-height:var(--bs-btn-line-height);color:var(--bs-btn-color);text-align:center;text-decoration:none;vertical-align:middle;cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;border:var(--bs-btn-border-width) solid var(--bs-btn-border-color);border-radius:var(--bs-btn-border-radius);background-color:var(--bs-btn-bg);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.btn{transition:none}}.btn:hover{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color)}.btn-check+.btn:hover{color:var(--bs-btn-color);background-color:var(--bs-btn-bg);border-color:var(--bs-btn-border-color)}.btn:focus-visible{color:var(--bs-btn-hover-color);background-color:var(--bs-btn-hover-bg);border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:focus-visible+.btn{border-color:var(--bs-btn-hover-border-color);outline:0;box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:checked+.btn,.btn.active,.btn.show,.btn:first-child:active,:not(.btn-check)+.btn:active{color:var(--bs-btn-active-color);background-color:var(--bs-btn-active-bg);border-color:var(--bs-btn-active-border-color)}.btn-check:checked+.btn:focus-visible,.btn.active:focus-visible,.btn.show:focus-visible,.btn:first-child:active:focus-visible,:not(.btn-check)+.btn:active:focus-visible{box-shadow:var(--bs-btn-focus-box-shadow)}.btn-check:checked:focus-visible+.btn{box-shadow:var(--bs-btn-focus-box-shadow)}.btn.disabled,.btn:disabled,fieldset:disabled .btn{color:var(--bs-btn-disabled-color);pointer-events:none;background-color:var(--bs-btn-disabled-bg);border-color:var(--bs-btn-disabled-border-color);opacity:var(--bs-btn-disabled-opacity)}.btn-primary{--bs-btn-color:#fff;--bs-btn-bg:#0d6efd;--bs-btn-border-color:#0d6efd;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#0b5ed7;--bs-btn-hover-border-color:#0a58ca;--bs-btn-focus-shadow-rgb:49,132,253;--bs-btn-active-color:#fff;--bs-btn-active-bg:#0a58ca;--bs-btn-active-border-color:#0a53be;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#0d6efd;--bs-btn-disabled-border-color:#0d6efd}.btn-secondary{--bs-btn-color:#fff;--bs-btn-bg:#6c757d;--bs-btn-border-color:#6c757d;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#5c636a;--bs-btn-hover-border-color:#565e64;--bs-btn-focus-shadow-rgb:130,138,145;--bs-btn-active-color:#fff;--bs-btn-active-bg:#565e64;--bs-btn-active-border-color:#51585e;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#6c757d;--bs-btn-disabled-border-color:#6c757d}.btn-success{--bs-btn-color:#fff;--bs-btn-bg:#198754;--bs-btn-border-color:#198754;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#157347;--bs-btn-hover-border-color:#146c43;--bs-btn-focus-shadow-rgb:60,153,110;--bs-btn-active-color:#fff;--bs-btn-active-bg:#146c43;--bs-btn-active-border-color:#13653f;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#198754;--bs-btn-disabled-border-color:#198754}.btn-info{--bs-btn-color:#000;--bs-btn-bg:#0dcaf0;--bs-btn-border-color:#0dcaf0;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#31d2f2;--bs-btn-hover-border-color:#25cff2;--bs-btn-focus-shadow-rgb:11,172,204;--bs-btn-active-color:#000;--bs-btn-active-bg:#3dd5f3;--bs-btn-active-border-color:#25cff2;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#0dcaf0;--bs-btn-disabled-border-color:#0dcaf0}.btn-warning{--bs-btn-color:#000;--bs-btn-bg:#ffc107;--bs-btn-border-color:#ffc107;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#ffca2c;--bs-btn-hover-border-color:#ffc720;--bs-btn-focus-shadow-rgb:217,164,6;--bs-btn-active-color:#000;--bs-btn-active-bg:#ffcd39;--bs-btn-active-border-color:#ffc720;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#ffc107;--bs-btn-disabled-border-color:#ffc107}.btn-danger{--bs-btn-color:#fff;--bs-btn-bg:#dc3545;--bs-btn-border-color:#dc3545;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#bb2d3b;--bs-btn-hover-border-color:#b02a37;--bs-btn-focus-shadow-rgb:225,83,97;--bs-btn-active-color:#fff;--bs-btn-active-bg:#b02a37;--bs-btn-active-border-color:#a52834;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#dc3545;--bs-btn-disabled-border-color:#dc3545}.btn-light{--bs-btn-color:#000;--bs-btn-bg:#f8f9fa;--bs-btn-border-color:#f8f9fa;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#d3d4d5;--bs-btn-hover-border-color:#c6c7c8;--bs-btn-focus-shadow-rgb:211,212,213;--bs-btn-active-color:#000;--bs-btn-active-bg:#c6c7c8;--bs-btn-active-border-color:#babbbc;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#000;--bs-btn-disabled-bg:#f8f9fa;--bs-btn-disabled-border-color:#f8f9fa}.btn-dark{--bs-btn-color:#fff;--bs-btn-bg:#212529;--bs-btn-border-color:#212529;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#424649;--bs-btn-hover-border-color:#373b3e;--bs-btn-focus-shadow-rgb:66,70,73;--bs-btn-active-color:#fff;--bs-btn-active-bg:#4d5154;--bs-btn-active-border-color:#373b3e;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#fff;--bs-btn-disabled-bg:#212529;--bs-btn-disabled-border-color:#212529}.btn-outline-primary{--bs-btn-color:#0d6efd;--bs-btn-border-color:#0d6efd;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#0d6efd;--bs-btn-hover-border-color:#0d6efd;--bs-btn-focus-shadow-rgb:13,110,253;--bs-btn-active-color:#fff;--bs-btn-active-bg:#0d6efd;--bs-btn-active-border-color:#0d6efd;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#0d6efd;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#0d6efd;--bs-gradient:none}.btn-outline-secondary{--bs-btn-color:#6c757d;--bs-btn-border-color:#6c757d;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#6c757d;--bs-btn-hover-border-color:#6c757d;--bs-btn-focus-shadow-rgb:108,117,125;--bs-btn-active-color:#fff;--bs-btn-active-bg:#6c757d;--bs-btn-active-border-color:#6c757d;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#6c757d;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#6c757d;--bs-gradient:none}.btn-outline-success{--bs-btn-color:#198754;--bs-btn-border-color:#198754;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#198754;--bs-btn-hover-border-color:#198754;--bs-btn-focus-shadow-rgb:25,135,84;--bs-btn-active-color:#fff;--bs-btn-active-bg:#198754;--bs-btn-active-border-color:#198754;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#198754;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#198754;--bs-gradient:none}.btn-outline-info{--bs-btn-color:#0dcaf0;--bs-btn-border-color:#0dcaf0;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#0dcaf0;--bs-btn-hover-border-color:#0dcaf0;--bs-btn-focus-shadow-rgb:13,202,240;--bs-btn-active-color:#000;--bs-btn-active-bg:#0dcaf0;--bs-btn-active-border-color:#0dcaf0;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#0dcaf0;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#0dcaf0;--bs-gradient:none}.btn-outline-warning{--bs-btn-color:#ffc107;--bs-btn-border-color:#ffc107;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#ffc107;--bs-btn-hover-border-color:#ffc107;--bs-btn-focus-shadow-rgb:255,193,7;--bs-btn-active-color:#000;--bs-btn-active-bg:#ffc107;--bs-btn-active-border-color:#ffc107;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#ffc107;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#ffc107;--bs-gradient:none}.btn-outline-danger{--bs-btn-color:#dc3545;--bs-btn-border-color:#dc3545;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#dc3545;--bs-btn-hover-border-color:#dc3545;--bs-btn-focus-shadow-rgb:220,53,69;--bs-btn-active-color:#fff;--bs-btn-active-bg:#dc3545;--bs-btn-active-border-color:#dc3545;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#dc3545;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#dc3545;--bs-gradient:none}.btn-outline-light{--bs-btn-color:#f8f9fa;--bs-btn-border-color:#f8f9fa;--bs-btn-hover-color:#000;--bs-btn-hover-bg:#f8f9fa;--bs-btn-hover-border-color:#f8f9fa;--bs-btn-focus-shadow-rgb:248,249,250;--bs-btn-active-color:#000;--bs-btn-active-bg:#f8f9fa;--bs-btn-active-border-color:#f8f9fa;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#f8f9fa;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#f8f9fa;--bs-gradient:none}.btn-outline-dark{--bs-btn-color:#212529;--bs-btn-border-color:#212529;--bs-btn-hover-color:#fff;--bs-btn-hover-bg:#212529;--bs-btn-hover-border-color:#212529;--bs-btn-focus-shadow-rgb:33,37,41;--bs-btn-active-color:#fff;--bs-btn-active-bg:#212529;--bs-btn-active-border-color:#212529;--bs-btn-active-shadow:inset 0 3px 5px rgba(0, 0, 0, 0.125);--bs-btn-disabled-color:#212529;--bs-btn-disabled-bg:transparent;--bs-btn-disabled-border-color:#212529;--bs-gradient:none}.btn-link{--bs-btn-font-weight:400;--bs-btn-color:var(--bs-link-color);--bs-btn-bg:transparent;--bs-btn-border-color:transparent;--bs-btn-hover-color:var(--bs-link-hover-color);--bs-btn-hover-border-color:transparent;--bs-btn-active-color:var(--bs-link-hover-color);--bs-btn-active-border-color:transparent;--bs-btn-disabled-color:#6c757d;--bs-btn-disabled-border-color:transparent;--bs-btn-box-shadow:0 0 0 #000;--bs-btn-focus-shadow-rgb:49,132,253;text-decoration:underline}.btn-link:focus-visible{color:var(--bs-btn-color)}.btn-link:hover{color:var(--bs-btn-hover-color)}.btn-group-lg>.btn,.btn-lg{--bs-btn-padding-y:0.5rem;--bs-btn-padding-x:1rem;--bs-btn-font-size:1.25rem;--bs-btn-border-radius:var(--bs-border-radius-lg)}.btn-group-sm>.btn,.btn-sm{--bs-btn-padding-y:0.25rem;--bs-btn-padding-x:0.5rem;--bs-btn-font-size:0.875rem;--bs-btn-border-radius:var(--bs-border-radius-sm)}.fade{transition:opacity .15s linear}@media (prefers-reduced-motion:reduce){.fade{transition:none}}.fade:not(.show){opacity:0}.collapse:not(.show){display:none}.collapsing{height:0;overflow:hidden;transition:height .35s ease}@media (prefers-reduced-motion:reduce){.collapsing{transition:none}}.collapsing.collapse-horizontal{width:0;height:auto;transition:width .35s ease}@media (prefers-reduced-motion:reduce){.collapsing.collapse-horizontal{transition:none}}.dropdown,.dropdown-center,.dropend,.dropstart,.dropup,.dropup-center{position:relative}.dropdown-toggle{white-space:nowrap}.dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid;border-right:.3em solid transparent;border-bottom:0;border-left:.3em solid transparent}.dropdown-toggle:empty::after{margin-left:0}.dropdown-menu{--bs-dropdown-zindex:1000;--bs-dropdown-min-width:10rem;--bs-dropdown-padding-x:0;--bs-dropdown-padding-y:0.5rem;--bs-dropdown-spacer:0.125rem;--bs-dropdown-font-size:1rem;--bs-dropdown-color:var(--bs-body-color);--bs-dropdown-bg:var(--bs-body-bg);--bs-dropdown-border-color:var(--bs-border-color-translucent);--bs-dropdown-border-radius:var(--bs-border-radius);--bs-dropdown-border-width:var(--bs-border-width);--bs-dropdown-inner-border-radius:calc(var(--bs-border-radius) - var(--bs-border-width));--bs-dropdown-divider-bg:var(--bs-border-color-translucent);--bs-dropdown-divider-margin-y:0.5rem;--bs-dropdown-box-shadow:var(--bs-box-shadow);--bs-dropdown-link-color:var(--bs-body-color);--bs-dropdown-link-hover-color:var(--bs-body-color);--bs-dropdown-link-hover-bg:var(--bs-tertiary-bg);--bs-dropdown-link-active-color:#fff;--bs-dropdown-link-active-bg:#0d6efd;--bs-dropdown-link-disabled-color:var(--bs-tertiary-color);--bs-dropdown-item-padding-x:1rem;--bs-dropdown-item-padding-y:0.25rem;--bs-dropdown-header-color:#6c757d;--bs-dropdown-header-padding-x:1rem;--bs-dropdown-header-padding-y:0.5rem;position:absolute;z-index:var(--bs-dropdown-zindex);display:none;min-width:var(--bs-dropdown-min-width);padding:var(--bs-dropdown-padding-y) var(--bs-dropdown-padding-x);margin:0;font-size:var(--bs-dropdown-font-size);color:var(--bs-dropdown-color);text-align:left;list-style:none;background-color:var(--bs-dropdown-bg);background-clip:padding-box;border:var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color);border-radius:var(--bs-dropdown-border-radius)}.dropdown-menu[data-bs-popper]{top:100%;left:0;margin-top:var(--bs-dropdown-spacer)}.dropdown-menu-start{--bs-position:start}.dropdown-menu-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-end{--bs-position:end}.dropdown-menu-end[data-bs-popper]{right:0;left:auto}@media (min-width:576px){.dropdown-menu-sm-start{--bs-position:start}.dropdown-menu-sm-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-sm-end{--bs-position:end}.dropdown-menu-sm-end[data-bs-popper]{right:0;left:auto}}@media (min-width:768px){.dropdown-menu-md-start{--bs-position:start}.dropdown-menu-md-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-md-end{--bs-position:end}.dropdown-menu-md-end[data-bs-popper]{right:0;left:auto}}@media (min-width:992px){.dropdown-menu-lg-start{--bs-position:start}.dropdown-menu-lg-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-lg-end{--bs-position:end}.dropdown-menu-lg-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1200px){.dropdown-menu-xl-start{--bs-position:start}.dropdown-menu-xl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xl-end{--bs-position:end}.dropdown-menu-xl-end[data-bs-popper]{right:0;left:auto}}@media (min-width:1400px){.dropdown-menu-xxl-start{--bs-position:start}.dropdown-menu-xxl-start[data-bs-popper]{right:auto;left:0}.dropdown-menu-xxl-end{--bs-position:end}.dropdown-menu-xxl-end[data-bs-popper]{right:0;left:auto}}.dropup .dropdown-menu[data-bs-popper]{top:auto;bottom:100%;margin-top:0;margin-bottom:var(--bs-dropdown-spacer)}.dropup .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:0;border-right:.3em solid transparent;border-bottom:.3em solid;border-left:.3em solid transparent}.dropup .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-menu[data-bs-popper]{top:0;right:auto;left:100%;margin-top:0;margin-left:var(--bs-dropdown-spacer)}.dropend .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:0;border-bottom:.3em solid transparent;border-left:.3em solid}.dropend .dropdown-toggle:empty::after{margin-left:0}.dropend .dropdown-toggle::after{vertical-align:0}.dropstart .dropdown-menu[data-bs-popper]{top:0;right:100%;left:auto;margin-top:0;margin-right:var(--bs-dropdown-spacer)}.dropstart .dropdown-toggle::after{display:inline-block;margin-left:.255em;vertical-align:.255em;content:""}.dropstart .dropdown-toggle::after{display:none}.dropstart .dropdown-toggle::before{display:inline-block;margin-right:.255em;vertical-align:.255em;content:"";border-top:.3em solid transparent;border-right:.3em solid;border-bottom:.3em solid transparent}.dropstart .dropdown-toggle:empty::after{margin-left:0}.dropstart .dropdown-toggle::before{vertical-align:0}.dropdown-divider{height:0;margin:var(--bs-dropdown-divider-margin-y) 0;overflow:hidden;border-top:1px solid var(--bs-dropdown-divider-bg);opacity:1}.dropdown-item{display:block;width:100%;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);clear:both;font-weight:400;color:var(--bs-dropdown-link-color);text-align:inherit;text-decoration:none;white-space:nowrap;background-color:transparent;border:0;border-radius:var(--bs-dropdown-item-border-radius,0)}.dropdown-item:focus,.dropdown-item:hover{color:var(--bs-dropdown-link-hover-color);background-color:var(--bs-dropdown-link-hover-bg)}.dropdown-item.active,.dropdown-item:active{color:var(--bs-dropdown-link-active-color);text-decoration:none;background-color:var(--bs-dropdown-link-active-bg)}.dropdown-item.disabled,.dropdown-item:disabled{color:var(--bs-dropdown-link-disabled-color);pointer-events:none;background-color:transparent}.dropdown-menu.show{display:block}.dropdown-header{display:block;padding:var(--bs-dropdown-header-padding-y) var(--bs-dropdown-header-padding-x);margin-bottom:0;font-size:.875rem;color:var(--bs-dropdown-header-color);white-space:nowrap}.dropdown-item-text{display:block;padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);color:var(--bs-dropdown-link-color)}.dropdown-menu-dark{--bs-dropdown-color:#dee2e6;--bs-dropdown-bg:#343a40;--bs-dropdown-border-color:var(--bs-border-color-translucent);--bs-dropdown-box-shadow: ;--bs-dropdown-link-color:#dee2e6;--bs-dropdown-link-hover-color:#fff;--bs-dropdown-divider-bg:var(--bs-border-color-translucent);--bs-dropdown-link-hover-bg:rgba(255, 255, 255, 0.15);--bs-dropdown-link-active-color:#fff;--bs-dropdown-link-active-bg:#0d6efd;--bs-dropdown-link-disabled-color:#adb5bd;--bs-dropdown-header-color:#adb5bd}.btn-group,.btn-group-vertical{position:relative;display:inline-flex;vertical-align:middle}.btn-group-vertical>.btn,.btn-group>.btn{position:relative;flex:1 1 auto}.btn-group-vertical>.btn-check:checked+.btn,.btn-group-vertical>.btn-check:focus+.btn,.btn-group-vertical>.btn.active,.btn-group-vertical>.btn:active,.btn-group-vertical>.btn:focus,.btn-group-vertical>.btn:hover,.btn-group>.btn-check:checked+.btn,.btn-group>.btn-check:focus+.btn,.btn-group>.btn.active,.btn-group>.btn:active,.btn-group>.btn:focus,.btn-group>.btn:hover{z-index:1}.btn-toolbar{display:flex;flex-wrap:wrap;justify-content:flex-start}.btn-toolbar .input-group{width:auto}.btn-group{border-radius:var(--bs-border-radius)}.btn-group>.btn-group:not(:first-child),.btn-group>:not(.btn-check:first-child)+.btn{margin-left:calc(-1 * var(--bs-border-width))}.btn-group>.btn-group:not(:last-child)>.btn,.btn-group>.btn.dropdown-toggle-split:first-child,.btn-group>.btn:not(:last-child):not(.dropdown-toggle){border-top-right-radius:0;border-bottom-right-radius:0}.btn-group>.btn-group:not(:first-child)>.btn,.btn-group>.btn:nth-child(n+3),.btn-group>:not(.btn-check)+.btn{border-top-left-radius:0;border-bottom-left-radius:0}.dropdown-toggle-split{padding-right:.5625rem;padding-left:.5625rem}.dropdown-toggle-split::after,.dropend .dropdown-toggle-split::after,.dropup .dropdown-toggle-split::after{margin-left:0}.dropstart .dropdown-toggle-split::before{margin-right:0}.btn-group-sm>.btn+.dropdown-toggle-split,.btn-sm+.dropdown-toggle-split{padding-right:.375rem;padding-left:.375rem}.btn-group-lg>.btn+.dropdown-toggle-split,.btn-lg+.dropdown-toggle-split{padding-right:.75rem;padding-left:.75rem}.btn-group-vertical{flex-direction:column;align-items:flex-start;justify-content:center}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group{width:100%}.btn-group-vertical>.btn-group:not(:first-child),.btn-group-vertical>.btn:not(:first-child){margin-top:calc(-1 * var(--bs-border-width))}.btn-group-vertical>.btn-group:not(:last-child)>.btn,.btn-group-vertical>.btn:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child)>.btn,.btn-group-vertical>.btn:nth-child(n+3),.btn-group-vertical>:not(.btn-check)+.btn{border-top-left-radius:0;border-top-right-radius:0}.nav{--bs-nav-link-padding-x:1rem;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color:var(--bs-link-color);--bs-nav-link-hover-color:var(--bs-link-hover-color);--bs-nav-link-disabled-color:var(--bs-secondary-color);display:flex;flex-wrap:wrap;padding-left:0;margin-bottom:0;list-style:none}.nav-link{display:block;padding:var(--bs-nav-link-padding-y) var(--bs-nav-link-padding-x);font-size:var(--bs-nav-link-font-size);font-weight:var(--bs-nav-link-font-weight);color:var(--bs-nav-link-color);text-decoration:none;background:0 0;border:0;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out}@media (prefers-reduced-motion:reduce){.nav-link{transition:none}}.nav-link:focus,.nav-link:hover{color:var(--bs-nav-link-hover-color)}.nav-link:focus-visible{outline:0;box-shadow:0 0 0 .25rem rgba(13,110,253,.25)}.nav-link.disabled,.nav-link:disabled{color:var(--bs-nav-link-disabled-color);pointer-events:none;cursor:default}.nav-tabs{--bs-nav-tabs-border-width:var(--bs-border-width);--bs-nav-tabs-border-color:var(--bs-border-color);--bs-nav-tabs-border-radius:var(--bs-border-radius);--bs-nav-tabs-link-hover-border-color:var(--bs-secondary-bg) var(--bs-secondary-bg) var(--bs-border-color);--bs-nav-tabs-link-active-color:var(--bs-emphasis-color);--bs-nav-tabs-link-active-bg:var(--bs-body-bg);--bs-nav-tabs-link-active-border-color:var(--bs-border-color) var(--bs-border-color) var(--bs-body-bg);border-bottom:var(--bs-nav-tabs-border-width) solid var(--bs-nav-tabs-border-color)}.nav-tabs .nav-link{margin-bottom:calc(-1 * var(--bs-nav-tabs-border-width));border:var(--bs-nav-tabs-border-width) solid transparent;border-top-left-radius:var(--bs-nav-tabs-border-radius);border-top-right-radius:var(--bs-nav-tabs-border-radius)}.nav-tabs .nav-link:focus,.nav-tabs .nav-link:hover{isolation:isolate;border-color:var(--bs-nav-tabs-link-hover-border-color)}.nav-tabs .nav-item.show .nav-link,.nav-tabs .nav-link.active{color:var(--bs-nav-tabs-link-active-color);background-color:var(--bs-nav-tabs-link-active-bg);border-color:var(--bs-nav-tabs-link-active-border-color)}.nav-tabs .dropdown-menu{margin-top:calc(-1 * var(--bs-nav-tabs-border-width));border-top-left-radius:0;border-top-right-radius:0}.nav-pills{--bs-nav-pills-border-radius:var(--bs-border-radius);--bs-nav-pills-link-active-color:#fff;--bs-nav-pills-link-active-bg:#0d6efd}.nav-pills .nav-link{border-radius:var(--bs-nav-pills-border-radius)}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:var(--bs-nav-pills-link-active-color);background-color:var(--bs-nav-pills-link-active-bg)}.nav-underline{--bs-nav-underline-gap:1rem;--bs-nav-underline-border-width:0.125rem;--bs-nav-underline-link-active-color:var(--bs-emphasis-color);gap:var(--bs-nav-underline-gap)}.nav-underline .nav-link{padding-right:0;padding-left:0;border-bottom:var(--bs-nav-underline-border-width) solid transparent}.nav-underline .nav-link:focus,.nav-underline .nav-link:hover{border-bottom-color:currentcolor}.nav-underline .nav-link.active,.nav-underline .show>.nav-link{font-weight:700;color:var(--bs-nav-underline-link-active-color);border-bottom-color:currentcolor}.nav-fill .nav-item,.nav-fill>.nav-link{flex:1 1 auto;text-align:center}.nav-justified .nav-item,.nav-justified>.nav-link{flex-grow:1;flex-basis:0;text-align:center}.nav-fill .nav-item .nav-link,.nav-justified .nav-item .nav-link{width:100%}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.navbar{--bs-navbar-padding-x:0;--bs-navbar-padding-y:0.5rem;--bs-navbar-color:rgba(var(--bs-emphasis-color-rgb), 0.65);--bs-navbar-hover-color:rgba(var(--bs-emphasis-color-rgb), 0.8);--bs-navbar-disabled-color:rgba(var(--bs-emphasis-color-rgb), 0.3);--bs-navbar-active-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-brand-padding-y:0.3125rem;--bs-navbar-brand-margin-end:1rem;--bs-navbar-brand-font-size:1.25rem;--bs-navbar-brand-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-brand-hover-color:rgba(var(--bs-emphasis-color-rgb), 1);--bs-navbar-nav-link-padding-x:0.5rem;--bs-navbar-toggler-padding-y:0.25rem;--bs-navbar-toggler-padding-x:0.75rem;--bs-navbar-toggler-font-size:1.25rem;--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%2833, 37, 41, 0.75%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e");--bs-navbar-toggler-border-color:rgba(var(--bs-emphasis-color-rgb), 0.15);--bs-navbar-toggler-border-radius:var(--bs-border-radius);--bs-navbar-toggler-focus-width:0.25rem;--bs-navbar-toggler-transition:box-shadow 0.15s ease-in-out;position:relative;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;padding:var(--bs-navbar-padding-y) var(--bs-navbar-padding-x)}.navbar>.container,.navbar>.container-fluid,.navbar>.container-lg,.navbar>.container-md,.navbar>.container-sm,.navbar>.container-xl,.navbar>.container-xxl{display:flex;flex-wrap:inherit;align-items:center;justify-content:space-between}.navbar-brand{padding-top:var(--bs-navbar-brand-padding-y);padding-bottom:var(--bs-navbar-brand-padding-y);margin-right:var(--bs-navbar-brand-margin-end);font-size:var(--bs-navbar-brand-font-size);color:var(--bs-navbar-brand-color);text-decoration:none;white-space:nowrap}.navbar-brand:focus,.navbar-brand:hover{color:var(--bs-navbar-brand-hover-color)}.navbar-nav{--bs-nav-link-padding-x:0;--bs-nav-link-padding-y:0.5rem;--bs-nav-link-font-weight: ;--bs-nav-link-color:var(--bs-navbar-color);--bs-nav-link-hover-color:var(--bs-navbar-hover-color);--bs-nav-link-disabled-color:var(--bs-navbar-disabled-color);display:flex;flex-direction:column;padding-left:0;margin-bottom:0;list-style:none}.navbar-nav .nav-link.active,.navbar-nav .nav-link.show{color:var(--bs-navbar-active-color)}.navbar-nav .dropdown-menu{position:static}.navbar-text{padding-top:.5rem;padding-bottom:.5rem;color:var(--bs-navbar-color)}.navbar-text a,.navbar-text a:focus,.navbar-text a:hover{color:var(--bs-navbar-active-color)}.navbar-collapse{flex-grow:1;flex-basis:100%;align-items:center}.navbar-toggler{padding:var(--bs-navbar-toggler-padding-y) var(--bs-navbar-toggler-padding-x);font-size:var(--bs-navbar-toggler-font-size);line-height:1;color:var(--bs-navbar-color);background-color:transparent;border:var(--bs-border-width) solid var(--bs-navbar-toggler-border-color);border-radius:var(--bs-navbar-toggler-border-radius);transition:var(--bs-navbar-toggler-transition)}@media (prefers-reduced-motion:reduce){.navbar-toggler{transition:none}}.navbar-toggler:hover{text-decoration:none}.navbar-toggler:focus{text-decoration:none;outline:0;box-shadow:0 0 0 var(--bs-navbar-toggler-focus-width)}.navbar-toggler-icon{display:inline-block;width:1.5em;height:1.5em;vertical-align:middle;background-image:var(--bs-navbar-toggler-icon-bg);background-repeat:no-repeat;background-position:center;background-size:100%}.navbar-nav-scroll{max-height:var(--bs-scroll-height,75vh);overflow-y:auto}@media (min-width:576px){.navbar-expand-sm{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-sm .navbar-nav{flex-direction:row}.navbar-expand-sm .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-sm .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-sm .navbar-nav-scroll{overflow:visible}.navbar-expand-sm .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-sm .navbar-toggler{display:none}.navbar-expand-sm .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-sm .offcanvas .offcanvas-header{display:none}.navbar-expand-sm .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:768px){.navbar-expand-md{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-md .navbar-nav{flex-direction:row}.navbar-expand-md .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-md .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-md .navbar-nav-scroll{overflow:visible}.navbar-expand-md .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-md .navbar-toggler{display:none}.navbar-expand-md .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-md .offcanvas .offcanvas-header{display:none}.navbar-expand-md .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:992px){.navbar-expand-lg{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-lg .navbar-nav{flex-direction:row}.navbar-expand-lg .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-lg .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-lg .navbar-nav-scroll{overflow:visible}.navbar-expand-lg .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-lg .navbar-toggler{display:none}.navbar-expand-lg .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-lg .offcanvas .offcanvas-header{display:none}.navbar-expand-lg .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1200px){.navbar-expand-xl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xl .navbar-nav{flex-direction:row}.navbar-expand-xl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xl .navbar-nav-scroll{overflow:visible}.navbar-expand-xl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xl .navbar-toggler{display:none}.navbar-expand-xl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-xl .offcanvas .offcanvas-header{display:none}.navbar-expand-xl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}@media (min-width:1400px){.navbar-expand-xxl{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand-xxl .navbar-nav{flex-direction:row}.navbar-expand-xxl .navbar-nav .dropdown-menu{position:absolute}.navbar-expand-xxl .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand-xxl .navbar-nav-scroll{overflow:visible}.navbar-expand-xxl .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand-xxl .navbar-toggler{display:none}.navbar-expand-xxl .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand-xxl .offcanvas .offcanvas-header{display:none}.navbar-expand-xxl .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}}.navbar-expand{flex-wrap:nowrap;justify-content:flex-start}.navbar-expand .navbar-nav{flex-direction:row}.navbar-expand .navbar-nav .dropdown-menu{position:absolute}.navbar-expand .navbar-nav .nav-link{padding-right:var(--bs-navbar-nav-link-padding-x);padding-left:var(--bs-navbar-nav-link-padding-x)}.navbar-expand .navbar-nav-scroll{overflow:visible}.navbar-expand .navbar-collapse{display:flex!important;flex-basis:auto}.navbar-expand .navbar-toggler{display:none}.navbar-expand .offcanvas{position:static;z-index:auto;flex-grow:1;width:auto!important;height:auto!important;visibility:visible!important;background-color:transparent!important;border:0!important;transform:none!important;transition:none}.navbar-expand .offcanvas .offcanvas-header{display:none}.navbar-expand .offcanvas .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible}.navbar-dark,.navbar[data-bs-theme=dark]{--bs-navbar-color:rgba(255, 255, 255, 0.55);--bs-navbar-hover-color:rgba(255, 255, 255, 0.75);--bs-navbar-disabled-color:rgba(255, 255, 255, 0.25);--bs-navbar-active-color:#fff;--bs-navbar-brand-color:#fff;--bs-navbar-brand-hover-color:#fff;--bs-navbar-toggler-border-color:rgba(255, 255, 255, 0.1);--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}[data-bs-theme=dark] .navbar-toggler-icon{--bs-navbar-toggler-icon-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e")}.card{--bs-card-spacer-y:1rem;--bs-card-spacer-x:1rem;--bs-card-title-spacer-y:0.5rem;--bs-card-title-color: ;--bs-card-subtitle-color: ;--bs-card-border-width:var(--bs-border-width);--bs-card-border-color:var(--bs-border-color-translucent);--bs-card-border-radius:var(--bs-border-radius);--bs-card-box-shadow: ;--bs-card-inner-border-radius:calc(var(--bs-border-radius) - (var(--bs-border-width)));--bs-card-cap-padding-y:0.5rem;--bs-card-cap-padding-x:1rem;--bs-card-cap-bg:rgba(var(--bs-body-color-rgb), 0.03);--bs-card-cap-color: ;--bs-card-height: ;--bs-card-color: ;--bs-card-bg:var(--bs-body-bg);--bs-card-img-overlay-padding:1rem;--bs-card-group-margin:0.75rem;position:relative;display:flex;flex-direction:column;min-width:0;height:var(--bs-card-height);color:var(--bs-body-color);word-wrap:break-word;background-color:var(--bs-card-bg);background-clip:border-box;border:var(--bs-card-border-width) solid var(--bs-card-border-color);border-radius:var(--bs-card-border-radius)}.card>hr{margin-right:0;margin-left:0}.card>.list-group{border-top:inherit;border-bottom:inherit}.card>.list-group:first-child{border-top-width:0;border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card>.list-group:last-child{border-bottom-width:0;border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card>.card-header+.list-group,.card>.list-group+.card-footer{border-top:0}.card-body{flex:1 1 auto;padding:var(--bs-card-spacer-y) var(--bs-card-spacer-x);color:var(--bs-card-color)}.card-title{margin-bottom:var(--bs-card-title-spacer-y);color:var(--bs-card-title-color)}.card-subtitle{margin-top:calc(-.5 * var(--bs-card-title-spacer-y));margin-bottom:0;color:var(--bs-card-subtitle-color)}.card-text:last-child{margin-bottom:0}.card-link+.card-link{margin-left:var(--bs-card-spacer-x)}.card-header{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);margin-bottom:0;color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-bottom:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-header:first-child{border-radius:var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius) 0 0}.card-footer{padding:var(--bs-card-cap-padding-y) var(--bs-card-cap-padding-x);color:var(--bs-card-cap-color);background-color:var(--bs-card-cap-bg);border-top:var(--bs-card-border-width) solid var(--bs-card-border-color)}.card-footer:last-child{border-radius:0 0 var(--bs-card-inner-border-radius) var(--bs-card-inner-border-radius)}.card-header-tabs{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-bottom:calc(-1 * var(--bs-card-cap-padding-y));margin-left:calc(-.5 * var(--bs-card-cap-padding-x));border-bottom:0}.card-header-tabs .nav-link.active{background-color:var(--bs-card-bg);border-bottom-color:var(--bs-card-bg)}.card-header-pills{margin-right:calc(-.5 * var(--bs-card-cap-padding-x));margin-left:calc(-.5 * var(--bs-card-cap-padding-x))}.card-img-overlay{position:absolute;top:0;right:0;bottom:0;left:0;padding:var(--bs-card-img-overlay-padding);border-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom,.card-img-top{width:100%}.card-img,.card-img-top{border-top-left-radius:var(--bs-card-inner-border-radius);border-top-right-radius:var(--bs-card-inner-border-radius)}.card-img,.card-img-bottom{border-bottom-right-radius:var(--bs-card-inner-border-radius);border-bottom-left-radius:var(--bs-card-inner-border-radius)}.card-group>.card{margin-bottom:var(--bs-card-group-margin)}@media (min-width:576px){.card-group{display:flex;flex-flow:row wrap}.card-group>.card{flex:1 0 0;margin-bottom:0}.card-group>.card+.card{margin-left:0;border-left:0}.card-group>.card:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.card-group>.card:not(:last-child)>.card-header,.card-group>.card:not(:last-child)>.card-img-top{border-top-right-radius:0}.card-group>.card:not(:last-child)>.card-footer,.card-group>.card:not(:last-child)>.card-img-bottom{border-bottom-right-radius:0}.card-group>.card:not(:first-child){border-top-left-radius:0;border-bottom-left-radius:0}.card-group>.card:not(:first-child)>.card-header,.card-group>.card:not(:first-child)>.card-img-top{border-top-left-radius:0}.card-group>.card:not(:first-child)>.card-footer,.card-group>.card:not(:first-child)>.card-img-bottom{border-bottom-left-radius:0}}.accordion{--bs-accordion-color:var(--bs-body-color);--bs-accordion-bg:var(--bs-body-bg);--bs-accordion-transition:color 0.15s ease-in-out,background-color 0.15s ease-in-out,border-color 0.15s ease-in-out,box-shadow 0.15s ease-in-out,border-radius 0.15s ease;--bs-accordion-border-color:var(--bs-border-color);--bs-accordion-border-width:var(--bs-border-width);--bs-accordion-border-radius:var(--bs-border-radius);--bs-accordion-inner-border-radius:calc(var(--bs-border-radius) - (var(--bs-border-width)));--bs-accordion-btn-padding-x:1.25rem;--bs-accordion-btn-padding-y:1rem;--bs-accordion-btn-color:var(--bs-body-color);--bs-accordion-btn-bg:var(--bs-accordion-bg);--bs-accordion-btn-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23212529' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='m2 5 6 6 6-6'/%3e%3c/svg%3e");--bs-accordion-btn-icon-width:1.25rem;--bs-accordion-btn-icon-transform:rotate(-180deg);--bs-accordion-btn-icon-transition:transform 0.2s ease-in-out;--bs-accordion-btn-active-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23052c65' stroke-linecap='round' stroke-linejoin='round'%3e%3cpath d='m2 5 6 6 6-6'/%3e%3c/svg%3e");--bs-accordion-btn-focus-box-shadow:0 0 0 0.25rem rgba(13, 110, 253, 0.25);--bs-accordion-body-padding-x:1.25rem;--bs-accordion-body-padding-y:1rem;--bs-accordion-active-color:var(--bs-primary-text-emphasis);--bs-accordion-active-bg:var(--bs-primary-bg-subtle)}.accordion-button{position:relative;display:flex;align-items:center;width:100%;padding:var(--bs-accordion-btn-padding-y) var(--bs-accordion-btn-padding-x);font-size:1rem;color:var(--bs-accordion-btn-color);text-align:left;background-color:var(--bs-accordion-btn-bg);border:0;border-radius:0;overflow-anchor:none;transition:var(--bs-accordion-transition)}@media (prefers-reduced-motion:reduce){.accordion-button{transition:none}}.accordion-button:not(.collapsed){color:var(--bs-accordion-active-color);background-color:var(--bs-accordion-active-bg);box-shadow:inset 0 calc(-1 * var(--bs-accordion-border-width)) 0 var(--bs-accordion-border-color)}.accordion-button:not(.collapsed)::after{background-image:var(--bs-accordion-btn-active-icon);transform:var(--bs-accordion-btn-icon-transform)}.accordion-button::after{flex-shrink:0;width:var(--bs-accordion-btn-icon-width);height:var(--bs-accordion-btn-icon-width);margin-left:auto;content:"";background-image:var(--bs-accordion-btn-icon);background-repeat:no-repeat;background-size:var(--bs-accordion-btn-icon-width);transition:var(--bs-accordion-btn-icon-transition)}@media (prefers-reduced-motion:reduce){.accordion-button::after{transition:none}}.accordion-button:hover{z-index:2}.accordion-button:focus{z-index:3;outline:0;box-shadow:var(--bs-accordion-btn-focus-box-shadow)}.accordion-header{margin-bottom:0}.accordion-item{color:var(--bs-accordion-color);background-color:var(--bs-accordion-bg);border:var(--bs-accordion-border-width) solid var(--bs-accordion-border-color)}.accordion-item:first-of-type{border-top-left-radius:var(--bs-accordion-border-radius);border-top-right-radius:var(--bs-accordion-border-radius)}.accordion-item:first-of-type>.accordion-header .accordion-button{border-top-left-radius:var(--bs-accordion-inner-border-radius);border-top-right-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:not(:first-of-type){border-top:0}.accordion-item:last-of-type{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-item:last-of-type>.accordion-header .accordion-button.collapsed{border-bottom-right-radius:var(--bs-accordion-inner-border-radius);border-bottom-left-radius:var(--bs-accordion-inner-border-radius)}.accordion-item:last-of-type>.accordion-collapse{border-bottom-right-radius:var(--bs-accordion-border-radius);border-bottom-left-radius:var(--bs-accordion-border-radius)}.accordion-body{padding:var(--bs-accordion-body-padding-y) var(--bs-accordion-body-padding-x)}.accordion-flush>.accordion-item{border-right:0;border-left:0;border-radius:0}.accordion-flush>.accordion-item:first-child{border-top:0}.accordion-flush>.accordion-item:last-child{border-bottom:0}.accordion-flush>.accordion-item>.accordion-collapse,.accordion-flush>.accordion-item>.accordion-header .accordion-button,.accordion-flush>.accordion-item>.accordion-header .accordion-button.collapsed{border-radius:0}[data-bs-theme=dark] .accordion-button::after{--bs-accordion-btn-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e");--bs-accordion-btn-active-icon:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%236ea8fe'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e")}.breadcrumb{--bs-breadcrumb-padding-x:0;--bs-breadcrumb-padding-y:0;--bs-breadcrumb-margin-bottom:1rem;--bs-breadcrumb-bg: ;--bs-breadcrumb-border-radius: ;--bs-breadcrumb-divider-color:var(--bs-secondary-color);--bs-breadcrumb-item-padding-x:0.5rem;--bs-breadcrumb-item-active-color:var(--bs-secondary-color);display:flex;flex-wrap:wrap;padding:var(--bs-breadcrumb-padding-y) var(--bs-breadcrumb-padding-x);margin-bottom:var(--bs-breadcrumb-margin-bottom);font-size:var(--bs-breadcrumb-font-size);list-style:none;background-color:var(--bs-breadcrumb-bg);border-radius:var(--bs-breadcrumb-border-radius)}.breadcrumb-item+.breadcrumb-item{padding-left:var(--bs-breadcrumb-item-padding-x)}.breadcrumb-item+.breadcrumb-item::before{float:left;padding-right:var(--bs-breadcrumb-item-padding-x);color:var(--bs-breadcrumb-divider-color);content:var(--bs-breadcrumb-divider, "/")}.breadcrumb-item.active{color:var(--bs-breadcrumb-item-active-color)}.pagination{--bs-pagination-padding-x:0.75rem;--bs-pagination-padding-y:0.375rem;--bs-pagination-font-size:1rem;--bs-pagination-color:var(--bs-link-color);--bs-pagination-bg:var(--bs-body-bg);--bs-pagination-border-width:var(--bs-border-width);--bs-pagination-border-color:var(--bs-border-color);--bs-pagination-border-radius:var(--bs-border-radius);--bs-pagination-hover-color:var(--bs-link-hover-color);--bs-pagination-hover-bg:var(--bs-tertiary-bg);--bs-pagination-hover-border-color:var(--bs-border-color);--bs-pagination-focus-color:var(--bs-link-hover-color);--bs-pagination-focus-bg:var(--bs-secondary-bg);--bs-pagination-focus-box-shadow:0 0 0 0.25rem rgba(13, 110, 253, 0.25);--bs-pagination-active-color:#fff;--bs-pagination-active-bg:#0d6efd;--bs-pagination-active-border-color:#0d6efd;--bs-pagination-disabled-color:var(--bs-secondary-color);--bs-pagination-disabled-bg:var(--bs-secondary-bg);--bs-pagination-disabled-border-color:var(--bs-border-color);display:flex;padding-left:0;list-style:none}.page-link{position:relative;display:block;padding:var(--bs-pagination-padding-y) var(--bs-pagination-padding-x);font-size:var(--bs-pagination-font-size);color:var(--bs-pagination-color);text-decoration:none;background-color:var(--bs-pagination-bg);border:var(--bs-pagination-border-width) solid var(--bs-pagination-border-color);transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}@media (prefers-reduced-motion:reduce){.page-link{transition:none}}.page-link:hover{z-index:2;color:var(--bs-pagination-hover-color);background-color:var(--bs-pagination-hover-bg);border-color:var(--bs-pagination-hover-border-color)}.page-link:focus{z-index:3;color:var(--bs-pagination-focus-color);background-color:var(--bs-pagination-focus-bg);outline:0;box-shadow:var(--bs-pagination-focus-box-shadow)}.active>.page-link,.page-link.active{z-index:3;color:var(--bs-pagination-active-color);background-color:var(--bs-pagination-active-bg);border-color:var(--bs-pagination-active-border-color)}.disabled>.page-link,.page-link.disabled{color:var(--bs-pagination-disabled-color);pointer-events:none;background-color:var(--bs-pagination-disabled-bg);border-color:var(--bs-pagination-disabled-border-color)}.page-item:not(:first-child) .page-link{margin-left:calc(-1 * var(--bs-border-width))}.page-item:first-child .page-link{border-top-left-radius:var(--bs-pagination-border-radius);border-bottom-left-radius:var(--bs-pagination-border-radius)}.page-item:last-child .page-link{border-top-right-radius:var(--bs-pagination-border-radius);border-bottom-right-radius:var(--bs-pagination-border-radius)}.pagination-lg{--bs-pagination-padding-x:1.5rem;--bs-pagination-padding-y:0.75rem;--bs-pagination-font-size:1.25rem;--bs-pagination-border-radius:var(--bs-border-radius-lg)}.pagination-sm{--bs-pagination-padding-x:0.5rem;--bs-pagination-padding-y:0.25rem;--bs-pagination-font-size:0.875rem;--bs-pagination-border-radius:var(--bs-border-radius-sm)}.badge{--bs-badge-padding-x:0.65em;--bs-badge-padding-y:0.35em;--bs-badge-font-size:0.75em;--bs-badge-font-weight:700;--bs-badge-color:#fff;--bs-badge-border-radius:var(--bs-border-radius);display:inline-block;padding:var(--bs-badge-padding-y) var(--bs-badge-padding-x);font-size:var(--bs-badge-font-size);font-weight:var(--bs-badge-font-weight);line-height:1;color:var(--bs-badge-color);text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:var(--bs-badge-border-radius)}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.alert{--bs-alert-bg:transparent;--bs-alert-padding-x:1rem;--bs-alert-padding-y:1rem;--bs-alert-margin-bottom:1rem;--bs-alert-color:inherit;--bs-alert-border-color:transparent;--bs-alert-border:var(--bs-border-width) solid var(--bs-alert-border-color);--bs-alert-border-radius:var(--bs-border-radius);--bs-alert-link-color:inherit;position:relative;padding:var(--bs-alert-padding-y) var(--bs-alert-padding-x);margin-bottom:var(--bs-alert-margin-bottom);color:var(--bs-alert-color);background-color:var(--bs-alert-bg);border:var(--bs-alert-border);border-radius:var(--bs-alert-border-radius)}.alert-heading{color:inherit}.alert-link{font-weight:700;color:var(--bs-alert-link-color)}.alert-dismissible{padding-right:3rem}.alert-dismissible .btn-close{position:absolute;top:0;right:0;z-index:2;padding:1.25rem 1rem}.alert-primary{--bs-alert-color:var(--bs-primary-text-emphasis);--bs-alert-bg:var(--bs-primary-bg-subtle);--bs-alert-border-color:var(--bs-primary-border-subtle);--bs-alert-link-color:var(--bs-primary-text-emphasis)}.alert-secondary{--bs-alert-color:var(--bs-secondary-text-emphasis);--bs-alert-bg:var(--bs-secondary-bg-subtle);--bs-alert-border-color:var(--bs-secondary-border-subtle);--bs-alert-link-color:var(--bs-secondary-text-emphasis)}.alert-success{--bs-alert-color:var(--bs-success-text-emphasis);--bs-alert-bg:var(--bs-success-bg-subtle);--bs-alert-border-color:var(--bs-success-border-subtle);--bs-alert-link-color:var(--bs-success-text-emphasis)}.alert-info{--bs-alert-color:var(--bs-info-text-emphasis);--bs-alert-bg:var(--bs-info-bg-subtle);--bs-alert-border-color:var(--bs-info-border-subtle);--bs-alert-link-color:var(--bs-info-text-emphasis)}.alert-warning{--bs-alert-color:var(--bs-warning-text-emphasis);--bs-alert-bg:var(--bs-warning-bg-subtle);--bs-alert-border-color:var(--bs-warning-border-subtle);--bs-alert-link-color:var(--bs-warning-text-emphasis)}.alert-danger{--bs-alert-color:var(--bs-danger-text-emphasis);--bs-alert-bg:var(--bs-danger-bg-subtle);--bs-alert-border-color:var(--bs-danger-border-subtle);--bs-alert-link-color:var(--bs-danger-text-emphasis)}.alert-light{--bs-alert-color:var(--bs-light-text-emphasis);--bs-alert-bg:var(--bs-light-bg-subtle);--bs-alert-border-color:var(--bs-light-border-subtle);--bs-alert-link-color:var(--bs-light-text-emphasis)}.alert-dark{--bs-alert-color:var(--bs-dark-text-emphasis);--bs-alert-bg:var(--bs-dark-bg-subtle);--bs-alert-border-color:var(--bs-dark-border-subtle);--bs-alert-link-color:var(--bs-dark-text-emphasis)}@keyframes progress-bar-stripes{0%{background-position-x:var(--bs-progress-height)}}.progress,.progress-stacked{--bs-progress-height:1rem;--bs-progress-font-size:0.75rem;--bs-progress-bg:var(--bs-secondary-bg);--bs-progress-border-radius:var(--bs-border-radius);--bs-progress-box-shadow:var(--bs-box-shadow-inset);--bs-progress-bar-color:#fff;--bs-progress-bar-bg:#0d6efd;--bs-progress-bar-transition:width 0.6s ease;display:flex;height:var(--bs-progress-height);overflow:hidden;font-size:var(--bs-progress-font-size);background-color:var(--bs-progress-bg);border-radius:var(--bs-progress-border-radius)}.progress-bar{display:flex;flex-direction:column;justify-content:center;overflow:hidden;color:var(--bs-progress-bar-color);text-align:center;white-space:nowrap;background-color:var(--bs-progress-bar-bg);transition:var(--bs-progress-bar-transition)}@media (prefers-reduced-motion:reduce){.progress-bar{transition:none}}.progress-bar-striped{background-image:linear-gradient(45deg,rgba(255,255,255,.15) 25%,transparent 25%,transparent 50%,rgba(255,255,255,.15) 50%,rgba(255,255,255,.15) 75%,transparent 75%,transparent);background-size:var(--bs-progress-height) var(--bs-progress-height)}.progress-stacked>.progress{overflow:visible}.progress-stacked>.progress>.progress-bar{width:100%}.progress-bar-animated{animation:1s linear infinite progress-bar-stripes}@media (prefers-reduced-motion:reduce){.progress-bar-animated{animation:none}}.list-group{--bs-list-group-color:var(--bs-body-color);--bs-list-group-bg:var(--bs-body-bg);--bs-list-group-border-color:var(--bs-border-color);--bs-list-group-border-width:var(--bs-border-width);--bs-list-group-border-radius:var(--bs-border-radius);--bs-list-group-item-padding-x:1rem;--bs-list-group-item-padding-y:0.5rem;--bs-list-group-action-color:var(--bs-secondary-color);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-tertiary-bg);--bs-list-group-action-active-color:var(--bs-body-color);--bs-list-group-action-active-bg:var(--bs-secondary-bg);--bs-list-group-disabled-color:var(--bs-secondary-color);--bs-list-group-disabled-bg:var(--bs-body-bg);--bs-list-group-active-color:#fff;--bs-list-group-active-bg:#0d6efd;--bs-list-group-active-border-color:#0d6efd;display:flex;flex-direction:column;padding-left:0;margin-bottom:0;border-radius:var(--bs-list-group-border-radius)}.list-group-numbered{list-style-type:none;counter-reset:section}.list-group-numbered>.list-group-item::before{content:counters(section, ".") ". ";counter-increment:section}.list-group-item{position:relative;display:block;padding:var(--bs-list-group-item-padding-y) var(--bs-list-group-item-padding-x);color:var(--bs-list-group-color);text-decoration:none;background-color:var(--bs-list-group-bg);border:var(--bs-list-group-border-width) solid var(--bs-list-group-border-color)}.list-group-item:first-child{border-top-left-radius:inherit;border-top-right-radius:inherit}.list-group-item:last-child{border-bottom-right-radius:inherit;border-bottom-left-radius:inherit}.list-group-item.disabled,.list-group-item:disabled{color:var(--bs-list-group-disabled-color);pointer-events:none;background-color:var(--bs-list-group-disabled-bg)}.list-group-item.active{z-index:2;color:var(--bs-list-group-active-color);background-color:var(--bs-list-group-active-bg);border-color:var(--bs-list-group-active-border-color)}.list-group-item+.list-group-item{border-top-width:0}.list-group-item+.list-group-item.active{margin-top:calc(-1 * var(--bs-list-group-border-width));border-top-width:var(--bs-list-group-border-width)}.list-group-item-action{width:100%;color:var(--bs-list-group-action-color);text-align:inherit}.list-group-item-action:not(.active):focus,.list-group-item-action:not(.active):hover{z-index:1;color:var(--bs-list-group-action-hover-color);text-decoration:none;background-color:var(--bs-list-group-action-hover-bg)}.list-group-item-action:not(.active):active{color:var(--bs-list-group-action-active-color);background-color:var(--bs-list-group-action-active-bg)}.list-group-horizontal{flex-direction:row}.list-group-horizontal>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal>.list-group-item.active{margin-top:0}.list-group-horizontal>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}@media (min-width:576px){.list-group-horizontal-sm{flex-direction:row}.list-group-horizontal-sm>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-sm>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-sm>.list-group-item.active{margin-top:0}.list-group-horizontal-sm>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-sm>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:768px){.list-group-horizontal-md{flex-direction:row}.list-group-horizontal-md>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-md>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-md>.list-group-item.active{margin-top:0}.list-group-horizontal-md>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-md>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:992px){.list-group-horizontal-lg{flex-direction:row}.list-group-horizontal-lg>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-lg>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-lg>.list-group-item.active{margin-top:0}.list-group-horizontal-lg>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-lg>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:1200px){.list-group-horizontal-xl{flex-direction:row}.list-group-horizontal-xl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xl>.list-group-item.active{margin-top:0}.list-group-horizontal-xl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}@media (min-width:1400px){.list-group-horizontal-xxl{flex-direction:row}.list-group-horizontal-xxl>.list-group-item:first-child:not(:last-child){border-bottom-left-radius:var(--bs-list-group-border-radius);border-top-right-radius:0}.list-group-horizontal-xxl>.list-group-item:last-child:not(:first-child){border-top-right-radius:var(--bs-list-group-border-radius);border-bottom-left-radius:0}.list-group-horizontal-xxl>.list-group-item.active{margin-top:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item{border-top-width:var(--bs-list-group-border-width);border-left-width:0}.list-group-horizontal-xxl>.list-group-item+.list-group-item.active{margin-left:calc(-1 * var(--bs-list-group-border-width));border-left-width:var(--bs-list-group-border-width)}}.list-group-flush{border-radius:0}.list-group-flush>.list-group-item{border-width:0 0 var(--bs-list-group-border-width)}.list-group-flush>.list-group-item:last-child{border-bottom-width:0}.list-group-item-primary{--bs-list-group-color:var(--bs-primary-text-emphasis);--bs-list-group-bg:var(--bs-primary-bg-subtle);--bs-list-group-border-color:var(--bs-primary-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-primary-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-primary-border-subtle);--bs-list-group-active-color:var(--bs-primary-bg-subtle);--bs-list-group-active-bg:var(--bs-primary-text-emphasis);--bs-list-group-active-border-color:var(--bs-primary-text-emphasis)}.list-group-item-secondary{--bs-list-group-color:var(--bs-secondary-text-emphasis);--bs-list-group-bg:var(--bs-secondary-bg-subtle);--bs-list-group-border-color:var(--bs-secondary-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-secondary-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-secondary-border-subtle);--bs-list-group-active-color:var(--bs-secondary-bg-subtle);--bs-list-group-active-bg:var(--bs-secondary-text-emphasis);--bs-list-group-active-border-color:var(--bs-secondary-text-emphasis)}.list-group-item-success{--bs-list-group-color:var(--bs-success-text-emphasis);--bs-list-group-bg:var(--bs-success-bg-subtle);--bs-list-group-border-color:var(--bs-success-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-success-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-success-border-subtle);--bs-list-group-active-color:var(--bs-success-bg-subtle);--bs-list-group-active-bg:var(--bs-success-text-emphasis);--bs-list-group-active-border-color:var(--bs-success-text-emphasis)}.list-group-item-info{--bs-list-group-color:var(--bs-info-text-emphasis);--bs-list-group-bg:var(--bs-info-bg-subtle);--bs-list-group-border-color:var(--bs-info-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-info-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-info-border-subtle);--bs-list-group-active-color:var(--bs-info-bg-subtle);--bs-list-group-active-bg:var(--bs-info-text-emphasis);--bs-list-group-active-border-color:var(--bs-info-text-emphasis)}.list-group-item-warning{--bs-list-group-color:var(--bs-warning-text-emphasis);--bs-list-group-bg:var(--bs-warning-bg-subtle);--bs-list-group-border-color:var(--bs-warning-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-warning-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-warning-border-subtle);--bs-list-group-active-color:var(--bs-warning-bg-subtle);--bs-list-group-active-bg:var(--bs-warning-text-emphasis);--bs-list-group-active-border-color:var(--bs-warning-text-emphasis)}.list-group-item-danger{--bs-list-group-color:var(--bs-danger-text-emphasis);--bs-list-group-bg:var(--bs-danger-bg-subtle);--bs-list-group-border-color:var(--bs-danger-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-danger-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-danger-border-subtle);--bs-list-group-active-color:var(--bs-danger-bg-subtle);--bs-list-group-active-bg:var(--bs-danger-text-emphasis);--bs-list-group-active-border-color:var(--bs-danger-text-emphasis)}.list-group-item-light{--bs-list-group-color:var(--bs-light-text-emphasis);--bs-list-group-bg:var(--bs-light-bg-subtle);--bs-list-group-border-color:var(--bs-light-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-light-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-light-border-subtle);--bs-list-group-active-color:var(--bs-light-bg-subtle);--bs-list-group-active-bg:var(--bs-light-text-emphasis);--bs-list-group-active-border-color:var(--bs-light-text-emphasis)}.list-group-item-dark{--bs-list-group-color:var(--bs-dark-text-emphasis);--bs-list-group-bg:var(--bs-dark-bg-subtle);--bs-list-group-border-color:var(--bs-dark-border-subtle);--bs-list-group-action-hover-color:var(--bs-emphasis-color);--bs-list-group-action-hover-bg:var(--bs-dark-border-subtle);--bs-list-group-action-active-color:var(--bs-emphasis-color);--bs-list-group-action-active-bg:var(--bs-dark-border-subtle);--bs-list-group-active-color:var(--bs-dark-bg-subtle);--bs-list-group-active-bg:var(--bs-dark-text-emphasis);--bs-list-group-active-border-color:var(--bs-dark-text-emphasis)}.btn-close{--bs-btn-close-color:#000;--bs-btn-close-bg:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23000'%3e%3cpath d='M.293.293a1 1 0 0 1 1.414 0L8 6.586 14.293.293a1 1 0 1 1 1.414 1.414L9.414 8l6.293 6.293a1 1 0 0 1-1.414 1.414L8 9.414l-6.293 6.293a1 1 0 0 1-1.414-1.414L6.586 8 .293 1.707a1 1 0 0 1 0-1.414'/%3e%3c/svg%3e");--bs-btn-close-opacity:0.5;--bs-btn-close-hover-opacity:0.75;--bs-btn-close-focus-shadow:0 0 0 0.25rem rgba(13, 110, 253, 0.25);--bs-btn-close-focus-opacity:1;--bs-btn-close-disabled-opacity:0.25;box-sizing:content-box;width:1em;height:1em;padding:.25em .25em;color:var(--bs-btn-close-color);background:transparent var(--bs-btn-close-bg) center/1em auto no-repeat;filter:var(--bs-btn-close-filter);border:0;border-radius:.375rem;opacity:var(--bs-btn-close-opacity)}.btn-close:hover{color:var(--bs-btn-close-color);text-decoration:none;opacity:var(--bs-btn-close-hover-opacity)}.btn-close:focus{outline:0;box-shadow:var(--bs-btn-close-focus-shadow);opacity:var(--bs-btn-close-focus-opacity)}.btn-close.disabled,.btn-close:disabled{pointer-events:none;-webkit-user-select:none;-moz-user-select:none;user-select:none;opacity:var(--bs-btn-close-disabled-opacity)}.btn-close-white{--bs-btn-close-filter:invert(1) grayscale(100%) brightness(200%)}:root,[data-bs-theme=light]{--bs-btn-close-filter: }[data-bs-theme=dark]{--bs-btn-close-filter:invert(1) grayscale(100%) brightness(200%)}.toast{--bs-toast-zindex:1090;--bs-toast-padding-x:0.75rem;--bs-toast-padding-y:0.5rem;--bs-toast-spacing:1.5rem;--bs-toast-max-width:350px;--bs-toast-font-size:0.875rem;--bs-toast-color: ;--bs-toast-bg:rgba(var(--bs-body-bg-rgb), 0.85);--bs-toast-border-width:var(--bs-border-width);--bs-toast-border-color:var(--bs-border-color-translucent);--bs-toast-border-radius:var(--bs-border-radius);--bs-toast-box-shadow:var(--bs-box-shadow);--bs-toast-header-color:var(--bs-secondary-color);--bs-toast-header-bg:rgba(var(--bs-body-bg-rgb), 0.85);--bs-toast-header-border-color:var(--bs-border-color-translucent);width:var(--bs-toast-max-width);max-width:100%;font-size:var(--bs-toast-font-size);color:var(--bs-toast-color);pointer-events:auto;background-color:var(--bs-toast-bg);background-clip:padding-box;border:var(--bs-toast-border-width) solid var(--bs-toast-border-color);box-shadow:var(--bs-toast-box-shadow);border-radius:var(--bs-toast-border-radius)}.toast.showing{opacity:0}.toast:not(.show){display:none}.toast-container{--bs-toast-zindex:1090;position:absolute;z-index:var(--bs-toast-zindex);width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:100%;pointer-events:none}.toast-container>:not(:last-child){margin-bottom:var(--bs-toast-spacing)}.toast-header{display:flex;align-items:center;padding:var(--bs-toast-padding-y) var(--bs-toast-padding-x);color:var(--bs-toast-header-color);background-color:var(--bs-toast-header-bg);background-clip:padding-box;border-bottom:var(--bs-toast-border-width) solid var(--bs-toast-header-border-color);border-top-left-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width));border-top-right-radius:calc(var(--bs-toast-border-radius) - var(--bs-toast-border-width))}.toast-header .btn-close{margin-right:calc(-.5 * var(--bs-toast-padding-x));margin-left:var(--bs-toast-padding-x)}.toast-body{padding:var(--bs-toast-padding-x);word-wrap:break-word}.modal{--bs-modal-zindex:1055;--bs-modal-width:500px;--bs-modal-padding:1rem;--bs-modal-margin:0.5rem;--bs-modal-color:var(--bs-body-color);--bs-modal-bg:var(--bs-body-bg);--bs-modal-border-color:var(--bs-border-color-translucent);--bs-modal-border-width:var(--bs-border-width);--bs-modal-border-radius:var(--bs-border-radius-lg);--bs-modal-box-shadow:var(--bs-box-shadow-sm);--bs-modal-inner-border-radius:calc(var(--bs-border-radius-lg) - (var(--bs-border-width)));--bs-modal-header-padding-x:1rem;--bs-modal-header-padding-y:1rem;--bs-modal-header-padding:1rem 1rem;--bs-modal-header-border-color:var(--bs-border-color);--bs-modal-header-border-width:var(--bs-border-width);--bs-modal-title-line-height:1.5;--bs-modal-footer-gap:0.5rem;--bs-modal-footer-bg: ;--bs-modal-footer-border-color:var(--bs-border-color);--bs-modal-footer-border-width:var(--bs-border-width);position:fixed;top:0;left:0;z-index:var(--bs-modal-zindex);display:none;width:100%;height:100%;overflow-x:hidden;overflow-y:auto;outline:0}.modal-dialog{position:relative;width:auto;margin:var(--bs-modal-margin);pointer-events:none}.modal.fade .modal-dialog{transform:translate(0,-50px);transition:transform .3s ease-out}@media (prefers-reduced-motion:reduce){.modal.fade .modal-dialog{transition:none}}.modal.show .modal-dialog{transform:none}.modal.modal-static .modal-dialog{transform:scale(1.02)}.modal-dialog-scrollable{height:calc(100% - var(--bs-modal-margin) * 2)}.modal-dialog-scrollable .modal-content{max-height:100%;overflow:hidden}.modal-dialog-scrollable .modal-body{overflow-y:auto}.modal-dialog-centered{display:flex;align-items:center;min-height:calc(100% - var(--bs-modal-margin) * 2)}.modal-content{position:relative;display:flex;flex-direction:column;width:100%;color:var(--bs-modal-color);pointer-events:auto;background-color:var(--bs-modal-bg);background-clip:padding-box;border:var(--bs-modal-border-width) solid var(--bs-modal-border-color);border-radius:var(--bs-modal-border-radius);outline:0}.modal-backdrop{--bs-backdrop-zindex:1050;--bs-backdrop-bg:#000;--bs-backdrop-opacity:0.5;position:fixed;top:0;left:0;z-index:var(--bs-backdrop-zindex);width:100vw;height:100vh;background-color:var(--bs-backdrop-bg)}.modal-backdrop.fade{opacity:0}.modal-backdrop.show{opacity:var(--bs-backdrop-opacity)}.modal-header{display:flex;flex-shrink:0;align-items:center;padding:var(--bs-modal-header-padding);border-bottom:var(--bs-modal-header-border-width) solid var(--bs-modal-header-border-color);border-top-left-radius:var(--bs-modal-inner-border-radius);border-top-right-radius:var(--bs-modal-inner-border-radius)}.modal-header .btn-close{padding:calc(var(--bs-modal-header-padding-y) * .5) calc(var(--bs-modal-header-padding-x) * .5);margin-top:calc(-.5 * var(--bs-modal-header-padding-y));margin-right:calc(-.5 * var(--bs-modal-header-padding-x));margin-bottom:calc(-.5 * var(--bs-modal-header-padding-y));margin-left:auto}.modal-title{margin-bottom:0;line-height:var(--bs-modal-title-line-height)}.modal-body{position:relative;flex:1 1 auto;padding:var(--bs-modal-padding)}.modal-footer{display:flex;flex-shrink:0;flex-wrap:wrap;align-items:center;justify-content:flex-end;padding:calc(var(--bs-modal-padding) - var(--bs-modal-footer-gap) * .5);background-color:var(--bs-modal-footer-bg);border-top:var(--bs-modal-footer-border-width) solid var(--bs-modal-footer-border-color);border-bottom-right-radius:var(--bs-modal-inner-border-radius);border-bottom-left-radius:var(--bs-modal-inner-border-radius)}.modal-footer>*{margin:calc(var(--bs-modal-footer-gap) * .5)}@media (min-width:576px){.modal{--bs-modal-margin:1.75rem;--bs-modal-box-shadow:var(--bs-box-shadow)}.modal-dialog{max-width:var(--bs-modal-width);margin-right:auto;margin-left:auto}.modal-sm{--bs-modal-width:300px}}@media (min-width:992px){.modal-lg,.modal-xl{--bs-modal-width:800px}}@media (min-width:1200px){.modal-xl{--bs-modal-width:1140px}}.modal-fullscreen{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen .modal-footer,.modal-fullscreen .modal-header{border-radius:0}.modal-fullscreen .modal-body{overflow-y:auto}@media (max-width:575.98px){.modal-fullscreen-sm-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-sm-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-sm-down .modal-footer,.modal-fullscreen-sm-down .modal-header{border-radius:0}.modal-fullscreen-sm-down .modal-body{overflow-y:auto}}@media (max-width:767.98px){.modal-fullscreen-md-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-md-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-md-down .modal-footer,.modal-fullscreen-md-down .modal-header{border-radius:0}.modal-fullscreen-md-down .modal-body{overflow-y:auto}}@media (max-width:991.98px){.modal-fullscreen-lg-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-lg-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-lg-down .modal-footer,.modal-fullscreen-lg-down .modal-header{border-radius:0}.modal-fullscreen-lg-down .modal-body{overflow-y:auto}}@media (max-width:1199.98px){.modal-fullscreen-xl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xl-down .modal-footer,.modal-fullscreen-xl-down .modal-header{border-radius:0}.modal-fullscreen-xl-down .modal-body{overflow-y:auto}}@media (max-width:1399.98px){.modal-fullscreen-xxl-down{width:100vw;max-width:none;height:100%;margin:0}.modal-fullscreen-xxl-down .modal-content{height:100%;border:0;border-radius:0}.modal-fullscreen-xxl-down .modal-footer,.modal-fullscreen-xxl-down .modal-header{border-radius:0}.modal-fullscreen-xxl-down .modal-body{overflow-y:auto}}.tooltip{--bs-tooltip-zindex:1080;--bs-tooltip-max-width:200px;--bs-tooltip-padding-x:0.5rem;--bs-tooltip-padding-y:0.25rem;--bs-tooltip-margin: ;--bs-tooltip-font-size:0.875rem;--bs-tooltip-color:var(--bs-body-bg);--bs-tooltip-bg:var(--bs-emphasis-color);--bs-tooltip-border-radius:var(--bs-border-radius);--bs-tooltip-opacity:0.9;--bs-tooltip-arrow-width:0.8rem;--bs-tooltip-arrow-height:0.4rem;z-index:var(--bs-tooltip-zindex);display:block;margin:var(--bs-tooltip-margin);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-tooltip-font-size);word-wrap:break-word;opacity:0}.tooltip.show{opacity:var(--bs-tooltip-opacity)}.tooltip .tooltip-arrow{display:block;width:var(--bs-tooltip-arrow-width);height:var(--bs-tooltip-arrow-height)}.tooltip .tooltip-arrow::before{position:absolute;content:"";border-color:transparent;border-style:solid}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow,.bs-tooltip-top .tooltip-arrow{bottom:calc(-1 * var(--bs-tooltip-arrow-height))}.bs-tooltip-auto[data-popper-placement^=top] .tooltip-arrow::before,.bs-tooltip-top .tooltip-arrow::before{top:-1px;border-width:var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;border-top-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow,.bs-tooltip-end .tooltip-arrow{left:calc(-1 * var(--bs-tooltip-arrow-height));width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=right] .tooltip-arrow::before,.bs-tooltip-end .tooltip-arrow::before{right:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height) calc(var(--bs-tooltip-arrow-width) * .5) 0;border-right-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow,.bs-tooltip-bottom .tooltip-arrow{top:calc(-1 * var(--bs-tooltip-arrow-height))}.bs-tooltip-auto[data-popper-placement^=bottom] .tooltip-arrow::before,.bs-tooltip-bottom .tooltip-arrow::before{bottom:-1px;border-width:0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);border-bottom-color:var(--bs-tooltip-bg)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow,.bs-tooltip-start .tooltip-arrow{right:calc(-1 * var(--bs-tooltip-arrow-height));width:var(--bs-tooltip-arrow-height);height:var(--bs-tooltip-arrow-width)}.bs-tooltip-auto[data-popper-placement^=left] .tooltip-arrow::before,.bs-tooltip-start .tooltip-arrow::before{left:-1px;border-width:calc(var(--bs-tooltip-arrow-width) * .5) 0 calc(var(--bs-tooltip-arrow-width) * .5) var(--bs-tooltip-arrow-height);border-left-color:var(--bs-tooltip-bg)}.tooltip-inner{max-width:var(--bs-tooltip-max-width);padding:var(--bs-tooltip-padding-y) var(--bs-tooltip-padding-x);color:var(--bs-tooltip-color);text-align:center;background-color:var(--bs-tooltip-bg);border-radius:var(--bs-tooltip-border-radius)}.popover{--bs-popover-zindex:1070;--bs-popover-max-width:276px;--bs-popover-font-size:0.875rem;--bs-popover-bg:var(--bs-body-bg);--bs-popover-border-width:var(--bs-border-width);--bs-popover-border-color:var(--bs-border-color-translucent);--bs-popover-border-radius:var(--bs-border-radius-lg);--bs-popover-inner-border-radius:calc(var(--bs-border-radius-lg) - var(--bs-border-width));--bs-popover-box-shadow:var(--bs-box-shadow);--bs-popover-header-padding-x:1rem;--bs-popover-header-padding-y:0.5rem;--bs-popover-header-font-size:1rem;--bs-popover-header-color:inherit;--bs-popover-header-bg:var(--bs-secondary-bg);--bs-popover-body-padding-x:1rem;--bs-popover-body-padding-y:1rem;--bs-popover-body-color:var(--bs-body-color);--bs-popover-arrow-width:1rem;--bs-popover-arrow-height:0.5rem;--bs-popover-arrow-border:var(--bs-popover-border-color);z-index:var(--bs-popover-zindex);display:block;max-width:var(--bs-popover-max-width);font-family:var(--bs-font-sans-serif);font-style:normal;font-weight:400;line-height:1.5;text-align:left;text-align:start;text-decoration:none;text-shadow:none;text-transform:none;letter-spacing:normal;word-break:normal;white-space:normal;word-spacing:normal;line-break:auto;font-size:var(--bs-popover-font-size);word-wrap:break-word;background-color:var(--bs-popover-bg);background-clip:padding-box;border:var(--bs-popover-border-width) solid var(--bs-popover-border-color);border-radius:var(--bs-popover-border-radius)}.popover .popover-arrow{display:block;width:var(--bs-popover-arrow-width);height:var(--bs-popover-arrow-height)}.popover .popover-arrow::after,.popover .popover-arrow::before{position:absolute;display:block;content:"";border-color:transparent;border-style:solid;border-width:0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow,.bs-popover-top>.popover-arrow{bottom:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::after,.bs-popover-top>.popover-arrow::before{border-width:var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::before,.bs-popover-top>.popover-arrow::before{bottom:0;border-top-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=top]>.popover-arrow::after,.bs-popover-top>.popover-arrow::after{bottom:var(--bs-popover-border-width);border-top-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow,.bs-popover-end>.popover-arrow{left:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::after,.bs-popover-end>.popover-arrow::before{border-width:calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height) calc(var(--bs-popover-arrow-width) * .5) 0}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::before,.bs-popover-end>.popover-arrow::before{left:0;border-right-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=right]>.popover-arrow::after,.bs-popover-end>.popover-arrow::after{left:var(--bs-popover-border-width);border-right-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow,.bs-popover-bottom>.popover-arrow{top:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width))}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::before{border-width:0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::before,.bs-popover-bottom>.popover-arrow::before{top:0;border-bottom-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=bottom]>.popover-arrow::after,.bs-popover-bottom>.popover-arrow::after{top:var(--bs-popover-border-width);border-bottom-color:var(--bs-popover-bg)}.bs-popover-auto[data-popper-placement^=bottom] .popover-header::before,.bs-popover-bottom .popover-header::before{position:absolute;top:0;left:50%;display:block;width:var(--bs-popover-arrow-width);margin-left:calc(-.5 * var(--bs-popover-arrow-width));content:"";border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-header-bg)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow,.bs-popover-start>.popover-arrow{right:calc(-1 * (var(--bs-popover-arrow-height)) - var(--bs-popover-border-width));width:var(--bs-popover-arrow-height);height:var(--bs-popover-arrow-width)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::after,.bs-popover-start>.popover-arrow::before{border-width:calc(var(--bs-popover-arrow-width) * .5) 0 calc(var(--bs-popover-arrow-width) * .5) var(--bs-popover-arrow-height)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::before,.bs-popover-start>.popover-arrow::before{right:0;border-left-color:var(--bs-popover-arrow-border)}.bs-popover-auto[data-popper-placement^=left]>.popover-arrow::after,.bs-popover-start>.popover-arrow::after{right:var(--bs-popover-border-width);border-left-color:var(--bs-popover-bg)}.popover-header{padding:var(--bs-popover-header-padding-y) var(--bs-popover-header-padding-x);margin-bottom:0;font-size:var(--bs-popover-header-font-size);color:var(--bs-popover-header-color);background-color:var(--bs-popover-header-bg);border-bottom:var(--bs-popover-border-width) solid var(--bs-popover-border-color);border-top-left-radius:var(--bs-popover-inner-border-radius);border-top-right-radius:var(--bs-popover-inner-border-radius)}.popover-header:empty{display:none}.popover-body{padding:var(--bs-popover-body-padding-y) var(--bs-popover-body-padding-x);color:var(--bs-popover-body-color)}.carousel{position:relative}.carousel.pointer-event{touch-action:pan-y}.carousel-inner{position:relative;width:100%;overflow:hidden}.carousel-inner::after{display:block;clear:both;content:""}.carousel-item{position:relative;display:none;float:left;width:100%;margin-right:-100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;transition:transform .6s ease-in-out}@media (prefers-reduced-motion:reduce){.carousel-item{transition:none}}.carousel-item-next,.carousel-item-prev,.carousel-item.active{display:block}.active.carousel-item-end,.carousel-item-next:not(.carousel-item-start){transform:translateX(100%)}.active.carousel-item-start,.carousel-item-prev:not(.carousel-item-end){transform:translateX(-100%)}.carousel-fade .carousel-item{opacity:0;transition-property:opacity;transform:none}.carousel-fade .carousel-item-next.carousel-item-start,.carousel-fade .carousel-item-prev.carousel-item-end,.carousel-fade .carousel-item.active{z-index:1;opacity:1}.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{z-index:0;opacity:0;transition:opacity 0s .6s}@media (prefers-reduced-motion:reduce){.carousel-fade .active.carousel-item-end,.carousel-fade .active.carousel-item-start{transition:none}}.carousel-control-next,.carousel-control-prev{position:absolute;top:0;bottom:0;z-index:1;display:flex;align-items:center;justify-content:center;width:15%;padding:0;color:#fff;text-align:center;background:0 0;filter:var(--bs-carousel-control-icon-filter);border:0;opacity:.5;transition:opacity .15s ease}@media (prefers-reduced-motion:reduce){.carousel-control-next,.carousel-control-prev{transition:none}}.carousel-control-next:focus,.carousel-control-next:hover,.carousel-control-prev:focus,.carousel-control-prev:hover{color:#fff;text-decoration:none;outline:0;opacity:.9}.carousel-control-prev{left:0}.carousel-control-next{right:0}.carousel-control-next-icon,.carousel-control-prev-icon{display:inline-block;width:2rem;height:2rem;background-repeat:no-repeat;background-position:50%;background-size:100% 100%}.carousel-control-prev-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M11.354 1.646a.5.5 0 0 1 0 .708L5.707 8l5.647 5.646a.5.5 0 0 1-.708.708l-6-6a.5.5 0 0 1 0-.708l6-6a.5.5 0 0 1 .708 0'/%3e%3c/svg%3e")}.carousel-control-next-icon{background-image:url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='%23fff'%3e%3cpath d='M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708'/%3e%3c/svg%3e")}.carousel-indicators{position:absolute;right:0;bottom:0;left:0;z-index:2;display:flex;justify-content:center;padding:0;margin-right:15%;margin-bottom:1rem;margin-left:15%}.carousel-indicators [data-bs-target]{box-sizing:content-box;flex:0 1 auto;width:30px;height:3px;padding:0;margin-right:3px;margin-left:3px;text-indent:-999px;cursor:pointer;background-color:var(--bs-carousel-indicator-active-bg);background-clip:padding-box;border:0;border-top:10px solid transparent;border-bottom:10px solid transparent;opacity:.5;transition:opacity .6s ease}@media (prefers-reduced-motion:reduce){.carousel-indicators [data-bs-target]{transition:none}}.carousel-indicators .active{opacity:1}.carousel-caption{position:absolute;right:15%;bottom:1.25rem;left:15%;padding-top:1.25rem;padding-bottom:1.25rem;color:var(--bs-carousel-caption-color);text-align:center}.carousel-dark{--bs-carousel-indicator-active-bg:#000;--bs-carousel-caption-color:#000;--bs-carousel-control-icon-filter:invert(1) grayscale(100)}:root,[data-bs-theme=light]{--bs-carousel-indicator-active-bg:#fff;--bs-carousel-caption-color:#fff;--bs-carousel-control-icon-filter: }[data-bs-theme=dark]{--bs-carousel-indicator-active-bg:#000;--bs-carousel-caption-color:#000;--bs-carousel-control-icon-filter:invert(1) grayscale(100)}.spinner-border,.spinner-grow{display:inline-block;width:var(--bs-spinner-width);height:var(--bs-spinner-height);vertical-align:var(--bs-spinner-vertical-align);border-radius:50%;animation:var(--bs-spinner-animation-speed) linear infinite var(--bs-spinner-animation-name)}@keyframes spinner-border{to{transform:rotate(360deg)}}.spinner-border{--bs-spinner-width:2rem;--bs-spinner-height:2rem;--bs-spinner-vertical-align:-0.125em;--bs-spinner-border-width:0.25em;--bs-spinner-animation-speed:0.75s;--bs-spinner-animation-name:spinner-border;border:var(--bs-spinner-border-width) solid currentcolor;border-right-color:transparent}.spinner-border-sm{--bs-spinner-width:1rem;--bs-spinner-height:1rem;--bs-spinner-border-width:0.2em}@keyframes spinner-grow{0%{transform:scale(0)}50%{opacity:1;transform:none}}.spinner-grow{--bs-spinner-width:2rem;--bs-spinner-height:2rem;--bs-spinner-vertical-align:-0.125em;--bs-spinner-animation-speed:0.75s;--bs-spinner-animation-name:spinner-grow;background-color:currentcolor;opacity:0}.spinner-grow-sm{--bs-spinner-width:1rem;--bs-spinner-height:1rem}@media (prefers-reduced-motion:reduce){.spinner-border,.spinner-grow{--bs-spinner-animation-speed:1.5s}}.offcanvas,.offcanvas-lg,.offcanvas-md,.offcanvas-sm,.offcanvas-xl,.offcanvas-xxl{--bs-offcanvas-zindex:1045;--bs-offcanvas-width:400px;--bs-offcanvas-height:30vh;--bs-offcanvas-padding-x:1rem;--bs-offcanvas-padding-y:1rem;--bs-offcanvas-color:var(--bs-body-color);--bs-offcanvas-bg:var(--bs-body-bg);--bs-offcanvas-border-width:var(--bs-border-width);--bs-offcanvas-border-color:var(--bs-border-color-translucent);--bs-offcanvas-box-shadow:var(--bs-box-shadow-sm);--bs-offcanvas-transition:transform 0.3s ease-in-out;--bs-offcanvas-title-line-height:1.5}@media (max-width:575.98px){.offcanvas-sm{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:575.98px) and (prefers-reduced-motion:reduce){.offcanvas-sm{transition:none}}@media (max-width:575.98px){.offcanvas-sm.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-sm.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-sm.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-sm.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-sm.show:not(.hiding),.offcanvas-sm.showing{transform:none}.offcanvas-sm.hiding,.offcanvas-sm.show,.offcanvas-sm.showing{visibility:visible}}@media (min-width:576px){.offcanvas-sm{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-sm .offcanvas-header{display:none}.offcanvas-sm .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:767.98px){.offcanvas-md{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:767.98px) and (prefers-reduced-motion:reduce){.offcanvas-md{transition:none}}@media (max-width:767.98px){.offcanvas-md.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-md.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-md.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-md.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-md.show:not(.hiding),.offcanvas-md.showing{transform:none}.offcanvas-md.hiding,.offcanvas-md.show,.offcanvas-md.showing{visibility:visible}}@media (min-width:768px){.offcanvas-md{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-md .offcanvas-header{display:none}.offcanvas-md .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:991.98px){.offcanvas-lg{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:991.98px) and (prefers-reduced-motion:reduce){.offcanvas-lg{transition:none}}@media (max-width:991.98px){.offcanvas-lg.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-lg.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-lg.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-lg.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-lg.show:not(.hiding),.offcanvas-lg.showing{transform:none}.offcanvas-lg.hiding,.offcanvas-lg.show,.offcanvas-lg.showing{visibility:visible}}@media (min-width:992px){.offcanvas-lg{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-lg .offcanvas-header{display:none}.offcanvas-lg .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:1199.98px){.offcanvas-xl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:1199.98px) and (prefers-reduced-motion:reduce){.offcanvas-xl{transition:none}}@media (max-width:1199.98px){.offcanvas-xl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-xl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-xl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xl.show:not(.hiding),.offcanvas-xl.showing{transform:none}.offcanvas-xl.hiding,.offcanvas-xl.show,.offcanvas-xl.showing{visibility:visible}}@media (min-width:1200px){.offcanvas-xl{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-xl .offcanvas-header{display:none}.offcanvas-xl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}@media (max-width:1399.98px){.offcanvas-xxl{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}}@media (max-width:1399.98px) and (prefers-reduced-motion:reduce){.offcanvas-xxl{transition:none}}@media (max-width:1399.98px){.offcanvas-xxl.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas-xxl.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas-xxl.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas-xxl.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas-xxl.show:not(.hiding),.offcanvas-xxl.showing{transform:none}.offcanvas-xxl.hiding,.offcanvas-xxl.show,.offcanvas-xxl.showing{visibility:visible}}@media (min-width:1400px){.offcanvas-xxl{--bs-offcanvas-height:auto;--bs-offcanvas-border-width:0;background-color:transparent!important}.offcanvas-xxl .offcanvas-header{display:none}.offcanvas-xxl .offcanvas-body{display:flex;flex-grow:0;padding:0;overflow-y:visible;background-color:transparent!important}}.offcanvas{position:fixed;bottom:0;z-index:var(--bs-offcanvas-zindex);display:flex;flex-direction:column;max-width:100%;color:var(--bs-offcanvas-color);visibility:hidden;background-color:var(--bs-offcanvas-bg);background-clip:padding-box;outline:0;transition:var(--bs-offcanvas-transition)}@media (prefers-reduced-motion:reduce){.offcanvas{transition:none}}.offcanvas.offcanvas-start{top:0;left:0;width:var(--bs-offcanvas-width);border-right:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(-100%)}.offcanvas.offcanvas-end{top:0;right:0;width:var(--bs-offcanvas-width);border-left:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateX(100%)}.offcanvas.offcanvas-top{top:0;right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-bottom:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(-100%)}.offcanvas.offcanvas-bottom{right:0;left:0;height:var(--bs-offcanvas-height);max-height:100%;border-top:var(--bs-offcanvas-border-width) solid var(--bs-offcanvas-border-color);transform:translateY(100%)}.offcanvas.show:not(.hiding),.offcanvas.showing{transform:none}.offcanvas.hiding,.offcanvas.show,.offcanvas.showing{visibility:visible}.offcanvas-backdrop{position:fixed;top:0;left:0;z-index:1040;width:100vw;height:100vh;background-color:#000}.offcanvas-backdrop.fade{opacity:0}.offcanvas-backdrop.show{opacity:.5}.offcanvas-header{display:flex;align-items:center;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x)}.offcanvas-header .btn-close{padding:calc(var(--bs-offcanvas-padding-y) * .5) calc(var(--bs-offcanvas-padding-x) * .5);margin-top:calc(-.5 * var(--bs-offcanvas-padding-y));margin-right:calc(-.5 * var(--bs-offcanvas-padding-x));margin-bottom:calc(-.5 * var(--bs-offcanvas-padding-y));margin-left:auto}.offcanvas-title{margin-bottom:0;line-height:var(--bs-offcanvas-title-line-height)}.offcanvas-body{flex-grow:1;padding:var(--bs-offcanvas-padding-y) var(--bs-offcanvas-padding-x);overflow-y:auto}.placeholder{display:inline-block;min-height:1em;vertical-align:middle;cursor:wait;background-color:currentcolor;opacity:.5}.placeholder.btn::before{display:inline-block;content:""}.placeholder-xs{min-height:.6em}.placeholder-sm{min-height:.8em}.placeholder-lg{min-height:1.2em}.placeholder-glow .placeholder{animation:placeholder-glow 2s ease-in-out infinite}@keyframes placeholder-glow{50%{opacity:.2}}.placeholder-wave{-webkit-mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);mask-image:linear-gradient(130deg,#000 55%,rgba(0,0,0,0.8) 75%,#000 95%);-webkit-mask-size:200% 100%;mask-size:200% 100%;animation:placeholder-wave 2s linear infinite}@keyframes placeholder-wave{100%{-webkit-mask-position:-200% 0%;mask-position:-200% 0%}}.clearfix::after{display:block;clear:both;content:""}.text-bg-primary{color:#fff!important;background-color:RGBA(var(--bs-primary-rgb),var(--bs-bg-opacity,1))!important}.text-bg-secondary{color:#fff!important;background-color:RGBA(var(--bs-secondary-rgb),var(--bs-bg-opacity,1))!important}.text-bg-success{color:#fff!important;background-color:RGBA(var(--bs-success-rgb),var(--bs-bg-opacity,1))!important}.text-bg-info{color:#000!important;background-color:RGBA(var(--bs-info-rgb),var(--bs-bg-opacity,1))!important}.text-bg-warning{color:#000!important;background-color:RGBA(var(--bs-warning-rgb),var(--bs-bg-opacity,1))!important}.text-bg-danger{color:#fff!important;background-color:RGBA(var(--bs-danger-rgb),var(--bs-bg-opacity,1))!important}.text-bg-light{color:#000!important;background-color:RGBA(var(--bs-light-rgb),var(--bs-bg-opacity,1))!important}.text-bg-dark{color:#fff!important;background-color:RGBA(var(--bs-dark-rgb),var(--bs-bg-opacity,1))!important}.link-primary{color:RGBA(var(--bs-primary-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-primary-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-primary-rgb),var(--bs-link-underline-opacity,1))!important}.link-primary:focus,.link-primary:hover{color:RGBA(10,88,202,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(10,88,202,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(10,88,202,var(--bs-link-underline-opacity,1))!important}.link-secondary{color:RGBA(var(--bs-secondary-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-secondary-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-secondary-rgb),var(--bs-link-underline-opacity,1))!important}.link-secondary:focus,.link-secondary:hover{color:RGBA(86,94,100,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(86,94,100,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(86,94,100,var(--bs-link-underline-opacity,1))!important}.link-success{color:RGBA(var(--bs-success-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-success-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-success-rgb),var(--bs-link-underline-opacity,1))!important}.link-success:focus,.link-success:hover{color:RGBA(20,108,67,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(20,108,67,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(20,108,67,var(--bs-link-underline-opacity,1))!important}.link-info{color:RGBA(var(--bs-info-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-info-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-info-rgb),var(--bs-link-underline-opacity,1))!important}.link-info:focus,.link-info:hover{color:RGBA(61,213,243,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(61,213,243,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(61,213,243,var(--bs-link-underline-opacity,1))!important}.link-warning{color:RGBA(var(--bs-warning-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-warning-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-warning-rgb),var(--bs-link-underline-opacity,1))!important}.link-warning:focus,.link-warning:hover{color:RGBA(255,205,57,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(255,205,57,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(255,205,57,var(--bs-link-underline-opacity,1))!important}.link-danger{color:RGBA(var(--bs-danger-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-danger-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-danger-rgb),var(--bs-link-underline-opacity,1))!important}.link-danger:focus,.link-danger:hover{color:RGBA(176,42,55,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(176,42,55,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(176,42,55,var(--bs-link-underline-opacity,1))!important}.link-light{color:RGBA(var(--bs-light-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-light-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-light-rgb),var(--bs-link-underline-opacity,1))!important}.link-light:focus,.link-light:hover{color:RGBA(249,250,251,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(249,250,251,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(249,250,251,var(--bs-link-underline-opacity,1))!important}.link-dark{color:RGBA(var(--bs-dark-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-dark-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-dark-rgb),var(--bs-link-underline-opacity,1))!important}.link-dark:focus,.link-dark:hover{color:RGBA(26,30,33,var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(26,30,33,var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(26,30,33,var(--bs-link-underline-opacity,1))!important}.link-body-emphasis{color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-opacity,1))!important;-webkit-text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,1))!important}.link-body-emphasis:focus,.link-body-emphasis:hover{color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-opacity,.75))!important;-webkit-text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,0.75))!important;text-decoration-color:RGBA(var(--bs-emphasis-color-rgb),var(--bs-link-underline-opacity,0.75))!important}.focus-ring:focus{outline:0;box-shadow:var(--bs-focus-ring-x,0) var(--bs-focus-ring-y,0) var(--bs-focus-ring-blur,0) var(--bs-focus-ring-width) var(--bs-focus-ring-color)}.icon-link{display:inline-flex;gap:.375rem;align-items:center;-webkit-text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,0.5));text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-opacity,0.5));text-underline-offset:0.25em;-webkit-backface-visibility:hidden;backface-visibility:hidden}.icon-link>.bi{flex-shrink:0;width:1em;height:1em;fill:currentcolor;transition:.2s ease-in-out transform}@media (prefers-reduced-motion:reduce){.icon-link>.bi{transition:none}}.icon-link-hover:focus-visible>.bi,.icon-link-hover:hover>.bi{transform:var(--bs-icon-link-transform,translate3d(.25em,0,0))}.ratio{position:relative;width:100%}.ratio::before{display:block;padding-top:var(--bs-aspect-ratio);content:""}.ratio>*{position:absolute;top:0;left:0;width:100%;height:100%}.ratio-1x1{--bs-aspect-ratio:100%}.ratio-4x3{--bs-aspect-ratio:75%}.ratio-16x9{--bs-aspect-ratio:56.25%}.ratio-21x9{--bs-aspect-ratio:42.8571428571%}.fixed-top{position:fixed;top:0;right:0;left:0;z-index:1030}.fixed-bottom{position:fixed;right:0;bottom:0;left:0;z-index:1030}.sticky-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}@media (min-width:576px){.sticky-sm-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-sm-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:768px){.sticky-md-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-md-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:992px){.sticky-lg-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-lg-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:1200px){.sticky-xl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-xl-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}@media (min-width:1400px){.sticky-xxl-top{position:-webkit-sticky;position:sticky;top:0;z-index:1020}.sticky-xxl-bottom{position:-webkit-sticky;position:sticky;bottom:0;z-index:1020}}.hstack{display:flex;flex-direction:row;align-items:center;align-self:stretch}.vstack{display:flex;flex:1 1 auto;flex-direction:column;align-self:stretch}.visually-hidden,.visually-hidden-focusable:not(:focus):not(:focus-within){width:1px!important;height:1px!important;padding:0!important;margin:-1px!important;overflow:hidden!important;clip:rect(0,0,0,0)!important;white-space:nowrap!important;border:0!important}.visually-hidden-focusable:not(:focus):not(:focus-within):not(caption),.visually-hidden:not(caption){position:absolute!important}.visually-hidden *,.visually-hidden-focusable:not(:focus):not(:focus-within) *{overflow:hidden!important}.stretched-link::after{position:absolute;top:0;right:0;bottom:0;left:0;z-index:1;content:""}.text-truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.vr{display:inline-block;align-self:stretch;width:var(--bs-border-width);min-height:1em;background-color:currentcolor;opacity:.25}.align-baseline{vertical-align:baseline!important}.align-top{vertical-align:top!important}.align-middle{vertical-align:middle!important}.align-bottom{vertical-align:bottom!important}.align-text-bottom{vertical-align:text-bottom!important}.align-text-top{vertical-align:text-top!important}.float-start{float:left!important}.float-end{float:right!important}.float-none{float:none!important}.object-fit-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-none{-o-object-fit:none!important;object-fit:none!important}.opacity-0{opacity:0!important}.opacity-25{opacity:.25!important}.opacity-50{opacity:.5!important}.opacity-75{opacity:.75!important}.opacity-100{opacity:1!important}.overflow-auto{overflow:auto!important}.overflow-hidden{overflow:hidden!important}.overflow-visible{overflow:visible!important}.overflow-scroll{overflow:scroll!important}.overflow-x-auto{overflow-x:auto!important}.overflow-x-hidden{overflow-x:hidden!important}.overflow-x-visible{overflow-x:visible!important}.overflow-x-scroll{overflow-x:scroll!important}.overflow-y-auto{overflow-y:auto!important}.overflow-y-hidden{overflow-y:hidden!important}.overflow-y-visible{overflow-y:visible!important}.overflow-y-scroll{overflow-y:scroll!important}.d-inline{display:inline!important}.d-inline-block{display:inline-block!important}.d-block{display:block!important}.d-grid{display:grid!important}.d-inline-grid{display:inline-grid!important}.d-table{display:table!important}.d-table-row{display:table-row!important}.d-table-cell{display:table-cell!important}.d-flex{display:flex!important}.d-inline-flex{display:inline-flex!important}.d-none{display:none!important}.shadow{box-shadow:var(--bs-box-shadow)!important}.shadow-sm{box-shadow:var(--bs-box-shadow-sm)!important}.shadow-lg{box-shadow:var(--bs-box-shadow-lg)!important}.shadow-none{box-shadow:none!important}.focus-ring-primary{--bs-focus-ring-color:rgba(var(--bs-primary-rgb), var(--bs-focus-ring-opacity))}.focus-ring-secondary{--bs-focus-ring-color:rgba(var(--bs-secondary-rgb), var(--bs-focus-ring-opacity))}.focus-ring-success{--bs-focus-ring-color:rgba(var(--bs-success-rgb), var(--bs-focus-ring-opacity))}.focus-ring-info{--bs-focus-ring-color:rgba(var(--bs-info-rgb), var(--bs-focus-ring-opacity))}.focus-ring-warning{--bs-focus-ring-color:rgba(var(--bs-warning-rgb), var(--bs-focus-ring-opacity))}.focus-ring-danger{--bs-focus-ring-color:rgba(var(--bs-danger-rgb), var(--bs-focus-ring-opacity))}.focus-ring-light{--bs-focus-ring-color:rgba(var(--bs-light-rgb), var(--bs-focus-ring-opacity))}.focus-ring-dark{--bs-focus-ring-color:rgba(var(--bs-dark-rgb), var(--bs-focus-ring-opacity))}.position-static{position:static!important}.position-relative{position:relative!important}.position-absolute{position:absolute!important}.position-fixed{position:fixed!important}.position-sticky{position:-webkit-sticky!important;position:sticky!important}.top-0{top:0!important}.top-50{top:50%!important}.top-100{top:100%!important}.bottom-0{bottom:0!important}.bottom-50{bottom:50%!important}.bottom-100{bottom:100%!important}.start-0{left:0!important}.start-50{left:50%!important}.start-100{left:100%!important}.end-0{right:0!important}.end-50{right:50%!important}.end-100{right:100%!important}.translate-middle{transform:translate(-50%,-50%)!important}.translate-middle-x{transform:translateX(-50%)!important}.translate-middle-y{transform:translateY(-50%)!important}.border{border:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-0{border:0!important}.border-top{border-top:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-top-0{border-top:0!important}.border-end{border-right:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-end-0{border-right:0!important}.border-bottom{border-bottom:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-bottom-0{border-bottom:0!important}.border-start{border-left:var(--bs-border-width) var(--bs-border-style) var(--bs-border-color)!important}.border-start-0{border-left:0!important}.border-primary{--bs-border-opacity:1;border-color:rgba(var(--bs-primary-rgb),var(--bs-border-opacity))!important}.border-secondary{--bs-border-opacity:1;border-color:rgba(var(--bs-secondary-rgb),var(--bs-border-opacity))!important}.border-success{--bs-border-opacity:1;border-color:rgba(var(--bs-success-rgb),var(--bs-border-opacity))!important}.border-info{--bs-border-opacity:1;border-color:rgba(var(--bs-info-rgb),var(--bs-border-opacity))!important}.border-warning{--bs-border-opacity:1;border-color:rgba(var(--bs-warning-rgb),var(--bs-border-opacity))!important}.border-danger{--bs-border-opacity:1;border-color:rgba(var(--bs-danger-rgb),var(--bs-border-opacity))!important}.border-light{--bs-border-opacity:1;border-color:rgba(var(--bs-light-rgb),var(--bs-border-opacity))!important}.border-dark{--bs-border-opacity:1;border-color:rgba(var(--bs-dark-rgb),var(--bs-border-opacity))!important}.border-black{--bs-border-opacity:1;border-color:rgba(var(--bs-black-rgb),var(--bs-border-opacity))!important}.border-white{--bs-border-opacity:1;border-color:rgba(var(--bs-white-rgb),var(--bs-border-opacity))!important}.border-primary-subtle{border-color:var(--bs-primary-border-subtle)!important}.border-secondary-subtle{border-color:var(--bs-secondary-border-subtle)!important}.border-success-subtle{border-color:var(--bs-success-border-subtle)!important}.border-info-subtle{border-color:var(--bs-info-border-subtle)!important}.border-warning-subtle{border-color:var(--bs-warning-border-subtle)!important}.border-danger-subtle{border-color:var(--bs-danger-border-subtle)!important}.border-light-subtle{border-color:var(--bs-light-border-subtle)!important}.border-dark-subtle{border-color:var(--bs-dark-border-subtle)!important}.border-1{border-width:1px!important}.border-2{border-width:2px!important}.border-3{border-width:3px!important}.border-4{border-width:4px!important}.border-5{border-width:5px!important}.border-opacity-10{--bs-border-opacity:0.1}.border-opacity-25{--bs-border-opacity:0.25}.border-opacity-50{--bs-border-opacity:0.5}.border-opacity-75{--bs-border-opacity:0.75}.border-opacity-100{--bs-border-opacity:1}.w-25{width:25%!important}.w-50{width:50%!important}.w-75{width:75%!important}.w-100{width:100%!important}.w-auto{width:auto!important}.mw-100{max-width:100%!important}.vw-100{width:100vw!important}.min-vw-100{min-width:100vw!important}.h-25{height:25%!important}.h-50{height:50%!important}.h-75{height:75%!important}.h-100{height:100%!important}.h-auto{height:auto!important}.mh-100{max-height:100%!important}.vh-100{height:100vh!important}.min-vh-100{min-height:100vh!important}.flex-fill{flex:1 1 auto!important}.flex-row{flex-direction:row!important}.flex-column{flex-direction:column!important}.flex-row-reverse{flex-direction:row-reverse!important}.flex-column-reverse{flex-direction:column-reverse!important}.flex-grow-0{flex-grow:0!important}.flex-grow-1{flex-grow:1!important}.flex-shrink-0{flex-shrink:0!important}.flex-shrink-1{flex-shrink:1!important}.flex-wrap{flex-wrap:wrap!important}.flex-nowrap{flex-wrap:nowrap!important}.flex-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-start{justify-content:flex-start!important}.justify-content-end{justify-content:flex-end!important}.justify-content-center{justify-content:center!important}.justify-content-between{justify-content:space-between!important}.justify-content-around{justify-content:space-around!important}.justify-content-evenly{justify-content:space-evenly!important}.align-items-start{align-items:flex-start!important}.align-items-end{align-items:flex-end!important}.align-items-center{align-items:center!important}.align-items-baseline{align-items:baseline!important}.align-items-stretch{align-items:stretch!important}.align-content-start{align-content:flex-start!important}.align-content-end{align-content:flex-end!important}.align-content-center{align-content:center!important}.align-content-between{align-content:space-between!important}.align-content-around{align-content:space-around!important}.align-content-stretch{align-content:stretch!important}.align-self-auto{align-self:auto!important}.align-self-start{align-self:flex-start!important}.align-self-end{align-self:flex-end!important}.align-self-center{align-self:center!important}.align-self-baseline{align-self:baseline!important}.align-self-stretch{align-self:stretch!important}.order-first{order:-1!important}.order-0{order:0!important}.order-1{order:1!important}.order-2{order:2!important}.order-3{order:3!important}.order-4{order:4!important}.order-5{order:5!important}.order-last{order:6!important}.m-0{margin:0!important}.m-1{margin:.25rem!important}.m-2{margin:.5rem!important}.m-3{margin:1rem!important}.m-4{margin:1.5rem!important}.m-5{margin:3rem!important}.m-auto{margin:auto!important}.mx-0{margin-right:0!important;margin-left:0!important}.mx-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-3{margin-right:1rem!important;margin-left:1rem!important}.mx-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-5{margin-right:3rem!important;margin-left:3rem!important}.mx-auto{margin-right:auto!important;margin-left:auto!important}.my-0{margin-top:0!important;margin-bottom:0!important}.my-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-0{margin-top:0!important}.mt-1{margin-top:.25rem!important}.mt-2{margin-top:.5rem!important}.mt-3{margin-top:1rem!important}.mt-4{margin-top:1.5rem!important}.mt-5{margin-top:3rem!important}.mt-auto{margin-top:auto!important}.me-0{margin-right:0!important}.me-1{margin-right:.25rem!important}.me-2{margin-right:.5rem!important}.me-3{margin-right:1rem!important}.me-4{margin-right:1.5rem!important}.me-5{margin-right:3rem!important}.me-auto{margin-right:auto!important}.mb-0{margin-bottom:0!important}.mb-1{margin-bottom:.25rem!important}.mb-2{margin-bottom:.5rem!important}.mb-3{margin-bottom:1rem!important}.mb-4{margin-bottom:1.5rem!important}.mb-5{margin-bottom:3rem!important}.mb-auto{margin-bottom:auto!important}.ms-0{margin-left:0!important}.ms-1{margin-left:.25rem!important}.ms-2{margin-left:.5rem!important}.ms-3{margin-left:1rem!important}.ms-4{margin-left:1.5rem!important}.ms-5{margin-left:3rem!important}.ms-auto{margin-left:auto!important}.p-0{padding:0!important}.p-1{padding:.25rem!important}.p-2{padding:.5rem!important}.p-3{padding:1rem!important}.p-4{padding:1.5rem!important}.p-5{padding:3rem!important}.px-0{padding-right:0!important;padding-left:0!important}.px-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-3{padding-right:1rem!important;padding-left:1rem!important}.px-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-5{padding-right:3rem!important;padding-left:3rem!important}.py-0{padding-top:0!important;padding-bottom:0!important}.py-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-0{padding-top:0!important}.pt-1{padding-top:.25rem!important}.pt-2{padding-top:.5rem!important}.pt-3{padding-top:1rem!important}.pt-4{padding-top:1.5rem!important}.pt-5{padding-top:3rem!important}.pe-0{padding-right:0!important}.pe-1{padding-right:.25rem!important}.pe-2{padding-right:.5rem!important}.pe-3{padding-right:1rem!important}.pe-4{padding-right:1.5rem!important}.pe-5{padding-right:3rem!important}.pb-0{padding-bottom:0!important}.pb-1{padding-bottom:.25rem!important}.pb-2{padding-bottom:.5rem!important}.pb-3{padding-bottom:1rem!important}.pb-4{padding-bottom:1.5rem!important}.pb-5{padding-bottom:3rem!important}.ps-0{padding-left:0!important}.ps-1{padding-left:.25rem!important}.ps-2{padding-left:.5rem!important}.ps-3{padding-left:1rem!important}.ps-4{padding-left:1.5rem!important}.ps-5{padding-left:3rem!important}.gap-0{gap:0!important}.gap-1{gap:.25rem!important}.gap-2{gap:.5rem!important}.gap-3{gap:1rem!important}.gap-4{gap:1.5rem!important}.gap-5{gap:3rem!important}.row-gap-0{row-gap:0!important}.row-gap-1{row-gap:.25rem!important}.row-gap-2{row-gap:.5rem!important}.row-gap-3{row-gap:1rem!important}.row-gap-4{row-gap:1.5rem!important}.row-gap-5{row-gap:3rem!important}.column-gap-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.font-monospace{font-family:var(--bs-font-monospace)!important}.fs-1{font-size:calc(1.375rem + 1.5vw)!important}.fs-2{font-size:calc(1.325rem + .9vw)!important}.fs-3{font-size:calc(1.3rem + .6vw)!important}.fs-4{font-size:calc(1.275rem + .3vw)!important}.fs-5{font-size:1.25rem!important}.fs-6{font-size:1rem!important}.fst-italic{font-style:italic!important}.fst-normal{font-style:normal!important}.fw-lighter{font-weight:lighter!important}.fw-light{font-weight:300!important}.fw-normal{font-weight:400!important}.fw-medium{font-weight:500!important}.fw-semibold{font-weight:600!important}.fw-bold{font-weight:700!important}.fw-bolder{font-weight:bolder!important}.lh-1{line-height:1!important}.lh-sm{line-height:1.25!important}.lh-base{line-height:1.5!important}.lh-lg{line-height:2!important}.text-start{text-align:left!important}.text-end{text-align:right!important}.text-center{text-align:center!important}.text-decoration-none{text-decoration:none!important}.text-decoration-underline{text-decoration:underline!important}.text-decoration-line-through{text-decoration:line-through!important}.text-lowercase{text-transform:lowercase!important}.text-uppercase{text-transform:uppercase!important}.text-capitalize{text-transform:capitalize!important}.text-wrap{white-space:normal!important}.text-nowrap{white-space:nowrap!important}.text-break{word-wrap:break-word!important;word-break:break-word!important}.text-primary{--bs-text-opacity:1;color:rgba(var(--bs-primary-rgb),var(--bs-text-opacity))!important}.text-secondary{--bs-text-opacity:1;color:rgba(var(--bs-secondary-rgb),var(--bs-text-opacity))!important}.text-success{--bs-text-opacity:1;color:rgba(var(--bs-success-rgb),var(--bs-text-opacity))!important}.text-info{--bs-text-opacity:1;color:rgba(var(--bs-info-rgb),var(--bs-text-opacity))!important}.text-warning{--bs-text-opacity:1;color:rgba(var(--bs-warning-rgb),var(--bs-text-opacity))!important}.text-danger{--bs-text-opacity:1;color:rgba(var(--bs-danger-rgb),var(--bs-text-opacity))!important}.text-light{--bs-text-opacity:1;color:rgba(var(--bs-light-rgb),var(--bs-text-opacity))!important}.text-dark{--bs-text-opacity:1;color:rgba(var(--bs-dark-rgb),var(--bs-text-opacity))!important}.text-black{--bs-text-opacity:1;color:rgba(var(--bs-black-rgb),var(--bs-text-opacity))!important}.text-white{--bs-text-opacity:1;color:rgba(var(--bs-white-rgb),var(--bs-text-opacity))!important}.text-body{--bs-text-opacity:1;color:rgba(var(--bs-body-color-rgb),var(--bs-text-opacity))!important}.text-muted{--bs-text-opacity:1;color:var(--bs-secondary-color)!important}.text-black-50{--bs-text-opacity:1;color:rgba(0,0,0,.5)!important}.text-white-50{--bs-text-opacity:1;color:rgba(255,255,255,.5)!important}.text-body-secondary{--bs-text-opacity:1;color:var(--bs-secondary-color)!important}.text-body-tertiary{--bs-text-opacity:1;color:var(--bs-tertiary-color)!important}.text-body-emphasis{--bs-text-opacity:1;color:var(--bs-emphasis-color)!important}.text-reset{--bs-text-opacity:1;color:inherit!important}.text-opacity-25{--bs-text-opacity:0.25}.text-opacity-50{--bs-text-opacity:0.5}.text-opacity-75{--bs-text-opacity:0.75}.text-opacity-100{--bs-text-opacity:1}.text-primary-emphasis{color:var(--bs-primary-text-emphasis)!important}.text-secondary-emphasis{color:var(--bs-secondary-text-emphasis)!important}.text-success-emphasis{color:var(--bs-success-text-emphasis)!important}.text-info-emphasis{color:var(--bs-info-text-emphasis)!important}.text-warning-emphasis{color:var(--bs-warning-text-emphasis)!important}.text-danger-emphasis{color:var(--bs-danger-text-emphasis)!important}.text-light-emphasis{color:var(--bs-light-text-emphasis)!important}.text-dark-emphasis{color:var(--bs-dark-text-emphasis)!important}.link-opacity-10{--bs-link-opacity:0.1}.link-opacity-10-hover:hover{--bs-link-opacity:0.1}.link-opacity-25{--bs-link-opacity:0.25}.link-opacity-25-hover:hover{--bs-link-opacity:0.25}.link-opacity-50{--bs-link-opacity:0.5}.link-opacity-50-hover:hover{--bs-link-opacity:0.5}.link-opacity-75{--bs-link-opacity:0.75}.link-opacity-75-hover:hover{--bs-link-opacity:0.75}.link-opacity-100{--bs-link-opacity:1}.link-opacity-100-hover:hover{--bs-link-opacity:1}.link-offset-1{text-underline-offset:0.125em!important}.link-offset-1-hover:hover{text-underline-offset:0.125em!important}.link-offset-2{text-underline-offset:0.25em!important}.link-offset-2-hover:hover{text-underline-offset:0.25em!important}.link-offset-3{text-underline-offset:0.375em!important}.link-offset-3-hover:hover{text-underline-offset:0.375em!important}.link-underline-primary{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-primary-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-primary-rgb),var(--bs-link-underline-opacity))!important}.link-underline-secondary{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-secondary-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-secondary-rgb),var(--bs-link-underline-opacity))!important}.link-underline-success{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-success-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-success-rgb),var(--bs-link-underline-opacity))!important}.link-underline-info{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-info-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-info-rgb),var(--bs-link-underline-opacity))!important}.link-underline-warning{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-warning-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-warning-rgb),var(--bs-link-underline-opacity))!important}.link-underline-danger{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-danger-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-danger-rgb),var(--bs-link-underline-opacity))!important}.link-underline-light{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-light-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-light-rgb),var(--bs-link-underline-opacity))!important}.link-underline-dark{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-dark-rgb),var(--bs-link-underline-opacity))!important;text-decoration-color:rgba(var(--bs-dark-rgb),var(--bs-link-underline-opacity))!important}.link-underline{--bs-link-underline-opacity:1;-webkit-text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-underline-opacity,1))!important;text-decoration-color:rgba(var(--bs-link-color-rgb),var(--bs-link-underline-opacity,1))!important}.link-underline-opacity-0{--bs-link-underline-opacity:0}.link-underline-opacity-0-hover:hover{--bs-link-underline-opacity:0}.link-underline-opacity-10{--bs-link-underline-opacity:0.1}.link-underline-opacity-10-hover:hover{--bs-link-underline-opacity:0.1}.link-underline-opacity-25{--bs-link-underline-opacity:0.25}.link-underline-opacity-25-hover:hover{--bs-link-underline-opacity:0.25}.link-underline-opacity-50{--bs-link-underline-opacity:0.5}.link-underline-opacity-50-hover:hover{--bs-link-underline-opacity:0.5}.link-underline-opacity-75{--bs-link-underline-opacity:0.75}.link-underline-opacity-75-hover:hover{--bs-link-underline-opacity:0.75}.link-underline-opacity-100{--bs-link-underline-opacity:1}.link-underline-opacity-100-hover:hover{--bs-link-underline-opacity:1}.bg-primary{--bs-bg-opacity:1;background-color:rgba(var(--bs-primary-rgb),var(--bs-bg-opacity))!important}.bg-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-rgb),var(--bs-bg-opacity))!important}.bg-success{--bs-bg-opacity:1;background-color:rgba(var(--bs-success-rgb),var(--bs-bg-opacity))!important}.bg-info{--bs-bg-opacity:1;background-color:rgba(var(--bs-info-rgb),var(--bs-bg-opacity))!important}.bg-warning{--bs-bg-opacity:1;background-color:rgba(var(--bs-warning-rgb),var(--bs-bg-opacity))!important}.bg-danger{--bs-bg-opacity:1;background-color:rgba(var(--bs-danger-rgb),var(--bs-bg-opacity))!important}.bg-light{--bs-bg-opacity:1;background-color:rgba(var(--bs-light-rgb),var(--bs-bg-opacity))!important}.bg-dark{--bs-bg-opacity:1;background-color:rgba(var(--bs-dark-rgb),var(--bs-bg-opacity))!important}.bg-black{--bs-bg-opacity:1;background-color:rgba(var(--bs-black-rgb),var(--bs-bg-opacity))!important}.bg-white{--bs-bg-opacity:1;background-color:rgba(var(--bs-white-rgb),var(--bs-bg-opacity))!important}.bg-body{--bs-bg-opacity:1;background-color:rgba(var(--bs-body-bg-rgb),var(--bs-bg-opacity))!important}.bg-transparent{--bs-bg-opacity:1;background-color:transparent!important}.bg-body-secondary{--bs-bg-opacity:1;background-color:rgba(var(--bs-secondary-bg-rgb),var(--bs-bg-opacity))!important}.bg-body-tertiary{--bs-bg-opacity:1;background-color:rgba(var(--bs-tertiary-bg-rgb),var(--bs-bg-opacity))!important}.bg-opacity-10{--bs-bg-opacity:0.1}.bg-opacity-25{--bs-bg-opacity:0.25}.bg-opacity-50{--bs-bg-opacity:0.5}.bg-opacity-75{--bs-bg-opacity:0.75}.bg-opacity-100{--bs-bg-opacity:1}.bg-primary-subtle{background-color:var(--bs-primary-bg-subtle)!important}.bg-secondary-subtle{background-color:var(--bs-secondary-bg-subtle)!important}.bg-success-subtle{background-color:var(--bs-success-bg-subtle)!important}.bg-info-subtle{background-color:var(--bs-info-bg-subtle)!important}.bg-warning-subtle{background-color:var(--bs-warning-bg-subtle)!important}.bg-danger-subtle{background-color:var(--bs-danger-bg-subtle)!important}.bg-light-subtle{background-color:var(--bs-light-bg-subtle)!important}.bg-dark-subtle{background-color:var(--bs-dark-bg-subtle)!important}.bg-gradient{background-image:var(--bs-gradient)!important}.user-select-all{-webkit-user-select:all!important;-moz-user-select:all!important;user-select:all!important}.user-select-auto{-webkit-user-select:auto!important;-moz-user-select:auto!important;user-select:auto!important}.user-select-none{-webkit-user-select:none!important;-moz-user-select:none!important;user-select:none!important}.pe-none{pointer-events:none!important}.pe-auto{pointer-events:auto!important}.rounded{border-radius:var(--bs-border-radius)!important}.rounded-0{border-radius:0!important}.rounded-1{border-radius:var(--bs-border-radius-sm)!important}.rounded-2{border-radius:var(--bs-border-radius)!important}.rounded-3{border-radius:var(--bs-border-radius-lg)!important}.rounded-4{border-radius:var(--bs-border-radius-xl)!important}.rounded-5{border-radius:var(--bs-border-radius-xxl)!important}.rounded-circle{border-radius:50%!important}.rounded-pill{border-radius:var(--bs-border-radius-pill)!important}.rounded-top{border-top-left-radius:var(--bs-border-radius)!important;border-top-right-radius:var(--bs-border-radius)!important}.rounded-top-0{border-top-left-radius:0!important;border-top-right-radius:0!important}.rounded-top-1{border-top-left-radius:var(--bs-border-radius-sm)!important;border-top-right-radius:var(--bs-border-radius-sm)!important}.rounded-top-2{border-top-left-radius:var(--bs-border-radius)!important;border-top-right-radius:var(--bs-border-radius)!important}.rounded-top-3{border-top-left-radius:var(--bs-border-radius-lg)!important;border-top-right-radius:var(--bs-border-radius-lg)!important}.rounded-top-4{border-top-left-radius:var(--bs-border-radius-xl)!important;border-top-right-radius:var(--bs-border-radius-xl)!important}.rounded-top-5{border-top-left-radius:var(--bs-border-radius-xxl)!important;border-top-right-radius:var(--bs-border-radius-xxl)!important}.rounded-top-circle{border-top-left-radius:50%!important;border-top-right-radius:50%!important}.rounded-top-pill{border-top-left-radius:var(--bs-border-radius-pill)!important;border-top-right-radius:var(--bs-border-radius-pill)!important}.rounded-end{border-top-right-radius:var(--bs-border-radius)!important;border-bottom-right-radius:var(--bs-border-radius)!important}.rounded-end-0{border-top-right-radius:0!important;border-bottom-right-radius:0!important}.rounded-end-1{border-top-right-radius:var(--bs-border-radius-sm)!important;border-bottom-right-radius:var(--bs-border-radius-sm)!important}.rounded-end-2{border-top-right-radius:var(--bs-border-radius)!important;border-bottom-right-radius:var(--bs-border-radius)!important}.rounded-end-3{border-top-right-radius:var(--bs-border-radius-lg)!important;border-bottom-right-radius:var(--bs-border-radius-lg)!important}.rounded-end-4{border-top-right-radius:var(--bs-border-radius-xl)!important;border-bottom-right-radius:var(--bs-border-radius-xl)!important}.rounded-end-5{border-top-right-radius:var(--bs-border-radius-xxl)!important;border-bottom-right-radius:var(--bs-border-radius-xxl)!important}.rounded-end-circle{border-top-right-radius:50%!important;border-bottom-right-radius:50%!important}.rounded-end-pill{border-top-right-radius:var(--bs-border-radius-pill)!important;border-bottom-right-radius:var(--bs-border-radius-pill)!important}.rounded-bottom{border-bottom-right-radius:var(--bs-border-radius)!important;border-bottom-left-radius:var(--bs-border-radius)!important}.rounded-bottom-0{border-bottom-right-radius:0!important;border-bottom-left-radius:0!important}.rounded-bottom-1{border-bottom-right-radius:var(--bs-border-radius-sm)!important;border-bottom-left-radius:var(--bs-border-radius-sm)!important}.rounded-bottom-2{border-bottom-right-radius:var(--bs-border-radius)!important;border-bottom-left-radius:var(--bs-border-radius)!important}.rounded-bottom-3{border-bottom-right-radius:var(--bs-border-radius-lg)!important;border-bottom-left-radius:var(--bs-border-radius-lg)!important}.rounded-bottom-4{border-bottom-right-radius:var(--bs-border-radius-xl)!important;border-bottom-left-radius:var(--bs-border-radius-xl)!important}.rounded-bottom-5{border-bottom-right-radius:var(--bs-border-radius-xxl)!important;border-bottom-left-radius:var(--bs-border-radius-xxl)!important}.rounded-bottom-circle{border-bottom-right-radius:50%!important;border-bottom-left-radius:50%!important}.rounded-bottom-pill{border-bottom-right-radius:var(--bs-border-radius-pill)!important;border-bottom-left-radius:var(--bs-border-radius-pill)!important}.rounded-start{border-bottom-left-radius:var(--bs-border-radius)!important;border-top-left-radius:var(--bs-border-radius)!important}.rounded-start-0{border-bottom-left-radius:0!important;border-top-left-radius:0!important}.rounded-start-1{border-bottom-left-radius:var(--bs-border-radius-sm)!important;border-top-left-radius:var(--bs-border-radius-sm)!important}.rounded-start-2{border-bottom-left-radius:var(--bs-border-radius)!important;border-top-left-radius:var(--bs-border-radius)!important}.rounded-start-3{border-bottom-left-radius:var(--bs-border-radius-lg)!important;border-top-left-radius:var(--bs-border-radius-lg)!important}.rounded-start-4{border-bottom-left-radius:var(--bs-border-radius-xl)!important;border-top-left-radius:var(--bs-border-radius-xl)!important}.rounded-start-5{border-bottom-left-radius:var(--bs-border-radius-xxl)!important;border-top-left-radius:var(--bs-border-radius-xxl)!important}.rounded-start-circle{border-bottom-left-radius:50%!important;border-top-left-radius:50%!important}.rounded-start-pill{border-bottom-left-radius:var(--bs-border-radius-pill)!important;border-top-left-radius:var(--bs-border-radius-pill)!important}.visible{visibility:visible!important}.invisible{visibility:hidden!important}.z-n1{z-index:-1!important}.z-0{z-index:0!important}.z-1{z-index:1!important}.z-2{z-index:2!important}.z-3{z-index:3!important}@media (min-width:576px){.float-sm-start{float:left!important}.float-sm-end{float:right!important}.float-sm-none{float:none!important}.object-fit-sm-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-sm-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-sm-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-sm-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-sm-none{-o-object-fit:none!important;object-fit:none!important}.d-sm-inline{display:inline!important}.d-sm-inline-block{display:inline-block!important}.d-sm-block{display:block!important}.d-sm-grid{display:grid!important}.d-sm-inline-grid{display:inline-grid!important}.d-sm-table{display:table!important}.d-sm-table-row{display:table-row!important}.d-sm-table-cell{display:table-cell!important}.d-sm-flex{display:flex!important}.d-sm-inline-flex{display:inline-flex!important}.d-sm-none{display:none!important}.flex-sm-fill{flex:1 1 auto!important}.flex-sm-row{flex-direction:row!important}.flex-sm-column{flex-direction:column!important}.flex-sm-row-reverse{flex-direction:row-reverse!important}.flex-sm-column-reverse{flex-direction:column-reverse!important}.flex-sm-grow-0{flex-grow:0!important}.flex-sm-grow-1{flex-grow:1!important}.flex-sm-shrink-0{flex-shrink:0!important}.flex-sm-shrink-1{flex-shrink:1!important}.flex-sm-wrap{flex-wrap:wrap!important}.flex-sm-nowrap{flex-wrap:nowrap!important}.flex-sm-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-sm-start{justify-content:flex-start!important}.justify-content-sm-end{justify-content:flex-end!important}.justify-content-sm-center{justify-content:center!important}.justify-content-sm-between{justify-content:space-between!important}.justify-content-sm-around{justify-content:space-around!important}.justify-content-sm-evenly{justify-content:space-evenly!important}.align-items-sm-start{align-items:flex-start!important}.align-items-sm-end{align-items:flex-end!important}.align-items-sm-center{align-items:center!important}.align-items-sm-baseline{align-items:baseline!important}.align-items-sm-stretch{align-items:stretch!important}.align-content-sm-start{align-content:flex-start!important}.align-content-sm-end{align-content:flex-end!important}.align-content-sm-center{align-content:center!important}.align-content-sm-between{align-content:space-between!important}.align-content-sm-around{align-content:space-around!important}.align-content-sm-stretch{align-content:stretch!important}.align-self-sm-auto{align-self:auto!important}.align-self-sm-start{align-self:flex-start!important}.align-self-sm-end{align-self:flex-end!important}.align-self-sm-center{align-self:center!important}.align-self-sm-baseline{align-self:baseline!important}.align-self-sm-stretch{align-self:stretch!important}.order-sm-first{order:-1!important}.order-sm-0{order:0!important}.order-sm-1{order:1!important}.order-sm-2{order:2!important}.order-sm-3{order:3!important}.order-sm-4{order:4!important}.order-sm-5{order:5!important}.order-sm-last{order:6!important}.m-sm-0{margin:0!important}.m-sm-1{margin:.25rem!important}.m-sm-2{margin:.5rem!important}.m-sm-3{margin:1rem!important}.m-sm-4{margin:1.5rem!important}.m-sm-5{margin:3rem!important}.m-sm-auto{margin:auto!important}.mx-sm-0{margin-right:0!important;margin-left:0!important}.mx-sm-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-sm-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-sm-3{margin-right:1rem!important;margin-left:1rem!important}.mx-sm-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-sm-5{margin-right:3rem!important;margin-left:3rem!important}.mx-sm-auto{margin-right:auto!important;margin-left:auto!important}.my-sm-0{margin-top:0!important;margin-bottom:0!important}.my-sm-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-sm-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-sm-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-sm-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-sm-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-sm-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-sm-0{margin-top:0!important}.mt-sm-1{margin-top:.25rem!important}.mt-sm-2{margin-top:.5rem!important}.mt-sm-3{margin-top:1rem!important}.mt-sm-4{margin-top:1.5rem!important}.mt-sm-5{margin-top:3rem!important}.mt-sm-auto{margin-top:auto!important}.me-sm-0{margin-right:0!important}.me-sm-1{margin-right:.25rem!important}.me-sm-2{margin-right:.5rem!important}.me-sm-3{margin-right:1rem!important}.me-sm-4{margin-right:1.5rem!important}.me-sm-5{margin-right:3rem!important}.me-sm-auto{margin-right:auto!important}.mb-sm-0{margin-bottom:0!important}.mb-sm-1{margin-bottom:.25rem!important}.mb-sm-2{margin-bottom:.5rem!important}.mb-sm-3{margin-bottom:1rem!important}.mb-sm-4{margin-bottom:1.5rem!important}.mb-sm-5{margin-bottom:3rem!important}.mb-sm-auto{margin-bottom:auto!important}.ms-sm-0{margin-left:0!important}.ms-sm-1{margin-left:.25rem!important}.ms-sm-2{margin-left:.5rem!important}.ms-sm-3{margin-left:1rem!important}.ms-sm-4{margin-left:1.5rem!important}.ms-sm-5{margin-left:3rem!important}.ms-sm-auto{margin-left:auto!important}.p-sm-0{padding:0!important}.p-sm-1{padding:.25rem!important}.p-sm-2{padding:.5rem!important}.p-sm-3{padding:1rem!important}.p-sm-4{padding:1.5rem!important}.p-sm-5{padding:3rem!important}.px-sm-0{padding-right:0!important;padding-left:0!important}.px-sm-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-sm-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-sm-3{padding-right:1rem!important;padding-left:1rem!important}.px-sm-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-sm-5{padding-right:3rem!important;padding-left:3rem!important}.py-sm-0{padding-top:0!important;padding-bottom:0!important}.py-sm-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-sm-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-sm-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-sm-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-sm-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-sm-0{padding-top:0!important}.pt-sm-1{padding-top:.25rem!important}.pt-sm-2{padding-top:.5rem!important}.pt-sm-3{padding-top:1rem!important}.pt-sm-4{padding-top:1.5rem!important}.pt-sm-5{padding-top:3rem!important}.pe-sm-0{padding-right:0!important}.pe-sm-1{padding-right:.25rem!important}.pe-sm-2{padding-right:.5rem!important}.pe-sm-3{padding-right:1rem!important}.pe-sm-4{padding-right:1.5rem!important}.pe-sm-5{padding-right:3rem!important}.pb-sm-0{padding-bottom:0!important}.pb-sm-1{padding-bottom:.25rem!important}.pb-sm-2{padding-bottom:.5rem!important}.pb-sm-3{padding-bottom:1rem!important}.pb-sm-4{padding-bottom:1.5rem!important}.pb-sm-5{padding-bottom:3rem!important}.ps-sm-0{padding-left:0!important}.ps-sm-1{padding-left:.25rem!important}.ps-sm-2{padding-left:.5rem!important}.ps-sm-3{padding-left:1rem!important}.ps-sm-4{padding-left:1.5rem!important}.ps-sm-5{padding-left:3rem!important}.gap-sm-0{gap:0!important}.gap-sm-1{gap:.25rem!important}.gap-sm-2{gap:.5rem!important}.gap-sm-3{gap:1rem!important}.gap-sm-4{gap:1.5rem!important}.gap-sm-5{gap:3rem!important}.row-gap-sm-0{row-gap:0!important}.row-gap-sm-1{row-gap:.25rem!important}.row-gap-sm-2{row-gap:.5rem!important}.row-gap-sm-3{row-gap:1rem!important}.row-gap-sm-4{row-gap:1.5rem!important}.row-gap-sm-5{row-gap:3rem!important}.column-gap-sm-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-sm-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-sm-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-sm-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-sm-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-sm-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-sm-start{text-align:left!important}.text-sm-end{text-align:right!important}.text-sm-center{text-align:center!important}}@media (min-width:768px){.float-md-start{float:left!important}.float-md-end{float:right!important}.float-md-none{float:none!important}.object-fit-md-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-md-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-md-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-md-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-md-none{-o-object-fit:none!important;object-fit:none!important}.d-md-inline{display:inline!important}.d-md-inline-block{display:inline-block!important}.d-md-block{display:block!important}.d-md-grid{display:grid!important}.d-md-inline-grid{display:inline-grid!important}.d-md-table{display:table!important}.d-md-table-row{display:table-row!important}.d-md-table-cell{display:table-cell!important}.d-md-flex{display:flex!important}.d-md-inline-flex{display:inline-flex!important}.d-md-none{display:none!important}.flex-md-fill{flex:1 1 auto!important}.flex-md-row{flex-direction:row!important}.flex-md-column{flex-direction:column!important}.flex-md-row-reverse{flex-direction:row-reverse!important}.flex-md-column-reverse{flex-direction:column-reverse!important}.flex-md-grow-0{flex-grow:0!important}.flex-md-grow-1{flex-grow:1!important}.flex-md-shrink-0{flex-shrink:0!important}.flex-md-shrink-1{flex-shrink:1!important}.flex-md-wrap{flex-wrap:wrap!important}.flex-md-nowrap{flex-wrap:nowrap!important}.flex-md-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-md-start{justify-content:flex-start!important}.justify-content-md-end{justify-content:flex-end!important}.justify-content-md-center{justify-content:center!important}.justify-content-md-between{justify-content:space-between!important}.justify-content-md-around{justify-content:space-around!important}.justify-content-md-evenly{justify-content:space-evenly!important}.align-items-md-start{align-items:flex-start!important}.align-items-md-end{align-items:flex-end!important}.align-items-md-center{align-items:center!important}.align-items-md-baseline{align-items:baseline!important}.align-items-md-stretch{align-items:stretch!important}.align-content-md-start{align-content:flex-start!important}.align-content-md-end{align-content:flex-end!important}.align-content-md-center{align-content:center!important}.align-content-md-between{align-content:space-between!important}.align-content-md-around{align-content:space-around!important}.align-content-md-stretch{align-content:stretch!important}.align-self-md-auto{align-self:auto!important}.align-self-md-start{align-self:flex-start!important}.align-self-md-end{align-self:flex-end!important}.align-self-md-center{align-self:center!important}.align-self-md-baseline{align-self:baseline!important}.align-self-md-stretch{align-self:stretch!important}.order-md-first{order:-1!important}.order-md-0{order:0!important}.order-md-1{order:1!important}.order-md-2{order:2!important}.order-md-3{order:3!important}.order-md-4{order:4!important}.order-md-5{order:5!important}.order-md-last{order:6!important}.m-md-0{margin:0!important}.m-md-1{margin:.25rem!important}.m-md-2{margin:.5rem!important}.m-md-3{margin:1rem!important}.m-md-4{margin:1.5rem!important}.m-md-5{margin:3rem!important}.m-md-auto{margin:auto!important}.mx-md-0{margin-right:0!important;margin-left:0!important}.mx-md-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-md-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-md-3{margin-right:1rem!important;margin-left:1rem!important}.mx-md-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-md-5{margin-right:3rem!important;margin-left:3rem!important}.mx-md-auto{margin-right:auto!important;margin-left:auto!important}.my-md-0{margin-top:0!important;margin-bottom:0!important}.my-md-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-md-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-md-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-md-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-md-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-md-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-md-0{margin-top:0!important}.mt-md-1{margin-top:.25rem!important}.mt-md-2{margin-top:.5rem!important}.mt-md-3{margin-top:1rem!important}.mt-md-4{margin-top:1.5rem!important}.mt-md-5{margin-top:3rem!important}.mt-md-auto{margin-top:auto!important}.me-md-0{margin-right:0!important}.me-md-1{margin-right:.25rem!important}.me-md-2{margin-right:.5rem!important}.me-md-3{margin-right:1rem!important}.me-md-4{margin-right:1.5rem!important}.me-md-5{margin-right:3rem!important}.me-md-auto{margin-right:auto!important}.mb-md-0{margin-bottom:0!important}.mb-md-1{margin-bottom:.25rem!important}.mb-md-2{margin-bottom:.5rem!important}.mb-md-3{margin-bottom:1rem!important}.mb-md-4{margin-bottom:1.5rem!important}.mb-md-5{margin-bottom:3rem!important}.mb-md-auto{margin-bottom:auto!important}.ms-md-0{margin-left:0!important}.ms-md-1{margin-left:.25rem!important}.ms-md-2{margin-left:.5rem!important}.ms-md-3{margin-left:1rem!important}.ms-md-4{margin-left:1.5rem!important}.ms-md-5{margin-left:3rem!important}.ms-md-auto{margin-left:auto!important}.p-md-0{padding:0!important}.p-md-1{padding:.25rem!important}.p-md-2{padding:.5rem!important}.p-md-3{padding:1rem!important}.p-md-4{padding:1.5rem!important}.p-md-5{padding:3rem!important}.px-md-0{padding-right:0!important;padding-left:0!important}.px-md-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-md-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-md-3{padding-right:1rem!important;padding-left:1rem!important}.px-md-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-md-5{padding-right:3rem!important;padding-left:3rem!important}.py-md-0{padding-top:0!important;padding-bottom:0!important}.py-md-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-md-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-md-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-md-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-md-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-md-0{padding-top:0!important}.pt-md-1{padding-top:.25rem!important}.pt-md-2{padding-top:.5rem!important}.pt-md-3{padding-top:1rem!important}.pt-md-4{padding-top:1.5rem!important}.pt-md-5{padding-top:3rem!important}.pe-md-0{padding-right:0!important}.pe-md-1{padding-right:.25rem!important}.pe-md-2{padding-right:.5rem!important}.pe-md-3{padding-right:1rem!important}.pe-md-4{padding-right:1.5rem!important}.pe-md-5{padding-right:3rem!important}.pb-md-0{padding-bottom:0!important}.pb-md-1{padding-bottom:.25rem!important}.pb-md-2{padding-bottom:.5rem!important}.pb-md-3{padding-bottom:1rem!important}.pb-md-4{padding-bottom:1.5rem!important}.pb-md-5{padding-bottom:3rem!important}.ps-md-0{padding-left:0!important}.ps-md-1{padding-left:.25rem!important}.ps-md-2{padding-left:.5rem!important}.ps-md-3{padding-left:1rem!important}.ps-md-4{padding-left:1.5rem!important}.ps-md-5{padding-left:3rem!important}.gap-md-0{gap:0!important}.gap-md-1{gap:.25rem!important}.gap-md-2{gap:.5rem!important}.gap-md-3{gap:1rem!important}.gap-md-4{gap:1.5rem!important}.gap-md-5{gap:3rem!important}.row-gap-md-0{row-gap:0!important}.row-gap-md-1{row-gap:.25rem!important}.row-gap-md-2{row-gap:.5rem!important}.row-gap-md-3{row-gap:1rem!important}.row-gap-md-4{row-gap:1.5rem!important}.row-gap-md-5{row-gap:3rem!important}.column-gap-md-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-md-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-md-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-md-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-md-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-md-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-md-start{text-align:left!important}.text-md-end{text-align:right!important}.text-md-center{text-align:center!important}}@media (min-width:992px){.float-lg-start{float:left!important}.float-lg-end{float:right!important}.float-lg-none{float:none!important}.object-fit-lg-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-lg-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-lg-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-lg-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-lg-none{-o-object-fit:none!important;object-fit:none!important}.d-lg-inline{display:inline!important}.d-lg-inline-block{display:inline-block!important}.d-lg-block{display:block!important}.d-lg-grid{display:grid!important}.d-lg-inline-grid{display:inline-grid!important}.d-lg-table{display:table!important}.d-lg-table-row{display:table-row!important}.d-lg-table-cell{display:table-cell!important}.d-lg-flex{display:flex!important}.d-lg-inline-flex{display:inline-flex!important}.d-lg-none{display:none!important}.flex-lg-fill{flex:1 1 auto!important}.flex-lg-row{flex-direction:row!important}.flex-lg-column{flex-direction:column!important}.flex-lg-row-reverse{flex-direction:row-reverse!important}.flex-lg-column-reverse{flex-direction:column-reverse!important}.flex-lg-grow-0{flex-grow:0!important}.flex-lg-grow-1{flex-grow:1!important}.flex-lg-shrink-0{flex-shrink:0!important}.flex-lg-shrink-1{flex-shrink:1!important}.flex-lg-wrap{flex-wrap:wrap!important}.flex-lg-nowrap{flex-wrap:nowrap!important}.flex-lg-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-lg-start{justify-content:flex-start!important}.justify-content-lg-end{justify-content:flex-end!important}.justify-content-lg-center{justify-content:center!important}.justify-content-lg-between{justify-content:space-between!important}.justify-content-lg-around{justify-content:space-around!important}.justify-content-lg-evenly{justify-content:space-evenly!important}.align-items-lg-start{align-items:flex-start!important}.align-items-lg-end{align-items:flex-end!important}.align-items-lg-center{align-items:center!important}.align-items-lg-baseline{align-items:baseline!important}.align-items-lg-stretch{align-items:stretch!important}.align-content-lg-start{align-content:flex-start!important}.align-content-lg-end{align-content:flex-end!important}.align-content-lg-center{align-content:center!important}.align-content-lg-between{align-content:space-between!important}.align-content-lg-around{align-content:space-around!important}.align-content-lg-stretch{align-content:stretch!important}.align-self-lg-auto{align-self:auto!important}.align-self-lg-start{align-self:flex-start!important}.align-self-lg-end{align-self:flex-end!important}.align-self-lg-center{align-self:center!important}.align-self-lg-baseline{align-self:baseline!important}.align-self-lg-stretch{align-self:stretch!important}.order-lg-first{order:-1!important}.order-lg-0{order:0!important}.order-lg-1{order:1!important}.order-lg-2{order:2!important}.order-lg-3{order:3!important}.order-lg-4{order:4!important}.order-lg-5{order:5!important}.order-lg-last{order:6!important}.m-lg-0{margin:0!important}.m-lg-1{margin:.25rem!important}.m-lg-2{margin:.5rem!important}.m-lg-3{margin:1rem!important}.m-lg-4{margin:1.5rem!important}.m-lg-5{margin:3rem!important}.m-lg-auto{margin:auto!important}.mx-lg-0{margin-right:0!important;margin-left:0!important}.mx-lg-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-lg-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-lg-3{margin-right:1rem!important;margin-left:1rem!important}.mx-lg-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-lg-5{margin-right:3rem!important;margin-left:3rem!important}.mx-lg-auto{margin-right:auto!important;margin-left:auto!important}.my-lg-0{margin-top:0!important;margin-bottom:0!important}.my-lg-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-lg-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-lg-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-lg-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-lg-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-lg-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-lg-0{margin-top:0!important}.mt-lg-1{margin-top:.25rem!important}.mt-lg-2{margin-top:.5rem!important}.mt-lg-3{margin-top:1rem!important}.mt-lg-4{margin-top:1.5rem!important}.mt-lg-5{margin-top:3rem!important}.mt-lg-auto{margin-top:auto!important}.me-lg-0{margin-right:0!important}.me-lg-1{margin-right:.25rem!important}.me-lg-2{margin-right:.5rem!important}.me-lg-3{margin-right:1rem!important}.me-lg-4{margin-right:1.5rem!important}.me-lg-5{margin-right:3rem!important}.me-lg-auto{margin-right:auto!important}.mb-lg-0{margin-bottom:0!important}.mb-lg-1{margin-bottom:.25rem!important}.mb-lg-2{margin-bottom:.5rem!important}.mb-lg-3{margin-bottom:1rem!important}.mb-lg-4{margin-bottom:1.5rem!important}.mb-lg-5{margin-bottom:3rem!important}.mb-lg-auto{margin-bottom:auto!important}.ms-lg-0{margin-left:0!important}.ms-lg-1{margin-left:.25rem!important}.ms-lg-2{margin-left:.5rem!important}.ms-lg-3{margin-left:1rem!important}.ms-lg-4{margin-left:1.5rem!important}.ms-lg-5{margin-left:3rem!important}.ms-lg-auto{margin-left:auto!important}.p-lg-0{padding:0!important}.p-lg-1{padding:.25rem!important}.p-lg-2{padding:.5rem!important}.p-lg-3{padding:1rem!important}.p-lg-4{padding:1.5rem!important}.p-lg-5{padding:3rem!important}.px-lg-0{padding-right:0!important;padding-left:0!important}.px-lg-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-lg-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-lg-3{padding-right:1rem!important;padding-left:1rem!important}.px-lg-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-lg-5{padding-right:3rem!important;padding-left:3rem!important}.py-lg-0{padding-top:0!important;padding-bottom:0!important}.py-lg-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-lg-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-lg-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-lg-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-lg-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-lg-0{padding-top:0!important}.pt-lg-1{padding-top:.25rem!important}.pt-lg-2{padding-top:.5rem!important}.pt-lg-3{padding-top:1rem!important}.pt-lg-4{padding-top:1.5rem!important}.pt-lg-5{padding-top:3rem!important}.pe-lg-0{padding-right:0!important}.pe-lg-1{padding-right:.25rem!important}.pe-lg-2{padding-right:.5rem!important}.pe-lg-3{padding-right:1rem!important}.pe-lg-4{padding-right:1.5rem!important}.pe-lg-5{padding-right:3rem!important}.pb-lg-0{padding-bottom:0!important}.pb-lg-1{padding-bottom:.25rem!important}.pb-lg-2{padding-bottom:.5rem!important}.pb-lg-3{padding-bottom:1rem!important}.pb-lg-4{padding-bottom:1.5rem!important}.pb-lg-5{padding-bottom:3rem!important}.ps-lg-0{padding-left:0!important}.ps-lg-1{padding-left:.25rem!important}.ps-lg-2{padding-left:.5rem!important}.ps-lg-3{padding-left:1rem!important}.ps-lg-4{padding-left:1.5rem!important}.ps-lg-5{padding-left:3rem!important}.gap-lg-0{gap:0!important}.gap-lg-1{gap:.25rem!important}.gap-lg-2{gap:.5rem!important}.gap-lg-3{gap:1rem!important}.gap-lg-4{gap:1.5rem!important}.gap-lg-5{gap:3rem!important}.row-gap-lg-0{row-gap:0!important}.row-gap-lg-1{row-gap:.25rem!important}.row-gap-lg-2{row-gap:.5rem!important}.row-gap-lg-3{row-gap:1rem!important}.row-gap-lg-4{row-gap:1.5rem!important}.row-gap-lg-5{row-gap:3rem!important}.column-gap-lg-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-lg-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-lg-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-lg-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-lg-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-lg-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-lg-start{text-align:left!important}.text-lg-end{text-align:right!important}.text-lg-center{text-align:center!important}}@media (min-width:1200px){.float-xl-start{float:left!important}.float-xl-end{float:right!important}.float-xl-none{float:none!important}.object-fit-xl-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-xl-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-xl-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-xl-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-xl-none{-o-object-fit:none!important;object-fit:none!important}.d-xl-inline{display:inline!important}.d-xl-inline-block{display:inline-block!important}.d-xl-block{display:block!important}.d-xl-grid{display:grid!important}.d-xl-inline-grid{display:inline-grid!important}.d-xl-table{display:table!important}.d-xl-table-row{display:table-row!important}.d-xl-table-cell{display:table-cell!important}.d-xl-flex{display:flex!important}.d-xl-inline-flex{display:inline-flex!important}.d-xl-none{display:none!important}.flex-xl-fill{flex:1 1 auto!important}.flex-xl-row{flex-direction:row!important}.flex-xl-column{flex-direction:column!important}.flex-xl-row-reverse{flex-direction:row-reverse!important}.flex-xl-column-reverse{flex-direction:column-reverse!important}.flex-xl-grow-0{flex-grow:0!important}.flex-xl-grow-1{flex-grow:1!important}.flex-xl-shrink-0{flex-shrink:0!important}.flex-xl-shrink-1{flex-shrink:1!important}.flex-xl-wrap{flex-wrap:wrap!important}.flex-xl-nowrap{flex-wrap:nowrap!important}.flex-xl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xl-start{justify-content:flex-start!important}.justify-content-xl-end{justify-content:flex-end!important}.justify-content-xl-center{justify-content:center!important}.justify-content-xl-between{justify-content:space-between!important}.justify-content-xl-around{justify-content:space-around!important}.justify-content-xl-evenly{justify-content:space-evenly!important}.align-items-xl-start{align-items:flex-start!important}.align-items-xl-end{align-items:flex-end!important}.align-items-xl-center{align-items:center!important}.align-items-xl-baseline{align-items:baseline!important}.align-items-xl-stretch{align-items:stretch!important}.align-content-xl-start{align-content:flex-start!important}.align-content-xl-end{align-content:flex-end!important}.align-content-xl-center{align-content:center!important}.align-content-xl-between{align-content:space-between!important}.align-content-xl-around{align-content:space-around!important}.align-content-xl-stretch{align-content:stretch!important}.align-self-xl-auto{align-self:auto!important}.align-self-xl-start{align-self:flex-start!important}.align-self-xl-end{align-self:flex-end!important}.align-self-xl-center{align-self:center!important}.align-self-xl-baseline{align-self:baseline!important}.align-self-xl-stretch{align-self:stretch!important}.order-xl-first{order:-1!important}.order-xl-0{order:0!important}.order-xl-1{order:1!important}.order-xl-2{order:2!important}.order-xl-3{order:3!important}.order-xl-4{order:4!important}.order-xl-5{order:5!important}.order-xl-last{order:6!important}.m-xl-0{margin:0!important}.m-xl-1{margin:.25rem!important}.m-xl-2{margin:.5rem!important}.m-xl-3{margin:1rem!important}.m-xl-4{margin:1.5rem!important}.m-xl-5{margin:3rem!important}.m-xl-auto{margin:auto!important}.mx-xl-0{margin-right:0!important;margin-left:0!important}.mx-xl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xl-auto{margin-right:auto!important;margin-left:auto!important}.my-xl-0{margin-top:0!important;margin-bottom:0!important}.my-xl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xl-0{margin-top:0!important}.mt-xl-1{margin-top:.25rem!important}.mt-xl-2{margin-top:.5rem!important}.mt-xl-3{margin-top:1rem!important}.mt-xl-4{margin-top:1.5rem!important}.mt-xl-5{margin-top:3rem!important}.mt-xl-auto{margin-top:auto!important}.me-xl-0{margin-right:0!important}.me-xl-1{margin-right:.25rem!important}.me-xl-2{margin-right:.5rem!important}.me-xl-3{margin-right:1rem!important}.me-xl-4{margin-right:1.5rem!important}.me-xl-5{margin-right:3rem!important}.me-xl-auto{margin-right:auto!important}.mb-xl-0{margin-bottom:0!important}.mb-xl-1{margin-bottom:.25rem!important}.mb-xl-2{margin-bottom:.5rem!important}.mb-xl-3{margin-bottom:1rem!important}.mb-xl-4{margin-bottom:1.5rem!important}.mb-xl-5{margin-bottom:3rem!important}.mb-xl-auto{margin-bottom:auto!important}.ms-xl-0{margin-left:0!important}.ms-xl-1{margin-left:.25rem!important}.ms-xl-2{margin-left:.5rem!important}.ms-xl-3{margin-left:1rem!important}.ms-xl-4{margin-left:1.5rem!important}.ms-xl-5{margin-left:3rem!important}.ms-xl-auto{margin-left:auto!important}.p-xl-0{padding:0!important}.p-xl-1{padding:.25rem!important}.p-xl-2{padding:.5rem!important}.p-xl-3{padding:1rem!important}.p-xl-4{padding:1.5rem!important}.p-xl-5{padding:3rem!important}.px-xl-0{padding-right:0!important;padding-left:0!important}.px-xl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xl-0{padding-top:0!important;padding-bottom:0!important}.py-xl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xl-0{padding-top:0!important}.pt-xl-1{padding-top:.25rem!important}.pt-xl-2{padding-top:.5rem!important}.pt-xl-3{padding-top:1rem!important}.pt-xl-4{padding-top:1.5rem!important}.pt-xl-5{padding-top:3rem!important}.pe-xl-0{padding-right:0!important}.pe-xl-1{padding-right:.25rem!important}.pe-xl-2{padding-right:.5rem!important}.pe-xl-3{padding-right:1rem!important}.pe-xl-4{padding-right:1.5rem!important}.pe-xl-5{padding-right:3rem!important}.pb-xl-0{padding-bottom:0!important}.pb-xl-1{padding-bottom:.25rem!important}.pb-xl-2{padding-bottom:.5rem!important}.pb-xl-3{padding-bottom:1rem!important}.pb-xl-4{padding-bottom:1.5rem!important}.pb-xl-5{padding-bottom:3rem!important}.ps-xl-0{padding-left:0!important}.ps-xl-1{padding-left:.25rem!important}.ps-xl-2{padding-left:.5rem!important}.ps-xl-3{padding-left:1rem!important}.ps-xl-4{padding-left:1.5rem!important}.ps-xl-5{padding-left:3rem!important}.gap-xl-0{gap:0!important}.gap-xl-1{gap:.25rem!important}.gap-xl-2{gap:.5rem!important}.gap-xl-3{gap:1rem!important}.gap-xl-4{gap:1.5rem!important}.gap-xl-5{gap:3rem!important}.row-gap-xl-0{row-gap:0!important}.row-gap-xl-1{row-gap:.25rem!important}.row-gap-xl-2{row-gap:.5rem!important}.row-gap-xl-3{row-gap:1rem!important}.row-gap-xl-4{row-gap:1.5rem!important}.row-gap-xl-5{row-gap:3rem!important}.column-gap-xl-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-xl-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-xl-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-xl-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-xl-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-xl-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-xl-start{text-align:left!important}.text-xl-end{text-align:right!important}.text-xl-center{text-align:center!important}}@media (min-width:1400px){.float-xxl-start{float:left!important}.float-xxl-end{float:right!important}.float-xxl-none{float:none!important}.object-fit-xxl-contain{-o-object-fit:contain!important;object-fit:contain!important}.object-fit-xxl-cover{-o-object-fit:cover!important;object-fit:cover!important}.object-fit-xxl-fill{-o-object-fit:fill!important;object-fit:fill!important}.object-fit-xxl-scale{-o-object-fit:scale-down!important;object-fit:scale-down!important}.object-fit-xxl-none{-o-object-fit:none!important;object-fit:none!important}.d-xxl-inline{display:inline!important}.d-xxl-inline-block{display:inline-block!important}.d-xxl-block{display:block!important}.d-xxl-grid{display:grid!important}.d-xxl-inline-grid{display:inline-grid!important}.d-xxl-table{display:table!important}.d-xxl-table-row{display:table-row!important}.d-xxl-table-cell{display:table-cell!important}.d-xxl-flex{display:flex!important}.d-xxl-inline-flex{display:inline-flex!important}.d-xxl-none{display:none!important}.flex-xxl-fill{flex:1 1 auto!important}.flex-xxl-row{flex-direction:row!important}.flex-xxl-column{flex-direction:column!important}.flex-xxl-row-reverse{flex-direction:row-reverse!important}.flex-xxl-column-reverse{flex-direction:column-reverse!important}.flex-xxl-grow-0{flex-grow:0!important}.flex-xxl-grow-1{flex-grow:1!important}.flex-xxl-shrink-0{flex-shrink:0!important}.flex-xxl-shrink-1{flex-shrink:1!important}.flex-xxl-wrap{flex-wrap:wrap!important}.flex-xxl-nowrap{flex-wrap:nowrap!important}.flex-xxl-wrap-reverse{flex-wrap:wrap-reverse!important}.justify-content-xxl-start{justify-content:flex-start!important}.justify-content-xxl-end{justify-content:flex-end!important}.justify-content-xxl-center{justify-content:center!important}.justify-content-xxl-between{justify-content:space-between!important}.justify-content-xxl-around{justify-content:space-around!important}.justify-content-xxl-evenly{justify-content:space-evenly!important}.align-items-xxl-start{align-items:flex-start!important}.align-items-xxl-end{align-items:flex-end!important}.align-items-xxl-center{align-items:center!important}.align-items-xxl-baseline{align-items:baseline!important}.align-items-xxl-stretch{align-items:stretch!important}.align-content-xxl-start{align-content:flex-start!important}.align-content-xxl-end{align-content:flex-end!important}.align-content-xxl-center{align-content:center!important}.align-content-xxl-between{align-content:space-between!important}.align-content-xxl-around{align-content:space-around!important}.align-content-xxl-stretch{align-content:stretch!important}.align-self-xxl-auto{align-self:auto!important}.align-self-xxl-start{align-self:flex-start!important}.align-self-xxl-end{align-self:flex-end!important}.align-self-xxl-center{align-self:center!important}.align-self-xxl-baseline{align-self:baseline!important}.align-self-xxl-stretch{align-self:stretch!important}.order-xxl-first{order:-1!important}.order-xxl-0{order:0!important}.order-xxl-1{order:1!important}.order-xxl-2{order:2!important}.order-xxl-3{order:3!important}.order-xxl-4{order:4!important}.order-xxl-5{order:5!important}.order-xxl-last{order:6!important}.m-xxl-0{margin:0!important}.m-xxl-1{margin:.25rem!important}.m-xxl-2{margin:.5rem!important}.m-xxl-3{margin:1rem!important}.m-xxl-4{margin:1.5rem!important}.m-xxl-5{margin:3rem!important}.m-xxl-auto{margin:auto!important}.mx-xxl-0{margin-right:0!important;margin-left:0!important}.mx-xxl-1{margin-right:.25rem!important;margin-left:.25rem!important}.mx-xxl-2{margin-right:.5rem!important;margin-left:.5rem!important}.mx-xxl-3{margin-right:1rem!important;margin-left:1rem!important}.mx-xxl-4{margin-right:1.5rem!important;margin-left:1.5rem!important}.mx-xxl-5{margin-right:3rem!important;margin-left:3rem!important}.mx-xxl-auto{margin-right:auto!important;margin-left:auto!important}.my-xxl-0{margin-top:0!important;margin-bottom:0!important}.my-xxl-1{margin-top:.25rem!important;margin-bottom:.25rem!important}.my-xxl-2{margin-top:.5rem!important;margin-bottom:.5rem!important}.my-xxl-3{margin-top:1rem!important;margin-bottom:1rem!important}.my-xxl-4{margin-top:1.5rem!important;margin-bottom:1.5rem!important}.my-xxl-5{margin-top:3rem!important;margin-bottom:3rem!important}.my-xxl-auto{margin-top:auto!important;margin-bottom:auto!important}.mt-xxl-0{margin-top:0!important}.mt-xxl-1{margin-top:.25rem!important}.mt-xxl-2{margin-top:.5rem!important}.mt-xxl-3{margin-top:1rem!important}.mt-xxl-4{margin-top:1.5rem!important}.mt-xxl-5{margin-top:3rem!important}.mt-xxl-auto{margin-top:auto!important}.me-xxl-0{margin-right:0!important}.me-xxl-1{margin-right:.25rem!important}.me-xxl-2{margin-right:.5rem!important}.me-xxl-3{margin-right:1rem!important}.me-xxl-4{margin-right:1.5rem!important}.me-xxl-5{margin-right:3rem!important}.me-xxl-auto{margin-right:auto!important}.mb-xxl-0{margin-bottom:0!important}.mb-xxl-1{margin-bottom:.25rem!important}.mb-xxl-2{margin-bottom:.5rem!important}.mb-xxl-3{margin-bottom:1rem!important}.mb-xxl-4{margin-bottom:1.5rem!important}.mb-xxl-5{margin-bottom:3rem!important}.mb-xxl-auto{margin-bottom:auto!important}.ms-xxl-0{margin-left:0!important}.ms-xxl-1{margin-left:.25rem!important}.ms-xxl-2{margin-left:.5rem!important}.ms-xxl-3{margin-left:1rem!important}.ms-xxl-4{margin-left:1.5rem!important}.ms-xxl-5{margin-left:3rem!important}.ms-xxl-auto{margin-left:auto!important}.p-xxl-0{padding:0!important}.p-xxl-1{padding:.25rem!important}.p-xxl-2{padding:.5rem!important}.p-xxl-3{padding:1rem!important}.p-xxl-4{padding:1.5rem!important}.p-xxl-5{padding:3rem!important}.px-xxl-0{padding-right:0!important;padding-left:0!important}.px-xxl-1{padding-right:.25rem!important;padding-left:.25rem!important}.px-xxl-2{padding-right:.5rem!important;padding-left:.5rem!important}.px-xxl-3{padding-right:1rem!important;padding-left:1rem!important}.px-xxl-4{padding-right:1.5rem!important;padding-left:1.5rem!important}.px-xxl-5{padding-right:3rem!important;padding-left:3rem!important}.py-xxl-0{padding-top:0!important;padding-bottom:0!important}.py-xxl-1{padding-top:.25rem!important;padding-bottom:.25rem!important}.py-xxl-2{padding-top:.5rem!important;padding-bottom:.5rem!important}.py-xxl-3{padding-top:1rem!important;padding-bottom:1rem!important}.py-xxl-4{padding-top:1.5rem!important;padding-bottom:1.5rem!important}.py-xxl-5{padding-top:3rem!important;padding-bottom:3rem!important}.pt-xxl-0{padding-top:0!important}.pt-xxl-1{padding-top:.25rem!important}.pt-xxl-2{padding-top:.5rem!important}.pt-xxl-3{padding-top:1rem!important}.pt-xxl-4{padding-top:1.5rem!important}.pt-xxl-5{padding-top:3rem!important}.pe-xxl-0{padding-right:0!important}.pe-xxl-1{padding-right:.25rem!important}.pe-xxl-2{padding-right:.5rem!important}.pe-xxl-3{padding-right:1rem!important}.pe-xxl-4{padding-right:1.5rem!important}.pe-xxl-5{padding-right:3rem!important}.pb-xxl-0{padding-bottom:0!important}.pb-xxl-1{padding-bottom:.25rem!important}.pb-xxl-2{padding-bottom:.5rem!important}.pb-xxl-3{padding-bottom:1rem!important}.pb-xxl-4{padding-bottom:1.5rem!important}.pb-xxl-5{padding-bottom:3rem!important}.ps-xxl-0{padding-left:0!important}.ps-xxl-1{padding-left:.25rem!important}.ps-xxl-2{padding-left:.5rem!important}.ps-xxl-3{padding-left:1rem!important}.ps-xxl-4{padding-left:1.5rem!important}.ps-xxl-5{padding-left:3rem!important}.gap-xxl-0{gap:0!important}.gap-xxl-1{gap:.25rem!important}.gap-xxl-2{gap:.5rem!important}.gap-xxl-3{gap:1rem!important}.gap-xxl-4{gap:1.5rem!important}.gap-xxl-5{gap:3rem!important}.row-gap-xxl-0{row-gap:0!important}.row-gap-xxl-1{row-gap:.25rem!important}.row-gap-xxl-2{row-gap:.5rem!important}.row-gap-xxl-3{row-gap:1rem!important}.row-gap-xxl-4{row-gap:1.5rem!important}.row-gap-xxl-5{row-gap:3rem!important}.column-gap-xxl-0{-moz-column-gap:0!important;column-gap:0!important}.column-gap-xxl-1{-moz-column-gap:0.25rem!important;column-gap:.25rem!important}.column-gap-xxl-2{-moz-column-gap:0.5rem!important;column-gap:.5rem!important}.column-gap-xxl-3{-moz-column-gap:1rem!important;column-gap:1rem!important}.column-gap-xxl-4{-moz-column-gap:1.5rem!important;column-gap:1.5rem!important}.column-gap-xxl-5{-moz-column-gap:3rem!important;column-gap:3rem!important}.text-xxl-start{text-align:left!important}.text-xxl-end{text-align:right!important}.text-xxl-center{text-align:center!important}}@media (min-width:1200px){.fs-1{font-size:2.5rem!important}.fs-2{font-size:2rem!important}.fs-3{font-size:1.75rem!important}.fs-4{font-size:1.5rem!important}}@media print{.d-print-inline{display:inline!important}.d-print-inline-block{display:inline-block!important}.d-print-block{display:block!important}.d-print-grid{display:grid!important}.d-print-inline-grid{display:inline-grid!important}.d-print-table{display:table!important}.d-print-table-row{display:table-row!important}.d-print-table-cell{display:table-cell!important}.d-print-flex{display:flex!important}.d-print-inline-flex{display:inline-flex!important}.d-print-none{display:none!important}} /*# sourceMappingURL=bootstrap.min.css.map */ \ No newline at end of file diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server/Microsoft/AspNetCore/Authentication/Cookies/CookieAuthenticationOptionsExtensions.cs b/framework/src/Volo.Abp.AspNetCore.Components.Server/Microsoft/AspNetCore/Authentication/Cookies/CookieAuthenticationOptionsExtensions.cs index 9d303579da..0dd2c33cb8 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.Server/Microsoft/AspNetCore/Authentication/Cookies/CookieAuthenticationOptionsExtensions.cs +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server/Microsoft/AspNetCore/Authentication/Cookies/CookieAuthenticationOptionsExtensions.cs @@ -1,6 +1,6 @@ using System; using System.Threading.Tasks; -using IdentityModel.Client; +using Duende.IdentityModel.Client; using Microsoft.AspNetCore.Authentication.OpenIdConnect; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Server/Volo.Abp.AspNetCore.Components.Server.csproj b/framework/src/Volo.Abp.AspNetCore.Components.Server/Volo.Abp.AspNetCore.Components.Server.csproj index a5d4f97e59..b1b3b39f6f 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.Server/Volo.Abp.AspNetCore.Components.Server.csproj +++ b/framework/src/Volo.Abp.AspNetCore.Components.Server/Volo.Abp.AspNetCore.Components.Server.csproj @@ -20,7 +20,7 @@ - + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo.Abp.AspNetCore.Components.WebAssembly.csproj b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo.Abp.AspNetCore.Components.WebAssembly.csproj index 084c8aad97..cfa9732d2c 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo.Abp.AspNetCore.Components.WebAssembly.csproj +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo.Abp.AspNetCore.Components.WebAssembly.csproj @@ -28,7 +28,7 @@ - + diff --git a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/WebAssemblyAuthenticationStateProvider.cs b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/WebAssemblyAuthenticationStateProvider.cs index 0018571c20..c11f6ae9a9 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/WebAssemblyAuthenticationStateProvider.cs +++ b/framework/src/Volo.Abp.AspNetCore.Components.WebAssembly/Volo/Abp/AspNetCore/Components/WebAssembly/WebAssemblyAuthenticationStateProvider.cs @@ -6,7 +6,7 @@ using System.Net.Http; using System.Security.Claims; using System.Text.Json.Serialization; using System.Threading.Tasks; -using IdentityModel.Client; +using Duende.IdentityModel.Client; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Components.WebAssembly.Authentication; diff --git a/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Microsoft/AspNetCore/Builder/AbpAspNetCoreMultiTenancyApplicationBuilderExtensions.cs b/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Microsoft/AspNetCore/Builder/AbpAspNetCoreMultiTenancyApplicationBuilderExtensions.cs index ceb872e5c3..7ccd1a73c3 100644 --- a/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Microsoft/AspNetCore/Builder/AbpAspNetCoreMultiTenancyApplicationBuilderExtensions.cs +++ b/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Microsoft/AspNetCore/Builder/AbpAspNetCoreMultiTenancyApplicationBuilderExtensions.cs @@ -1,12 +1,34 @@ -using Volo.Abp.AspNetCore.MultiTenancy; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Volo.Abp.AspNetCore.MultiTenancy; +using Volo.Abp.MultiTenancy; namespace Microsoft.AspNetCore.Builder; public static class AbpAspNetCoreMultiTenancyApplicationBuilderExtensions { + private const string AuthenticationMiddlewareSetKey = "__AuthenticationMiddlewareSet"; + public static IApplicationBuilder UseMultiTenancy(this IApplicationBuilder app) { - return app - .UseMiddleware(); + var multiTenancyOptions = app.ApplicationServices.GetRequiredService>(); + var hasCurrentUserTenantResolveContributor = multiTenancyOptions.Value.TenantResolvers.Any(r => r is CurrentUserTenantResolveContributor); + if (hasCurrentUserTenantResolveContributor) + { + var authenticationMiddlewareSet = app.Properties.TryGetValue(AuthenticationMiddlewareSetKey, out var value) && value is true; + if (!authenticationMiddlewareSet) + { + var logger = app.ApplicationServices.GetService>(); + logger?.LogWarning( + "MultiTenancyMiddleware is being registered before the authentication middleware. " + + "This may lead to incorrect tenant resolution if the resolution depends on the authenticated user. " + + "Ensure app.UseAuthentication() is called before app.UseMultiTenancy()." + ); + } + } + + return app.UseMiddleware(); } } diff --git a/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/Views/MultiTenancyMiddlewareErrorPage.Designer.cs b/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/Views/MultiTenancyMiddlewareErrorPage.Designer.cs index 751eec21bb..e1c2947c97 100644 --- a/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/Views/MultiTenancyMiddlewareErrorPage.Designer.cs +++ b/framework/src/Volo.Abp.AspNetCore.MultiTenancy/Volo/Abp/AspNetCore/MultiTenancy/Views/MultiTenancyMiddlewareErrorPage.Designer.cs @@ -42,10 +42,10 @@ using Volo.Abp.AspNetCore.RazorViews; #nullable disable WriteLiteral("\n"); WriteLiteral("\n